前回書いた、Java EE Advent Calendar 2013向けの記事の、スピンオフなネタです。実は、こちらのネタを先に考えてコードまで書いたのですが、JCacheよりも興味を引けなさそうだなぁと思って、ボツにしました。
こちらは、通常のエントリとして書きます。
テーマは、「上から下まで全部JSON!!」です。
JPA × NoSQL
JPAの実装であるEclipseLinkには、NoSQLと連携する拡張モジュールがあります。こちら、その紹介です。
JPA and NoSQL using EclipseLink - MongoDB supported
http://orablogs-jp.blogspot.jp/2012/04/jpa-and-nosql-using-eclipselink-mongodb.html
Using NoSQL with JPA, EclipseLink and Java EE
http://www.slideshare.net/reza_rahman/using-nosql-with-jpa-eclipselink-and-java-ee
類似のライブラリとして、Hibernate OGMやKunderaがあります。
Hibernate OGM
http://www.hibernate.org/subprojects/ogm.html
Kundera
https://github.com/impetus-opensource/Kundera
Hibernate OGMはInfinispanをサポートしているので、方向性を考えればこちらを採用なのですが、JPQLにはまだ対応していないため、Java EEっぽさがなくなるので外しました…。
個人的には、Kundera+Cassandraもやりたかったのですが、まだ最新版のCassandaraには対応してないんですよね。
EclipseLink NoSQL Extension
というわけで、今回はEclipseLinkのNoSQL Extensionを使用します。
http://www.eclipse.org/eclipselink/documentation/2.5/jpa/extensions/a_nosql.htm
対応しているデータストア(?)は、MongoDB、Oracle NoSQL(Coherence?)、JMS、XML、Oracle AQが対象となります。一部、どう見てもファイルなものが混じってますね…。
基本的には、こちらのサンプルやドキュメントを見ながらやっていきます。
非リレーショナル・データ・ソースの理解
NoSQLデータベースでTopLinkを使用する方法
http://www.eclipse.org/eclipselink/documentation/2.5/jpa/extensions/toc.htm
EclipseLink/Examples/JPA/NoSQL - Eclipsepedia
@NoSql | EclipseLink 2.5.x Java Persistence API (JPA) Extensions Reference
今回選んだデータストアは、みんな大好き(?)MongoDBです。
前に、このブログでもJava系のクライアントコードを書きました。
Java/Groovy/ScalaでMongoDBクライアントプログラミング - CLOVER
ClojureでMongoDBクライアントプログラミング - CLOVER
EclipseLinkでMongoDBを使った、サンプルコードも公開されています。
http://dev.eclipse.org/svnroot/rt/org.eclipse.persistence/trunk/examples/org.eclipse.persistence.example.jpa.nosql.mongo/org.eclipse.persistence.example.jpa.nosql.mongo.zip
とはいえ、全般的に用意されているのはスタンドアロンなものが多いので、今回はJava EE環境で使用してみたいと思います。
…と、実際のコードに入る前に注意点としては、
などなど。このあたりは、先ほどご紹介した非リレーショナル・データ・ソースの理解に書かれています。まあ、NoSQLですしね。EntityManager#flushなんてしたりすると、もうロールバックできないみたいです。
それでも、普通にEntityManagerは使えますし、NamedQuery、そしてNativeQueryまで使用することができます。若干、怪しいところはありましたが…。
MongoDBのインストール
では、まずMongoDBのインストールから。オフィシャルサイトから、アーカイブをダウンロードしてきてください。
MongoDB
http://www.mongodb.org/
展開します。
$ tar -zxvf mongodb-linux-x86_64-2.4.8.tgz
このまま起動する場合、MongoDBは「/data/db」にデータを書き込もうとするので、今回は展開したディレクトリの付近にディレクトリを作って
$ mongodb-linux-x86_64-2.4.8/bin/mongod
「--dbpath」でデータ保存先を指定して、起動します。
$ mongodb-linux-x86_64-2.4.8/bin/mongod --dbpath data
別のターミナルから、接続して確認してみてください。
$ mongodb-linux-x86_64-2.4.8/bin/mongo MongoDB shell version: 2.4.8 connecting to: test > show dbs; local 0.078125GB
これで、MongoDBの準備は完了です。ここでデータベースは、作らなくて大丈夫です。
GlassFish 4のインストール
何かと話題のGlassFishさんですが、今回何年振りかに使います。
GlassFish
https://glassfish.java.net/
Web Profile、マルチリンガル版をダウンロードして、展開。
$ unzip glassfish-4.0-web-ml.zip
とりあえず、起きててもらいます。
$ glassfish4/bin/asadmin start-domain
EclipseLink NoSQL Extensionを使ってみよう
では、そろそろ本題の方に移りましょう。まずは、Maven依存関係を定義します。
<dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>org.eclipse.persistence.nosql</artifactId> <version>2.5.0</version> <exclusions> <exclusion> <groupId>org.eclipse.persistence</groupId> <artifactId>javax.persistence</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.mongodb</groupId> <artifactId>mongo-java-driver</artifactId> <version>2.11.3</version> </dependency> </dependencies>
EclipseLinkは最新版ではありませんが、GlassFish 4に入っているEclipseLinkが2.5.0だったので、そちらの合わせました。あと、MongoDBのドライバを入れています。
永続性ユニットの定義。
src/main/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8" ?> <persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"> <persistence-unit name="javaee7.web.pu.nosql.mongodb" transaction-type="JTA"> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <properties> <property name="eclipselink.target-database" value="org.eclipse.persistence.nosql.adapters.mongo.MongoPlatform" /> <property name="eclipselink.nosql.connection-spec" value="org.eclipse.persistence.nosql.adapters.mongo.MongoConnectionSpec" /> <!-- 接続先がデフォルト(localhost:27017)の場合は、指定しなくてもよい --> <property name="eclipselink.nosql.property.mongo.host" value="localhost" /> <property name="eclipselink.nosql.property.mongo.port" value="27017" /> <!-- 使用するデータベース名 --> <property name="eclipselink.nosql.property.mongo.db" value="testdb" /> <property name="eclipselink.logging.logger" value="JavaLogger" /> <property name="eclipselink.logging.level" value="FINEST" /> </properties> </persistence-unit> </persistence>
一応、transaction-typeにはJTAを指定します。EntityManagerを使いますし。
MongoDB用独特の設定は、このあたりですね。
<property name="eclipselink.target-database" value="org.eclipse.persistence.nosql.adapters.mongo.MongoPlatform" /> <property name="eclipselink.nosql.connection-spec" value="org.eclipse.persistence.nosql.adapters.mongo.MongoConnectionSpec" /> <!-- 接続先がデフォルト(localhost:27017)の場合は、指定しなくてもよい --> <property name="eclipselink.nosql.property.mongo.host" value="localhost" /> <property name="eclipselink.nosql.property.mongo.port" value="27017" /> <!-- 使用するデータベース名 --> <property name="eclipselink.nosql.property.mongo.db" value="testdb" />
データベースは、先のmongoコマンドで接続した時にまだできていないものを指定していますが、やっぱり作らなくて大丈夫です。
続いて、Entityクラス。
src/main/java/javaee7/web/entity/Book.java
package javaee7.web.entity; import java.io.Serializable; import java.util.*; import javax.persistence.CascadeType; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.NamedQuery; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.xml.bind.annotation.XmlRootElement; import org.eclipse.persistence.nosql.annotations.DataFormatType; import org.eclipse.persistence.nosql.annotations.Field; import org.eclipse.persistence.nosql.annotations.NoSql; @XmlRootElement @Entity @NamedQuery(name = "findByPrice", query = "SELECT b FROM Book b WHERE b.price >= :price ORDER BY b.price ASC") @NoSql(dataFormat = DataFormatType.MAPPED) public class Book implements Serializable { @Id @GeneratedValue @Field(name="_id") private String isbn; @Field(name = "title") private String title; @Field(name = "price") private Integer price; @Temporal(TemporalType.DATE) @Field(name = "publish_date") private Date publishDate; @ElementCollection @Field(name = "tags") private List<String> tags = new ArrayList<>(); @ElementCollection @Field(name = "authors") private List<EmbeddedAuthor> authors = new ArrayList<>(); public Book addAuthor(EmbeddedAuthor author) { authors.add(author); return this; } // getter/setterは省略 }
テーマは、思いっきり書籍です。
特徴的なのは、
@NoSql(dataFormat = DataFormatType.MAPPED)
でデータタイプ(MongoDBのようなMap構造なもの)を指定していることと、
@Field(name="_id")
でフィールド名を指定していることですね。Columnアノテーションは意味がないので、代わりにFieldアノテーションを指定してカラム名を決めます。指定しないと、全部大文字のカラムになります…。また、コレクションの名前(MondoDB的でのテーブルみたいなもの)は、設定できないので全部大文字になります。
ここでは、MongoDBの主キーである「_id」と本のISBNを紐付けています。何もしないとMongoDBが自動でキーを採番してしまいますが、今回は指定する形を取りました。
XmlRootElementアノテーションが付いているのは、この後JAX-RSで使用するためで、EclipseLink NoSQL Extensionとは関係がありません。
そして、しれっとNamedQueryまで入っています。
続いて、
src/main/java/javaee7/web/entity/EmbeddedAuthor.java
package javaee7.web.entity; import java.io.Serializable; import javax.persistence.Embeddable; import org.eclipse.persistence.nosql.annotations.DataFormatType; import org.eclipse.persistence.nosql.annotations.Field; import org.eclipse.persistence.nosql.annotations.NoSql; @Embeddable @NoSql(dataFormat = DataFormatType.MAPPED) public class EmbeddedAuthor implements Serializable { @Field(name = "name") private String name; // getter/setterは省略 }
書籍の著者ということで。今回は、Embeddedとしました。一応、OneToManyとかもサポートしているようなのですが、今回はくどくなるのでやめます…。
サービスクラス。
src/main/java/javaee7/web/service/BookService.java
package javaee7.web.service; import java.util.List; import javax.enterprise.context.RequestScoped; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.transaction.Transactional; import javaee7.web.entity.Book; @RequestScoped public class BookService { @PersistenceContext private EntityManager em; @Transactional public void create(Book book) { em.persist(book); } @Transactional public Book update(Book book) { return em.merge(book); } @Transactional public void remove(Book book) { em.remove(em.merge(book)); } public Book find(String isbn) { return em.find(Book.class, isbn); } @SuppressWarnings("unchecked") public List<Book> findAll() { return (List<Book>)em .createQuery("SELECT b FROM Book b ORDER BY b.price ASC") .getResultList(); } @SuppressWarnings("unchecked") public List<Book> findByPrice(int price) { return (List<Book>)em .createNamedQuery("findByPrice", Book.class) .setParameter("price", price) .getResultList(); } public Book findNative(String isbn) { return (Book) em .createNativeQuery("db.BOOK.findOne({\"_id\": \"" + isbn + "\"})", Book.class) .getSingleResult(); } }
実は、Java EE Advent Calendarでもやらなかった、Java EE 7です。普通にEntityManagerを使っているだけと思いきや、NativeQueryが入っています。これが、MongoDBのクエリです。
JAX-RS関連。
src/main/java/javaee7/web/rest/BookResource.java
package javaee7.web.rest; import java.net.URI; import java.util.List; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.DELETE; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.POST; import javax.ws.rs.Produces; import javax.ws.rs.PUT; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javaee7.web.entity.Book; import javaee7.web.service.BookService; @RequestScoped @Path("books") public class BookResource { @Inject private BookService bookService; @Path("{isbn}") @GET @Produces(MediaType.APPLICATION_JSON) public Book book(@PathParam("isbn") String isbn, @QueryParam("native") String nativeQuery) { if (Boolean.valueOf(nativeQuery)) { return bookService.findNative(isbn); } else { return bookService.find(isbn); } } @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Book create(Book book) { bookService.create(book); return book; } @PUT @Consumes(MediaType.APPLICATION_JSON) public void update(Book book) { bookService.update(book); } @Path("findAll") @GET @Produces(MediaType.APPLICATION_JSON) public List<Book> findAll() { return bookService.findAll(); } @Path("find") @GET @Produces(MediaType.APPLICATION_JSON) public List<Book> find(@QueryParam("price") int price) { return bookService.findByPrice(price); } @Path("{isbn}") @DELETE public void delete(@PathParam("isbn") String isbn) { Book book = bookService.find(isbn); if (book != null) { bookService.remove(book); } } }
割と単純なクラスですが、JSONでやり取りするようにしています。だって、バックエンドはMongoDBですしね。
なので、「上から下まで全部JSON」です。
単一の書籍取得だけは、パラメータでNativeQueryかどうかを分けるようにしています。
@Path("{isbn}") @GET @Produces(MediaType.APPLICATION_JSON) public Book book(@PathParam("isbn") String isbn, @QueryParam("native") String nativeQuery) { if (Boolean.valueOf(nativeQuery)) { return bookService.findNative(isbn); } else { return bookService.find(isbn); } }
JAX-RS有効化。
src/main/java/javaee7/web/rest/NoSqlApplication.java
package javaee7.web.rest; import java.util.HashSet; import java.util.Set; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; @ApplicationPath("nosql") public class NoSqlApplication extends Application { }
動かしてみる
それでは、ビルドして
$ mvn package -DfailOnMissingWebXml=false
デプロイ〜。
glassfish4/bin/asadmin deploy javaee7-web.war
とりあえず、findAllしてみましょう。
$ curl http://localhost:8080/javaee7-web/nosql/books/findAll []
当然ですが、データが無いので0件です。
そこで、こんなデータを用意します。
// javaee5.json { "isbn": "978-4798120546", "title": "マスタリングJavaEE5 第2版", "price": 5670, "publishDate": "2009-11-28T00:00:00+09:00", "tags": ["java", "ejb", "jpa", "jax-ws"], "authors": [ {"name": "斉藤 賢哉"} ] } // javaee6.json { "isbn": "978-4798124605", "title": "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", "price": 4410, "publishDate": "2012-03-09T00:00:00+09:00", "tags": ["java", "ejb", "jpa", "jax-rs"], "authors": [ {"name": "Antonio Goncalves"} ] } // jax-rs.json { "isbn": "978-4873114675", "title": "JavaによるRESTfulシステム構築", "price": 3360, "publishDate": "2010-08-23T09:00:00+09:00", "tags": ["java", "jax-rs"], "authors": [ {"name": "Bill Burke"}, {"name": "arton"}, {"name": "菅野 良二"} ] }
curlで、一気に叩き込みます。
$ find *.json | xargs -i curl -X POST -H "Content-Type: application/json;" -d@{} http://localhost:8080/javaee7-web/nosql/books/ {"authors":[{"name":"斉藤 賢哉"}],"isbn":"978-4798120546","price":5670,"publishDate":"2009-11-28T00:00:00","tags":["java","ejb","jpa","jax-ws"],"title":"マスタリングJavaEE5 第2版"}{"authors":[{"name":"Antonio Goncalves"}],"isbn":"978-4798124605","price":4410,"publishDate":"2012-03-09T00:00:00","tags":["java","ejb","jpa","jax-rs"],"title":"Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava"}{"authors":[{"name":"Bill Burke"},{"name":"arton"},{"name":"菅野 良二"}],"isbn":"978-4873114675","price":3360,"publishDate":"2010-08-23T09:00:00","tags":["java","jax-rs"],"title":"JavaによるRESTfulシステム構築"}
先ほどの、findAllをもう1度実行してみましょう。
$ curl http://localhost:8080/javaee7-web/nosql/books/findAll [{"authors":[{"name":"Bill Burke"},{"name":"arton"},{"name":"菅野 良二"}],"isbn":"978-4873114675","price":3360,"publishDate":"2010-08-23T09:00:00","tags":["java","jax-rs"],"title":"JavaによるRESTfulシステム構築"}, {"authors":[{"name":"Antonio Goncalves"}],"isbn":"978-4798124605","price":4410,"publishDate":"2012-03-09T00:00:00","tags":["java","ejb","jpa","jax-rs"],"title":"Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava"}, {"authors":[{"name":"斉藤 賢哉"}],"isbn":"978-4798120546","price":5670,"publishDate":"2009-11-28T00:00:00","tags":["java","ejb","jpa","jax-ws"],"title":"マスタリングJavaEE5 第2版"}]
わかりにくいですけど、3冊分返ってきています。
*一応、改行を入れました…。
1件だけ検索しても、ちゃんと結果が返ってきます。
$ curl http://localhost:8080/javaee {"authors":[{"name":"Antonio Goncalves"}],"isbn":"978-4798124605","price":4410,"publishDate":"2012-03-09T00:00:00","tags":["java","ejb","jpa","jax-rs"],"title":"Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava"}
結果からはわかりにくいですが、NativeQueryも動きます。
$ curl http://localhost:8080/javaee7-web/nosql/books/978-4798124605?native=true
NamedQuery。
$ curl http://localhost:8080/ja7-web/nosql/books/find?price=4000 [{"authors":[{"name":"Antonio Goncalves"}],"isbn":"978-4798124605","price":4410,"publishDate":"2012-03-09T00:00:00","tags":["java","ejb","jpa","jax-rs"],"title":"Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava"},{"authors":[{"name":"斉藤 賢哉"}],"isbn":"978-4798120546","price":5670,"publishDate":"2009-11-28T00:00:00","tags":["java","ejb","jpa","jax-ws"],"title":"マスタリングJavaEE5 第2版"}]
4,000円以上の本を。
ここまでで、EntityManager#persist、
@Transactional public void create(Book book) { em.persist(book); }
JPQLや
@SuppressWarnings("unchecked") public List<Book> findAll() { return (List<Book>)em .createQuery("SELECT b FROM Book b ORDER BY b.price ASC") .getResultList(); }
NamedQuery、
@NamedQuery(name = "findByPrice", query = "SELECT b FROM Book b WHERE b.price >= :price ORDER BY b.price ASC") @SuppressWarnings("unchecked") public List<Book> findByPrice(int price) { return (List<Book>)em .createNamedQuery("findByPrice", Book.class) .setParameter("price", price) .getResultList(); }
そしてNativeQueryを確認しました。
public Book findNative(String isbn) { return (Book) em .createNativeQuery("db.BOOK.findOne({\"_id\": \"" + isbn + "\"})", Book.class) .getSingleResult(); }
が、実はNativeQueryは、findOneは動かせましたけど、findは動かせませんでした…。
ちなみに、MongoDBの方ですが、勝手にデータベースができています。
> show dbs; local 0.078125GB testdb 0.203125GB
「testdb」にスイッチしてみると
> use testdb;
switched to db testdb
「BOOK」というコレクションができています。
> show collections;
BOOK
system.indexes
もちろん、中身もできています。
> db.BOOK.find(); { "_id" : "978-4798120546", "tags" : [ "java", "ejb", "jpa", "jax-ws" ], "authors" : [ { "name" : "斉藤 賢哉" } ], "title" : "マスタリングJavaEE5 第2版", "price" : 5670, "publish_date" : ISODate("2009-11-27T15:00:00Z") } { "_id" : "978-4798124605", "tags" : [ "java", "ejb", "jpa", "jax-rs" ], "authors" : [ { "name" : "Antonio Goncalves" } ], "title" : "Beginning Java EE 6 GlassFish 3で始めるエンタープライズJava", "price" : 4410, "publish_date" : ISODate("2012-03-08T15:00:00Z") } { "_id" : "978-4873114675", "tags" : [ "java", "jax-rs" ], "authors" : [ { "name" : "Bill Burke" }, { "name" : "arton" }, { "name" : "菅野 良二" } ], "title" : "JavaによるRESTfulシステム構築", "price" : 3360, "publish_date" : ISODate("2010-08-22T15:00:00Z") }
ちょんと、著者が埋め込まれていますね。
個人的にはけっこう面白いテーマかなぁと思いましたが、実際に使うかというと疑問符が付くこともありますし、書いていてすごく自己満足な感じがしたので(JCacheもそうですが…)、Advent Calendarからは外しました。
標準化できるかというと、ねぇ…。
とはいえ、Advent Calendarのオマケということで、ソースはJCacheの時と同じところに置いておきます〜。
https://github.com/kazuhira-r/javaee-advent-calendar/tree/master/2013/eclipselink-mongodb