CLOVER🍀

That was when it all began.

Spring Data Hazelcastで遊ぶ

HazelcastによるSpring Data向けのモジュールが、8月末くらいにリリースされているのに気付きました。

GitHub - hazelcast/spring-data-hazelcast: Hazelcast Spring Data integration Project http://projects.spring.io/spring-data/

現時点のバージョンは、1.0です。

せっかくなので、こちらで遊んでみたいと思います。

Spring Data Hazelcastとは

Spring Data Key Valueをベースにしたモジュールのようです。

Spring Data Key-Value Reference Guide

Spring Data KeyValue 1.1.2.RELEASE API

GitHub - spring-projects/spring-data-keyvalue: Project to provide infrastructure to implement Spring Data repositories on top of key-value-based, in-memory data stores.

こちらを使うことで、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

指定しない場合はクラスのFQCNIMap名が取られてしまうようなので、指定しておく方がいいのかなと思います。

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など、基本的なメソッドが使用できるのはよいなと思います。

Core concepts

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のドキュメントを参照のこと…。

Distributed Query

@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