Vers des API haut niveau pour Java et NoSQL avec Spring Data

A l'heure où les nouvelles technologies de stockage de données regroupées sous les termes NoSQL et Distributed Data Grid deviennent populaires, il est intéressant de suivre l'évolution de cet écosystème et notamment des librairies d'intégration avec ces outils. Des librairies apportant un certain niveau d'abstraction émergent, avec l'espoir de voir apparaître des solutions de haut niveau comparables aux ORM que nous utilisons pour les bases relationnelles. Nous allons nous intéresser aujourd'hui au projet Spring Data, qui propose une certaine unification pour les accès aux bases de données dites NoSQL.

Spring Data

Spring Data est un projet en développement et lancé en 2010, visant à simplifier l'utilisation des bases NoSQL, des frameworks Map-Reduce, ou d'apporter des extensions pour le support des bases relationnelles. Le projet intègre aussi progressivement le framework Hades qui apporte la notion de repository pour une approche Domain Driven Developpment.

Spring Data regroupe déjà plusieurs sous-projets dédiés à des implémentations particulières :

  • JPA, qui apporte de nouvelles fonctionnalités pour une approche DDD avec des bases relationnelles,
  • Graph, avec une implémentation pour Neo4j,
  • Document, avec une implémentation pour MongoDB,
  • Key - Value, des implémentations pour Redis et Riak,
  • Extensions JDBC.

Dans l'ensemble, les projets existants consistent à apporter une couche d'abstraction facilitant la manipulation de données avec les différents systèmes de stockage. L'apport principal de ce projet est bien sûr de faciliter l'intégration de ces technologies à un applicatif Spring, et de profiter des facilités de configuration du framework : injection de beans, configuration xml et programmation orientée aspect.

Nous allons voir ce que peuvent apporter ces librairies par rapport à une utilisation directe des connecteurs existants. L'équipe prévoit également le support d'autres produits tels qu'Amazon S3, Cassandra ou Hadoop.

En parallèle, le projet Spring Data Mapping, actuellement hébergé côté Groovy, aurait pour objectif d'apporter une solution générique de mapping Objet-Base pour plusieurs bases NoSQL.

Spring Data JPA et Domain-Driven-Developpment

Une des vocations du projet Spring Data est d'apporter le support pour une approche Domain Driven Developpment (DDD) et l'utilisation du pattern Repository. Spring Data JPA en est la première implémentation, dédiée aux bases relationnelles. Le projet est directement repris depuis le framework Hades, dont les fonctionnalités sont progressivement intégrées au socle commun de Spring Data, et doit permettre à terme une approche DDD pour les différents types de stockage supportés.

Génération de Repository

Le développement d'un Repository est facilité en générant automatiquement les fonctions de CRUD les plus communes d'un service d'accès aux données :

  • save
  • findById
  • findAll (avec gestion de résultats paginés)
  • count
  • delete
  • exists

L'utilisation de l'API est plutôt simple, il suffit de définir son repository via une interface étendant l'interface JpaRepository de base, et le framework se chargera du reste.

public interface PersonRepository extends JpaRepository<Person, Long> { … }

De plus, l'API permet, comme cela se fait en Groovy et Rails, de générer les méthodes de requêtes à partir du nom de la méthode ou en utilisant une requête nommée (@NamedQuery). Par exemple, si on souhaite ajouter une méthode de recherche par nom à notre PersonRepository :

public interface PersonRepository extends JpaRepository<Person, Long> {
 List<Person> findByLastname(String lastname);
}

Le bean injecté par Spring comportera alors une méthode générée avec l'exécution de la requête correspondante : "SELECT * FROM User WHERE lastname = ?".

La librairie permet principalement de réduire le volume de code à écrire lorsque l'on code un Repository, mais Spring Data JPA apporte d'autres fonctionnalités intéressantes:

  • L'approche DDD est facilitée grâce aux supports des Specifications, qui utilisent des closures pour définir des prédicats, tout en utilisant l'API Criteria de JPA2.
public static Specification isPersonMinor() {
	return new Specification() {
		Predicate toPredicate(Root root, CriteriaQuery<!--?--> query,
			CriteriaBuilder builder) {
			LocalDate date = new LocalDate().minusYears(18);
			return builder.lessThan(root.get(Person_.birthDate), date);
		}
	};
}
  • Ajout de fonctions pour la gestion des transactions au niveau Repository
  • Entités auditables (suivi des dates et utilisateurs de création et modification). Il s'agit d'un sujet classique en entreprise lorsque le suivi des modifications de données est stratégique. Il sera intéressant de comparer cette fonctionnalité à son équivalent Hibernate Envers.
  • Support de la librairie Querydsl.

