Hector で JPA の Entity オブジェクトを Cassandra に登録

Java 用 Cassandra クライアント Hector の Object Mapper を使って JPA の Entity オブジェクトを Cassandra に保存する方法をご紹介します。

ソースは http://github.com/fits/try_samples/tree/master/blog/20120311/

事前準備

Cassandra サーバーと CLI を起動して、今回使用するキースペースとカラムファミリを作成しておきます。

キースペース Sample とカラムファミリ Order を作成
> cassandra-cli.bat
Starting Cassandra Client
Welcome to Cassandra CLI version 1.0.8
・・・
[default@unknown] connect localhost/9160;
Connected to: "Test Cluster" on localhost/9160

[default@unknown] create keyspace Sample;
・・・
[default@unknown] use Sample;
Authenticated to keyspace: Sample

[default@Sample] create column family Order with comparator = UTF8Type;
・・・

なお、Cassandra 1.0.8 の Windows 用実行スクリプト(cassandra.bat や cassandra-cli.bat)は環境変数 CASSANDRA_CONF で設定ファイルの場所を指定できるようになっていなかったので*1、以下のように変更して使いました。

cassandra.bat の変更箇所
・・・
REM Ensure that any user defined CLASSPATH variables are not used on startup
if NOT DEFINED CASSANDRA_CONF set CASSANDRA_CONF="%CASSANDRA_HOME%\conf"
set CLASSPATH=%CASSANDRA_CONF%
・・・

Hector のサンプル

まず、通常の Hector を使ったデータ登録の Groovy サンプルです。commons-pool を @GrabExclude で指定する必要がありました。

hector_sample.groovy
package fits.sample

@Grapes([
    @Grab('me.prettyprint:hector-core:1.0-3'),
    @GrabExclude('commons-pool#commons-pool')
])
@Grab('org.slf4j:slf4j-jdk14:1.6.4')
import me.prettyprint.hector.api.factory.HFactory
import me.prettyprint.cassandra.serializers.StringSerializer

def cluster = HFactory.getOrCreateCluster("Test Cluster", "localhost:9160")
def keyspace = HFactory.createKeyspace('Sample', cluster)

def mutator = HFactory.createMutator(keyspace, StringSerializer.get())

mutator.addInsertion("key1", "Order", HFactory.createStringColumn("user_id", "U2"))

def res = mutator.execute()
println res

cluster.getConnectionManager().shutdown()

CLI で確認した登録内容は以下の通りです。

CLI での確認結果
[default@Sample] get Order[utf8('key1')];
=> (column=user_id, value=5532, timestamp=1331465077828000)
Returned 1 results.
・・・

Hector Object Mapper のサンプル(Groovy版)

それでは本題の JPA の Entity アノテーションを付与したオブジェクトを登録するサンプルです。


まずは Groovy で実装してみます。

通常は、EntityManagerImpl コンストラクタの第2引数で指定したパッケージ名から永続対象クラスを自動スキャンしてくれるのですが、今回は同一スクリプト内で永続クラスの Order を定義しており、このままでは永続対象とならないので ClassCacheMgr を使って対応しました。

JPA の @Embedded や @OneToMany は今のところサポートされていないみたいで、コレクション (今回のサンプルでは List) の内容を保存・復元するには me.prettyprint.hom.annotations.Column アノテーションを使う必要がありました。

また、EntityManagerImpl は今のところ javax.persistence.EntityManager インターフェースを実装しているわけでは無く、persist() と find() メソッド程度しか使えません。

ちなみに、@Id アノテーションを付与したプロパティの値がキーとして登録されます。

hector_om_sample.groovy
package fits.sample

@Grapes([
    @Grab('me.prettyprint:hector-object-mapper:3.0-02'),
    @GrabExclude('commons-pool#commons-pool')
])
@Grab('org.slf4j:slf4j-jdk14:1.6.4')
import javax.persistence.*
import me.prettyprint.hector.api.factory.HFactory
import me.prettyprint.hom.EntityManagerImpl
import me.prettyprint.hom.ClassCacheMgr

@Entity
@Table(name = "Order")
class Order {
    @Id
    String id

    @Column(name = "user_id")
    String userId

    @me.prettyprint.hom.annotations.Column(name = "lines")
    List<OrderLine> lines
}

class OrderLine implements Serializable {
    String productId
    int quantity
}

