En guide til Cassandra med Java

1. Oversigt

Denne vejledning er en introduktionsvejledning til Apache Cassandra-databasen ved hjælp af Java.

Du finder forklarede nøglekoncepter sammen med et arbejdseksempel, der dækker de grundlæggende trin for at oprette forbindelse til og begynde at arbejde med denne NoSQL-database fra Java.

2. Cassandra

Cassandra er en skalerbar NoSQL-database, der giver kontinuerlig tilgængelighed uden et enkelt fejlpunkt og giver mulighed for at håndtere store mængder data med enestående ydelse.

Denne database bruger et ringdesign i stedet for at bruge en master-slave-arkitektur. I ringdesignet er der ingen masternode - alle deltagende noder er identiske og kommunikerer med hinanden som jævnaldrende.

Dette gør Cassandra til et vandret skalerbart system ved at muliggøre den inkrementelle tilføjelse af noder uden behov for omkonfiguration.

2.1. Nøglekoncepter

Lad os starte med en kort oversigt over nogle af nøglebegreberne i Cassandra:

  • Klynge - en samling af noder eller datacentre arrangeret i en ringarkitektur. Der skal tildeles et navn til hver klynge, som efterfølgende vil blive brugt af de deltagende noder
  • Keyspace - Hvis du kommer fra en relationsdatabase, er skemaet det respektive nøgleområde i Cassandra. Nøgleområdet er den yderste container til data i Cassandra. De vigtigste attributter, der skal indstilles pr. Nøgleområde, er Replikeringsfaktor, det Replika placeringsstrategi og Kolonnefamilier
  • Kolonnefamilie - Kolonnefamilier i Cassandra er som tabeller i relationelle databaser. Hver kolonnefamilie indeholder en samling af rækker, der er repræsenteret af en Kort. Nøglen giver mulighed for at få adgang til relaterede data sammen
  • Kolonne - En kolonne i Cassandra er en datastruktur, der indeholder et kolonnenavn, en værdi og et tidsstempel. Kolonnerne og antallet af kolonner i hver række kan variere i kontrast til en relationsdatabase, hvor data er godt strukturerede

3. Brug af Java-klienten

3.1. Maven afhængighed

Vi er nødt til at definere følgende Cassandra afhængighed i pom.xml, den seneste version kan findes her:

 com.datastax.cassandra cassandra-driver-core 3.1.0 

For at teste koden med en integreret databaseserver skal vi også tilføje Cassandra-enhed afhængighed, hvis seneste version kan findes her:

 org.cassandraunit cassandra-enhed 3.0.0.1 

3.2. Opretter forbindelse til Cassandra

For at oprette forbindelse til Cassandra fra Java er vi nødt til at opbygge en Klynge objekt.

En kontaktadresses adresse skal angives som et kontaktpunkt. Hvis vi ikke angiver et portnummer, bruges standardporten (9042).

Disse indstillinger giver føreren mulighed for at finde den aktuelle topologi i en klynge.

offentlig klasse CassandraConnector {privat klyngeklynge; privat session session; public void connect (String node, Integer port) {Builder b = Cluster.builder (). addContactPoint (node); hvis (port! = null) {b.withPort (port); } klynge = b.build (); session = cluster.connect (); } public Session getSession () {returner this.session; } offentlig tomrum lukket () {session.close (); cluster.close (); }}

3.3. Oprettelse af Keyspace

Lad os oprette vores “bibliotek”Nøgleområde:

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 ("}; "); Strengeforespørgsel = sb.toString (); session.execute (forespørgsel); }

Bortset fra keypaceName vi er nødt til at definere yderligere to parametre, nemlig replikationsfaktor og replikeringStrategi. Disse parametre bestemmer antallet af replikaer, og hvordan replikerne fordeles på henholdsvis ringen.

Med replikering sikrer Cassandra pålidelighed og fejltolerance ved at gemme kopier af data i flere noder.

På dette tidspunkt kan vi teste, at vores nøgleområde er oprettet med succes:

