Un guide de Cassandra avec Java

1. Vue d'ensemble

Ce didacticiel est un guide d'introduction à la base de données Apache Cassandra utilisant Java.

Vous trouverez des explications sur les concepts clés, ainsi qu'un exemple fonctionnel qui couvre les étapes de base pour se connecter et commencer à travailler avec cette base de données NoSQL de Java.

2. Cassandra

Cassandra est une base de données NoSQL évolutive qui offre une disponibilité continue sans point de défaillance unique et permet de gérer de grandes quantités de données avec des performances exceptionnelles.

Cette base de données utilise une conception en anneau au lieu d'utiliser une architecture maître-esclave. Dans la conception en anneau, il n'y a pas de nœud maître - tous les nœuds participants sont identiques et communiquent entre eux en tant que pairs.

Cela fait de Cassandra un système évolutif horizontalement en permettant l'ajout incrémentiel de nœuds sans avoir besoin de reconfiguration.

2.1. Concepts clés

Commençons par un bref aperçu de certains des concepts clés de Cassandra:

  • Cluster - un ensemble de nœuds ou de centres de données disposés dans une architecture en anneau. Un nom doit être attribué à chaque cluster, qui sera ensuite utilisé par les nœuds participants
  • Keyspace - Si vous venez d'une base de données relationnelle, le schéma est l'espace de clés respectif dans Cassandra. L'espace de clés est le conteneur le plus externe pour les données dans Cassandra. Les principaux attributs à définir par espace de clé sont le facteur de réplication , la stratégie de placement de réplique et les familles de colonnes.
  • Famille de colonnes - Les familles de colonnes dans Cassandra sont comme des tables dans les bases de données relationnelles. Chaque famille de colonnes contient une collection de lignes qui sont représentées par une carte . La clé donne la possibilité d'accéder ensemble aux données associées
  • Colonne - Une colonne dans Cassandra est une structure de données qui contient un nom de colonne, une valeur et un horodatage. Les colonnes et le nombre de colonnes dans chaque ligne peuvent varier contrairement à une base de données relationnelle où les données sont bien structurées

3. Utilisation du client Java

3.1. Dépendance de Maven

Nous devons définir la dépendance Cassandra suivante dans le pom.xml , dont la dernière version peut être trouvée ici:

 com.datastax.cassandra cassandra-driver-core 3.1.0 

Afin de tester le code avec un serveur de base de données embarqué, nous devons également ajouter la dépendance cassandra-unit , dont la dernière version peut être trouvée ici:

 org.cassandraunit cassandra-unit 3.0.0.1 

3.2. Connexion à Cassandra

Pour nous connecter à Cassandra depuis Java, nous devons créer un objet Cluster .

Une adresse d'un nœud doit être fournie comme point de contact. Si nous ne fournissons pas de numéro de port, le port par défaut (9042) sera utilisé.

Ces paramètres permettent au pilote de découvrir la topologie actuelle d'un cluster.

public class CassandraConnector { private Cluster cluster; private Session session; public void connect(String node, Integer port) { Builder b = Cluster.builder().addContactPoint(node); if (port != null) { b.withPort(port); } cluster = b.build(); session = cluster.connect(); } public Session getSession() { return this.session; } public void close() { session.close(); cluster.close(); } }

3.3. Création du Keyspace

Créons notre espace de clés « bibliothèque »:

public void createKeyspace( String keyspaceName, String replicationStrategy, int replicationFactor) { StringBuilder sb = new StringBuilder("CREATE KEYSPACE IF NOT EXISTS ") .append(keyspaceName).append(" WITH replication = {") .append("'class':'").append(replicationStrategy) .append("','replication_factor':").append(replicationFactor) .append("};"); String query = sb.toString(); session.execute(query); }

À l'exception du keyspaceName, nous devons définir deux autres paramètres, le replicationFactor et le replicationStrategy . Ces paramètres déterminent le nombre de répliques et la façon dont les répliques seront distribuées sur l'anneau, respectivement.

Avec la réplication, Cassandra garantit la fiabilité et la tolérance aux pannes en stockant des copies de données sur plusieurs nœuds.

À ce stade, nous pouvons tester que notre espace de clés a été créé avec succès:

private KeyspaceRepository schemaRepository; private Session session; @Before public void connect() { CassandraConnector client = new CassandraConnector(); client.connect("127.0.0.1", 9142); this.session = client.getSession(); schemaRepository = new KeyspaceRepository(session); }
@Test public void whenCreatingAKeyspace_thenCreated() { String keyspaceName = "library"; schemaRepository.createKeyspace(keyspaceName, "SimpleStrategy", 1); ResultSet result = session.execute("SELECT * FROM system_schema.keyspaces;"); List matchedKeyspaces = result.all() .stream() .filter(r -> r.getString(0).equals(keyspaceName.toLowerCase())) .map(r -> r.getString(0)) .collect(Collectors.toList()); assertEquals(matchedKeyspaces.size(), 1); assertTrue(matchedKeyspaces.get(0).equals(keyspaceName.toLowerCase())); }