def cluster = HFactory.getOrCreateCluster("Test Cluster", "localhost:9160")
def keyspace = HFactory.createKeyspace('Sample', cluster)

//Order クラスを永続対象にするための設定(自動スキャンされないため)
def cacheMgr = new ClassCacheMgr()
cacheMgr.initializeCacheForClass(Order)

def em = new EntityManagerImpl(keyspace, null, cacheMgr, null)

def data = new Order(id: "id1", userId: "U1", lines: [
    new OrderLine(productId: "P1", quantity: 1),
    new OrderLine(productId: "P2", quantity: 2)
])
//保存
em.persist(data)

//取得
def res = em.find(Order, "id1")
println res.dump()

res.lines.each {
    println "${it.productId}, ${it.quantity}"
}
実行結果
> groovy hector_om_sample.groovy
・・・
<fits.sample.Order@140bd470 id=id1 userId=U1 lines=[fits.sample.OrderLine@4a7d5381, fits.sample.OrderLine@69f5605b]>
P1, 1
P2, 2

CLI で確認した結果は以下の通り。lines の部分が複数カラムに分かれている点に注目です。

CLI での確認結果
[default@Sample] get Order[utf8('id1')];
=> (column=lines, value=6a6176612e7574696c2e41727261794c6973743a32, timestamp=1331466085998000)
=> (column=lines:0, value=aced000573720015666974732e73616d706c652e4f726465724c696e6511ad8a3d5308252a0200024900087175616e746974794c000970726f6475637449647400124c6a6176612f6c616e672f537472696e673b7870000000017400025031, timestamp=1331466086016000)
=> (column=lines:1, value=aced000573720015666974732e73616d706c652e4f726465724c696e6511ad8a3d5308252a0200024900087175616e746974794c000970726f6475637449647400124c6a6176612f6c616e672f537472696e673b7870000000027400025032, timestamp=1331466086016001)
=> (column=user_id, value=5531, timestamp=1331466085993000)
Returned 4 results.
・・・

Hector Object Mapper のサンプル(Java版)

先ほどのサンプルを Java で実装した版は以下の通りです。
fits.sample パッケージを自動スキャンして Order を永続対象とするので ClassCacheMgr は不要です。

Order.java
package fits.sample;

import java.util.List;
import javax.persistence.*;

@Entity
@Table(name = "Order")
public class Order {
    @Id
    private String id;
    @Column(name = "user_id")
    private String userId;
    @me.prettyprint.hom.annotations.Column(name = "lines")
    private List<OrderLine> lines;

    ・・・ 各種アクセサメソッド ・・・
}
Main.java
package fits.sample;

import java.util.ArrayList;

import me.prettyprint.hector.api.Cluster;
import me.prettyprint.hector.api.Keyspace;
import me.prettyprint.hector.api.factory.HFactory;
import me.prettyprint.hom.EntityManagerImpl;

public class Main {
    public static void main(String[] args) {
        Cluster cluster = HFactory.getOrCreateCluster("Test Cluster", "localhost:9160");
        Keyspace keyspace = HFactory.createKeyspace("Sample", cluster);

        EntityManagerImpl em = new EntityManagerImpl(keyspace, "fits.sample");

        Order data = new Order();
        data.setId("jid1");
        data.setUserId("U1");
        data.setLines(new ArrayList<OrderLine>());

        OrderLine line1 = new OrderLine();
        line1.productId = "P1";
        line1.quantity = 1;
        data.getLines().add(line1);
        ・・・
        //保存
        em.persist(data);

        Order res = em.find(Order.class, "jid1");
        System.out.printf("%s - %s\n", res.getId(), res.getUserId());

        for (OrderLine line : res.getLines()) {
            System.out.printf("%s, %d\n", line.productId, line.quantity);
        }
    }
}
pom.xml
<project ・・・>
  ・・・
  <dependencies>
    <dependency>
      <groupId>me.prettyprint</groupId>
      <artifactId>hector-object-mapper</artifactId>
      <version>3.0-02</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-jdk14</artifactId>
      <version>1.6.4</version>
    </dependency>
  </dependencies>
  ・・・
</project>
実行結果
> mvn compile exec:java
・・・
jid1 - U1
P1, 1
P2, 2
・・・

*1:sh 用の cassandra は CASSANDRA_CONF で指定できるようになっている