CLOVER🍀

That was when it all began.

1.0.0.INCUBATINGになったSpring Data Geodeを軽く試す

Apache Geodeが1.0.0-incubatingとしてリリースされていますが、合わせてSpring Data Geodeも
1.0.0.INCUBATING-RELEASEになっています。

とはいえ、Spring Data Geode用のページはなくて、相変わらずSpring Data Gemfireな
わけですが。

Spring Data for Pivotal GemFire

Spring Data Geodeは、Spring Data GemfireのGitHubリポジトリ内で、別ブランチとして
管理されています。

GitHub - spring-projects/spring-data-gemfire at apache-geode

ドキュメント(Spring Data Gemfire)はこちら。

Spring Data GemFire Reference Guide

今回、Apache Geodeが1.0.0-incubatingになって初めてSpring Data Geode含めて扱うので、
まずは軽く試してみたいと思います。

本エントリとしてのお題は、

  • Apache GeodeはClient/Server Modeで動かす
  • Spring Data GeodeをSpring Bootを含めて起動できる環境を作る
  • Spring Data Geodeで、GemfireRepositoryを使ってみる(Queryは含まない)

とします。要するに、Hello Worldレベルですね。

めっちゃハマりましたが。

では、いってみたいと思います。

準備

アプリケーション側(Maven

Mavenの依存関係などは、こんな感じ。

    <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.4.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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-geode</artifactId>
            <version>1.0.0.INCUBATING-RELEASE</version>
        </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>

Spring Bootは、1.4.4.RELEASEとしました。最新の1.5.1.RELEASEではありません。

また、Spring Data Geodeにはstarterがないので、直接Spring Data Geodeを依存関係に足しています。

Apache GeodeのLocator/Serverの準備

LocatorとServerをそれぞれ起動しておきます。今回は、ひとつずつ起動しておくものとします。

Cacheの設定は、以下のようにしておきました。
cache.xml

<?xml version="1.0" encoding="UTF-8"?>
<cache
    xmlns="http://geode.apache.org/schema/cache"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://geode.apache.org/schema/cache
                        http://geode.apache.org/schema/cache/cache-1.0.xsd"
    version="1.0">
  <region name="myRegion" refid="PARTITION_REDUNDANT">
  </region>
</cache>

Locator、Server、そしてRegion。

gfsh>list members
        Name         | Id
-------------------- | --------------------------------------------------------
locator-geodelocator | 172.19.0.2(locator-geodelocator:35:locator)<ec><v0>:1024
server-1fcdc39cf249  | 172.19.0.3(server-1fcdc39cf249:154)<v3>:1024

gfsh>list regions
List of regions
---------------
myRegion

アプリケーションの作成

では、Spring Data Geodeで作成するアプリケーションを作っていきます。

最初に、Apache Geodeに保存するエンティティを作成。お題は書籍とします。
src/main/java/org/littlewings/geode/spring/Book.java

package org.littlewings.geode.spring;

import java.io.Serializable;

import org.springframework.data.annotation.Id;

public class Book implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    private String isbn;
    private String title;
    private Integer price;

    public Book(String isbn, String title, Integer price) {
        this.isbn = isbn;
        this.title = title;
        this.price = price;
    }

    public Book() {
    }

    // getter/setterは省略
}

Serializableを実装し、Spring Dataの@Idを付与している以外はふつうのJavaBeansです。

次に、このBookクラスを使うRepositoryを作成。今回は、GemfireRepositoryインターフェースを継承しているだけで、
追加メソッドは作成しません。また、@Regionでエンティティの保存先のRegionを指定しておきます。
src/main/java/org/littlewings/geode/spring/BookRepository.java

package org.littlewings.geode.spring;

import org.springframework.data.gemfire.mapping.Region;
import org.springframework.data.gemfire.repository.GemfireRepository;

@Region("myRegion")
public interface BookRepository extends GemfireRepository<Book, String> {
}

Spring Data Geodeの設定。@EnableGemfireRepositoriesアノテーションを付与するとともに、ClientCacheとRegionの
Bean定義を行います。
src/main/java/org/littlewings/geode/spring/GeodeConfig.java

package org.littlewings.geode.spring;

import org.apache.geode.cache.client.ClientCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.gemfire.client.ClientCacheFactoryBean;
import org.springframework.data.gemfire.client.ClientRegionFactoryBean;
import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories;

@Configuration
@EnableGemfireRepositories
public class GeodeConfig {
    @Bean
    public ClientCacheFactoryBean geodeCache() throws Exception {
        ClientCacheFactoryBean clientCacheFactory = new ClientCacheFactoryBean();
        clientCacheFactory.setCacheXml(new ClassPathResource("client-cache.xml"));
        clientCacheFactory.afterPropertiesSet();
        return clientCacheFactory;
    }

    @Bean
    public ClientRegionFactoryBean<String, Book> region(ClientCache cache) throws Exception {
        ClientRegionFactoryBean<String, Book> clientRegionFactory = new ClientRegionFactoryBean<>();
        clientRegionFactory.setCache(cache);
        clientRegionFactory.setRegionName("myRegion");
        clientRegionFactory.afterPropertiesSet();
        return clientRegionFactory;
    }
}

Cacheの設定は、Cache XMLで指定するようにしました。

        clientCacheFactory.setCacheXml(new ClassPathResource("client-cache.xml"));

また、RegionのBean定義はしておかないと、RepositoryがRegionを参照できなくてエラーになります…。
Cache XMLを使用しているからといって、Region自体のBeanの定義は省略できず行っておく必要があります、と。

ところで、Apache Geodeは1.0.0-incubatingになるに伴いパッケージ名が「org.apache.geode」になったの
ですが、Spring Data Geodeは未だにいろいろとGemfireなので混乱しますね?

Spring Boot有効化のためのクラス。特にすることはありません。
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
public class App {
}

ClientCacheの設定

先ほどBean定義で参照していた、Cache XMLは以下のように定義しました。
src/test/resources/client-cache.xml

<?xml version="1.0" encoding="UTF-8"?>
<client-cache
        xmlns="http://geode.apache.org/schema/cache"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://geode.apache.org/schema/cache
                      http://geode.apache.org/schema/cache/cache-1.0.xsd"
        version="1.0">
    <pool name="client-pool" subscription-enabled="true">
        <locator host="localhost" port="10334"/>
    </pool>

    <region name="myRegion" refid="PROXY">
        <region-attributes pool-name="client-pool"/>
    </region>
</client-cache>

単純にServerに接続しにいくRegion定義です。

テストコード

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

テストコードの雛形としては、以下のようなものを用意。
src/test/java/org/littlewings/geode/spring/SimpleDataGeodeTest.java

package org.littlewings.geode.spring;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.geode.cache.client.ServerOperationException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.test.context.junit4.SpringRunner;

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

@RunWith(SpringRunner.class)
@SpringBootTest
public class SimpleDataGeodeTest {
    @Autowired
    BookRepository bookRepository;

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

作成したRepositoryは、@Autowiredでインジェクションしています。

まずは基本的なCRUD

    @Test
    public void gettingStarted() {
        Book book = new Book("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320);

        // 保存
        bookRepository.save(book);

        // 1件取得
        Book foundBook = bookRepository.findOne("978-4798142470");
        assertThat(foundBook.getTitle())
                .isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発");
        assertThat(foundBook.getPrice())
                .isEqualTo(4320);

        // 削除
        bookRepository.delete("978-4798142470");

        // 確認
        assertThat(bookRepository.findOne("978-4798142470"))
                .isNull();
        assertThat(bookRepository.count())
                .isZero();
    }

findAllなどの、Iterableを使うもの。

    @Test
    public void iterable() {
        List<Book> books =
                Arrays.asList(
                        new Book("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320),
                        new Book("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4104),
                        new Book("978-4777519699", "はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発", 2700)
                );

        // 複数保存
        bookRepository.save(books);

        // 全件保存
        Iterable<Book> foundBooks = bookRepository.findAll();
        List<Book> foundBooksAsList = (List<Book>) foundBooks;

        assertThat(foundBooksAsList)
                .hasSize(3);
        assertThat(foundBooksAsList.stream().map(b -> b.getIsbn()).collect(Collectors.toList()))
                .containsAll(books.stream().map(b -> b.getIsbn()).collect(Collectors.toList()));

        // 削除
        bookRepository.delete(books);

        // 確認
        assertThat(bookRepository.count())
                .isZero();
    }

deleteAllはサポートしていなかったりします。

    @Test
    public void deleteAllUnsupported() {
        assertThatThrownBy(() -> bookRepository.deleteAll())
                .isInstanceOf(DataAccessResourceFailureException.class)
                .hasCauseInstanceOf(ServerOperationException.class);  // さらにcauseを見るとUnsupportedOperationException
    }

とりあえず、OKそうですね。

ハマったこと

それはもうハマりました。

まあ、ハマったといっても1点だけで、Spring Bootを1.5.1.RELEASEでSpring Data Geodeを合わせようとするとRepositoryにうまく
DIできなくなるようで、こんな感じでコケてしまいます…。

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'region' defined in class path resource [org/littlewings/geode/spring/GeodeConfig.class]: Unsatisfied dependency expressed through method 'region' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'bookRepository': Could not resolve matching constructor (hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)

Spring Bootのバージョンを下げるとうまく動くのですが、それに気づくまでにかなり時間がかかりました…。
なお、原因自体はちゃんと追えてません…。

というか

Spring Data GeodeをSpring Bootのstarterに加えて欲しいんですけど、どうやらそうもいかない様子。

Add auto-configuration support for Spring Data Geode Repositories. by jxblum · Pull Request #6952 · spring-projects/spring-boot · GitHub

Add auto-configuration support for Apache Geode as a caching provider. by jxblum · Pull Request #6967 · spring-projects/spring-boot · GitHub

うーん。

ホント、Spring Data Geodeって今後どうなるんでしょ…。

まとめ

とりあえず触りだけですが、Spring Data Geodeの1.0.0.INCUBATING-RELEASEを試してみました。

Spring BootとSpring Data GeodeのRepositoryの関連にだいぶハマりましたが、最低限動いたので…とは言いきりにくい
ところがあります。

SpringのCache Abstractionとも、そのうち組み合わせてみようかな?(こっちは、Spring Boot 1.5.1.RELEASEで
動いて欲しい)
あと、Queryとかも試しておきたいですね。

まずはこんなところで。