Présentation de Spring Data Neo4j
Dans un précédant article je vous ai présenté la base de données Neo4j ainsi que son API standard. Vous avez pu remarquer qu’il n’est pas facile d’implémenter un dao sur cette base dans vos projets. Exemple : les nœuds et les relations ne sont pas typés. Mais la communauté Spring a sorti il y a plus d’an un autre module pour son framework Spring Data. Il va nous permettre, dans le même principe que JPA, de mapper nos entités avec la base de données Neo4j.
Présentation
Le mapping des entités ce fait grâce à des annotations spécifique à Neo4j. Si on reprend l’exemple de mon précédant article nous pouvons modéliser nos utilisateur avec un simple POJO annoté :
@NodeEntity public class User { @GraphId Long id; private String name; Set<User> friends = new HashSet<User>(); // Getter - Setter }
Ici l’annotation NodeEntity indique au framework que cette classe est une entité Neo4j. Chaque nœud doit avoir un id unique. Il est donc obligatoire, comme pour JPA d’annoter un attribut qui représentera cet id avec l’annotation @GraphId.
Grâce au template Neo4j fourni, nous allons tester l’insertion de nœud dans la base de données. Mais avant cela il faut configurer Spring pour lui indiquer ou sera stockés les fichiers de la base de données. Voici le fichier xml de configuration :
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:neo4j="http://www.springframework.org/schema/data/neo4j" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/data/neo4j http://www.springframework.org/schema/data/neo4j/spring-neo4j-2.0.xsd"> <neo4j:config storeDirectory="target/neo4j-db"/> </beans>
Maintenant nous pouvons écrire notre premier test :
@ContextConfiguration(locations = "classpath*:spring/socialContext.xml") @RunWith(SpringJUnit4ClassRunner.class) public class TestEntity { @Autowired private Neo4jTemplate template; @Rollback(false) @BeforeTransaction public void clearDatabase() { Neo4jHelper.cleanDb(template); } @Test @Transactional public void testPersistUser() { User martin = template.save(new User("Martin")); User retrieveMartin = template.findOne(martin.getId(), User.class); assertEquals(martin, retrieveMartin); } }
Une autre classe bien pratique qui s’appelle Neo4jHelper nous propose de supprimer le contenu de notre base de données. Dans l’exemple elle sert à supprimer le contenu de la base avant chaque transaction. D’ailleurs ces transactions sont obligatoires pour persister dans la base de données.
Pour récupérer facilement nos utilisateurs en fonction de leur nom par exemple, nous allons indexer l’attribut « name » avec l’annotation Index :
@Indexed private String name;
Et de même voici le test :
@Test @Transactional public void testIndex() { String name = "Martin"; User martin = template.save(new User(name)); GraphRepository<User> userRepository = template.repositoryFor(User.class); User retrieveMartin = userRepository.findByPropertyValue("name", name); assertEquals(martin, retrieveMartin); }
Ici on récupère un repository fourni par Spring qui propose des méthodes de type DAO pour manipuler nos entités. Et grâce à la méthode findByPropertyValue qui prend en paramètre le nom de l’attribut annoté par un index et la valeur à rechercher, nous pouvons récupérer facilement nos objets.
Maintenant nous allons mettre en place les relations entre nos utilisateurs. Elles sont représentées par une collection d’utilisateurs. Il existe deux manières de déclarer cette collection : si vous en voulez une en lecture seule alors il faut utiliser un objet de type Iterable ou sinon il faut la déclarer en tant qu’objet de type Set. Ensuite il suffit de l’annoter avec @RelatedTo qui prend en option le type de la relation, le type de la classe ciblée, et la direction. Voici l’exemple :
@RelatedTo(type = "Friend", direction = Direction.OUTGOING) Set<User> friends = new HashSet<User>();
Et son test associé :
@Test @Transactional public void testRelationship() { User martin = template.save(new User("Martin")); User simon = template.save(new User("Simon")); User pierre = template.save(new User("Pierre")); martin.addFriend(simon); martin.addFriend(pierre); GraphRepository<User> userRepository = template.repositoryFor(User.class); User retrieveMartin = userRepository.findOne(martin.getId()); Set<User> martinFriends = template.fetch(retrieveMartin.getFriends()); for (User friends : martinFriends) { assertThat(friends, Is.is(AnyOf.anyOf(equalTo(simon), equalTo(pierre)))); } }
Par default, comme pour JPA, la collection n’est pas chargée lors de la récupération puisque c’est du mode Lazy. C’est pour ça qu’il faut la charger avec le template.
Le repository GraphRepository nous fournit pas mal de méthodes pour manipuler nos entités, mais nous allons l’étendre, ou plutôt le compléter en lui ajoutant d’autres méthodes. Par exemple on pourrait ajouter le service de mon précédant article, qui permettait à l’utilisateur de savoir quels amis ont au moins deux amis en commun avec lui.
Pour rajouter ce service nous allons déclarer une interface qui contiendra sa signature avec une autre méthode qui nous facilitera l’ajout d’utilisateur :
public interface MySocialRepository { @Transactional User addUser(String name); Iterable<User> findCommonsFriend(User user, int nbCommonsFriend); }
Ensuite nous allons créer une autre interface qui représentera nos services personnalisés avec les services de base du framework :
public interface SocialRepository extends MySocialRepository, GraphRepository<User> { }
Après implémentons nos services (Attention vous devez respecter le pattern nom_de_l’interface + impl au niveau du nom de votre classe sinon l’injection ne fonctionne pas):
public class SocialRepositoryImpl implements MySocialRepository { @Autowired private SocialRepository repository; @Override @Transactional public User addUser(String name) { User user = findUserByName(name); if (user == null) { user = new User(name); repository.save(user); } return user; } @Override public Iterable<User> findCommonsFriend(User user, int nbCommonsFriend) { TraversalDescription description = Traversal.description().evaluator(new Evaluator() { @Override public Evaluation evaluate(Path path) { if (path.length() == 0) { return Evaluation.EXCLUDE_AND_CONTINUE; } Node node = path.endNode(); Iterable<Relationship> relationships = node.getRelationships(withName(RelationshipTypes.FRIEND), Direction.OUTGOING); int i = 0; for (Relationship relationship : relationships) { i++; } if (i >= nbCommonsFriend) { return Evaluation.INCLUDE_AND_CONTINUE; } else { return Evaluation.EXCLUDE_AND_CONTINUE; } } }).relationships(withName(RelationshipTypes.FRIEND), Direction.OUTGOING); return repository.findAllByTraversal(user, description); } }
La méthode findAllByTraversal prend en paramètre le nœud de départ et une description de la traversée. Dans cette description je créé un Evaluator qui indique si l’on inclut ou non le nœud traversé, et je renseigne le type et la direction des relations à traverser. Puis dans cet Evaluator je teste si le nœud qui en train d’être traversé possèdent un nombre de relations supérieures ou égales au nombre passé en paramètre de mon service. Pour finir il ne faut pas oublier d’exclure le premier nœud qui correspond au départ, plus précisément à l’utilisateur passé en paramètre de notre service.
Et pour finir voici les tests de notre repository :
@ContextConfiguration(locations = "classpath:spring/socialContext.xml") @RunWith(SpringJUnit4ClassRunner.class) @Transactional public class TestRepository { @Autowired private SocialRepository repository; @Autowired private Neo4jTemplate template; @Rollback(false) @BeforeTransaction public void clearDatabase() { Neo4jHelper.cleanDb(template); } @Test public void testUserCreation() { User user = repository.save(new User("Humain")); User userRetrieved = repository.findOne(user.getId()); assertEquals(user.getName(), userRetrieved.getName()); } @Test public void findCommonsFriend() { repository.makeSomeUser(); User rootUser = repository.findUserByName("Martin"); for (User user : repository.findCommonsFriend(rootUser, 2)) { assertThat(user.getName(), Is.is(AnyOf.anyOf(StringContains.containsString("Matthieu"), StringContains.containsString("Romain")))); } } }
On peut remarquer que grâce à l’Autowired notre interface SocialRepository nous fournit nos services plus ceux qui sont par default. Le framework route automatiquement la bonne implémentation en fonction de la méthode appelée.
Conclusion
Le concept des bases de données graphe n’est pas facile à prendre en main, surtout pour quelqu’un qui a l’habitude de travailler avec une API comme JPA. Mais Spring Data Neo4j simplifie grandement cette approche grâce aux annotations et aux templates. D’ailleurs vous pouvez coupler l’API JPA avec Spring Data Neo4j pour stoker une partie de vos entités dans une base de données et l’autre dans Neo4j.
Si vous voulez approfondir ce framework, je vous conseille de lire ce livre gratuit disponible à cette adresse : http://spring.neo4j.org/guide, et je vous assure qu’il y a encore plein de fonctionnalités à découvrir…
Code source
Voici le code source de l’exemple avec les dépendances Maven : Example source code
Bonjour.
Exemple très sympa et abordable.
Par contre la partie repository en test unitaire ne passe pas dans mon cas.
“Unable to lock store C:…targetneo4j-dbneostore, this is usually a result of some other Neo4j kernel running using the same store.”
Si quelqu’un trouve la solution, qu’il la poste en commentaire pour le bonheur de tous 🙂
Merci encore.