CLOVER🍀

That was when it all began.

Spring Data GemFire × Apache Geode

Spring Data GemFireとApache Geodeを、組み合わせて使えると聞いて。

Spring Data for Pivotal GemFire

Spring Data GemFire supports Apache Geode

New in the 1.7 Release

Spring Data GemFire 1.7から、Apache GeodeのEarly Access supportが始まっているそうです。

Spring Data GemFireについては、詳細はドキュメントを参照のこと、ですが

Spring Data GemFire Reference Guide

GemFireへのデータアクセスやOQLによるクエリの発行、トランザクション管理、Function実行などをSpring Dataと統合できる仕組みのようです。

Apache Geode で始める Spring Data Gemfire

Spring Data GemFireで、GemFireの代わりにApache Geodeを使いたければSprinngのマイルストーンリポジトリを足して

http://repo.spring.io/milestone/org/springframework/data/spring-data-gemfire/1.7.0.APACHE-GEODE-EA-M1/

Geode向けの1.7.0リリースを使うのかなと思っていたのですが

GitHub - spring-projects/spring-data-gemfire at 1.7.0.APACHE-GEODE-EA-M1

こちらを見ていると、Spring Data GemFireにApache Geodeを足す感じでも大丈夫そうだったので、このアプローチでいくことにしました。

Apache Geode で始める Spring Data Gemfire

では、早速試してみます。

構成は、Spring Bootを一緒に使い、Spring Data GemFireとApache Geodeを合わせて使い、テストコードで確認する流れにしたいと思います。なお、構成はPeer-to-Peerとします。

サンプルとして参考にしたのは、Spring Bootのサンプルプロジェクトですね。

https://github.com/spring-projects/spring-boot/tree/v1.3.3.RELEASE/spring-boot-samples/spring-boot-sample-data-gemfire

準備

Mavenの依存関係およびプラグインの設定は、こんな感じ。「spring-boot-starter-data-gemfire」を入れて、GemFireを抜いてApache Geodeを足しています。

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.3.RELEASE</version>
    </parent>

    <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>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.geode</groupId>
            <artifactId>gemfire-core</artifactId>
            <version>1.0.0-incubating.M1</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-slf4j-impl</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-gemfire</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.gemstone.gemfire</groupId>
                    <artifactId>gemfire</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.4.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

まあ、Spring BootのMavenプラグインは、今回なくてもよかった気がしますが…。

あと、ふつうに起動しようとするとLog4j2とSLF4Jが衝突して起動できなかったので、SLF4Jに肩寄せておきました。

Entity

お題は書籍ということで、Apache Geodeに格納するためのクラスを作成します。@Regionアノテーションを付与して、Regionの名前を指定しておきます。今回は、「bookRegion」とします。また、Serializableにしておくことを、お忘れなく。

あと、キーとなるものには@Idアノテーションを付与しておきます。
src/main/java/org/littlewings/geode/spring/entity/Book.java

package org.littlewings.geode.spring.entity;

import java.io.Serializable;

import org.springframework.data.annotation.Id;
import org.springframework.data.gemfire.mapping.Region;

@Region("bookRegion")
public class Book implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    private String isbn;

    private String title;

    private Integer price;

    public static Book create(String isbn, String title, Integer 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 Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }
}

Repository

Repositoryは、GemfireRepositoryインターフェースを継承したインターフェースを作って作成します。
src/main/java/org/littlewings/geode/spring/repository/BookRepository.java

package org.littlewings.geode.spring.repository;

import java.util.List;

import org.littlewings.geode.spring.entity.Book;
import org.springframework.data.gemfire.repository.GemfireRepository;
import org.springframework.data.gemfire.repository.Query;

public interface BookRepository extends GemfireRepository<Book, String> {
    Book findByTitle(String title);

    Book findByTitleAndPrice(String title, int price);

    @Query("<TRACE> SELECT * FROM /bookRegion b WHERE b.price > $1 AND b.price < $2 ORDER BY b.price ASC")
    List<Book> findByPriceLessThanAndGreaterThan(int priceLower, int priceUpper);
}

GemfireRepositoryインターフェースの継承により、ある程度のCRUD操作は実現できますが、Spring Data JPAなどと同じように命名規約に沿ったfind〜メソッドを定義することで、OQLを自動的に作成して発行することもできます。

Executing OQL Queries

また、@Queryアノテーションにより直接OQLを記述することもできます。

Service

