En guide till Cassandra med Java

1. Översikt

Denna handledning är en introduktionsguide till Apache Cassandra-databasen med Java.

Du hittar nyckelbegrepp förklarade tillsammans med ett fungerande exempel som täcker de grundläggande stegen för att ansluta till och börja arbeta med denna NoSQL-databas från Java.

2. Cassandra

Cassandra är en skalbar NoSQL-databas som ger kontinuerlig tillgänglighet utan en enda felpunkt och ger möjlighet att hantera stora mängder data med exceptionell prestanda.

Denna databas använder en ringdesign istället för att använda en master-slave-arkitektur. I ringdesignen finns det ingen masternod - alla deltagande noder är identiska och kommunicerar med varandra som kamrater.

Detta gör Cassandra till ett horisontellt skalbart system genom att möjliggöra inkrementell tillägg av noder utan att behöva omkonfigureras.

2.1. Nyckelbegrepp

Låt oss börja med en kort undersökning av några av de viktigaste begreppen i Cassandra:

  • Cluster - en samling noder eller datacenter ordnade i en ringarkitektur. Ett namn måste tilldelas varje kluster, som därefter kommer att användas av de deltagande noderna
  • Keyspace - Om du kommer från en relationsdatabas är schemat respektive tangentutrymme i Cassandra. Nyckelutrymmet är den yttersta behållaren för data i Cassandra. De viktigaste attributen för att ställa per keyspace är Replication Factor , den Replica Placering strategi och kolumn Families
  • Kolumnfamilj - Kolonnfamiljer i Cassandra är som tabeller i relationsdatabaser. Varje kolumnfamilj innehåller en samling rader som representeras av en karta . Nyckeln ger möjlighet att komma åt relaterad data tillsammans
  • Kolumn - En kolumn i Cassandra är en datastruktur som innehåller ett kolumnnamn, ett värde och en tidsstämpel. Kolumnerna och antalet kolumner i varje rad kan variera i kontrast till en relationsdatabas där data är välstrukturerade

3. Använda Java-klienten

3.1. Maven beroende

Vi måste definiera följande Cassandra-beroende i pom.xml , vars senaste version finns här:

 com.datastax.cassandra cassandra-driver-core 3.1.0 

För att testa koden med en inbäddad databasserver bör vi också lägga till kassandra-enhetsberoende , vars senaste version finns här:

 org.cassandraunit cassandra-unit 3.0.0.1 

3.2. Ansluter till Cassandra

För att kunna ansluta till Cassandra från Java måste vi bygga ett klusterobjekt .

En adress till en nod måste anges som en kontaktpunkt. Om vi ​​inte anger ett portnummer används standardporten (9042).

Dessa inställningar gör det möjligt för föraren att upptäcka den aktuella topologin för ett kluster.

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. Skapa Keyspace

Låt oss skapa vårt " biblioteks " nyckelområ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("};"); String query = sb.toString(); session.execute(query); }

Förutom keypaceName måste vi definiera ytterligare två parametrar, replicationFactor och replicationStrategy . Dessa parametrar bestämmer antalet repliker och hur replikerna kommer att fördelas över ringen.

Med replikering säkerställer Cassandra tillförlitlighet och feltolerans genom att lagra kopior av data i flera noder.

Vid denna tidpunkt kan vi testa att vårt nyckelområde har skapats framgångsrikt:

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. Skapa en kolumnfamilj

Nu kan vi lägga till de första kolumnfamiljens "böcker" i det befintliga nyckelområdet:

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

Koden för att testa att kolumnfamiljen har skapats finns nedan:

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. Ändra kolonnfamiljen

En bok har också ett förlag, men ingen sådan kolumn finns i den skapade tabellen. Vi kan använda följande kod för att ändra tabellen och lägga till en ny kolumn:

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

Låt oss se till att den nya kolumnen utgivaren har lagts till:

@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. Infoga data i kolumnfamiljen

Nu när böktabellen har skapats är vi redo att börja lägga till data i 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("');"); String query = sb.toString(); session.execute(query); }

En ny rad har lagts till i tabellen 'böcker' så att vi kan testa om raden finns:

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

I testkoden ovan har vi använt en annan metod för att skapa en tabell med namnet 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); }

I Cassandra är en av de bästa metoderna att använda ett-tabell-per-fråga-mönster. Det betyder att för en annan fråga behövs en annan tabell.

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

Obs: Från och med version 3.0, finns en ny funktion som kallas ”materia sidor”, som vi kan använda i stället för sats frågor. Ett väldokumenterat exempel för ”Materialiserade vyer” finns här.

3.7. Radera kolumnfamiljen

Koden nedan visar hur man tar bort en tabell:

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

Att välja en tabell som inte finns i nyckelutrymmet resulterar i ett InvalidQueryException: okonfigurerade tabellböcker :

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

3.8. Ta bort Keyspace

Slutligen, låt oss ta bort nyckelområdet:

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

Och testa att nyckelutrymmet har tagits bort:

@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. Slutsats

Denna handledning behandlade de grundläggande stegen för att ansluta till och använda Cassandra-databasen med Java. Några av de viktigaste begreppen i denna databas har också diskuterats för att hjälpa dig att starta.

Det fullständiga genomförandet av denna handledning finns i Github-projektet.