Spring Data Graph

Ce projet est dédié aux bases proposant une implémentation de type graphe et fournit une implémentation pour Neo4j. Neo4j est une base de données graphe, qui répond également aux enjeux traditionnels d'une base de données (ACIDité des transactions, gestion des accès concurrents,rollback de transaction, haute disponibilité). Pour avoir un rapide tour d'horizon de ce qu'apporte une base de données graphe et plus particulièrement Neo4j, voir ici.

Spring Data Graph propose une véritable couche d'abstraction permettant un mapping entre un modèle objet utilisé dans l'application Java et les entités stockées physiquement dans la base graphe. Spring Data Graph définit donc les notions suivantes pour la modélisation au niveau des objets (les exemples proviennent de la documentation officielle, qui propose par ailleurs un exemple complet d'une application web utilisant les différents composants de Spring) :

  • Annotations pour définir noeuds (@NodeEntity) et relations (@RelationshipEntity)
  • Gestion des index (@Indexed)
@NodeEntity
class Movie {
 @Indexed
 int id;

 String title;
 int year;    

 @RelatedTo(elementClass = Actor.class, type = "ACTS_IN", direction = Direction.INCOMING)
 Set<Actor> cast;

 @RelatedToVia(elementClass = Role.class, type = "ACTS_IN", direction = Direction.INCOMING)
 Iterable<Role> roles;
}

@NodeEntity
class Actor {    

 @Indexed
 int id;
 String name;

 @RelatedTo(elementClass = Movie.class, type = "ACTS_IN")
 Set<Movie> cast; 

 public Role playedIn(Movie movie, String roleName) {
 Role role = relateTo(movie, Role.class, "ACTS_IN");
 role.setRole(roleName);       
 return role;
 }
}

@RelationshipEntity
class Role {
 @EndNode
 Movie movie;

 @StartNode
 Actor actor;

 String role;
}
  • Utilisation du pattern Repository, avec des implémentations pour gérer des opérations de CRUD, index et recherches; ainsi que les algorithmes de parcours de graphes.
GraphRepository<Person> graphRepository = graphRepositoryFactory.createGraphRepository(Person.class);

Spring Graph apporte également une couche d'abstraction sur des aspects plus techniques, entre autres:

  • Gérer des recherches:
    • en utilisant des algorithmes spécifiques de parcours de graphe (Traversal)
    • en utilisant le système de recherche indexée avec un moteur d'indexation séparé (Apache Lucene est inclus par défaut).

Pour cela, il suffit d'annoter le champ à indexer: @Indexed(fulltext = true, indexName = "search"), puis d'appeler la méthode findAllByQuery(indexedField, query))

  • Gérer les transactions avec la configuration Spring standard et SpringTransactionManager
  • Gestion de la persistance des entités avec la méthode persist()
  • Validation de beans (JSR 303) via les annotations de définition de contraintes (@Min, @Max, @Size, @Email, ...)

Définition d'entités cross-store

Un autre aspect très intéressant dans cette API est le support multi-base en définissant des entités partielles, qui permet d'utiliser plusieurs systèmes de stockage pour une même entité. Actuellement, il est possible de définir une entité dont une partie des champs sera géré via JPA dans une base relationnelle classique, tandis que l'autre partie est gérée dans une base de données graphe.

L'exemple suivant est une modélisation simpliste d'un système de vente dans lequel les articles et les ventes sont stockés dans une base relationnelle classique, hormis les liens entre les objets vendus ("Les acheteurs qui ont acheté ce produit ont également acheté...), ainsi que le total des ventes pour chaque article.

@Entity
@Table(name = "product")
@NodeEntity(partial = true)
class Product {
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "id")
	private Long id;

	private String name;
	private String description
	private Double price;

	@GraphProperty
	Double totalSales;

	@RelatedTo(elementClass = Product.class, type = "SAME_SALE", direction = Direction.INCOMING)
	private Set<Product> linkedSales;

	@RelatedToVia(elementClass = SaleConnection.class, type = "SAME_SALE", direction = Direction.INCOMING)
	private Iterable<SaleConnection> salesConnections;

	@OneToMany
	private Set<SoldItem> sales;

	@Transactional
	public SaleConnection soldTogether(Product item) {
		SaleConnection connection = relateTo(item, SaleConnection.class, "SAME_SALE");
		connection.incrementLinkWidth();
		return connection;
	}
}

@RelationshipEntity
class SaleConnection {
	@EndNode
	Product item1;

	@StartNode
	Product item2;

	int linkWidth = 0;

	public void incrementLinkWidth(){
		linkWidth++;
	}
}

@Entity
@Table(name = "sales")
class SoldItem {
	@ManyToOne
	Product productReference;

