Een gids voor Cassandra met Java

1. Overzicht

Deze tutorial is een inleidende gids voor de Apache Cassandra-database met Java.

U vindt er uitleg over de belangrijkste concepten, samen met een werkvoorbeeld dat de basisstappen omvat om verbinding te maken met en aan de slag te gaan met deze NoSQL-database vanuit Java.

2. Cassandra

Cassandra is een schaalbare NoSQL-database die continue beschikbaarheid biedt zonder een enkel storingspunt en de mogelijkheid biedt om grote hoeveelheden gegevens met uitzonderlijke prestaties te verwerken.

Deze database gebruikt een ringontwerp in plaats van een master-slave-architectuur. In het ringontwerp is er geen masterknooppunt - alle deelnemende knooppunten zijn identiek en communiceren met elkaar als peers.

Dit maakt Cassandra een horizontaal schaalbaar systeem door de incrementele toevoeging van knooppunten mogelijk te maken zonder dat herconfiguratie nodig is.

2.1. Sleutelbegrippen

Laten we beginnen met een kort overzicht van enkele van de belangrijkste concepten van Cassandra:

  • TROS - een verzameling knooppunten of datacenters gerangschikt in een ringarchitectuur. Aan elk cluster moet een naam worden toegekend, die vervolgens door de deelnemende knooppunten wordt gebruikt
  • Sleutelruimte - Als u uit een relationele database komt, is het schema de respectieve sleutelruimte in Cassandra. De keyspace is de buitenste container voor gegevens in Cassandra. De belangrijkste attributen die u per toetsruimte kunt instellen, zijn de Replicatiefactor, de Replica plaatsingsstrategie en de Column Families
  • Column familie - Kolomfamilies in Cassandra zijn als tabellen in relationele databases. Elke kolomfamilie bevat een verzameling rijen die worden weergegeven door een Kaart. De sleutel geeft de mogelijkheid om samen toegang te krijgen tot gerelateerde gegevens
  • Kolom - Een kolom in Cassandra is een datastructuur die een kolomnaam, een waarde en een tijdstempel bevat. De kolommen en het aantal kolommen in elke rij kunnen variëren in tegenstelling tot een relationele database waarin de gegevens goed gestructureerd zijn

3. Met behulp van de Java Client

3.1. Afhankelijkheid van Maven

We moeten de volgende Cassandra-afhankelijkheid definiëren in het pom.xml, waarvan de laatste versie hier te vinden is:

 com.datastax.cassandra cassandra-driver-core 3.1.0 

Om de code te testen met een embedded databaseserver, moeten we ook de cassandra-eenheid afhankelijkheid, waarvan de laatste versie hier te vinden is:

 org.cassandraunit cassandra-eenheid 3.0.0.1 

3.2. Verbinden met Cassandra

Om vanuit Java verbinding te maken met Cassandra, moeten we een TROS voorwerp.

Als contactpunt moet een adres van een knooppunt worden opgegeven. Als we geen poortnummer opgeven, wordt de standaardpoort (9042) gebruikt.

Met deze instellingen kan het stuurprogramma de huidige topologie van een cluster ontdekken.

openbare klasse CassandraConnector {privéclustercluster; privé sessie sessie; public void connect (String-knooppunt, Integer-poort) {Builder b = Cluster.builder (). addContactPoint (knooppunt); if (poort! = null) {b.withPort (poort); } cluster = b.build (); session = cluster.connect (); } openbare sessie getSession () {retourneer this.session; } public void close () {session.close (); cluster.close (); }}

3.3. De keyspace maken

Laten we onze “bibliotheek”Keyspace:

public void createKeyspace (String keyspaceName, String replicationStrategy, int replicationFactor) {StringBuilder sb = new StringBuilder ("CREATE KEYSPACE INDIEN NIET BESTAAT") .append (keyspaceName) .append ("WITH replication = {") .append ("'class' : '"). append (replicationStrategy) .append ("', 'replication_factor': "). append (replicationFactor) .append ("}; "); Tekenreeksquery = sb.toString (); session.execute (vraag); }

Behalve de keyspaceName we moeten nog twee parameters definiëren, de replicationFactor en de replicationStrategy. Deze parameters bepalen respectievelijk het aantal replica's en hoe de replica's over de ring worden verdeeld.

Met replicatie zorgt Cassandra voor betrouwbaarheid en fouttolerantie door kopieën van gegevens op meerdere knooppunten op te slaan.

Op dit punt kunnen we testen of onze keyspace met succes is aangemaakt:

privé KeyspaceRepository schemaRepository; privé sessie sessie; @Before public void connect () {CassandraConnector-client = nieuwe CassandraConnector (); client.connect ("127.0.0.1", 9142); this.session = client.getSession (); schemaRepository = nieuwe KeyspaceRepository (sessie); }
@Test openbare leegte whenCreatingAKeyspace_thenCreated () {String keyspaceName = "bibliotheek"; schemaRepository.createKeyspace (keyspaceName, "SimpleStrategy", 1); ResultSet result = session.execute ("SELECT * FROM system_schema.keyspaces;"); Lijst 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. Een kolomfamilie maken

Nu kunnen we de eerste "boeken" van de Kolomfamilie toevoegen aan de bestaande sleutelruimte:

private static final String TABLE_NAME = "books"; privé sessie sessie; public void createTable () {StringBuilder sb = nieuwe StringBuilder ("CREATE TABLE IF NOT EXISTS") .append (TABLE_NAME) .append ("(") .append ("id uuid PRIMARY KEY,") .append ("title text, ") .append (" onderwerptekst); "); Tekenreeksquery = sb.toString (); session.execute (vraag); }

De code om te testen of de Column Family is gemaakt, wordt hieronder gegeven:

privé BookRepository bookRepository; privé sessie sessie; @Before public void connect () {CassandraConnector-client = nieuwe CassandraConnector (); client.connect ("127.0.0.1", 9142); this.session = client.getSession (); bookRepository = nieuwe BookRepository (sessie); }
@Test openbare leegte whenCreatingATable_thenCreatedCorrectly () {bookRepository.createTable (); ResultSet resultaat = session.execute ("SELECT * FROM" + KEYSPACE_NAME + ".books;"); Lijst 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. De kolomfamilie wijzigen

Een boek heeft ook een uitgever, maar zo'n kolom is niet te vinden in de aangemaakte tabel. We kunnen de volgende code gebruiken om de tabel te wijzigen en een nieuwe kolom toe te voegen:

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

Laten we ervoor zorgen dat de nieuwe column uitgever is toegevoegd:

@Test openbare leegte whenAlteringTable_thenAddedColumnExists () {bookRepository.createTable (); bookRepository.alterTablebooks ("uitgever", "tekst"); ResultSet resultaat = session.execute ("SELECT * FROM" + KEYSPACE_NAME + "." + "Books" + ";"); boolean columnExists = result.getColumnDefinitions (). asList (). stream () .anyMatch (cl -> cl.getName (). equals ("publisher")); assertTrue (columnExists); }

3.6. Gegevens invoegen in de kolomfamilie

Nu dat de boeken tabel is gemaakt, we zijn klaar om gegevens aan de tabel toe te voegen:

public void insertbookByTitle (Boek boek) {StringBuilder sb = nieuwe StringBuilder ("INSERT INTO") .append (TABLE_NAME_BY_TITLE) .append ("(id, title)") .append ("VALUES (") .append (book.getId ( )) .append (", '") .append (book.getTitle ()). append ("');"); Tekenreeksquery = sb.toString (); session.execute (vraag); }

Er is een nieuwe rij toegevoegd aan de 'boeken'-tabel, zodat we kunnen testen of de rij bestaat:

@Test openbare leegte whenAddingANewBook_thenBookExists () {bookRepository.createTableBooksByTitle (); String title = "Effectieve Java"; Boek boek = nieuw boek (UUIDs.timeBased (), titel, "Programmering"); bookRepository.insertbookByTitle (boek); Boek opgeslagenBook = bookRepository.selectByTitle (titel); assertEquals (book.getTitle (), savedBook.getTitle ()); }

In de bovenstaande testcode hebben we een andere methode gebruikt om een ​​tabel met de naam te maken booksByTitle:

public void createTableBooksByTitle () {StringBuilder sb = nieuwe StringBuilder ("CREATE TABLE IF NOT EXISTS") .append ("booksByTitle"). append ("(") .append ("id uuid,") .append ("title text, ") .append (" PRIMAIRE SLEUTEL (titel, id)); "); Tekenreeksquery = sb.toString (); session.execute (vraag); }

In Cassandra is een van de best practices het gebruik van één tabel per querypatroon. Dit betekent dat voor een andere zoekopdracht een andere tabel nodig is.

In ons voorbeeld hebben we ervoor gekozen om een ​​boek op titel te selecteren. Om te voldoen aan de selectByTitle query, hebben we een tabel gemaakt met een compound HOOFDSLEUTEL met behulp van de kolommen, titel en ID kaart. De kolom titel is de partitioneringssleutel terwijl de ID kaart column is de clustersleutel.

Op deze manier bevatten veel van de tabellen in uw gegevensmodel dubbele gegevens. Dit is geen nadeel van deze database. Integendeel, deze praktijk optimaliseert de prestaties van de leesbewerkingen.

Laten we eens kijken naar de gegevens die momenteel in onze tabel zijn opgeslagen:

openbare lijst selectAll () {StringBuilder sb = nieuwe StringBuilder ("SELECT * FROM") .append (TABLE_NAME); Tekenreeksquery = sb.toString (); ResultSet rs = session.execute (query); Lijstboeken = nieuwe ArrayList (); rs.forEach (r -> {books.add (nieuw boek (r.getUUID ("id"), r.getString ("titel"), r.getString ("onderwerp")));}); boeken teruggeven; }

Een test voor query die verwachte resultaten oplevert:

@Test openbare leegte whenSelectingAll_thenReturnAllRecords () {bookRepository.createTable (); Boek boek = nieuw boek (UUIDs.timeBased (), "Effectieve Java", "Programmering"); bookRepository.insertbook (boek); book = nieuw boek (UUIDs.timeBased (), "Clean Code", "Programming"); bookRepository.insertbook (boek); Lijstboeken = bookRepository.selectAll (); assertEquals (2, books.size ()); assertTrue (books.stream (). anyMatch (b -> b.getTitle () .equals ("Effectieve Java"))); assertTrue (books.stream (). anyMatch (b -> b.getTitle () .equals ("Clean Code"))); }

Alles is in orde tot nu toe, maar één ding moet worden gerealiseerd. We begonnen met tafel te werken boeken, maar in de tussentijd, om te voldoen aan de selecteer vraag door titel kolom, moesten we een andere tabel maken met de naam booksByTitle.

De twee tabellen zijn identiek en bevatten dubbele kolommen, maar we hebben alleen gegevens in de booksByTitle tafel. Als gevolg hiervan zijn de gegevens in twee tabellen momenteel inconsistent.

We kunnen dit oplossen met een partij query, die twee invoeginstructies bevat, één voor elke tabel. EEN partij query voert meerdere DML-instructies uit als een enkele bewerking.

Een voorbeeld van een dergelijke zoekopdracht wordt gegeven:

public void insertBookBatch (Boek boek) {StringBuilder sb = nieuwe StringBuilder ("BEGIN BATCH") .append ("INSERT INTO") .append (TABLE_NAME) .append ("(id, titel, onderwerp)") .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;"); Tekenreeksquery = sb.toString (); session.execute (vraag); }

We testen de resultaten van de batchquery opnieuw als volgt:

@Test openbare leegte whenAddingANewBookBatch_ThenBookAddedInAllTables () {bookRepository.createTable (); bookRepository.createTableBooksByTitle (); String title = "Effectieve Java"; Boek boek = nieuw boek (UUIDs.timeBased (), titel, "Programmering"); bookRepository.insertBookBatch (boek); Lijstboeken = bookRepository.selectAll (); assertEquals (1, books.size ()); assertTrue (books.stream (). anyMatch (b -> b.getTitle (). equals ("Effectieve Java"))); Lijst booksByTitle = bookRepository.selectAllBookByTitle (); assertEquals (1, booksByTitle.size ()); assertTrue (booksByTitle.stream (). anyMatch (b -> b.getTitle (). equals ("Effectieve Java"))); }

Opmerking: vanaf versie 3.0 is er een nieuwe functie genaamd "Gematerialiseerde weergaven" beschikbaar, die we in plaats van kunnen gebruiken partij vragen. Een goed gedocumenteerd voorbeeld voor "Gematerialiseerde weergaven" is hier beschikbaar.

3.7. De kolomfamilie verwijderen

De onderstaande code laat zien hoe u een tabel verwijdert:

public void deleteTable () {StringBuilder sb = nieuwe StringBuilder ("DROP TABLE IF EXISTS") .append (TABLE_NAME); Tekenreeksquery = sb.toString (); session.execute (vraag); }

Het selecteren van een tabel die niet bestaat in de keyspace resulteert in een InvalidQueryException: niet-geconfigureerde tabelboeken:

@Test (verwacht = InvalidQueryException.class) openbare leegte whenDeletingATable_thenUnconfiguredTable () {bookRepository.createTable (); bookRepository.deleteTable ("boeken"); session.execute ("SELECT * FROM" + KEYSPACE_NAME + ".books;"); }

3.8. De keyspace verwijderen

Laten we tot slot de keyspace verwijderen:

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

En test of de keyspace is verwijderd:

@Test openbare leegte whenDeletingAKeyspace_thenDoesNotExist () {String keyspaceName = "bibliotheek"; 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. Conclusie

Deze tutorial behandelde de basisstappen om verbinding te maken met en gebruik te maken van de Cassandra-database met Java. Enkele van de belangrijkste concepten van deze database zijn ook besproken om u op weg te helpen.

De volledige implementatie van deze tutorial is te vinden in het Github-project.