Serviceクラスは、以下のようにRepositoryを利用する形で実装。全体的に@Transactionalアノテーションを付与しています。
src/main/java/org/littlewings/geode/spring/service/BookService.java

package org.littlewings.geode.spring.service;

import org.littlewings.geode.spring.entity.Book;
import org.littlewings.geode.spring.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BookService {
    @Autowired
    protected BookRepository bookRepository;

    @Transactional(readOnly = false)
    public long count() {
        return bookRepository.count();
    }

    @Transactional(readOnly = false)
    public Book get(String title) {
        return bookRepository.findByTitle(title);
    }

    @Transactional(readOnly = false)
    public Iterable<Book> list() {
        return bookRepository.findAll();
    }

    @Transactional
    public Book save(Book book) {
        return bookRepository.save(book);
    }

    @Transactional
    public Book saveThrown(Book book) {
        bookRepository.save(book);

        throw new RuntimeException("Oops!!");
    }

    @Transactional
    public void deleteAll() {
        bookRepository.deleteAll();
    }
}

今回のサンプルでは@TransactionalアノテーションはRead系のメソッドについてはあってもなくてもよいのですが、saveでは例外スロー時にロールバックすることを確認するために付与しています。

設定と@SpringBoootApplication

Apache GeodeのCacheやRegionを、JavaConfigで設定します。
src/main/java/org/littlewings/geode/spring/config/GeodeConfig.java

package org.littlewings.geode.spring.config;

import com.gemstone.gemfire.cache.Cache;
import com.gemstone.gemfire.cache.PartitionAttributes;
import com.gemstone.gemfire.cache.RegionAttributes;
import org.littlewings.geode.spring.entity.Book;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.gemfire.CacheFactoryBean;
import org.springframework.data.gemfire.GemfireTransactionManager;
import org.springframework.data.gemfire.PartitionAttributesFactoryBean;
import org.springframework.data.gemfire.PartitionedRegionFactoryBean;
import org.springframework.data.gemfire.RegionAttributesFactoryBean;

@Configuration
public class GeodeConfig {
    @Bean
    public CacheFactoryBean geodeCache() {
        CacheFactoryBean cacheFactory = new CacheFactoryBean();
        cacheFactory.setClose(true);
        return cacheFactory;
    }

    @Bean(name = "bookRegion")
    public PartitionedRegionFactoryBean<String, Book> partitionedRegion(Cache cache, RegionAttributes<String, Book> regionAttributes) {
        PartitionedRegionFactoryBean<String, Book> partitionedRegionFactory = new PartitionedRegionFactoryBean<>();
        partitionedRegionFactory.setAttributes(regionAttributes);
        partitionedRegionFactory.setClose(false);
        partitionedRegionFactory.setCache(cache);
        partitionedRegionFactory.setRegionName("bookRegion");
        partitionedRegionFactory.setPersistent(false);
        return partitionedRegionFactory;
    }

    @Bean
    public RegionAttributesFactoryBean regionAttributes(PartitionAttributes<String, Book> partitionAttributes) {
        RegionAttributesFactoryBean regionAttributesFactory = new RegionAttributesFactoryBean();
        regionAttributesFactory.setKeyConstraint(String.class);
        regionAttributesFactory.setValueConstraint(Book.class);
        regionAttributesFactory.setPartitionAttributes(partitionAttributes);
        return regionAttributesFactory;
    }

    @Bean
    public PartitionAttributesFactoryBean partitionAttributes() {
        PartitionAttributesFactoryBean partitionAttributesFactory = new PartitionAttributesFactoryBean();
        partitionAttributesFactory.setRedundantCopies(1);
        return partitionAttributesFactory;
    }

    @Bean
    public GemfireTransactionManager gemfireTransactionManager(Cache cache) {
        return new GemfireTransactionManager(cache);
    }
}

今回はPartitioned Regionとし、region-attributesもいくつか設定しました…が、Cache XMLで書いた方が簡単な気も…。

とりあえず、サンプルに習いFactoryBeanで頑張って作ってみました。ここで、今回作ったEntityやRepositoryで使う「bookRegion」を作っておきます。

最後のTransactionManagerは、Apache Geodeトランザクションを利用(@Transactional)するために使います。

あとは、mainメソッドはないですけど、本来アプリケーションのエントリポイントになりそうなクラスを。
src/main/java/org/littlewings/geode/spring/App.java

package org.littlewings.geode.spring;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableGemfireRepositories
@EnableTransactionManagement
public class App {
}