privat KeyspaceRepository skemaRepository; privat session session; @Før offentlig ugyldig forbindelse () {CassandraConnector-klient = ny CassandraConnector (); client.connect ("127.0.0.1", 9142); this.session = client.getSession (); schemaRepository = nyt KeyspaceRepository (session); }
@Test offentlig ugyldig nårCreatingAKeyspace_thenCreated () {String keyspaceName = "bibliotek"; schemaRepository.createKeyspace (keyspaceName, "SimpleStrategy", 1); ResultSet resultat = session.execute ("VÆLG * FRA 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. Oprettelse af en kolonnefamilie

Nu kan vi tilføje de første "bøger" i kolonnefamilien til det eksisterende nøgleområde:

privat statisk endelig String TABLE_NAME = "bøger"; privat 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 (" emnetekst); "); Strengeforespørgsel = sb.toString (); session.execute (forespørgsel); }

Koden til at teste, at kolonnefamilien er oprettet, findes nedenfor:

privat BookRepository bookRepository; privat session session; @Før offentlig ugyldig forbindelse () {CassandraConnector-klient = ny CassandraConnector (); client.connect ("127.0.0.1", 9142); this.session = client.getSession (); bookRepository = nyt BookRepository (session); }
@Test offentlig ugyldig nårCreatingATable_thenCreatedCorrectly () {bookRepository.createTable (); ResultSet resultat = session.execute ("VÆLG * FRA" + KEYSPACE_NAME + ".bøger;"); Vis kolonnenavne = resultat.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 ("emne")); }

3.5. Ændring af søjlefamilien

En bog har også en udgiver, men der findes ingen sådan kolonne i den oprettede tabel. Vi kan bruge følgende kode til at ændre tabellen og tilføje en ny kolonne:

public void alterTablebooks (String columnName, String columnType) {StringBuilder sb = new StringBuilder ("ALTER TABLE") .append (TABLE_NAME) .append ("ADD") .append (columnName) .append ("") .append (columnType) .Tilføj(";"); Strengeforespørgsel = sb.toString (); session.execute (forespørgsel); }

Lad os sørge for, at den nye kolonne forlægger er tilføjet:

@Test offentlig ugyldig nårAlteringTable_thenAddedColumnExists () {bookRepository.createTable (); bookRepository.alterTablebooks ("udgiver", "tekst"); ResultSet resultat = session.execute ("VÆLG * FRA" + KEYSPACE_NAME + "." + "Bøger" + ";"); boolske columnExists = result.getColumnDefinitions (). asList (). stream () .anyMatch (cl -> cl.getName (). er lig med ("publisher")); assertTrue (columnExists); }

3.6. Indsættelse af data i kolonnefamilien

Nu hvor bøger tabellen er oprettet, er vi klar til at begynde at tilføje data til tabellen:

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 ("');"); Strengeforespørgsel = sb.toString (); session.execute (forespørgsel); }

En ny række er tilføjet i tabellen 'bøger', så vi kan teste, om rækken findes:

@Test offentlig ugyldig nårAddingANewBook_thenBookExists () {bookRepository.createTableBooksByTitle (); String title = "Effektiv Java"; Bogbog = ny bog (UUIDs.timeBased (), titel, "Programmering"); bookRepository.insertbookByTitle (bog); Bog gemtBog = bookRepository.selectByTitle (titel); assertEquals (book.getTitle (), gemtBook.getTitle ()); }

I testkoden ovenfor har vi brugt en anden metode til at oprette en tabel med navnet booksByTitle:

public void createTableBooksByTitle () {StringBuilder sb = new StringBuilder ("CREATE TABLE IF NOT EXISTS") .append ("booksByTitle"). append ("(") .append ("id uuid,") .append ("title text, ") .append (" PRIMÆR NØGLE (titel, id)); "); Strengeforespørgsel = sb.toString (); session.execute (forespørgsel); }

I Cassandra er en af ​​de bedste fremgangsmåder at bruge mønster med én tabel pr. Forespørgsel. Dette betyder, at for en anden forespørgsel er der brug for en anden tabel.

I vores eksempel har vi valgt at vælge en bog efter dens titel. For at tilfredsstille vælgByTitle forespørgsel, vi har oprettet en tabel med en forbindelse PRIMÆRNØGLE ved hjælp af kolonnerne titel og id. Kolonnen titel er partitioneringsnøglen, mens id kolonne er klyngenøglen.

På denne måde indeholder mange af tabellerne i din datamodel duplikatdata. Dette er ikke en ulempe ved denne database. Tværtimod optimerer denne praksis udførelsen af ​​læsningerne.

Lad os se de data, der i øjeblikket er gemt i vores tabel:

public List selectAll () {StringBuilder sb = new StringBuilder ("SELECT * FROM") .append (TABLE_NAME); Strengeforespørgsel = sb.toString (); ResultSet rs = session.execute (forespørgsel); Listebøger = ny ArrayList (); rs.forEach (r -> {books.add (ny bog (r.getUUID ("id"), r.getString ("titel"), r.getString ("emne")))}}; returnere bøger; }