	Double soldPrice;
}

Spring Data Document

Ce projet est dédié aux bases proposant une implémentation de type document, et fournit pour l'instant une implémentation pour MongoDB.

L'implémentation apporte une surcouche au client Java fourni par Mongo et en reproduit l'essentiel des fonctionnalités en s'intégrant au framework Spring, entre autres :

  • Configuration Spring via XML ou classes @Configuration pour les instances Mongo et la gestion des replica set

  • MongoTemplate qui offre des mécanismes d'abstraction pour interagir avec MongoDB et pour gérer la persistance des objets Java avec MongoDB de manière transparente, la gestion des collections, exécuter des commandes shell internes.

  • Fonctions de recherche avec notamment :

    • possibilité d'utiliser des Criteria

    • support des requêtes géographiques

    • gestion d'index

Par ailleurs, la librairie apporte le support du pattern Repository de manière similaire à Spring Data JPA  avec MongoRepository:

  • Génération des fonctions de CRUD
  • Gestion de requêtes prédéfinies au format JSON, par exemple:
public interface PersonRepository extends MongoRepository<Person, String>
  @Query("{ 'firstname' : ?0 }")
  List<Person> findByThePersonsFirstname(String firstname);
}
  • Support de Querydsl

Enfin, la dernière version fraichement sortie de l'API propose d'une part un système de mapping à base d'annotations, avec notamment la possibilité de gérer les collections au sein d'un document par référence ou non:

@Document
public class Account {
  @Id
  private ObjectId id;
  private Float total;
}

@Document
public class Person {
  @Id
  private ObjectId id;
  @Indexed
  private Integer ssn;
  @DBRef
  private List<Account> accounts;
}

D'autre part, cette dernière release apporte comme l'API Graph un support cross-store persistence, permettant d'utiliser conjointement plusieurs systèmes de stockage de données.

Cette librairie semble plutôt aboutie et vient sans doute concurrencer sérieusement les librairies existantes comme Morphia, qui propose également des fonctions de mapping notamment.

Spring Data Key-Value

Le projet Key-Value (SDKV) est dédié aux bases proposant une implémentation clé-valeurs, et fournit des implémentations pour Redis et Riak.

Il s'agit pour l'instant essentiellement d'une couche d'abstraction au dessus des librairies de bas niveau existantes, qui permet principalement de tirer avantage d'une configuration Spring. Ces deux librairies semblent encore à un niveau de  maturité assez faible et n'apportent pour l'instant pas beaucoup par rapport à une utilisation directe des connecteurs.

Redis

L'implémentation SDKV pour Redis est une surcouche aux connecteurs Jedis, JRedis et RJC, qui apporte notamment:

  • Configuration simple via XML, par exemple :
<bean id="jedisConnectionFactory" class="org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory">
	<property name="hostName" value="localhost"/>
	<property name="port" value="6379"/>
	<property name="timeout" value="5000"/>
	<property name="pooling" value="true"/>
</bean>

<bean id="redisTemplate" class="org.springframework.data.keyvalue.redis.core.RedisTemplate"
	p:connection-factory-ref="jedisConnectionFactory"/>
  • Utilisation d'objets plutôt que de dialoguer directement avec Redis grâce à la classe RedisTemplate
    • classes correspondant aux opérations de Redis (Value, List, Set, ZSet, Hash et Bound*)
    • fonctions de messaging (Send/Publish/Subscribe)
  • Possibilité d'utiliser différents mécanismes de sérialisation pour stocker des objets

Plusieurs articles on déjà été publiés sur le sujet:

Riak

Là aussi, SDKV propose une surcouche au client Java développé par Basho (la société derrière Riak). Une base de données Riak est nativement accédée via une API REST/HTTP, utilisant les drivers Apache Commons.

La librairie apporte notamment :

  • Configuration simple par XML, par exemple:
<bean id="riakTemplate"
      p:defaultUri="http://localhost:8098/riak/{bucket}/{key}"
      p:mapReduceUri="http://localhost:8098/mapred"/>
  • Utilisation d'objets plutôt que de dialoguer directement avec Riak grâce à la classe RiakTemplate
    • pour les fonctions d'accès et de stockage
    • l'utilisation de l'algorithme Map/Reduce
    • les fonction de liaison entre les entrées et de parcours des listes liées
  • Gestion asynchrone des échanges avec la base de données avec des callbacks
  • Utilisation de services personnalisés de conversion d'objets vers Riak ou de ClassLoader depuis Riak

Spring Data JDBC Extensions

Cette partie de Spring Data se situe un peu en marge du reste car il s'agit ici de fournir des extensions de configuration spécifiques à certaines implémentations de bases de données relationnelles.

