Apache Igniteはインメモリ・データグリッドの一種ですが、SQL(ANSI-99)をサポートしています。
What is Ignite? / Complete SQL Support
なんとまあ、JDBCドライバまであるわけですよ。
ちょっと面白そうだったので、試してみることにしました。
Apache IgniteのSQLサポート
ここを見ればよいのですが、
ANSI-99に準拠している、分散SQLデータベースをApache Igniteをサポートしています。
DMLおよびDDLをサポートし、さらにDistributed JOINが可能です。データの配置(collocated and non-collocated)に関わらず、
JOINを行うことができます。
データをインメモリにのみ持つか、ディスクも含めて永続化するかについては、設定次第です。
環境
確認した環境は、こちら。
$ java -version openjdk version "1.8.0_171" OpenJDK Runtime Environment (build 1.8.0_171-8u171-b11-0ubuntu0.16.04.1-b11) OpenJDK 64-Bit Server VM (build 25.171-b11, mixed mode) $ mvn -version Apache Maven 3.5.3 (3383c37e1f9e9b3bc3df5050c29c8aff9f295297; 2018-02-25T04:49:05+09:00) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 1.8.0_171, vendor: Oracle Corporation Java home: /usr/lib/jvm/java-8-openjdk-amd64/jre Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "4.4.0-104-generic", arch: "amd64", family: "unix"
お題
今回は、Apache IgniteをEmbeddedに使います。テストコード内でJDBC API越しにSQLを使い、Apache Igniteをインメモリ・データベース
として使います。
簡単なSQLばかりにしますが、一応JOINもやってみようかなと。
準備
Maven依存関係は、こちら。
<dependency> <groupId>org.apache.ignite</groupId> <artifactId>ignite-core</artifactId> <version>2.5.0</version> </dependency> <dependency> <groupId>org.apache.ignite</groupId> <artifactId>ignite-indexing</artifactId> <version>2.5.0</version> </dependency>
Apache IgniteをSQLデータベースとしてEmbeddedに扱う場合は、「ignite-core」と「ignite-indexing」の2つのモジュールが必要です。
JDBCドライバは、「ignite-core」に含まれています。
進めるにあたっての、Getting Startedはこちら。
使用できる、SQLのリファレンスはこちらです。
また、テストコードで確認していくので、JUnitとAssertJを依存関係に追加しておきます。
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.2.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.10.0</version> <scope>test</scope> </dependency>
テストコードの雛形
テストコードの雛形は、こちらです。
src/test/java/org/littlewings/ignite/sql/IgniteSqlTest.java
package org.littlewings.ignite.sql; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.apache.ignite.Ignite; import org.apache.ignite.Ignition; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class IgniteSqlTest { // ここに、テストを書く! }
使ってみる
では、とりあえず接続してテーブルを作ってみます。
@Test public void gettingStarted() throws SQLException { try (Ignite ignite = Ignition.start(); Connection conn = DriverManager.getConnection("jdbc:ignite:thin://localhost:10800"); PreparedStatement createTablePs = conn.prepareStatement( "create table book(" + "isbn varchar primary key," + "title varchar," + "price int)" )) { createTablePs.executeUpdate(); .... } }
Ignition#startで、Apache Igniteを開始しておきます。IgniteクラスはAutoCloseableを実装しているので、try-with-resources文でクローズが可能です。
JDBC URLは、こんな感じで接続。今回は、特に認証はありません。
Getting Started / Connectivity
Connection conn = DriverManager.getConnection("jdbc:ignite:thin://localhost:10800");
テーブル作成は、こちら。
PreparedStatement createTablePs = conn.prepareStatement( "create table book(" + "isbn varchar primary key," + "title varchar," + "price int)" )) { createTablePs.executeUpdate();
CREATE TABLEの構文は、こちらを見ます。今回は、簡単に作成しました。
データ型については、こちらです。VARCHARやCHARに、サイズは指定しないみたいです。
INSERTやSELECTは、ふつうにできます。
try (PreparedStatement insertPs = conn.prepareStatement( "insert into book(isbn, title, price) values(?, ?, ?)")) { insertPs.setString(1, "978-1365732355"); insertPs.setString(2, "High Performance In-Memory Computing with Apache Ignite"); insertPs.setInt(3, 5282); insertPs.executeUpdate(); insertPs.setString(1, "978-1782169970"); insertPs.setString(2, "Infinispan Data Grid Platform Definitive Guide"); insertPs.setInt(3, 4947); insertPs.executeUpdate(); insertPs.setString(1, "978-1785285332"); insertPs.setString(2, "Getting Started With Hazelcast - Second Edition"); insertPs.setInt(3, 3848); insertPs.executeUpdate(); } try (PreparedStatement selectPs = conn.prepareStatement("select count(*) from book"); ResultSet rs = selectPs.executeQuery()) { assertThat(rs.next()).isTrue(); assertThat(rs.getInt(1)).isEqualTo(3); assertThat(rs.next()).isFalse(); } try (PreparedStatement selectPs = conn.prepareStatement( "select isbn, title, price from book where isbn = ?")) { selectPs.setString(1, "978-1365732355"); try (ResultSet rs = selectPs.executeQuery()) { assertThat(rs.next()).isTrue(); assertThat(rs.getString(1)).isEqualTo("978-1365732355"); assertThat(rs.getString(2)).isEqualTo("High Performance In-Memory Computing with Apache Ignite"); assertThat(rs.getInt(3)).isEqualTo(5282); assertThat(rs.next()).isFalse(); } }
COUNTもできています。
try (PreparedStatement selectPs = conn.prepareStatement("select count(*) from book");
UPDATEもOKです。
try (PreparedStatement updatePs = conn.prepareStatement( "update book set price = ? where isbn = ?")) { updatePs.setInt(1, 6000); updatePs.setString(2, "978-1365732355"); updatePs.executeUpdate(); } try (PreparedStatement selectPs = conn.prepareStatement( "select isbn, title, price from book where isbn = ?")) { selectPs.setString(1, "978-1365732355"); try (ResultSet rs = selectPs.executeQuery()) { assertThat(rs.next()).isTrue(); assertThat(rs.getString(1)).isEqualTo("978-1365732355"); assertThat(rs.getString(2)).isEqualTo("High Performance In-Memory Computing with Apache Ignite"); assertThat(rs.getInt(3)).isEqualTo(6000); assertThat(rs.next()).isFalse(); } }
条件を指定して、DELETE。
try (PreparedStatement deletePs = conn.prepareStatement("delete from book where isbn = ?")) { deletePs.setString(1, "978-1785285332"); deletePs.executeUpdate(); } try (PreparedStatement selectPs = conn.prepareStatement("select count(*) from book"); ResultSet rs = selectPs.executeQuery()) { assertThat(rs.next()).isTrue(); assertThat(rs.getInt(1)).isEqualTo(2); assertThat(rs.next()).isFalse(); }
データを全部消してみましょう。
try (PreparedStatement deletePs = conn.prepareStatement("delete from book")) { deletePs.executeUpdate(); } try (PreparedStatement selectPs = conn.prepareStatement("select count(*) from book"); ResultSet rs = selectPs.executeQuery()) { assertThat(rs.next()).isTrue(); assertThat(rs.getInt(1)).isEqualTo(0); assertThat(rs.next()).isFalse(); }
さらっとは使えそうな感じです。
いくつか気になるところ
ここまでで書いたコードですが、トランザクションを使っていませんね?
現時点のApache Igniteでは、SQLを使う場合はトランザクションは未サポートです。
Ignite Facts / Is Ignite a transactional database?
2.4.0以降で追加されそうな雰囲気でしたが、2.5.0では入らず。2.6.0で入ったりするのでしょうかね。
IGNITE-4191 / SQL: support transactions
トランザクションは未サポートですが、1回の更新処理についてはアトミックに行われます。
あと、主な制限。
How Ignite SQL Works / Known Limitations
WHERE句で使うサブクエリについては、データの配置を意識する必要があるようです。EXPLAINも未サポートみたいですね。
DDL "With"
では、少し方向性を変えて。
Apache Igniteでは、テーブルを作成する際に「WITH」でいろいろ指定することができます。
「template」で背後にあるCacheの種類を指定したり([])、「backups」でバックアップ数を指定したり、「AFFINITY_KEY」でデータの配置をコントロールする
キーを指定したり。
今回は、Cacheの種類を指定してみました。
@Test public void template() throws SQLException { try (Ignite ignite = Ignition.start(); Connection conn = DriverManager.getConnection("jdbc:ignite:thin://localhost:10800"); PreparedStatement createPartitionedTable = conn.prepareStatement( "create table p_book(" + "isbn varchar primary key," + "title varchar," + "price int) " + "with \"template = partitioned\""); PreparedStatement createReplicatedTable = conn.prepareStatement( "create table r_book(" + "isbn varchar primary key," + "title varchar," + "price int) " + "with \"template = replicated\"")) { createPartitionedTable.executeUpdate(); createReplicatedTable.executeUpdate(); try (PreparedStatement insertPs = conn.prepareStatement( "insert into p_book(isbn, title, price) values(?, ?, ?)")) { insertPs.setString(1, "978-1365732355"); insertPs.setString(2, "High Performance In-Memory Computing with Apache Ignite"); insertPs.setInt(3, 5282); insertPs.executeUpdate(); } try (PreparedStatement insertPs = conn.prepareStatement( "insert into r_book(isbn, title, price) values(?, ?, ?)")) { insertPs.setString(1, "978-1785285332"); insertPs.setString(2, "Getting Started With Hazelcast - Second Edition"); insertPs.setInt(3, 3848); insertPs.executeUpdate(); } try (PreparedStatement selectPs = conn.prepareStatement( "select isbn, title, price from p_book where isbn = ?")) { selectPs.setString(1, "978-1365732355"); try (ResultSet rs = selectPs.executeQuery()) { assertThat(rs.next()).isTrue(); assertThat(rs.getString(1)).isEqualTo("978-1365732355"); assertThat(rs.getString(2)).isEqualTo("High Performance In-Memory Computing with Apache Ignite"); assertThat(rs.getInt(3)).isEqualTo(5282); assertThat(rs.next()).isFalse(); } } try (PreparedStatement selectPs = conn.prepareStatement( "select isbn, title, price from r_book where isbn = ?")) { selectPs.setString(1, "978-1785285332"); try (ResultSet rs = selectPs.executeQuery()) { assertThat(rs.next()).isTrue(); assertThat(rs.getString(1)).isEqualTo("978-1785285332"); assertThat(rs.getString(2)).isEqualTo("Getting Started With Hazelcast - Second Edition"); assertThat(rs.getInt(3)).isEqualTo(3848); assertThat(rs.next()).isFalse(); } } } }
Partitioned Cacheと
PreparedStatement createPartitionedTable = conn.prepareStatement( "create table p_book(" + "isbn varchar primary key," + "title varchar," + "price int) " + "with \"template = partitioned\"");
Replicated Cacheで。
PreparedStatement createReplicatedTable = conn.prepareStatement( "create table r_book(" + "isbn varchar primary key," + "title varchar," + "price int) " + "with \"template = replicated\"")) {
Distributed Joins
最後に、JOINを試してみましょう。
データの配置(collocated)を意識すべきな気はしますが、今回はEmbeddedだし簡単にいってみましょう。一応、データの配置がNodeをまたいでもいいような
設定(「distributedJoins=true」)は入れておきます。
*今回はNodeがひとつなので、あんまり関係ないんですけどね…
つまり、JDBC URLがこうなります、と。
jdbc:ignite:thin://localhost:10800?distributedJoins=true
とりあえず、Partitionedなテーブルと、Replicatedなテーブルを作成してみます。
@Test public void joinAndAggregate() throws SQLException { try (Ignite ignite = Ignition.start(); Connection conn = DriverManager.getConnection("jdbc:ignite:thin://localhost:10800?distributedJoins=true"); PreparedStatement createBookTablePs = conn.prepareStatement( "create table book(" + "isbn varchar primary key," + "title varchar," + "price int," + "category_id int)" + "with \"template = partitioned\""); PreparedStatement createCategoryTablePs = conn.prepareStatement( "create table category(" + "id int primary key," + "name varchar) " + "with \"template = replicated\"")) { createBookTablePs.executeUpdate(); createCategoryTablePs.executeUpdate(); ... } }
データを入れて
try (PreparedStatement insertPs = conn.prepareStatement( "insert into book(isbn, title, price, category_id) values(?, ?, ?, ?)")) { insertPs.setString(1, "978-1365732355"); insertPs.setString(2, "High Performance In-Memory Computing with Apache Ignite"); insertPs.setInt(3, 5282); insertPs.setInt(4, 1); insertPs.executeUpdate(); insertPs.setString(1, "978-1782169970"); insertPs.setString(2, "Infinispan Data Grid Platform Definitive Guide"); insertPs.setInt(3, 4947); insertPs.setInt(4, 1); insertPs.executeUpdate(); insertPs.setString(1, "978-4798142470"); insertPs.setString(2, "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発"); insertPs.setInt(3, 4320); insertPs.setInt(4, 2); insertPs.executeUpdate(); insertPs.setString(1, "978-4774182179"); insertPs.setString(2, "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ"); insertPs.setInt(3, 4104); insertPs.setInt(4, 2); insertPs.executeUpdate(); insertPs.setString(1, "978-4774183169"); insertPs.setString(2, "パーフェクト Java EE"); insertPs.setInt(3, 3456); insertPs.setInt(4, 3); insertPs.executeUpdate(); } try (PreparedStatement insertPs = conn.prepareStatement( "insert into category(id, name) values(?, ?)")) { insertPs.setInt(1, 1); insertPs.setString(2, "In Memory Data Grid"); insertPs.executeUpdate(); insertPs.setInt(1, 2); insertPs.setString(2, "Spring"); insertPs.executeUpdate(); insertPs.setInt(1, 3); insertPs.setString(2, "Java EE"); insertPs.executeUpdate(); }
JOIN。
try (PreparedStatement selectPs = conn.prepareStatement( "select b.isbn, b.title, c.name " + "from book b inner join category c on b.category_id = c.id " + "where b.price > 4300 " + "order by b.price desc"); ResultSet rs = selectPs.executeQuery()) { assertThat(rs.next()).isTrue(); assertThat(rs.getString(1)).isEqualTo("978-1365732355"); assertThat(rs.getString(2)).isEqualTo("High Performance In-Memory Computing with Apache Ignite"); assertThat(rs.getString(3)).isEqualTo("In Memory Data Grid"); assertThat(rs.next()).isTrue(); assertThat(rs.getString(1)).isEqualTo("978-1782169970"); assertThat(rs.getString(2)).isEqualTo("Infinispan Data Grid Platform Definitive Guide"); assertThat(rs.getString(3)).isEqualTo("In Memory Data Grid"); assertThat(rs.next()).isTrue(); assertThat(rs.getString(1)).isEqualTo("978-4798142470"); assertThat(rs.getString(2)).isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発"); assertThat(rs.getString(3)).isEqualTo("Spring"); assertThat(rs.next()).isFalse(); }
GROUP BYに集計関数。
try (PreparedStatement selectPs = conn.prepareStatement( "select c.name, sum(b.price) " + "from book b inner join category c on b.category_id = c.id " + "group by c.name " + "order by sum(b.price) desc"); ResultSet rs = selectPs.executeQuery()) { assertThat(rs.next()).isTrue(); assertThat(rs.getString(1)).isEqualTo("In Memory Data Grid"); assertThat(rs.getInt(2)).isEqualTo(10229); assertThat(rs.next()).isTrue(); assertThat(rs.getString(1)).isEqualTo("Spring"); assertThat(rs.getInt(2)).isEqualTo(8424); assertThat(rs.next()).isTrue(); assertThat(rs.getString(1)).isEqualTo("Java EE"); assertThat(rs.getInt(2)).isEqualTo(3456); assertThat(rs.next()).isFalse(); }