HazelcastによるSpring Data向けのモジュールが、8月末くらいにリリースされているのに気付きました。
現時点のバージョンは、1.0です。
せっかくなので、こちらで遊んでみたいと思います。
Spring Data Hazelcastとは
Spring Data Key Valueをベースにしたモジュールのようです。
Spring Data Key-Value Reference Guide
Spring Data KeyValue 1.1.2.RELEASE API
こちらを使うことで、Key Valueなデータ構造に対して、Spring Dataでのアクセスができるようになるみたいですね。Spring Data Key Valueでは、Mapに対してのAdapterがあります。
で、これのHazelcast版のAdapterやクエリを実行する仕組みを備えたのが、Spring Data Hazelcastのようです。
Hazelcastでの利用するデータ構造は、IMap(Distributed Map)となります。
それでは、README.mdに沿って試してみましょう。
準備
Spring Data Hazelcastを使う際のMaven依存関係は、こちら。
<dependency> <groupId>com.hazelcast</groupId> <artifactId>spring-data-hazelcast</artifactId> <version>1.0</version> </dependency>
Spring Data Hazelcast 1.0で引き込まれてくるHazelcastのバージョンは、3.6.4です。現時点のHazelcastの最新版は、3.7.1ですが。
サンプルとして動かす際にはSpring Bootを使用したいと思いますので、pom.xmlの全体的な構成は以下のようになりました。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.littlewings</groupId> <artifactId>hazelcast-spring-data</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring.boot.version>1.4.0.RELEASE</spring.boot.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>com.hazelcast</groupId> <artifactId>spring-data-hazelcast</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
Entity
では、まずEntityを作成します。お題は書籍とします。
src/main/java/org/littlewings/hazelcast/spring/entity/Book.java
package org.littlewings.hazelcast.spring.entity; import java.io.Serializable; import org.springframework.data.annotation.Id; import org.springframework.data.keyvalue.annotation.KeySpace; @KeySpace("books") // @KeySpace を付けない場合は、EntityのFQCNがIMapの名前になる public class Book implements Serializable { @Id private String isbn; private String title; private int price; public static Book create(String isbn, String title, int price) { Book book = new Book(); book.setIsbn(isbn); book.setTitle(title); book.setPrice(price); return book; } public String getIsbn() { return isbn; } public void setIsbn(String isbn) { this.isbn = isbn; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } }
Serializableは実装しておきましょう。
キーとなる項目に対しては、@Idアノテーションを付与しておく必要があります。
@KeySpaceについてはあってもなくてもいいのですが、Hazelcastの場合はここで指定した名前がIMap(Distributed Map)の名前(データ格納先)となります。
Keyspaces
Repository
Spring Dataよろしく、こんなインターフェースを作成します。
src/main/java/org/littlewings/hazelcast/spring/repository/BookRepository.java
package org.littlewings.hazelcast.spring.repository; import java.util.List; import org.littlewings.hazelcast.spring.entity.Book; import org.springframework.data.hazelcast.repository.HazelcastRepository; public interface BookRepository extends HazelcastRepository<Book, String> { }
この時に継承するインターフェースは、HazelcastRepositoryインターフェースとなります。
Usage
現時点では、特にメソッド定義は行いません。これだけでもfindAllやcountなど、基本的なメソッドが使用できるのはよいなと思います。
Configuration
最後に、Spring Data Hazelcastの設定を行う必要があります。
src/main/java/org/littlewings/hazelcast/spring/HazelcastConfig.java
package org.littlewings.hazelcast.spring; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.hazelcast.HazelcastKeyValueAdapter; import org.springframework.data.hazelcast.repository.config.EnableHazelcastRepositories; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.KeyValueTemplate; @Configuration @EnableHazelcastRepositories public class HazelcastConfig { @Bean(destroyMethod = "shutdown") public HazelcastInstance hazelcastInstance() { return Hazelcast.newHazelcastInstance(); } @Bean public KeyValueOperations keyValueTemplate(HazelcastKeyValueAdapter keyValueAdapter) { return new KeyValueTemplate(keyValueAdapter); } @Bean public HazelcastKeyValueAdapter hazelcastKeyValueAdapter(HazelcastInstance hazelcast) { return new HazelcastKeyValueAdapter(hazelcast); } }
まず、@EnableHazelcastRepositoriesアノテーションを付けておくことがポイントです。
@Configuration @EnableHazelcastRepositories public class HazelcastConfig {
あとは、HazelcastInstance、HazelcastKeyValueAdapter、KeyValuteTemplateをセットアップする必要があります。
Usage
Hazelcast自体の設定は、この時にConfigで構築するか、設定ファイルから読み込んでHazelcastInstanceを作成すればよいでしょう。
使ってみる
それでは、使ってみます。実行は、テストコードで行うものとします。
まずは雛形。
src/test/java/org/littlewings/hazelcast/spring/SpringDataHazelcastTest.java
package org.littlewings.hazelcast.spring; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.query.EntryObject; import com.hazelcast.query.Predicate; import com.hazelcast.query.PredicateBuilder; import com.hazelcast.query.Predicates; import com.hazelcast.query.SqlPredicate; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.littlewings.hazelcast.spring.entity.Book; import org.littlewings.hazelcast.spring.repository.BookRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Sort; import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = HazelcastConfig.class) public class SpringDataHazelcastTest { // ここに、テストを書く! }
とりあえず、作成したRepositoryを@Autowiredしておきます。
@Autowired
BookRepository bookRepository;
とすると、Repository越しにsave、findAll、countなど基本的なメソッドが使用できるようになります。
@Test public void saveAndFind() { bookRepository.save(Book.create("978-1785285332", "Getting Started With Hazelcast", 3848)); assertThat(bookRepository.findOne("978-1785285332").getTitle()) .isEqualTo("Getting Started With Hazelcast"); assertThat(bookRepository.findAll()) .hasSize(1); assertThat(bookRepository.count()) .isEqualTo(1); }
また、HazelcastInstanceも@Autowiredして、実際にHazelcastのIMap(Distributed Map)にもデータが入ったことを確認してみましょう。
@Autowired HazelcastInstance hazelcast; @Test public void saveAndFindAndUnderlying() { bookRepository.save(Book.create("978-1785285332", "Getting Started With Hazelcast", 3848)); assertThat(bookRepository.findOne("978-1785285332").getTitle()) .isEqualTo("Getting Started With Hazelcast"); assertThat(bookRepository.findAll()) .hasSize(1); assertThat(bookRepository.count()) .isEqualTo(1); // assertThat(hazelcast.getMap("org.littlewings.hazelcast.spring.entity.Book")) // .hasSize(1); assertThat(hazelcast.getMap("books")) .hasSize(1); assertThat(hazelcast.<String, Book>getMap("books").get("978-1785285332").getTitle()) .isEqualTo("Getting Started With Hazelcast"); }
HazelcastInstance#getMapする時の名前が、@KeySpaceで指定した名前(未指定の場合はEntityのFQCN)となります。
Queryを投げる
Spring Data Hazelcastでも、作成したRepositoryに命名規則に沿ったメソッドを定義することで、Queryを定義することができます。
というか、これ自体はSpring Data Key Valueの話ですね。
Query methods
そんなわけで、先ほどのBookRepositoryにメソッドを追加します。
public interface BookRepository extends HazelcastRepository<Book, String> { Book findByTitle(String title); List<Book> findByPriceGreaterThan(int price); }
これで、Queryが投げられるようになります。
@Test public void query() { bookRepository.save(Arrays.asList(Book.create("978-1785285332", "Getting Started With Hazelcast", 3848), Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947), Book.create("978-1783988181", "Mastering Redis", 6172))); assertThat(bookRepository.findByTitle("Getting Started With Hazelcast").getPrice()) .isEqualTo(3848); List<Book> books = bookRepository.findByPriceGreaterThan(4000); assertThat(books.stream().map(Book::getTitle).collect(Collectors.toList())) .hasSize(2) .containsExactly("Infinispan Data Grid Platform Definitive Guide", "Mastering Redis"); }
すべての命名に沿ったQueryがサポートされているわけではないと思いますが、使えるのはこのあたりではないでしょうか。
https://github.com/hazelcast/spring-data-hazelcast/blob/v1.0/src/main/java/org/springframework/data/hazelcast/repository/query/HazelcastQueryCreator.java#L137-L185
Predicateを使う
先ほどのRepositoryインターフェースにメソッドを定義する形以外に、もうちょっと凝ったことをしたいと思う場合には、HazelcastのPredicateとSpring Data Key ValueのKeyValueTemplateとKeyValueQueryを使うのかなと思います。
※Spring Data Key ValueにQueryDSLのサポートがあったみたいでしたが、こちらは今回は置いておきます
Querydsl Extension
Configurationで定義した、KeyValueTemplate(KeyValueOperations)を@Autowiredします。
@Autowired
KeyValueOperations keyValueOperations;
あとは、こちらにKeyValueQueryを渡してQueryを作ります。KeyValueQueryには、HazelcastのPredicateを渡せばよさそうです。
以下、パターン別に見てみましょう。
PredicatesでQuery
@Test public void usingPredicates() { bookRepository.save(Arrays.asList(Book.create("978-1785285332", "Getting Started With Hazelcast", 3848), Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947), Book.create("978-1783988181", "Mastering Redis", 6172))); Predicate<String, Book> predicate = Predicates.and(Predicates.equal("isbn", "978-1785285332"), Predicates.greaterEqual("price", 3000)); KeyValueQuery<Predicate<String, Book>> query = new KeyValueQuery<>(predicate); Iterable<Book> books = keyValueOperations.find(query, Book.class); assertThat(StreamSupport.stream(books.spliterator(), false).map(Book::getTitle).collect(Collectors.toList())) .hasSize(1) .containsExactly("Getting Started With Hazelcast"); }
PredicateBuilder/EntryObjectでQuery
@Test public void usingPredicateBuilder() { bookRepository.save(Arrays.asList(Book.create("978-1785285332", "Getting Started With Hazelcast", 3848), Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947), Book.create("978-1783988181", "Mastering Redis", 6172))); EntryObject e = new PredicateBuilder().getEntryObject(); Predicate<String, Book> predicate = e.get("isbn").equal("978-1785285332").and(e.get("price").greaterEqual(3000)); KeyValueQuery<Predicate<String, Book>> query = new KeyValueQuery<>(predicate); Iterable<Book> books = keyValueOperations.find(query, Book.class); assertThat(StreamSupport.stream(books.spliterator(), false).map(Book::getTitle).collect(Collectors.toList())) .hasSize(1) .containsExactly("Getting Started With Hazelcast"); }
SqlPredicateでQuery
@Test public void usingSqlPredicate() { bookRepository.save(Arrays.asList(Book.create("978-1785285332", "Getting Started With Hazelcast", 3848), Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947), Book.create("978-1783988181", "Mastering Redis", 6172))); Predicate<String, Book> predicate = new SqlPredicate("price > 4000"); KeyValueQuery<Predicate<String, Book>> query = new KeyValueQuery<>(predicate); query.setSort(new Sort(Sort.Direction.DESC, "price")); Iterable<Book> books = keyValueOperations.find(query, Book.class); assertThat(StreamSupport.stream(books.spliterator(), false).map(Book::getTitle).collect(Collectors.toList())) .hasSize(2) .containsExactly("Mastering Redis", "Infinispan Data Grid Platform Definitive Guide"); }
しれっと、SqlPredicatesだけソートを入れてあります…。
KeyValueQueryに対してソートを仕込めるので、これを使えばいいのかな?
Sorting
Predicateでやる場合は、PagingPredicateを使うのだと思いますが、HazelcastのPredicateの説明自体はHazelcastのドキュメントを参照のこと…。
@Transactionalはどうした?
今回試していません。Hazelcast 3.7からSpringの@Transactionalのサポートが追加されているようなので、Hazelcastを3.7にしてhazelcast-springを使ったらできたりするのかな?
https://github.com/hazelcast/hazelcast/blob/v3.7.1/hazelcast-spring/src/main/java/com/hazelcast/spring/transaction/HazelcastTransactionManager.java
まとめ
Spring Data Hazelcastを試してみました。ベースのSpring Data Key Value自体、初めて使うものだったので少々設定などでオロオロしたところもありましたが、ふつうに使うことができました。
Springとの統合が進んでて、いいですねー。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/hazelcast-spring-data