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.