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の書き方ですが、こちらを参考に
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
で、実現方法としては
- PdxSerializerを実装したクラスを作成する「Serializing Your Domain Object with a PdxSerializer | Geode Docs」
- 対象のオブジェクトにPdxSerializableを実装する「Implementing PdxSerializable in Your Domain Object | Geode Docs」
- リフレクションベースで動作する「Using Automatic Reflection-Based 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
まとめ
Spring Data Geodeで、軽くRepositoryを試してみたのと、合わせてPDX Serializationも見てみました。
シリアライズまわりの話題も、ちょくちょく気にしないといけないのでしょうけれど。
まずは、触りだけでも押さえられたので良かったかなと思います。