3.4. Création d'une famille de poteaux

Maintenant, nous pouvons ajouter les premiers «livres» de la famille de colonnes à l'espace de clés existant:

private static final String TABLE_NAME = "books"; private Session session; public void createTable() { StringBuilder sb = new StringBuilder("CREATE TABLE IF NOT EXISTS ") .append(TABLE_NAME).append("(") .append("id uuid PRIMARY KEY, ") .append("title text,") .append("subject text);"); String query = sb.toString(); session.execute(query); }

Le code pour tester que la famille de colonnes a été créée est fourni ci-dessous:

private BookRepository bookRepository; private Session session; @Before public void connect() { CassandraConnector client = new CassandraConnector(); client.connect("127.0.0.1", 9142); this.session = client.getSession(); bookRepository = new BookRepository(session); }
@Test public void whenCreatingATable_thenCreatedCorrectly() { bookRepository.createTable(); ResultSet result = session.execute( "SELECT * FROM " + KEYSPACE_NAME + ".books;"); List columnNames = result.getColumnDefinitions().asList().stream() .map(cl -> cl.getName()) .collect(Collectors.toList()); assertEquals(columnNames.size(), 3); assertTrue(columnNames.contains("id")); assertTrue(columnNames.contains("title")); assertTrue(columnNames.contains("subject")); }

3.5. Modification de la famille de colonnes

Un livre a également un éditeur, mais aucune colonne de ce type ne peut être trouvée dans le tableau créé. Nous pouvons utiliser le code suivant pour modifier la table et ajouter une nouvelle colonne:

public void alterTablebooks(String columnName, String columnType) { StringBuilder sb = new StringBuilder("ALTER TABLE ") .append(TABLE_NAME).append(" ADD ") .append(columnName).append(" ") .append(columnType).append(";"); String query = sb.toString(); session.execute(query); }

Assurons-nous que le nouvel éditeur de colonne a été ajouté:

@Test public void whenAlteringTable_thenAddedColumnExists() { bookRepository.createTable(); bookRepository.alterTablebooks("publisher", "text"); ResultSet result = session.execute( "SELECT * FROM " + KEYSPACE_NAME + "." + "books" + ";"); boolean columnExists = result.getColumnDefinitions().asList().stream() .anyMatch(cl -> cl.getName().equals("publisher")); assertTrue(columnExists); }

3.6. Insertion de données dans la famille de poteaux

Maintenant que la table books a été créée, nous sommes prêts à commencer à ajouter des données à la table:

public void insertbookByTitle(Book book) { StringBuilder sb = new StringBuilder("INSERT INTO ") .append(TABLE_NAME_BY_TITLE).append("(id, title) ") .append("VALUES (").append(book.getId()) .append(", '").append(book.getTitle()).append("');"); String query = sb.toString(); session.execute(query); }

Une nouvelle ligne a été ajoutée dans la table 'books', afin que nous puissions tester si la ligne existe:

@Test public void whenAddingANewBook_thenBookExists() { bookRepository.createTableBooksByTitle(); String title = "Effective Java"; Book book = new Book(UUIDs.timeBased(), title, "Programming"); bookRepository.insertbookByTitle(book); Book savedBook = bookRepository.selectByTitle(title); assertEquals(book.getTitle(), savedBook.getTitle()); }

Dans le code de test ci-dessus, nous avons utilisé une méthode différente pour créer une table nommée booksByTitle:

public void createTableBooksByTitle() { StringBuilder sb = new StringBuilder("CREATE TABLE IF NOT EXISTS ") .append("booksByTitle").append("(") .append("id uuid, ") .append("title text,") .append("PRIMARY KEY (title, id));"); String query = sb.toString(); session.execute(query); }

Dans Cassandra, l'une des meilleures pratiques consiste à utiliser un modèle de table par requête. Cela signifie que pour une requête différente, une table différente est nécessaire.

In our example, we have chosen to select a book by its title. In order to satisfy the selectByTitle query, we have created a table with a compound PRIMARY KEY using the columns, title and id. The column title is the partitioning key while the id column is the clustering key.

This way, many of the tables in your data model contain duplicate data. This is not a downside of this database. On the contrary, this practice optimizes the performance of the reads.

Let's see the data that are currently saved in our table:

