Un guide sur Cassandra avec Java

Un guide de Cassandra avec Java

1. Vue d'ensemble

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

Vous trouverez les concepts clés expliqués, ainsi qu'un exemple de travail décrivant les étapes de base pour se connecter et commencer à travailler avec cette base de données NoSQL à partir 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 noeud principal - tous les noeuds participants sont identiques et communiquent entre eux en tant qu'homologues.

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

2.1. Concepts clés

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

  • Cluster - une collection 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, alors 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és sont lesReplication Factor, lesReplica Placement Strategy et lesColumn Families

  • Column Family - 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 unMap<RowKey, SortedMap<ColumnKey, ColumnValue>>. La clé donne la possibilité d’accéder ensemble aux données connexes

  • Column - 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 par rapport à une base de données relationnelle où les données sont bien structurées.

3. Utilisation du client Java

3.1. Dépendance Maven

Nous devons définir la dépendance Cassandra suivante dans lespom.xml, dont la dernière version peut être trouvéehere:


    com.datastax.cassandra
    cassandra-driver-core
    3.1.0

Afin de tester le code avec un serveur de base de données intégré, nous devons également ajouter la dépendancecassandra-unit, dont la dernière version peut être trouvéehere:


    org.cassandraunit
    cassandra-unit
    3.0.0.1

3.2. Connexion à Cassandra

Afin de se connecter à Cassandra depuis Java, nous devons construire un objetCluster.

Une adresse d'un nœud doit être fournie en tant que 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 de l'espace clé

Créons notre espace de clés «library»:

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);
}

Sauf pour leskeyspaceName, nous devons définir deux autres paramètres, lesreplicationFactor et lesreplicationStrategy. Ces paramètres déterminent respectivement le nombre de répliques et leur répartition dans l’anneau.

Avec la réplication, Cassandra assure la fiabilité et la tolérance aux pannes en stockant des copies de données dans 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é 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 permettant de vérifier 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 la table créée. 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);
}

Vérifions que la nouvelle colonnepublisher a été ajoutée:

@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 tablebooks 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 vérifier 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éebooksByTitle:

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 à une table par requête. Cela signifie qu'une table différente est nécessaire pour une requête différente.

Dans notre exemple, nous avons choisi de sélectionner un livre par son titre. Afin de satisfaire la requêteselectByTitle, nous avons créé une table avec unPRIMARY KEY composé en utilisant les colonnes,title etid. La colonnetitle est la clé de partitionnement tandis que la colonneid est la clé de clustering.

De cette manière, de nombreuses tables de votre modèle de données contiennent des données en double. Ce n'est pas un inconvénient de cette base de données. Au contraire, cette pratique optimise les performances des lectures.

Voyons les données actuellement enregistrées dans notre tableau:

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;
}

Un test de requête renvoyant les résultats attendus:

@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")));
}

Tout va bien jusqu'à maintenant, mais une chose doit être réalisée. Nous avons commencé à travailler avec la tablebooks, mais en attendant, afin de satisfaire la requêteselect par la colonnetitle, nous avons dû créer une autre table nomméebooksByTitle.

Les deux tables sont identiques contenant des colonnes dupliquées, mais nous n'avons inséré que des données dans la tablebooksByTitle. En conséquence, les données de deux tableaux sont actuellement incohérentes.

Nous pouvons résoudre ce problème en utilisant une requêtebatch, qui comprend deux instructions d'insertion, une pour chaque table. Une requêtebatch exécute plusieurs instructions DML en une seule opération.

Un exemple d'une telle requête est fourni:

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);
}

Encore une fois, nous testons les résultats de la requête par lots comme suit:

@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: Depuis la version 3.0, une nouvelle fonctionnalité appelée «Vues matérialisées» est disponible, que nous pouvons utiliser à la place des requêtesbatch. Un exemple bien documenté de «Vues matérialisées» est disponiblehere.

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 unInvalidQueryException: unconfigured table books:

@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 vérifiez que l’espace clavier 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 tutoriel couvre 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 peut être trouvée dans lesGithub project.