Développer avec Cassandra 2

Depuis l’arrivée de CQL3, et la marginalisation progressive de Thrift, certaines librairies 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.

Le driver Java

Les base 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:

session.execute(
        "insert into utilisateur (id, nom, prenom, date_naissance) values (?,?,?,?)",
        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:

    Statement statement = session
        .prepare("select id, nom, prenom from utilisateur where id = ?")
        .setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM)
        .bind(id);
    ResultSet resultSet = session.execute(statement);
    Row row = resultSet.one();
    Utilisateur utilisateur = new Utilisateur(
        row.getString("id"), row.getString("prenom"),
        row.getString("nom"), row.getDate("date_naissance"));

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

    Statement statement = session.prepare("select id, nom, prenom, date_naissance from utilisateur where id = :id")
            .setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM)
            .bind().setString("id", id);
    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):

    ResultSet resultSet = session.execute(
        select("id","nom","prenom").from("utilisateur").where(eq(id, id))
        .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és é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.

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:

    @Bean(destroyMethod = "close")
    public Cluster cluster() {
        return Cluster.builder()
                .addContactPoint("zencassandra1")
                .addContactPoint("zencassandra2")
                .withClusterName("zencluster")
                .build();
    }
    @Bean(destroyMethod = "close")
    public Session session() {
        return cluster().connect("zenmessage");
    }

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:

    <cassandra:cluster contact-points="zencassandra1,zencassandra2" />
    <cassandra:session keyspace-name="zenmessage" />

Spring Data Cassandra

A 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.

public interface UtilisateurRepository extends CassandraRepository<Utilisateur, String> {
}

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

    @Table
    public class Utilisateur {
        @PrimaryKey String id;
        String prenom;
        String nom;
        @Column("date_naissance") Date dateNaissance;

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

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

A 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.

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.

    @Entity(table="utilisateur")
    public class Utilisateur {
        @Id      String id;
        @Column  String prenom;
        @Column  String nom;
        @Column(name = "date_naissance") Date dateNaissance;
    persistenceManager.insert(utilisateur);
    List<Utilisateur> utilisateurs = persistenceManager.typedQuery(Utilisateur.class,
        select().from("utilisateur")).get();
    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.

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:

    @Table(name = "utilisateur")
    public class Utilisateur {
        @PartitionKey String id;
        String prenom;
        String nom;
        @Column(name = "date_naissance") Date dateNaissance;
    Mapper<Utilisateur> mapper = mappingManager.mapper(Utilisateur.class);
    Utilisateur utilisateur = mapper.get(id);
    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!

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.

    // Cassandra Unit
    EmbeddedCassandraServerHelper.startEmbeddedCassandra(
        "/cassandra.yaml", // Fichier de configuration Cassandra
        "target/cassandra" // Dossier contenant les données, logs...
        );
    // Achilles Embedded
    Session session = CassandraEmbeddedServerBuilder
            .withEntities(Utilisateur.class)
            .withClusterName("test_cluster")
            .cleanDataFilesAtStartup(true)
            .withKeyspaceName("test_ks")
            .withCQLPort(9042)
            .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

    @Rule
    public CassandraCQLUnit cassandra = new CassandraCQLUnit(
        new ClassPathCQLDataSet("zenmessage-data.cql", "zenmessage"),
        "/cassandra.yaml", "localhost", 9042);

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

@RunWith(SpringJUnit4ClassRunner.class)
@TestExecutionListeners(CassandraUnitTestExecutionListener.class)
@CassandraDataSet(value="zenmessage-data.cql")
@EmbeddedCassandra
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

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 a rien à envier aux bases de données relationnelles classiques.

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :