CLOVER🍀

That was when it all began.

Spring Data GeodeでRepositoryを使う(ちょっとだけPDXも)

Spring Data Geodeで、Repositoryを試してみます。こちらのエントリの続きです。

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

今回は、Queryを使ってみましょう。とても簡単な例でいってみます。あと、ちょこっと
PDXもからめてみたいと思います。

構成は、今回もClient/Server構成です。

準備

まずは、Spring Data Geodeを使う準備から。Mavenでの定義は、こちら。
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>client-server-spring-data-geode-repository</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.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-cache</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>
</project>

Spring Bootのバージョンは、Spring Data Geode 1.0.0-INCUBATINGの都合上、1.4.4.RELEASEです。

また、Apache Geodeも1.0.0-incubatingということになります。

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>

この設定ファイルを使って、Serverを起動しておきます。

サンプルアプリケーション

では、Spring Data Geodeで使うサンプルアプリケーションを書いていきます。お題は、書籍とします。

まずはEntity。
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は省略
}

Repository。使用するRegionは、「myRegion」とします。
src/main/java/org/littlewings/geode/spring/BookRepository.java

package org.littlewings.geode.spring;

import java.util.List;

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

@Region("myRegion")
public interface BookRepository extends GemfireRepository<Book, String> {
    List<Book> findByTitleLikeOrderByPriceDesc(String title);

    @Query("<TRACE> SELECT * FROM /myRegion b WHERE b.price > $1 ORDER BY price ASC")
    List<Book> findByPriceGreaterThan(int price);
}

Queryの書き方ですが、こちらを参考に

Executing OQL Queries

Spring Dataよろしく、メソッドのネーミングでQueryを組み立てる方法と、@QueryでQueryを指定する方法とが
あります。

今回は、その両方を簡単なパターンで作成しています。

Spring Data Geodeの設定。
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;
    }
}

RepositoryでRegionを使用するので、「myRegion」もBean定義しておきます。

起動クラス…というか、@SpringBootApplicationアノテーションが付与されただけのクラスです…。
src/main/java/org/littlewings/geode/spring/App.java

package org.littlewings.geode.spring;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
}

これで、コードとしてはある程度準備ができました。

Client Cacheの設定

先ほどのSpring Data Geodeの設定で、Apache Geodeの設定ファイルを読ませていたのですが、その中身は
こんな感じで用意。
src/main/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>

とりあえず、接続設定のみです。

テストコード

動作確認のためのテストコードを作成します。雛形は、このように。
src/test/java/org/littlewings/geode/spring/GeodeRepositoryTest.java

package org.littlewings.geode.spring;

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

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.test.context.junit4.SpringRunner;

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

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

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

先ほど作成した、BookRepositoryインターフェースを@Autowiredしておきます。

で、テストコード。まずはメソッドのネーミングに沿ってQueryを発行する方。

    @Test
    public void keywordQuery() {
        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);

        assertThat(bookRepository.count())
                .isEqualTo(3L);

        List<Book> queryResultBooks = (List<Book>) bookRepository.findByTitleLikeOrderByPriceDesc("%フレームワーク%");
        assertThat(queryResultBooks)
                .hasSize(2);
        assertThat(queryResultBooks.get(0).getTitle())
                .isEqualTo("[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ");
        assertThat(queryResultBooks.get(1).getTitle())
                .isEqualTo("はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発");

        bookRepository.delete(books);
    }

続いて、OQLを@Queryアノテーションで指定する方。

    @Test
    public void oqlQuery() {
        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);

        assertThat(bookRepository.count())
                .isEqualTo(3L);

        List<Book> queryResultBooks = (List<Book>) bookRepository.findByPriceGreaterThan(4000);
        assertThat(queryResultBooks)
                .hasSize(2);
        assertThat(queryResultBooks.get(0).getTitle())
                .isEqualTo("[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ");
        assertThat(queryResultBooks.get(1).getTitle())
                .isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発");

        bookRepository.delete(books);
    }

こんな感じです。

で、どうなるか

クエリが問題なく発行できればこのテストコードはパスするはずですが、この状態のまま実行すると、
こんな例外が発生します。

org.springframework.dao.DataAccessResourceFailureException: remote server on 172.19.0.1(17540:loner):46135:43193a66: org.apache.geode.SerializationException: A ClassNotFoundException was thrown while trying to deserialize cached value.; nested exception is org.apache.geode.cache.client.ServerOperationException: remote server on 172.19.0.1(17540:loner):46135:43193a66: org.apache.geode.SerializationException: A ClassNotFoundException was thrown while trying to deserialize cached value.

Caused by: org.apache.geode.cache.client.ServerOperationException: remote server on 172.19.0.1(17540:loner):46135:43193a66: org.apache.geode.SerializationException: A ClassNotFoundException was thrown while trying to deserialize cached value.

Caused by: org.apache.geode.SerializationException: A ClassNotFoundException was thrown while trying to deserialize cached value.

Caused by: java.lang.ClassNotFoundException: org.littlewings.geode.spring.Book

Server側で、今回作成されたBookクラスがわからないようで、デシリアライズに失敗します。なるほど。

でも、前回のエントリの時(GemfireRepository#findAll)は、こんなエラーは出ていませんでした。

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

今回はQueryを投げるので、Server側に保存したオブジェクトのフィールドの値まで見ることになるからでしょうね。
findAllだと、中身までのぞき込む必要はありませんし。

対処

では、どうしましょうか。いくつか方法があります。

Entityをデプロイする

Entityを含めたJARファイルを、Serverにデプロイするとこの問題は解決します。

JARファイルを作成。

$ cd target/classes
$ jar -cvf entity.jar org/littlewings/geode/spring/Book.class 
マニフェストが追加されました
org/littlewings/geode/spring/Book.classを追加中です(=1348)(=616)(54%収縮されました)

これを、Server側にデプロイします。

gfsh>deploy --jar=/path/to/entity.jar
      Member        | Deployed JAR | Deployed JAR Location
------------------- | ------------ | --------------------------------------------------------
server-ddb3ce1d76ff | entity.jar   | /opt/apache-geode/server-ddb3ce1d76ff/vf.gf#entity.jar#1

これで、テストにパスするようになります。めでたし、めでたし。

でも

JARファイルをServerにデプロイしなくてはいけないのは、こういうちょっと動かしたいとかいう時には面倒です。
他の方法はないのでしょうか?

ここで、ちょっとApache Geodeシリアライズまわりの情報を見てみます。

Overview of Data Serialization | Geode Docs

通常はJava標準のシリアライズですが、他に「Geode PDX Serialization」、「Geode Data Serialization」の2つがあるようです。

対象のオブジェクトのバージョニングに対する考え方など違いがありそうですが、

Provides single field access of serialized data, without full deserialization - supported also for OQL querying.

http://geode.apache.org/docs/guide/10/developing/data_serialization/data_serialization_options.html

などがあるあたりを見るに、今回は「Geode PDX Serialization」を使ってみることにします。

Geode PDX Serialization

PDFというのは、「Geode’s Portable Data eXchange」の略らしく、複数の言語で使えるよりシリアライズ/デシリアライズ
コストを抑えたデータフォーマットだとか。

Geode PDX Serialization | Geode Docs

Geode PDX Serialization Features | Geode Docs

High Level Steps for Using PDX Serialization | Geode Docs

で、実現方法としては

の3つがあります。

今回は、PdxSerializerを実装する方法と、リフレクションベースのReflectionBasedAutoSerializerを使う方法を試してみましょう。

PdxSerializerを実装したクラスを作成する

では、PdxSerializerを実装したクラスを作成します。

以下のような感じで、PdxSerializerインターフェースを実装してtoData/fromDataメソッドを実装します。
src/main/java/org/littlewings/geode/spring/BookPdxSerializer.java

package org.littlewings.geode.spring;

import java.util.Properties;

import org.apache.geode.cache.Declarable;
import org.apache.geode.pdx.PdxReader;
import org.apache.geode.pdx.PdxSerializer;
import org.apache.geode.pdx.PdxWriter;

public class BookPdxSerializer implements PdxSerializer, Declarable {
    @Override
    public void init(Properties props) {
        // no-op
    }

    @Override
    public boolean toData(Object o, PdxWriter out) {
        if (!(o instanceof Book)) {
            return false;
        }

        Book book = (Book) o;

        out.writeString("isbn", book.getIsbn());
        out.writeString("title", book.getTitle());
        out.writeInt("price", book.getPrice());

        return true;
    }

    @Override
    public Object fromData(Class<?> clazz, PdxReader in) {
        if (!clazz.equals(Book.class)) {
            return null;
        }

        Book book = new Book();

        book.setIsbn(in.readString("isbn"));
        book.setTitle(in.readString("title"));
        book.setPrice(in.readInt("price"));

        return book;
    }
}

Declarableインターフェースを実装しているのは、設定ファイル上に書く場合のためです。

このクラスを作成し、Client側のCacheの設定に以下のようにpdfタグ配下に指定します。

<?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>

    <pdx>
        <pdx-serializer>
            <class-name>org.littlewings.geode.spring.BookPdxSerializer</class-name>
        </pdx-serializer>
    </pdx>

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

これで、JARファイルをデプロイせずとも先ほどのテストコードが動作するようになります。

なお、実行するとこんな感じのログがClient側に出力されます。

[info 2017/02/22 23:52:13.259 JST <main> tid=0x1] Defining: PdxType[
    dsid=0, typenum=1, name=org.littlewings.geode.spring.Book, fields=[
        isbn:String:0:idx0(relativeOffset)=0:idx1(vlfOffsetIndex)=-1
        title:String:1:1:idx0(relativeOffset)=0:idx1(vlfOffsetIndex)=1
        price:int:2:1:idx0(relativeOffset)=-4:idx1(vlfOffsetIndex)=-1]]

PdxSerializableインターフェースを実装する方は、対象のクラス自身にPdxSerializableインターフェースを実装して
toData/fromDataメソッドを実装する感じになるだけなので割愛。

Implementing PdxSerializable in Your Domain Object | Geode Docs

ReflectionBasedAutoSerializerを使う

で、さらにこれを自動でやりたい場合は、リフレクションベースで動作するReflectionBasedAutoSerializerを使います。

Using Automatic Reflection-Based PDX Serialization | Geode Docs

使い方としては、先ほどのClient側のCache設定で、pdfタグのところを次の様に変更します。

    <pdx>
        <pdx-serializer>
            <class-name>org.apache.geode.pdx.ReflectionBasedAutoSerializer</class-name>
            <parameter name="classes">
                <string>org\.littlewings\.geode\.spring\..+</string>
            </parameter>
        </pdx-serializer>
    </pdx>

parameter/classesには、対象となるクラスを正規表現で指定します。

この場合には、先ほど作成したPdfSerializerも不要になります。

実行した場合に出力されるログは、こんな感じに。

[info 2017/02/22 23:56:09.141 JST <main> tid=0x1] Auto serializer generating type for class org.littlewings.geode.spring.Book for fields: 
    isbn: private java.lang.String org.littlewings.geode.spring.Book.isbn
    title: private java.lang.String org.littlewings.geode.spring.Book.title
    price: private java.lang.Integer org.littlewings.geode.spring.Book.price


[info 2017/02/22 23:56:09.196 JST <main> tid=0x1] Defining: PdxType[
    dsid=0, typenum=1, name=org.littlewings.geode.spring.Book, fields=[
        isbn:String:0:idx0(relativeOffset)=0:idx1(vlfOffsetIndex)=-1
        title:String:1:1:idx0(relativeOffset)=0:idx1(vlfOffsetIndex)=1
        price:Object:2:2:idx0(relativeOffset)=0:idx1(vlfOffsetIndex)=2]]

これでもテストはパスするようになります。

ReflectionBasedAutoSerializerに関する、その他の話題はこちら。

Customizing Serialization with Class Pattern Strings | Geode Docs

Extending the ReflectionBasedAutoSerializer | Geode Docs

まとめ

Spring Data Geodeで、軽くRepositoryを試してみたのと、合わせてPDX Serializationも見てみました。

シリアライズまわりの話題も、ちょくちょく気にしないといけないのでしょうけれど。
まずは、触りだけでも押さえられたので良かったかなと思います。