Interacción con datos almacenados usando Spring Boot y Neo4j
Muchos contextos de la vida real pueden ser modelados y almacenados como grafos teniendo en cuenta que son tan importantes los objetos o entidades involucradas como la relación que existe entre ellos.
Existen diversos ejemplos donde los grafos o redes son la herramienta para modelar, encontrar y entender propiedades o comportamientos a veces no tan obvios:
-Un partido de fútbol puede ser visto como un grafo, donde los jugadores se relacionan por la cantidad de pases del balón.
-La interacción financiera entre prestamistas y clientes también es un ejemplo interesante, el cual tomaremos como estudio de caso en este artículo.
Imaginemos un conjunto de prestamistas que prestaron dinero a sus clientes .
Y entre los clientes en algunos casos unos proveen servicios o insumos a otros clientes.
En este artículo estaremos desarrollando un ejemplo para almacenar datos de Prestamistas, Clientes y sus Relaciones en la base de datos orientada a grafos Neo4j y luego interactuar con estos datos desde una aplicación usando Spring Boot.
Bueno manos a la obra.
Primero creamos una base de datos en Neo4j , ejecutamos Start para iniciar la base de datos,importamos los datos de ejemplo: financial.cypher y le damos Open para usar Neo4j Browser para ejecutar y visualizar los datos. Si ejecutamos la siguiente consulta usando el lenguaje cypher:
MATCH (n)-[r]-() RETURN n,r;
Obtenemos una visualización del grafo a partir de los datos importados similar a la siguiente:
Hasta esto momento tenemos los datos listos para interactuar desde nuestra aplicación, la cual construiremos en Java usando Spring Boot.
Dependencias necesarias:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-neo4j</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
Lo primero será crear los modelos de nuestra aplicación:
@NodeEntity @Data @NoArgsConstructor public class Lender { @Id @GeneratedValue private Long id; private String name; private Double interestRate; @Relationship(type = "LOAN") private List<LoanAmount> clients = new ArrayList<LoanAmount>(); }
Con la anotación @NodeEntity indicamos que el modelo Lender es un nodo del grafo que tiene como atributo name e interestRate. Más adelante explicaremos el atributo List<LoanAmount> clients.
Contamos además con el modelo Client:
@NodeEntity @Data @NoArgsConstructor public class Client { @Id @GeneratedValue private Long id; private String name; private Double totalCredit; private Double totalDebit; @Relationship(type = "PROVIDER") private List<Client> clients = new ArrayList<Client>(); }
Con atributos name,totalCredit y totalDebit, además una lista de clientes, para este caso usamos @Relationship(type = “PROVIDER”) para indicar que un cliente puede estar relacionado con otros clientes y esta relación esta registrada en la base de datos como PROVIDER, en otras palabras un cliente puede ser proveedor de otros clientes. Recuerdan que en el modelo Lender teníamos un caso similar:
@Relationship(type = "LOAN") private List<LoanAmount> clients = new ArrayList<LoanAmount>();
En este caso indicamos que un Lender se puede estar relacionados con un grupo de clientes con una relación llamada LOAN, para este caso esta relación tiene un atributo amount para indicar la cantidad del préstamo en sí, para este caso creamos modelo que represente la relación y sus propiedades.
@RelationshipEntity(type = "LOAN") @Getter @Setter @NoArgsConstructor public class LoanAmount { @Id @GeneratedValue private Long relationshipId; @Property private Double amount; @StartNode private Lender lender; @EndNode private Client client; @Override public String toString() { // TODO Auto-generated method stub return "-[:LOAN {amount:"+amount+"}]-> "+client.getName()+")"; } }
@RelationshipEntity indica que este modelo corresponde a una relación entre nodos,como se muestra el modelo además del atributo amount, se indica el nodo inicial y el nodo final, con el uso de @StartNode y @EndNode respectivamente.
Si revisamos el fichero financial.cypher podemos ver la manera que se insertaron los datos a partir de sentencias cypher como las siguientes:
Insertar un nuevo Lender:
CREATE (Lender1:Lender {name:'Lender1',interestRate:0.3});
Insertar un nuevo Client:
CREATE (Client1:Client{name:'Client1',totalCredit:12500,totalDebit:1780});
Insertar una relación entre Lender y Client:
CREATE (Lender1)-[:LOAN {amount:520}]->(Client1);
Insertar una relación entre Client y Client:
CREATE (Client1)-[:PROVIDER]->(Client3);
Ya que tenemos listos los modelos pasamos a crear los repositorios.
public interface LenderRepository extends Neo4jRepository<Lender, Long>{ List<Lender> findByInterestRateBetween(Double min,Double max); }
Nuestras interfaces heredan de Neo4jRepository<T,ID> que proporciona todos los métodos de repositorio deseados y permitiendo agregar otras operaciones si se requiere. Como es el caso que incluimos el método:
findByInterestRateBetween(Double min,Double max) para encontrar los Lenders con interestRate en un rango especificado por parámetro.
Para el caso del modelo Client también creamos un repositorio y agregamos operaciones usando @Query para indicar la implementación de la operación pero esta vez usando una query cypher:
public interface ClientRepository extends Neo4jRepository<Client, Long> { @Query("MATCH (client:Client) " + "WHERE (client.totalCredit - client.totalDebit) > $0 " + "RETURN client") List<Client> findByBalanceGreaterThan(Double balance); @Query("MATCH (client:Client)<-[r:LOAN]-(lender:Lender) " + "WHERE client.name=$0 " + "RETURN SUM(r.amount)") Double getTotalLoansAmount(String clientName); }
Para la primera operación el objetivo es listar todos los clientes con un balance superior al parámetro especificado.
Y la segunda operación el objetivo es calcular el monto total de los préstamos que tiene un cliente.
Nos queda entonces crear una clase que permita ejecutar estas operaciones y invocarla desde Nuestra clase principal.
@Component public class GraphDatabaseService { @Autowired LenderRepository lenderRepository; @Autowired ClientRepository clientRepository; public void runExample() { System.out.println("All Lenders with interest rate between 0.0 and 0.29"); lenderRepository.findByInterestRateBetween(0.0, 0.29).forEach(System.out::println); System.out.println("All Clients with balance amount greater than 1500 , Balance = client.totalCredit - client.totalDebit"); clientRepository.findByBalanceGreaterThan(1500.0).forEach(System.out::println); System.out.println("Total Loans amount Client: Client1"); System.out.println(clientRepository.getTotalLoansAmount("Client1")); }
@SpringBootApplication @EnableNeo4jRepositories("com.example.graphDatabase.repository") public class GraphDatabaseApplication implements CommandLineRunner{ @Autowired GraphDatabaseService graphDatabaseService; public static void main(String[] args) { SpringApplication.run(GraphDatabaseApplication.class, args); } @Override public void run(String... args) throws Exception { graphDatabaseService.runExample(); } }
Importante agregar @EnableNeo4jRepositories indicando donde están nuestras clases modelos e indicar la configuración en nuestro application.properties para poder acceder a la base de datos:
spring.data.neo4j.uri=bolt://localhost:7687 spring.data.neo4j.username=neo4j spring.data.neo4j.password=financial
De esta manera podemos interactuar con datos almacenados en Neo4j desde nuestra aplicación, espero les sea útil, aquí puedes consultar la versión completa del código fuente utilizada en el artículo.
Hasta la próxima.