@EnableGemfireRepositoriesを付与してSpring Data GemFireを利用するようにし、トランザクションも使えるように@EnableTransactionManagementを付与しておきます。

確認

それでは、テストコードを書いて確認してみます。

テストコード全体としては、こんな感じ。
src/test/java/org/littlewings/geode/spring/SpringDataGemfireTest.java

package org.littlewings.geode.spring;

import java.util.Arrays;
import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.littlewings.geode.spring.entity.Book;
import org.littlewings.geode.spring.repository.BookRepository;
import org.littlewings.geode.spring.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(App.class)
public class SpringDataGemfireTest {
    private static final Book SPRING_BOOT_BOOK = Book.create("978-4777518654", "はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発", 2700);
    private static final Book JAVA_EE_7_BOOK = Book.create("978-4798140926", "Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104);
    private static final Book ELASTICSEARCH_BOOK = Book.create("978-4048662024", "高速スケーラブル検索エンジン ElasticSearch Server", 3024);
    private static List<Book> ALL_BOOKS = Arrays.asList(SPRING_BOOT_BOOK, JAVA_EE_7_BOOK, ELASTICSEARCH_BOOK);

    // ここに、テストを書く!
}


まずは、Repositoryを@Autowiredしてみます。

    @Autowired
    protected BookRepository bookRepository;

あらかじめGemfireRepositoryで用意されたメソッドや、今回作成したメソッドを利用してみます。

    @Test
    public void springDataGemfireGettingStarted() {
        bookRepository.save(ALL_BOOKS);

        assertThat(bookRepository.findAll())
                .hasSize(3);
        assertThat(bookRepository.count())
                .isEqualTo(3);

        assertThat(bookRepository.findByTitle("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発").getIsbn())
                .isEqualTo("978-4777518654");

        assertThat(bookRepository.findByTitleAndPrice("Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築", 4104).getIsbn())
                .isEqualTo("978-4798140926");

        bookRepository.deleteAll();

        assertThat(bookRepository.findAll())
                .isEmpty();
    }

続いて、自分で@QueryでOQLを書いた場合。

    @Test
    public void usingOql() {
        bookRepository.save(ALL_BOOKS);

        List<Book> books = bookRepository.findByPriceLessThanAndGreaterThan(3000, 4500);

        assertThat(books)
                .hasSize(2);
        assertThat(books.get(0).getTitle())
                .isEqualTo("高速スケーラブル検索エンジン ElasticSearch Server");
        assertThat(books.get(1).getTitle())
                .isEqualTo("Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築");
    }

OKそうですね。

なお、テストの開始時にデータは毎回クリアするようにしています。

    @Before
    public void setUp() {
        bookRepository.deleteAll();
    }

今度は、Serviceを使ってみましょう。

    @Autowired
    protected BookService bookService;

Read Onlyなトランザクションを使うパターン。

    @Test
    public void usingService() {
        ALL_BOOKS.forEach(bookService::save);

        assertThat(bookService.get("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発").getIsbn())
                .isEqualTo("978-4777518654");

        assertThat(bookService.list())
                .hasSize(3);
    }

更新、およびロールバックの確認。

    @Test
    public void transactional() {
        bookService.save(SPRING_BOOT_BOOK);
        assertThat(bookService.get("はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発").getIsbn())
                .isEqualTo("978-4777518654");

        assertThatThrownBy(() -> bookService.saveThrown(JAVA_EE_7_BOOK))
                .isInstanceOf(RuntimeException.class)
                .hasMessage("Oops!!");
        assertThat(bookService.get("Java EE 7徹底入門 標準Javaフレームワークによる高信頼性Webシステムの構築"))
                .isNull();
    }

こちらもOKそうです。

ハマったこと

実は、最初はClient/Server構成で書いていました。

が、Client/Server構成の時にトランザクション内でOQLを投げると軒並みエラーになって、断念…。

ここでキャストに失敗し続けていたので、今回はパスしました…。
https://github.com/apache/incubator-geode/blob/rel/v1.0.0-incubating.M1/gemfire-core/src/main/java/com/gemstone/gemfire/internal/cache/EntriesSet.java#L192

またバージョン上がったら、試してみる…かも?

まとめ

Spring Data GemFireを、Apache Geodeと合わせて動かしてみました。

Repositoryを使ってOQLを投げられたり、@Transactionalも使えて便利ですね(Functionの実行は今回試しませんでしたが)。

こういうふうにSpring Dataと統合されていると、使う側としては利用しやすそうだなーと思いました。