Les bases de données orientées graphe excellent dans la modélisation et l'interrogation de réseaux complexes. Contrairement aux systèmes relationnels traditionnels, elles stockent les relations au même niveau que les nœuds, offrant des performances optimales pour les requêtes de parcours, telles que la recherche du chemin le plus court. Neo4j est l'une des solutions les plus abouties dans ce domaine. Bien qu'il existe des alternatives comme Nebula Graph ou ArangoDB, Neo4j reste une référence grâce à son langage de requête déclaratif, Cypher.
Déploiement de Neo4j avec Docker
Pour un environnement de développement ou de test, l'édition communautaire de Neo4j déployée via Docker est une approche rapide et efficace. Voici une configuraton docker-compose.yml structurée pour isoler les données et configurer les ressources mémoire :
version: '3.8'
services:
graph-database:
image: neo4j:5.15-community
container_name: neo4j-instance
restart: unless-stopped
ports:
- "7475:7474"
- "7688:7687"
environment:
- NEO4J_AUTH=neo4j/SecureP@ssw0rd!
- NEO4J_server_memory_heap_initial__size=256M
- NEO4J_server_memory_heap_max__size=512M
- NEO4J_server_memory_pagecache_size=256M
- NEO4J_server_directories_import=/data/import
volumes:
- ./neo4j_data:/data
- ./neo4j_logs:/logs
- ./neo4j_plugins:/plugins
Exécutez la commande docker compose up -d pour initialiser le conteneur. L'interface d'administration web sera ensuite accessible via http://localhost:7475. Vous pouvez vérifier la version du moteur en exécutant la requête Cypher suivante dans le navigateur intégré :
CALL dbms.components() YIELD name, versions, edition
RETURN name, versions, edition
Modélisation du graphe et données d'exemple
Pour démontrer les capacités d'interroagtion, nous allons modéliser un réseau reliant des développeurs à des projets logiciels. Les nœuds seront Developer et Software, reliés par des relations CONTRIBUTED_TO (incluant des rôles spécifiques) et ARCHITECTED.
Exécutez le script Cypher suivant pour peupler la base de données :
CREATE (linux:Software {title: 'Linux Kernel', released: 1991, description: 'Unix-like OS kernel'})
CREATE (python:Software {title: 'Python', released: 1991, description: 'High-level programming language'})
CREATE (java:Software {title: 'Java', released: 1995, description: 'Object-oriented language'})
CREATE (cpp:Software {title: 'C++', released: 1985, description: 'General-purpose programming language'})
CREATE (linus:Developer {name: 'Linus Torvalds', born: 1969})
CREATE (guido:Developer {name: 'Guido van Rossum', born: 1956})
CREATE (james:Developer {name: 'James Gosling', born: 1955})
CREATE (bjarne:Developer {name: 'Bjarne Stroustrup', born: 1950})
CREATE (andrew:Developer {name: 'Andrew Tanenbaum', born: 1944})
CREATE
(linus)-[:CONTRIBUTED_TO {roles: ['Creator', 'Lead Maintainer']}]->(linux),
(andrew)-[:CONTRIBUTED_TO {roles: ['Reviewer', 'Inspiration']}]->(linux),
(guido)-[:CONTRIBUTED_TO {roles: ['Creator', 'BDFL']}]->(python),
(james)-[:ARCHITECTED]->(java),
(james)-[:CONTRIBUTED_TO {roles: ['Original Author']}]->(java),
(bjarne)-[:ARCHITECTED]->(cpp),
(bjarne)-[:CONTRIBUTED_TO {roles: ['Designer']}]->(cpp);
Intégration Spring Boot avec Spring Data Neo4j
Depuis Spring Boot 2.4, Spring Data Neo4j (SDN) a été entièrement réécrit pour s'aligner sur les standards modernes de Spring Data. Nous utiliserons ici une approche basée sur SDN 6+ avec les annotations @Node et @RelationshipProperties.
Configuration des dépendances Maven (pom.xml) :
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>graph-integration</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Configuration de la connexion dans application.yml :
spring:
neo4j:
uri: bolt://localhost:7688
authentication:
username: neo4j
password: SecureP@ssw0rd!
Définition des entités
Les classes d'entité mappent directement les nœuds et les relations riches du graphe.
package com.example.graph.entity;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
@Node("Software")
public class Software {
@Id
private String title;
private Integer released;
private String description;
// Constructeurs, getters et setters omis pour la concision
}
package com.example.graph.entity;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Relationship;
import java.util.List;
@Node("Developer")
public class Developer {
@Id
private String name;
private Integer born;
@Relationship(type = "CONTRIBUTED_TO")
private List<Contribution> contributions;
@Relationship(type = "ARCHITECTED")
private List<Software> architectedProjects;
// Constructeurs, getters et setters omis pour la concision
}
package com.example.graph.entity;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
import java.util.List;
@RelationshipProperties
public class Contribution {
@Id
@GeneratedValue
private Long id;
private List<String> roles;
@TargetNode
private Software software;
// Constructeurs, getters et setters omis pour la concision
}
Interfaces Repository
Les repositories permettent d'effectuer des opérations CRUD standards ainsi que des requêtes Cypher personnalisées.
package com.example.graph.repository;
import com.example.graph.entity.Software;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface SoftwareRepository extends Neo4jRepository<Software, String> {
@Query("MATCH (d:Developer {name: $devName})-[:CONTRIBUTED_TO]->(s:Software) RETURN s")
List<Software> findSoftwareByContributor(String devName);
}
package com.example.graph.repository;
import com.example.graph.entity.Developer;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface DeveloperRepository extends Neo4jRepository<Developer, String> {
@Query("MATCH (d:Developer)-[:ARCHITECTED]->(s:Software {title: $softTitle}) RETURN d")
List<Developer> findArchitectsBySoftware(String softTitle);
}
Tests d'intégration
La classe de test suivante valide le chargement des entités et l'exécution des requêtes personnalisées.
package com.example.graph;
import com.example.graph.entity.Developer;
import com.example.graph.entity.Software;
import com.example.graph.repository.DeveloperRepository;
import com.example.graph.repository.SoftwareRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class GraphIntegrationTest {
@Autowired
private DeveloperRepository developerRepository;
@Autowired
private SoftwareRepository softwareRepository;
@Test
public void testFindSoftwareByContributor() {
List<Software> projects = softwareRepository.findSoftwareByContributor("James Gosling");
projects.forEach(software ->
System.out.println("Projet contribué : " + software.getTitle())
);
}
@Test
public void testFindArchitects() {
List<Developer> architects = developerRepository.findArchitectsBySoftware("C++");
architects.forEach(dev ->
System.out.println("Architecte : " + dev.getName())
);
}
@Test
public void testCreateNewNode() {
Developer newDev = new Developer();
newDev.setName("Ada Lovelace");
newDev.setBorn(1815);
developerRepository.save(newDev);
System.out.println("Nouveau développeur enregistré.");
}
}