La première version du projet comporte des extensions pour les bases de données Oracle, qui constituent un portage du code d'un module auparavant payant de Spring, le Advanced Pack for Oracle Database. Cette extension doit notamment permettre un support transparent pour les applications de fonctionnalités telles que le Fast Connection Failover d'Oracle RAC, Advanced Queuing, et des types de données complexes (XML, STRUCT, ARRAY).

Vers une abstraction de plus haut niveau?

Spring Data Mapping

Ce dernier projet plutôt ambitieux vise à fournir un framework mutualisé de mapping objet pour les différents implémentations de stockage de données. C’est finalement une surcouche aux projets précédents qui cherche, j’imagine, à décorréler la modélisation du stockage physique. A l'heure actuelle, le projet est simplement mentionné dans la page du projet Spring Data, il n'y a pas de version SNAPSHOT disponible. Le projet est pour l'instant en Groovy, mais devrait être migré en Java pour s'intégrer à Spring Data.

D'après ce qu'on peut voir des sources existantes, il serait bien possible de définir un mapping d'objets de manière relativement  indépendante de la base de données : on trouve un projet core, et des implémentations pour de nombreuses technologies: Gemfire, Hibernate, AppEngine, Redis, Mongo, Cassandra et d'autres...

Hibernate OGM

Hibernate OGM est un projet démarré en juillet 2010 et mené par Emmanuel Bernard chez JBoss. Le projet a été repris d'un essai d'une implémentation JPA-like pour Infinispan, le moteur de grille de données open source de JBoss. A l'heure actuelle, le projet semble être à un stade plutôt expérimental et bien moins avancé que Spring Data, mais il sera intéressant de suivre son évolution et l'orientation qui sera suivie, car les deux projets seront sans doute concurrents.

Concrètement, Hibernate OGM souhaite utiliser Hibernate Core pour stocker des données dans une grille de données distribuée, en l'occurrence Infinispan. L'objectif est de réutiliser l'implémentation existante de JPA plutôt que de repartir de zéro, pour mapper des entités et des relations.

Pour conclure

Pour l'instant, en dehors de l'API Graph, Spring Data est en cours de développement et il n'est donc pas encore question d'utiliser ces librairies en production. Néanmoins, l'avancement actuel des différentes librairies laisse déjà présager de plusieurs atouts à leur utilisation :

  • intégration au framework Spring et apport de ses facilités de configuration.
  • abstraction par rapport aux implémentations des différentes solutions de stockage NoSQL et apport de surcouches aux connecteurs, notamment grâce aux systèmes de mapping par annotations. Il y a encore du travail sur ce point pour offrir toutes les fonctionnalités de chaque base de données et y gagner réellement par rapport à l'utilisation d'une API de plus bas niveau.
  • développement facilité dans une approche orientée Domain-Driven-Developpment.

Mais la perspective la plus intéressante est l'émergence de solutions unifiées de mapping objet/base permettant de limiter les spécificités à chaque base. Spring Data et Hibernate OGM ont un potentiel intéressant car ils ouvrent la voie vers une abstraction de plus haut niveau pour des implémentations et surtout des concepts de stockage de données très divers.

En s'attaquant à ces problématiques de mutualisation, ces projets attaquent la question complexe des frontières entre ces différents concepts :

  • Peut-on espérer avoir une abstraction suffisante pour passer d'une manière transparente d'une implémentation relationnelle à certains modes de stockage NoSQL?

Spring Data tente d'adresser ce problème avec le socle commun du framework et le projet Spring Data Mapping, de même qu'Hibernate OGM en réutilisant Hibernate Core pour Infinispan. Mais il faudra être très prudent sur ce point et ne pas tomber dans le piège de vouloir faire du relationnel classique dans une base clé-valeur ou document par exemple, en risquant de perdre les avantages de ces représentations.

  • Dans quelle mesure peut-on outiller les développements pour utiliser conjointement et de manière transparente plusieurs systèmes de représentation des données?

Dans ce sens, Spring Data adresse directement cette frontière, avec la notion d'entités partielles pour l'API Graph : une même application peut être amené à utiliser plusieurs techniques de stockage conjointement, et nous avons besoin d'outils pour faciliter ces développements.

Finalement, les deux projets étudiés ici adressent un point essentiel : le mouvement NoSQL ne propose pas simplement de nouvelles techniques de stockage, mais vise surtout à adresser les solutions adaptées à chaque problème, en arguant le fait qu'il n'existe pas de solution unique parfaite.

L'écosystème autour de NoSQL et du stockage distribué est bien vivant, et surtout il est bien parti pour gagner en maturité et en accessibilité pour les développements Java. On suivra donc avec attention la sortie des premières versions définitives!