完全に、ネタです。
Apache IgniteではSQL(ANSI-99)を使えるのですが、
これをDoma(2)と組み合わせて使ってみたいと思います。
https://doma.readthedocs.io/ja/2.19.2/
お題
Domaには各RDBMSの方言を吸収するDialectというものがあるわけですが、
標準実装っぽいものとしてStandardDialectというのがあるので、標準SQLを実装したApache Igniteで使えるんじゃなかろうか?
ちょっと遊んでみよう、というネタです。
今回は、Apache Igniteでクラスタを構成して、JDBC Driver経由でDomaを使ったアプリケーションに対してアクセスしてみます。
分散インメモリデータベースに対して、SQLを実行してみよう!と。
まあ、現在のApache Ignite+SQLではトランザクションが使えないのですが、そこはちょっと目をつぶる感じで。
構成としては、以下でいきます。
使用するApache Igniteのバージョンは2.5.0、Domaのバージョンは2.19.2とします。
Apache Igniteについては、マルチキャストでクラスタを構成します。設定ファイルは、こんな感じで。
config/default-config.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration"> <property name="discoverySpi"> <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi"> <property name="ipFinder"> <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder"> <property name="multicastGroup" value="228.10.10.157"/> </bean> </property> </bean> </property> </bean> </beans>
マルチキャスト通信が可能な環境で、bin/ignite.shで起動すると、クラスタが構成されます。
JavaとApache Mavenについては、こんな感じで。
$ 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: /path/to/.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"
では、いってみましょう。
準備
Maven依存関係は、こちら。
<dependency> <groupId>org.apache.ignite</groupId> <artifactId>ignite-core</artifactId> <version>2.5.0</version> </dependency> <dependency> <groupId>org.seasar.doma</groupId> <artifactId>doma</artifactId> <version>2.19.2</version> </dependency>
Apache Igniteについては、今回は「ignire-core」だけで大丈夫です。あと、Domaを足しておきます。
実行はテストコードで確認するので、JUnit 5と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>
Entity
実装するEntityは、書籍とそのカテゴリ、ということでいきましょう。
src/main/java/org/littlewings/ignite/entity/Book.java
package org.littlewings.ignite.entity; import org.seasar.doma.Entity; import org.seasar.doma.Id; @Entity public class Book { @Id private String isbn; private String title; private Integer price; private Integer categoryId; public static Book create(String isbn, String title, Integer price) { return create(isbn, title, price, null); } public static Book create(String isbn, String title, Integer price, Integer categoryId) { Book book = new Book(); book.setIsbn(isbn); book.setTitle(title); book.setPrice(price); book.setCategoryId(categoryId); return book; } // getter/setter }
src/main/java/org/littlewings/ignite/entity/Category.java
package org.littlewings.ignite.entity; import org.seasar.doma.Entity; import org.seasar.doma.Id; @Entity public class Category { @Id private Integer id; private String name; public static Category create(Integer id, String name) { Category category = new Category(); category.setId(id); category.setName(name); return category; } // getter/setter }
JOIN想定のEntityも作っておきます。
src/main/java/org/littlewings/ignite/entity/CategorizedBook.java
package org.littlewings.ignite.entity; import org.seasar.doma.Entity; import org.seasar.doma.Id; @Entity public class CategorizedBook { @Id private String isbn; private String title; private Integer price; private Integer categoryId; private String categoryName; // getter/setter }
Dao+SQL
Daoも作っておきます。テーブルのCREATE/DROPについては、今回はDaoの@Scriptで作ることにします。
src/main/java/org/littlewings/ignite/dao/BookDao.java
package org.littlewings.ignite.dao; import java.util.List; import org.littlewings.ignite.IgniteConfig; import org.littlewings.ignite.entity.Book; import org.seasar.doma.Dao; import org.seasar.doma.Insert; import org.seasar.doma.Script; import org.seasar.doma.Select; import org.seasar.doma.jdbc.SelectOptions; @Dao(config = IgniteConfig.class) public interface BookDao { @Script void dropTableIfExists(); @Script void createTable(); @Insert int insert(Book book); @Select List<Book> findAll(SelectOptions options); @Select Book findByIsbn(String isbn); }
なお、Configについてはまた後で。
@Dao(config = IgniteConfig.class)
CREATE TABLE。こちらは、Partitionedなテーブルとして定義しました。
src/main/resources/META-INF/org/littlewings/ignite/dao/BookDao/createTable.script
create table book( isbn varchar primary key, title varchar, price int, category_id int ) with "template = partitioned";
DROP TABLE。
src/main/resources/META-INF/org/littlewings/ignite/dao/BookDao/dropTableIfExists.script
drop table if exists book;
主キー検索。
src/main/resources/META-INF/org/littlewings/ignite/dao/BookDao/findByIsbn.sql
select /*%expand*/* from book where isbn = /* isbn */'foo'
全件取得
src/main/resources/META-INF/org/littlewings/ignite/dao/BookDao/findAll.sql
select /*%expand*/* from book
カテゴリについては、JOIN用途なので簡単に。
src/main/java/org/littlewings/ignite/dao/CategoryDao.java
package org.littlewings.ignite.dao; import org.littlewings.ignite.IgniteConfig; import org.littlewings.ignite.entity.Category; import org.seasar.doma.Dao; import org.seasar.doma.Insert; import org.seasar.doma.Script; @Dao(config = IgniteConfig.class) public interface CategoryDao { @Script void dropTableIfExists(); @Script void createTable(); @Insert int insert(Category category); }
CREATE TABLE。こちらのテーブルは、Replicatedにしました。
src/main/resources/META-INF/org/littlewings/ignite/dao/CategoryDao/createTable.script
create table category( id int primary key, name varchar ) with "template = replicated";
DROP TABLE。
src/main/resources/META-INF/org/littlewings/ignite/dao/CategoryDao/dropTableIfExists.script
drop table if exists category;
JOIN用途。
src/main/java/org/littlewings/ignite/dao/CategorizedBookDao.java
package org.littlewings.ignite.dao; import java.util.List; import org.littlewings.ignite.IgniteConfig; import org.littlewings.ignite.entity.CategorizedBook; import org.seasar.doma.Dao; import org.seasar.doma.Select; @Dao(config = IgniteConfig.class) public interface CategorizedBookDao { @Select List<CategorizedBook> findAllOrderByPriceDesc(); @Select List<CategorizedBook> sumGroupByCategory(Integer price); }
単純なJOINと
src/main/resources/META-INF/org/littlewings/ignite/dao/CategorizedBookDao/findAllOrderByPriceDesc.sql
select b.isbn, b.title, b.price, c.id as category_id, c.name as category_name from book b inner join category c on b.category_id = c.id order by b.price desc
ちょっと強引ですが、集約を利用。
src/main/resources/META-INF/org/littlewings/ignite/dao/CategorizedBookDao/sumGroupByCategory.sql
select c.name as category_name, sum(b.price) as price from book b inner join category c on b.category_id = c.id where b.price > /* price */100 group by category_name order by price desc
Config
ここまでは、ふつうにRDBMSへアクセスするかのごとくソースコードを書くだけですが、Apache Igniteとの接続はConfigインターフェースの実装クラスに
書くことになります。
https://doma.readthedocs.io/ja/2.19.2/config/
作成したのは、こちら。
src/main/java/org/littlewings/ignite/IgniteConfig.java
package org.littlewings.ignite; import javax.sql.DataSource; import org.seasar.doma.SingletonConfig; import org.seasar.doma.jdbc.Config; import org.seasar.doma.jdbc.Naming; import org.seasar.doma.jdbc.SimpleDataSource; import org.seasar.doma.jdbc.dialect.Dialect; import org.seasar.doma.jdbc.dialect.StandardDialect; @SingletonConfig public class IgniteConfig implements Config { private static final IgniteConfig CONFIG = new IgniteConfig(); private IgniteConfig() { } @Override public DataSource getDataSource() { SimpleDataSource dataSource = new SimpleDataSource(); dataSource.setUrl("jdbc:ignite:thin://172.26.0.2:10800,172.26.0.3:10800,172.26.0.4:10800?distributedJoins=true"); return dataSource; } @Override public Dialect getDialect() { return new StandardDialect(); } @Override public Naming getNaming() { return Naming.SNAKE_LOWER_CASE; } public static IgniteConfig singleton() { return CONFIG; } }
ポイントは、DialectはStandardDialectを選択したことと、
@Override public Dialect getDialect() { return new StandardDialect(); }
データソースの定義ですね。
@Override public DataSource getDataSource() { SimpleDataSource dataSource = new SimpleDataSource(); dataSource.setUrl("jdbc:ignite:thin://172.26.0.2:10800,172.26.0.3:10800,172.26.0.4:10800?distributedJoins=true"); return dataSource; }
Apache IgniteのSQL機能では、トランザクションが使えないのでLocalTransactionDataSourceではなくSimpleDataSourceを使っています。
JDBC URLについては、こちらを参照。
接続先が複数ホストある場合は、「,」で区切って指定します。
dataSource.setUrl("jdbc:ignite:thin://172.26.0.2:10800,172.26.0.3:10800,172.26.0.4:10800?distributedJoins=true");
ここに設定されたそれぞれのホストとは、通信を試みるようです。
参考までに、JDBCの接続URLの書式は、次の2通り。
// URL query pattern jdbc:ignite:thin://<hostAndPortRange0>[,<hostAndPortRange1>]...[,<hostAndPortRangeN>][/schema][?<params>] hostAndPortRange := host[:port_from[..port_to]] params := param1=value1[¶m2=value2]...[¶mN=valueN] // Semicolon pattern jdbc:ignite:thin://<hostAndPortRange0>[,<hostAndPortRange1>]...[,<hostAndPortRangeN>][;schema=<schema_name>][;param1=value1]...[;paramN=valueN]
なお、Apache Ignite 2.5.0のドキュメントには、IgniteJdbcThinDataSourceというデータソースが登場するのですが
ドキュメントに載っている割には、Apache Ignite 2.5.0には含まれていませんでした。現在のmasterブランチには存在するので、そのうち入るでしょう。
テストコードの雛形
テストコードの雛形は、こちら。
src/test/java/org/littlewings/ignite/IgniteDomaTest.java
package org.littlewings.ignite; import java.util.List; import org.junit.jupiter.api.Test; import org.littlewings.ignite.dao.BookDao; import org.littlewings.ignite.dao.BookDaoImpl; import org.littlewings.ignite.dao.CategorizedBookDao; import org.littlewings.ignite.dao.CategorizedBookDaoImpl; import org.littlewings.ignite.dao.CategoryDao; import org.littlewings.ignite.dao.CategoryDaoImpl; import org.littlewings.ignite.entity.Book; import org.littlewings.ignite.entity.CategorizedBook; import org.littlewings.ignite.entity.Category; import org.seasar.doma.jdbc.SelectOptions; import static org.assertj.core.api.Assertions.assertThat; class IgniteDomaTest { // ここに、テストを書く!! }
使ってみる
それでは、まずは簡単に使ってみましょう。
@Test public void simpleGettingStarted() { BookDao dao = new BookDaoImpl(); dao.dropTableIfExists(); dao.createTable(); dao.insert(Book.create("978-1365732355", "High Performance In-Memory Computing with Apache Ignite", 5282)); dao.insert(Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947)); dao.insert(Book.create("978-1785285332", "Getting Started With Hazelcast - Second Edition", 3848)); Book book = dao.findByIsbn("978-1365732355"); assertThat(book.getIsbn()).isEqualTo("978-1365732355"); assertThat(book.getTitle()).isEqualTo("High Performance In-Memory Computing with Apache Ignite"); assertThat(book.getPrice()).isEqualTo(5282); SelectOptions options = SelectOptions.get().count(); List<Book> books = dao.findAll(options); assertThat(books).hasSize(3); assertThat(options.getCount()).isEqualTo(3L); }
Daoのインスタンスを作成し、テーブルのDROP/CREATE。
BookDao dao = new BookDaoImpl();
dao.dropTableIfExists();
dao.createTable();
データの登録。
dao.insert(Book.create("978-1365732355", "High Performance In-Memory Computing with Apache Ignite", 5282)); dao.insert(Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947)); dao.insert(Book.create("978-1785285332", "Getting Started With Hazelcast - Second Edition", 3848));
検索。COUNTもできます。
Book book = dao.findByIsbn("978-1365732355"); assertThat(book.getIsbn()).isEqualTo("978-1365732355"); assertThat(book.getTitle()).isEqualTo("High Performance In-Memory Computing with Apache Ignite"); assertThat(book.getPrice()).isEqualTo(5282); SelectOptions options = SelectOptions.get().count(); List<Book> books = dao.findAll(options); assertThat(books).hasSize(3); assertThat(options.getCount()).isEqualTo(3L);
接続設定さえできていれば、割とあっさりと動きました。
JOINしてみる
続いて、JOINのパターン。
@Test public void distributedJoin() { BookDao bookDao = new BookDaoImpl(); CategoryDao categoryDao = new CategoryDaoImpl(); bookDao.dropTableIfExists(); bookDao.createTable(); categoryDao.dropTableIfExists(); categoryDao.createTable(); bookDao.insert(Book.create("978-1365732355", "High Performance In-Memory Computing with Apache Ignite", 5282, 1)); bookDao.insert(Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947, 1)); bookDao.insert(Book.create("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320, 2)); bookDao.insert(Book.create("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4104, 2)); bookDao.insert(Book.create("978-4774183169", "パーフェクト Java EE", 3456, 3)); categoryDao.insert(Category.create(1, "In Memory Data Grid")); categoryDao.insert(Category.create(2, "Spring")); categoryDao.insert(Category.create(3, "Java EE")); CategorizedBookDao categorizedBookDao = new CategorizedBookDaoImpl(); List<CategorizedBook> books = categorizedBookDao.findAllOrderByPriceDesc(); assertThat(books).hasSize(5); assertThat(books.get(0).getIsbn()).isEqualTo("978-1365732355"); assertThat(books.get(0).getTitle()).isEqualTo("High Performance In-Memory Computing with Apache Ignite"); assertThat(books.get(0).getPrice()).isEqualTo(5282); assertThat(books.get(0).getCategoryId()).isEqualTo(1); assertThat(books.get(0).getCategoryName()).isEqualTo("In Memory Data Grid"); assertThat(books.get(4).getIsbn()).isEqualTo("978-4774183169"); assertThat(books.get(4).getTitle()).isEqualTo("パーフェクト Java EE"); assertThat(books.get(4).getPrice()).isEqualTo(3456); assertThat(books.get(4).getCategoryId()).isEqualTo(3); assertThat(books.get(4).getCategoryName()).isEqualTo("Java EE"); List<CategorizedBook> summarizedBooks = categorizedBookDao.sumGroupByCategory(4200); assertThat(summarizedBooks).hasSize(2); assertThat(summarizedBooks.get(0).getCategoryName()).isEqualTo("In Memory Data Grid"); assertThat(summarizedBooks.get(0).getPrice()).isEqualTo(10229); assertThat(summarizedBooks.get(1).getCategoryName()).isEqualTo("Spring"); assertThat(summarizedBooks.get(1).getPrice()).isEqualTo(4320); }
2つのテーブル向けのDaoのインスタンスと、テーブルの作成。
BookDao bookDao = new BookDaoImpl(); CategoryDao categoryDao = new CategoryDaoImpl(); bookDao.dropTableIfExists(); bookDao.createTable(); categoryDao.dropTableIfExists(); categoryDao.createTable();
データの登録。
bookDao.insert(Book.create("978-1365732355", "High Performance In-Memory Computing with Apache Ignite", 5282, 1)); bookDao.insert(Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947, 1)); bookDao.insert(Book.create("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320, 2)); bookDao.insert(Book.create("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4104, 2)); bookDao.insert(Book.create("978-4774183169", "パーフェクト Java EE", 3456, 3)); categoryDao.insert(Category.create(1, "In Memory Data Grid")); categoryDao.insert(Category.create(2, "Spring")); categoryDao.insert(Category.create(3, "Java EE"));
JOIN。
CategorizedBookDao categorizedBookDao = new CategorizedBookDaoImpl(); List<CategorizedBook> books = categorizedBookDao.findAllOrderByPriceDesc(); assertThat(books).hasSize(5); assertThat(books.get(0).getIsbn()).isEqualTo("978-1365732355"); assertThat(books.get(0).getTitle()).isEqualTo("High Performance In-Memory Computing with Apache Ignite"); assertThat(books.get(0).getPrice()).isEqualTo(5282); assertThat(books.get(0).getCategoryId()).isEqualTo(1); assertThat(books.get(0).getCategoryName()).isEqualTo("In Memory Data Grid"); assertThat(books.get(4).getIsbn()).isEqualTo("978-4774183169"); assertThat(books.get(4).getTitle()).isEqualTo("パーフェクト Java EE"); assertThat(books.get(4).getPrice()).isEqualTo(3456); assertThat(books.get(4).getCategoryId()).isEqualTo(3); assertThat(books.get(4).getCategoryName()).isEqualTo("Java EE");
JOIN+集約。
List<CategorizedBook> summarizedBooks = categorizedBookDao.sumGroupByCategory(4200); assertThat(summarizedBooks).hasSize(2); assertThat(summarizedBooks.get(0).getCategoryName()).isEqualTo("In Memory Data Grid"); assertThat(summarizedBooks.get(0).getPrice()).isEqualTo(10229); assertThat(summarizedBooks.get(1).getCategoryName()).isEqualTo("Spring"); assertThat(summarizedBooks.get(1).getPrice()).isEqualTo(4320);
こちらも、割とあっさりと。
ハマったところは接続設定くらいで、それ以外については動きそうな感じです。
ハマったこと
では、ここからは少しハマったことについて書いていこうと思います。
JDBC Driver or JDBC Client Driver
Apache IgniteのSQLドキュメントを見ると、JDBCには「JDBC Driver」と「JDBC Client Driver」の2種類があります。
JDBC Client Driverは、通常のApache IgniteのClient/Serverの関係だ、という感じだったので、最初はこちらを選ぼうとしました。ですが、結果として
選んだのはJDBC Driverです。
まず、どうにもダメだったのが「テーブル名にスキーマを明示的に付与しないと動かない」、でした。
例えば、bookテーブルであれば
select ... from public.book
といった感じに、publicスキーマであることを明示しないと動きません。INSERT文などを自動生成するDomaでは、これはとても痛いです。
その他、このあたりも気になるところです。
- 設定ファイルが必須になる(JDBC URLで、設定ファイルへのパスを指定する)
- 設定ファイルが必要になる関係上、「ignite-spring」モジュールが必要になる。接続したいだけなのに
- Node Discoveryの設定が必要
- クラスタのメンバー(Client)として参加
クライアント側にデータを持ちたくないのでJDBC Client Driverを使おうと思ったのですが、JDBC Driverの方もドライバが受けた内容をServerへ送って処理を
するようになっているみたいなので、実質同じような気が…(データを持たないという意味で)。
こう見ると、JDBC Driverの方がJDBC Client Driverに比べると便利なように見えるのですが…どうなのでしょうね。
JDBC Driver側で気になることがあるとすれば、接続先ホストを列挙しなくてはいけないことでしょうかね。Node Discoveryによる接続先Nodeの探索は
できません。
この点はちょっと気になりますが、スキーマ名をSQLに明示しなくてはいけないのはとても辛いので、JDBC Driverにしました。
デフォルトエンコーディング
今回のテストコードを使ってテストを行う時に、日本語でデータを登録すると、取得時に文字化けするという現象にあたりました。
CategorizedBookDao categorizedBookDao = new CategorizedBookDaoImpl(); List<CategorizedBook> books = categorizedBookDao.findAllOrderByPriceDesc(); assertThat(books).hasSize(5); assertThat(books.get(0).getIsbn()).isEqualTo("978-1365732355"); assertThat(books.get(0).getTitle()).isEqualTo("High Performance In-Memory Computing with Apache Ignite"); assertThat(books.get(0).getPrice()).isEqualTo(5282); assertThat(books.get(0).getCategoryId()).isEqualTo(1); assertThat(books.get(0).getCategoryName()).isEqualTo("In Memory Data Grid"); assertThat(books.get(4).getIsbn()).isEqualTo("978-4774183169"); assertThat(books.get(4).getTitle()).isEqualTo("パーフェクト Java EE"); assertThat(books.get(4).getPrice()).isEqualTo(3456); assertThat(books.get(4).getCategoryId()).isEqualTo(3); assertThat(books.get(4).getCategoryName()).isEqualTo("Java EE");
でも、接続先にcharsetの指定ができるわけでもなく、CREATE TABLE時にも指定できないので、どうすればいいのかな?と思ったのですが、指定する方法が
ないあたりから試してみて、Apache IgniteのSQL機能がデフォルトのcharsetで動作していることがなんとなくわかりました。
結果として、Apache IgniteのServer起動時に、デフォルトのcharsetをUTF-8にすると、解消しましたからね…。
-Dfile.encoding=UTF-8
特に設定まわりでだいぶてこずりましたが、一応目的となるポイントまでは確認できたのでよかった、かな。