Panduan untuk Cassandra dengan Java

1. Ikhtisar

Tutorial ini adalah panduan pengantar ke database Apache Cassandra menggunakan Java.

Anda akan menemukan penjelasan konsep utama, bersama dengan contoh kerja yang mencakup langkah-langkah dasar untuk terhubung dan mulai bekerja dengan database NoSQL dari Java ini.

2. Cassandra

Cassandra adalah database NoSQL yang dapat diskalakan yang menyediakan ketersediaan berkelanjutan tanpa satu titik kegagalan dan memberikan kemampuan untuk menangani data dalam jumlah besar dengan kinerja luar biasa.

Database ini menggunakan desain cincin daripada menggunakan arsitektur master-slave. Dalam desain ring, tidak ada node master - semua node yang berpartisipasi identik dan berkomunikasi satu sama lain sebagai peer.

Hal ini membuat Cassandra menjadi sistem yang dapat diskalakan secara horizontal dengan memungkinkan penambahan node secara bertahap tanpa memerlukan konfigurasi ulang.

2.1. Konsep Utama

Mari kita mulai dengan survei singkat tentang beberapa konsep utama Cassandra:

  • Cluster - kumpulan node atau Pusat Data yang diatur dalam arsitektur cincin. Sebuah nama harus diberikan ke setiap cluster, yang selanjutnya akan digunakan oleh node yang berpartisipasi
  • Keyspace - Jika Anda berasal dari database relasional, maka skema adalah ruang kunci masing-masing di Cassandra. Ruang kunci adalah wadah terluar untuk data di Cassandra. Atribut utama yang harus disetel per ruang kunci adalah Faktor Replikasi , Strategi Penempatan Replika, dan Keluarga Kolom
  • Keluarga Kolom - Keluarga Kolom di Cassandra seperti tabel di Database Relasional. Setiap Keluarga Kolom berisi kumpulan baris yang diwakili oleh Peta . Kuncinya memberikan kemampuan untuk mengakses data terkait bersama
  • Kolom - Kolom di Cassandra adalah struktur data yang berisi nama kolom, nilai, dan stempel waktu. Kolom dan jumlah kolom di setiap baris mungkin berbeda dengan database relasional yang datanya terstruktur dengan baik

3. Menggunakan Klien Java

3.1. Ketergantungan Maven

Kita perlu mendefinisikan dependensi Cassandra berikut di pom.xml , versi terbaru dapat ditemukan di sini:

 com.datastax.cassandra cassandra-driver-core 3.1.0 

Untuk menguji kode dengan server database tertanam, kami juga harus menambahkan dependensi cassandra-unit , versi terbaru dapat ditemukan di sini:

 org.cassandraunit cassandra-unit 3.0.0.1 

3.2. Menghubungkan ke Cassandra

Untuk terhubung ke Cassandra dari Java, kita perlu membangun objek Cluster .

Alamat node perlu diberikan sebagai titik kontak. Jika kami tidak memberikan nomor port, port default (9042) akan digunakan.

Pengaturan ini memungkinkan pengemudi untuk menemukan topologi cluster saat ini.

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

Mari buat ruang kunci " perpustakaan " kita:

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

Kecuali dari keyspaceName kita perlu mendefinisikan dua parameter lagi, repicationFactor dan replicationStrategy . Parameter ini menentukan jumlah replika dan bagaimana replika akan didistribusikan ke seluruh ring.

Dengan replikasi Cassandra memastikan keandalan dan toleransi kesalahan dengan menyimpan salinan data di banyak node.

Pada titik ini kami dapat menguji bahwa ruang kunci kami telah berhasil dibuat:

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. Membuat Keluarga Kolom

Sekarang, kita dapat menambahkan "buku" Keluarga Kolom pertama ke ruang kunci yang ada:

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

Kode untuk menguji bahwa Keluarga Kolom telah dibuat, disediakan di bawah ini:

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. Mengubah Keluarga Kolom

Sebuah buku juga memiliki penerbit, tetapi kolom seperti itu tidak dapat ditemukan di tabel yang dibuat. Kita dapat menggunakan kode berikut untuk mengubah tabel dan menambahkan kolom baru:

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

Mari kita pastikan bahwa penerbit kolom baru telah ditambahkan:

@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. Memasukkan Data di Keluarga Kolom

Sekarang tabel buku telah dibuat, kami siap untuk mulai menambahkan data ke tabel:

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

Baris baru telah ditambahkan ke dalam tabel 'books', jadi kita bisa menguji apakah baris tersebut ada:

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

Dalam kode pengujian di atas kami telah menggunakan metode berbeda untuk membuat tabel bernama 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); }

Di Cassandra, salah satu praktik terbaik adalah menggunakan pola satu tabel per kueri. Artinya, untuk query yang berbeda dibutuhkan tabel yang berbeda.

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

Catatan: Mulai versi 3.0, fitur baru yang disebut "Tampilan Terwujud" telah tersedia, yang dapat kami gunakan sebagai pengganti kueri batch . Contoh yang terdokumentasi dengan baik untuk "Tampilan Terwujud" tersedia di sini.

3.7. Menghapus Keluarga Kolom

Kode di bawah ini menunjukkan cara menghapus tabel:

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

Memilih tabel yang tidak ada di ruang kunci menghasilkan InvalidQueryException: buku tabel yang tidak dikonfigurasi :

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

3.8. Menghapus Keyspace

Terakhir, mari hapus ruang kunci:

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

Dan uji bahwa ruang kunci telah dihapus:

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

Tutorial ini membahas langkah-langkah dasar menghubungkan dan menggunakan database Cassandra dengan Java. Beberapa konsep kunci dari database ini juga telah dibahas untuk membantu Anda memulai.

Implementasi lengkap dari tutorial ini dapat ditemukan di proyek Github.