Tutoriel sur le développement Java avec Cassandra 2

| Gérald Quintana (Zenika) | Zenika |

Depuis l'arrivée de CQL3, et la marginalisation progressive de Thrift, certaines bibliothèques comme Hector ou Astyanax autrefois célèbres, sont aujourd'hui démodées. Dans cet article, nous ferons le point sur les outils actuels pour développer en Java avec Cassandra et CQL3.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel sur le développement Java avec Cassandra 2

- Auteurs : Gérald Quintana (Zenika), Zenika

- Parution : 9 mars 2015

- Licence : Copyright ® 2015 Gérald Quintana. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Le driver Java

- II. Intégration avec Spring

- III. Spring Data Cassandra

- IV. Achilles

- V. Cassandra Java Mapper

- VI. CassandraUnit pour les tests automatisés

- VII. Conclusion

- VIII. Remerciements

I - Le driver Java

Les bases de données relationnelles ont leur driver JDBC, Cassandra a son driver Java. Certes il n'implémente pas JDBC, mais il s'en inspire fortement. L'API proposée est même plus moderne et mieux conçue que le JDBC :

1. session.execute(

2. "insert into utilisateur (id, nom, prenom, date_naissance) values (?,?,?,?)",

3. utilisateur.getId(), utilisateur.getNom(), utilisateur.getPrenom(), utilisateur.getDateNaissance());

La Session est l'équivalent d'une connexion, elle est associée à un keyspace et est thread-safe. On retrouve les notions de PreparedStatement et de ResultSet sauf qu'il n'est pas nécessaire de les fermer après usage :

1. Statement statement = session

2. .prepare("select id, nom, prenom from utilisateur where id = ?")

3. .setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM)

4. .bind(id);

5. ResultSet resultSet = session.execute(statement);

6. Row row = resultSet.one();

7. Utilisateur utilisateur = new Utilisateur(

8. row.getString("id"), row.getString("prenom"),

9. row.getString("nom"), row.getDate("date_naissance"));

On peut utiliser les PreparedStatement avec des paramètres nommés plutôt qu'indexés :

1. Statement statement = session.prepare("select id, nom, prenom, date_naissance from utilisateur where id = :id")

2. .setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM)

3. .bind().setString("id", id);

4. Row row = session.execute(statement).one();

On trouve le nécessaire pour construire les requêtes à la sauce « fluent » comme QueryDSL ou les Criteria JPA (le typage fort en moins) :

1. ResultSet resultSet = session.execute(

2. select("id","nom","prenom").from("utilisateur").where(eq(id, id))

3. .setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM));

Les requêtes peuvent être exécutées de manière asynchrone, le driver s'appuie sur les ListenableFuture de Guava, les adeptes d'architectures orientées événements (VertX par exemple) apprécieront.

Néanmoins, le driver en lui-même ne propose pas de mécanisme de conversion de la Row en objet.

II - Intégration avec Spring

Dans une application Spring, selon que l'on utilise la configuration XML traditionnelle ou la JavaConfig la manière de se connecter à Cassandra sera un peu différente.

Si la configuration Spring est exprimée en Java, l'utilisation du Driver Java suffit à configurer la connexion au cluster Cassandra :

1. @Bean(destroyMethod = "close")

2. public Cluster cluster() {

3. return Cluster.builder()

4. .addContactPoint("zencassandra1")

5. .addContactPoint("zencassandra2")

6. .withClusterName("zencluster")

7. .build();

8. }

10. @Bean(destroyMethod = "close")

11. public Session session() {

12. return cluster().connect("zenmessage");

13. }

Comme la Session est thread-safe, on peut en faire un singleton et l'injecter telle quelle dans les repositories.

Si la configuration Spring est exprimée en XML, il faudra aller chercher dans Spring Data Cassandra, les FactoryBean ou bien le namespace <cassandra:...> nécessaires à la configuration :

1. <cassandra:cluster contact-points="zencassandra1,zencassandra2" />

2. <cassandra:session keyspace-name="zenmessage" />

III - Spring Data Cassandra

À l'instar des autres membres du groupe Spring Data (JPA, MongoDB, etc.), Spring Data Cassandra permet de générer dynamiquement les repositories et d'automatiser les opérations de CRUD. Pour cela, Spring Data Cassandra s'appuie sur le driver Java.

1. public interface UtilisateurRepository extends CassandraRepository<Utilisateur, String> {

2. }

La classe Utilisateur se voit affublée d'annotations décrivant le mapping et permettant d'automatiser le mapping ligne/objet :

1. @Table

