Spring Data GemFireとApache Geodeを、組み合わせて使えると聞いて。
Spring Data for Pivotal GemFire
Spring Data GemFire supports Apache Geode
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のマイルストーンリポジトリを足して
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のサンプルプロジェクトですね。
準備
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を自動的に作成して発行することもできます。
また、@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と統合されていると、使う側としては利用しやすそうだなーと思いました。