En test til forespørgsel, der returnerer forventede resultater:

@Test offentligt ugyldigt nårSelectingAll_thenReturnAllRecords () {bookRepository.createTable (); Bogbog = ny bog (UUIDs.timeBased (), "Effektiv Java", "Programmering"); bookRepository.insertbook (bog); bog = ny bog (UUIDs.timeBased (), "Ren kode", "Programmering"); bookRepository.insertbook (bog); Listebøger = bookRepository.selectAll (); assertEquals (2, books.size ()); assertTrue (books.stream (). anyMatch (b -> b.getTitle () .equals ("Effektiv Java"))); assertTrue (books.stream (). anyMatch (b -> b.getTitle () .equals ("Clean Code"))); }

Alt er i orden indtil nu, men én ting skal realiseres. Vi begyndte at arbejde med bordet bøger, men i mellemtiden for at tilfredsstille Vælg forespørgsel af titel kolonne måtte vi oprette en anden tabel med navnet booksByTitle.

De to tabeller er identiske med duplikerede kolonner, men vi har kun indsat data i booksByTitle bord. Som en konsekvens er data i to tabeller i øjeblikket inkonsekvente.

Vi kan løse dette ved hjælp af en parti forespørgsel, som består af to indsætningsudsagn, en for hver tabel. EN parti forespørgsel udfører flere DML-sætninger som en enkelt handling.

Et eksempel på en sådan forespørgsel gives:

offentlig ugyldig insertBookBatch (bogbog) {StringBuilder sb = ny 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;"); Strengeforespørgsel = sb.toString (); session.execute (forespørgsel); }

Igen tester vi batchforespørgselsresultaterne sådan:

@Test offentlig ugyldig nårAddingANewBookBatch_ThenBookAddedInAllTables () {bookRepository.createTable (); bookRepository.createTableBooksByTitle (); String title = "Effektiv Java"; Bogbog = ny bog (UUIDs.timeBased (), titel, "Programmering"); bookRepository.insertBookBatch (bog); Listebøger = bookRepository.selectAll (); assertEquals (1, books.size ()); assertTrue (books.stream (). anyMatch (b -> b.getTitle (). er lig med ("Effektiv Java"))); Liste over booksByTitle = bookRepository.selectAllBookByTitle (); assertEquals (1, booksByTitle.size ()); assertTrue (booksByTitle.stream (). anyMatch (b -> b.getTitle (). er lig med ("Effektiv Java"))); }

Bemærk: Fra version 3.0 er der en ny funktion kaldet “Materialiserede visninger” tilgængelig, som vi muligvis bruger i stedet for parti forespørgsler. Et veldokumenteret eksempel på "Materialiserede visninger" er tilgængelig her.

3.7. Sletning af kolonnefamilien

Koden nedenfor viser, hvordan du sletter en tabel:

public void deleteTable () {StringBuilder sb = new StringBuilder ("DROP TABLE IF EXISTS") .append (TABLE_NAME); Strengeforespørgsel = sb.toString (); session.execute (forespørgsel); }

Valg af en tabel, der ikke findes i nøgleområdet, resulterer i en InvalidQueryException: ukonfigurerede tabelbøger:

@Test (forventet = InvalidQueryException.class) offentlig ugyldig nårDeletingATable_thenUnconfiguredTable () {bookRepository.createTable (); bookRepository.deleteTable ("bøger"); session.execute ("VÆLG * FRA" + KEYSPACE_NAME + ".bøger;"); }

3.8. Sletning af Keyspace

Lad os endelig slette nøgleområdet:

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

Og test, at nøgleområdet er slettet:

@Test offentlig ugyldig nårDeletingAKeyspace_thenDoesNotExist () {String keyspaceName = "bibliotek"; schemaRepository.deleteKeyspace (keypaceName); ResultSet resultat = session.execute ("VÆLG * FRA system_schema.keyspaces;"); boolsk isKeyspaceCreated = result.all (). stream () .anyMatch (r -> r.getString (0) .equals (keyspaceName.toLowerCase ())); assertFalse (isKeyspaceCreated); }

4. Konklusion

Denne vejledning dækkede de grundlæggende trin til at oprette forbindelse til og bruge Cassandra-databasen med Java. Nogle af nøglebegreberne i denne database er også blevet diskuteret for at hjælpe dig med at starte.

Den fulde implementering af denne vejledning kan findes i Github-projektet.