2. public class Utilisateur {

3. @PrimaryKey String id;

4. String prenom;

5. String nom;

6. @Column("date_naissance") Date dateNaissance;

Calqué sur le JdbcTemplate, Spring Data Cassandra fournit un CassandraTemplate, ou plutôt des CassandraTemplates puisqu'il y en a deux. Le CqlTemplate reste très proche du JdbcTemplate et apporte peu de chose par rapport au driver. Le CassandraTemplate apporte les fonctionnalités de conversion de ligne/objet basée sur annotations :

1. Utilisateur utilisateur = cassandraTemplate.selectOne(

2. select("id","nom","prenom","date_naissance").from("utilisateur").where(eq("id", id)).limit(1),Utilisateur.class);

À l'heure où j'écris ces lignes, Spring Data Cassandra n'est pas compatible avec les drivers de la version 2.1, il faut se limiter à la version 2.0.

IV - Achilles

Achilles s'inspire fortement de JPA pour proposer un outil de mapping Objet. Comme JPA, il est capable de créer les tables (DDL), de marquer les changements sur les entités (dirty checking), de s'intégrer avec Bean Validation… Les possibilités de mapping sont aussi nettement supérieures à celles de Spring Data.

1. @Entity(table="utilisateur")

2. public class Utilisateur {

3. @Id      String id;

4. @Column  String prenom;

5. @Column  String nom;

6. @Column(name = "date_naissance") Date dateNaissance;

8. persistenceManager.insert(utilisateur);

10. List<Utilisateur> utilisateurs = persistenceManager.typedQuery(Utilisateur.class,

11. select().from("utilisateur")).get();

13. Utilisateur utilisateur = persistenceManager.find(Utilisateur.class, id);

Contrairement à JPA, Achilles est stateless : les entités ne sont jamais attachées, il n'y a pas de cache de premier niveau. Pour prototyper une modélisation et la valider rapidement, c'est un outil très efficace : on pose quelques annotations sur des POJO et les tables se créent, s'alimentent, et se requêtent les yeux fermés.

V - Cassandra Java Mapper

Depuis la toute récente version 2.1 de Cassandra, DataStax fournit une extension au driver Java, le mapper. Lui aussi utilise des annotations pour décrire le mapping objet :

1. @Table(name = "utilisateur")

2. public class Utilisateur {

3. @PartitionKey String id;

4. String prenom;

5. String nom;

6. @Column(name = "date_naissance") Date dateNaissance;

8. Mapper<Utilisateur> mapper = mappingManager.mapper(Utilisateur.class);

10. Utilisateur utilisateur = mapper.get(id);

12. List<Utilisateur> utilisateurs = mapper.map(session.execute("select id, nom, prenom, date_naissance from utilisateur")).all();

Le mapping ligne/objet, c'était justement ce qu'il manquait au driver de base. Voilà une lacune comblée !

VI - CassandraUnit pour les tests automatisés

Pour tester une couche d'accès aux données Cassandra (DAO), il est très pratique de pouvoir démarrer un Cassandra embarqué : Achilles et Cassandra Unit permettent ça.

1. // Cassandra Unit

2. EmbeddedCassandraServerHelper.startEmbeddedCassandra(

3. "/cassandra.yaml", // Fichier de configuration Cassandra

4. "target/cassandra" // Dossier contenant les données, logs...

5. );

1. // Achilles Embedded

2. Session session = CassandraEmbeddedServerBuilder

3. .withEntities(Utilisateur.class)

4. .withClusterName("test_cluster")

5. .cleanDataFilesAtStartup(true)

6. .withKeyspaceName("test_ks")

7. .withCQLPort(9042)

8. .buildNativeSessionOnly();

En pratique, le temps de démarrage d'un Cassandra est plus long et gourmand qu'une base de données du style H2DB, l'utilisation d'un Cassandra embarqué est donc discutable. La variante Achilles Embedded configure Cassandra avec des valeurs par défaut convenables, ce qui évite d'avoir un fichier de configuration cassandra.yaml.

Pour charger des jeux de données dans les tables, CassandraUnit imite DBUnit. Les DataSets seront toutefois exprimés sous forme de scripts CQL. Pour charger ces DataSet et faire le nettoyage, il y a une Rule JUnit.

1. @Rule

2. public CassandraCQLUnit cassandra = new CassandraCQLUnit(

3. new ClassPathCQLDataSet("zenmessage-data.cql", "zenmessage"),

4. "/cassandra.yaml", "localhost", 9042);

Et, luxe ultime, avec le SpringJUnit4ClassRunner, il y a un TestExecutionListener et des annotations.

1. @RunWith(SpringJUnit4ClassRunner.class)

2. @TestExecutionListeners(CassandraUnitTestExecutionListener.class)

3. @CassandraDataSet(value="zenmessage-data.cql")

4. @EmbeddedCassandra

5. public class UtilisateurRepositoryTest {

On regrettera peut-être de ne pas pouvoir utiliser des jeux de données dans des formats plus descriptifs comme XML, CSV… En réalité, la fonctionnalité existe, mais s'appuie sur les API Thrift et sur Hector. Cela impose donc de connaître le modèle de données sous forme de ColumnFamily....

VII - Conclusion

Pour lire/écrire des objets depuis/dans Cassandra, le développeur Java a l'embarras du choix. Le driver Java et le mapper associé fournis par DataStax sont remarquablement agréables à utiliser. Quant aux tests automatisés, avec Cassandra Unit, on n'a rien à envier aux bases de données relationnelles classiques.

VIII - Remerciements

Cet article a été publié avec l'aimable autorisation de Zenika, experts en technologies Open Source et méthodes Agiles.

Nous tenons à remercier Claude Leloup pour la relecture orthographique et Mickaël Baron pour la mise au gabarit.