public List selectAll() { StringBuilder sb = new StringBuilder("SELECT * FROM ").append(TABLE_NAME); String query = sb.toString(); ResultSet rs = session.execute(query); List books = new ArrayList(); rs.forEach(r -> { books.add(new Book( r.getUUID("id"), r.getString("title"), r.getString("subject"))); }); return books; }

A test for query returning expected results:

@Test public void whenSelectingAll_thenReturnAllRecords() { bookRepository.createTable(); Book book = new Book( UUIDs.timeBased(), "Effective Java", "Programming"); bookRepository.insertbook(book); book = new Book( UUIDs.timeBased(), "Clean Code", "Programming"); bookRepository.insertbook(book); List books = bookRepository.selectAll(); assertEquals(2, books.size()); assertTrue(books.stream().anyMatch(b -> b.getTitle() .equals("Effective Java"))); assertTrue(books.stream().anyMatch(b -> b.getTitle() .equals("Clean Code"))); }

Everything is fine till now, but one thing has to be realized. We started working with table books, but in the meantime, in order to satisfy the select query by title column, we had to create another table named booksByTitle.

The two tables are identical containing duplicated columns, but we have only inserted data in the booksByTitle table. As a consequence, data in two tables is currently inconsistent.

We can solve this using a batch query, which comprises two insert statements, one for each table. A batch query executes multiple DML statements as a single operation.

An example of such query is provided:

public void insertBookBatch(Book book) { StringBuilder sb = new StringBuilder("BEGIN BATCH ") .append("INSERT INTO ").append(TABLE_NAME) .append("(id, title, subject) ") .append("VALUES (").append(book.getId()).append(", '") .append(book.getTitle()).append("', '") .append(book.getSubject()).append("');") .append("INSERT INTO ") .append(TABLE_NAME_BY_TITLE).append("(id, title) ") .append("VALUES (").append(book.getId()).append(", '") .append(book.getTitle()).append("');") .append("APPLY BATCH;"); String query = sb.toString(); session.execute(query); }

Again we test the batch query results like so:

@Test public void whenAddingANewBookBatch_ThenBookAddedInAllTables() { bookRepository.createTable(); bookRepository.createTableBooksByTitle(); String title = "Effective Java"; Book book = new Book(UUIDs.timeBased(), title, "Programming"); bookRepository.insertBookBatch(book); List books = bookRepository.selectAll(); assertEquals(1, books.size()); assertTrue( books.stream().anyMatch( b -> b.getTitle().equals("Effective Java"))); List booksByTitle = bookRepository.selectAllBookByTitle(); assertEquals(1, booksByTitle.size()); assertTrue( booksByTitle.stream().anyMatch( b -> b.getTitle().equals("Effective Java"))); }

Remarque: à partir de la version 3.0, une nouvelle fonctionnalité appelée «Vues matérialisées» est disponible, que nous pouvons utiliser à la place des requêtes par lots . Un exemple bien documenté de «Vues matérialisées» est disponible ici.

3.7. Suppression de la famille de poteaux

Le code ci-dessous montre comment supprimer une table:

public void deleteTable() { StringBuilder sb = new StringBuilder("DROP TABLE IF EXISTS ").append(TABLE_NAME); String query = sb.toString(); session.execute(query); }

La sélection d'une table qui n'existe pas dans l'espace de clés entraîne une InvalidQueryException: livres de table non configurés :

@Test(expected = InvalidQueryException.class) public void whenDeletingATable_thenUnconfiguredTable() { bookRepository.createTable(); bookRepository.deleteTable("books"); session.execute("SELECT * FROM " + KEYSPACE_NAME + ".books;"); }

3.8. Suppression de l'espace clé

Enfin, supprimons l'espace de clés:

public void deleteKeyspace(String keyspaceName) { StringBuilder sb = new StringBuilder("DROP KEYSPACE ").append(keyspaceName); String query = sb.toString(); session.execute(query); }

Et testez que l'espace de clés a été supprimé:

@Test public void whenDeletingAKeyspace_thenDoesNotExist() { String keyspaceName = "library"; schemaRepository.deleteKeyspace(keyspaceName); ResultSet result = session.execute("SELECT * FROM system_schema.keyspaces;"); boolean isKeyspaceCreated = result.all().stream() .anyMatch(r -> r.getString(0).equals(keyspaceName.toLowerCase())); assertFalse(isKeyspaceCreated); }

4. Conclusion

Ce didacticiel a couvert les étapes de base de la connexion et de l'utilisation de la base de données Cassandra avec Java. Certains des concepts clés de cette base de données ont également été discutés afin de vous aider à démarrer.

L'implémentation complète de ce tutoriel se trouve dans le projet Github.