CLOVER🍀

That was when it all began.

Apache Ignite上で、SQLを動かして遊ぶ

Apache Igniteはインメモリ・データグリッドの一種ですが、SQLANSI-99)をサポートしています。

What is Ignite? / Complete SQL Support

Overview

なんとまあ、JDBCドライバまであるわけですよ。

JDBC Driver

ちょっと面白そうだったので、試してみることにしました。

Apache IgniteSQLサポート

ここを見ればよいのですが、

Overview

ANSI-99に準拠している、分散SQLデータベースをApache Igniteをサポートしています。

DMLおよびDDLをサポートし、さらにDistributed JOINが可能です。データの配置(collocated and non-collocated)に関わらず、
JOINを行うことができます。

データをインメモリにのみ持つか、ディスクも含めて永続化するかについては、設定次第です。

SQLの機能は、JDBCODBCから利用することができます。

環境

確認した環境は、こちら。

$ 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バージョンは、2.5.0です。

お題

今回は、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 IgniteSQLデータベースとしてEmbeddedに扱う場合は、「ignite-core」と「ignite-indexing」の2つのモジュールが必要です。
JDBCドライバは、「ignite-core」に含まれています。

進めるにあたっての、Getting Startedはこちら。

Getting Started

使用できる、SQLのリファレンスはこちらです。

SQL Reference Overview

また、テストコードで確認していくので、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 {
    // ここに、テストを書く!
}

この中に、テストコードを書きつつ、Apache IgniteSQLデータベースとして使っていってみましょう。

使ってみる

では、とりあえず接続してテーブルを作ってみます。

    @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の構文は、こちらを見ます。今回は、簡単に作成しました。

CREATE TABLE

データ型については、こちらです。VARCHARやCHARに、サイズは指定しないみたいです。

Data Types

INSERTやSELECTは、ふつうにできます。

SELECT

INSERT

            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です。

UPDATE

            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。

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」でいろいろ指定することができます。

CREATE TABLE

「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を試してみましょう。

Distributed Joins

データの配置(collocated)を意識すべきな気はしますが、今回はEmbeddedだし簡単にいってみましょう。一応、データの配置がNodeをまたいでもいいような
設定(「distributedJoins=true」)は入れておきます。
*今回はNodeがひとつなので、あんまり関係ないんですけどね…

JDBC Driver / Parameters

つまり、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();
            }

まとめ

とりあえず、簡単に、かつEmbeddedな使い方ではありますが、Apache IgniteSQLデータベースとして使ってみました。

Client/Serverはまた別にやりたいのと、これくらいの使い方ではとりあえず困らない感じ…?

どちらかというと、接続まわりの情報でいろいろ悩むことが多かったですね…。