Panduan untuk Cassandra dengan Java

1. Gambaran keseluruhan

Tutorial ini adalah panduan pengantar pangkalan data Apache Cassandra menggunakan Java.

Anda akan menemui konsep utama yang dijelaskan, bersama dengan contoh kerja yang merangkumi langkah-langkah asas untuk menyambung dan mula bekerja dengan pangkalan data NoSQL ini dari Java.

2. Cassandra

Cassandra adalah pangkalan data NoSQL yang berskala yang menyediakan ketersediaan berterusan tanpa titik kegagalan tunggal dan memberikan kemampuan untuk menangani sejumlah besar data dengan prestasi yang luar biasa.

Pangkalan data ini menggunakan reka bentuk cincin dan bukannya menggunakan seni bina master-slave. Dalam reka bentuk gelang, tidak ada nod utama - semua simpul peserta adalah sama dan berkomunikasi antara satu sama lain sebagai rakan sebaya.

Ini menjadikan Cassandra sebagai sistem yang dapat diskalakan secara mendatar dengan memungkinkan penambahan node secara bertahap tanpa memerlukan konfigurasi ulang.

2.1. Konsep kunci

Mari kita mulakan dengan tinjauan ringkas mengenai beberapa konsep utama Cassandra:

  • Kluster - kumpulan nod atau Pusat Data yang disusun dalam seni bina cincin. Nama mesti diberikan kepada setiap kluster, yang kemudiannya akan digunakan oleh nod yang mengambil bahagian
  • Keyspace - Jika anda berasal dari pangkalan data relasional, maka skema adalah ruang kunci masing-masing di Cassandra. Ruang kunci adalah wadah terluar untuk data di Cassandra. Atribut utama yang harus ditetapkan setiap ruang kunci adalah Faktor Replikasi , Strategi Penempatan Replika dan Keluarga Kolum
  • Keluarga Kolum - Keluarga Kolum di Cassandra adalah seperti jadual dalam Pangkalan Data Relasional. Setiap Keluarga Kolum mengandungi koleksi baris yang diwakili oleh Peta . Kuncinya memberikan keupayaan untuk mengakses data yang berkaitan bersama-sama
  • Lajur - Lajur di Cassandra adalah struktur data yang mengandungi nama lajur, nilai dan cap waktu. Lajur dan jumlah lajur di setiap baris mungkin berbeza dengan pangkalan data relasional di mana data tersusun dengan baik

3. Menggunakan Java Java Client

3.1. Ketergantungan Maven

Kita perlu menentukan ketergantungan Cassandra berikut dalam pom.xml , versi terbaru yang terdapat di sini:

 com.datastax.cassandra cassandra-driver-core 3.1.0 

Untuk menguji kod dengan pelayan pangkalan data tertanam, kita juga harus menambahkan kebergantungan unit cassandra , versi terbaru yang terdapat di sini:

 org.cassandraunit cassandra-unit 3.0.0.1 

3.2. Bersambung ke Cassandra

Untuk menyambung ke Cassandra dari Java, kita perlu membuat objek Cluster .

Alamat nod perlu disediakan sebagai titik hubungan. Sekiranya kita tidak memberikan nombor port, port lalai (9042) akan digunakan.

Tetapan ini membolehkan pemandu mengetahui topologi kluster semasa.

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 Ruang Kekunci

Mari buat ruang kekunci " perpustakaan " kami:

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 daripada keyspaceName kita perlu menentukan dua parameter, replicationFactor dan replicationStrategy . Parameter ini menentukan bilangan replika dan bagaimana replika akan diedarkan di seluruh gelang, masing-masing.

Dengan replikasi, Cassandra memastikan kebolehpercayaan dan toleransi kesalahan dengan menyimpan salinan data dalam beberapa nod.

Pada ketika ini, kami mungkin menguji bahawa ruang kunci kami berjaya 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. Mewujudkan Keluarga Kolum

Sekarang, kita dapat menambahkan "buku" Keluarga Kolom pertama ke ruang kekunci 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); }

Kod untuk menguji bahawa Keluarga Kolum telah dibuat, disediakan di bawah:

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 Kolum

Sebuah buku juga mempunyai penerbit, tetapi tiang tersebut tidak dapat dijumpai di jadual yang dibuat. Kita boleh menggunakan kod berikut untuk mengubah jadual dan menambahkan lajur 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 pastikan bahawa penerbit lajur 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 dalam Keluarga Kolum

Sekarang jadual buku telah dibuat, kami sudah bersedia untuk mula menambahkan data ke jadual:

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 dalam jadual 'buku', jadi kami dapat menguji apakah baris itu 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 kod ujian di atas, kami telah menggunakan kaedah yang berbeza untuk membuat jadual bernama bukuByTitle:

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 amalan terbaik adalah menggunakan corak satu jadual per pertanyaan. Ini bermaksud, untuk pertanyaan yang berbeza diperlukan jadual yang berbeza.

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: Pada versi 3.0, tersedia fitur baru yang disebut "Tampilan Terwujud", yang dapat kami gunakan sebagai ganti pertanyaan batch . Contoh yang didokumentasikan dengan baik untuk "Pandangan Terwujud" terdapat di sini.

3.7. Memadamkan Keluarga Lajur

Kod di bawah menunjukkan cara menghapus jadual:

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

Memilih jadual yang tidak ada di ruang kunci menghasilkan InvalidQueryException: buku jadual 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. Memadamkan Ruang Kekunci

Akhirnya, mari padamkan ruang kekunci:

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

Dan uji bahawa ruang kekunci telah dipadamkan:

@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 merangkumi langkah-langkah asas menghubungkan dan menggunakan pangkalan data Cassandra dengan Java. Beberapa konsep penting pangkalan data ini juga telah dibincangkan untuk membantu anda memulakannya.

Pelaksanaan penuh tutorial ini boleh didapati dalam projek Github.