CLOVER🍀

That was when it all began.

EclipseLink × MongoDB

前回書いた、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環境で使用してみたいと思います。

…と、実際のコードに入る前に注意点としては、

  • すべてのJPAの機能が使えるわけではありません。使えるアノテーションなどには、制限があります
  • トランザクションが使用できるかどうかは、データストアに依存します。MongoDBは非サポートです

などなど。このあたりは、先ほどご紹介した非リレーショナル・データ・ソースの理解に書かれています。まあ、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