第5回 MongoDB 勉強会 in Tokyo 〜真夏の大Mongo祭り〜

久々に社外の勉強会に参加してきました。MongoDBはちょっと触った程度であまり知識がなく、しかも勉強会が久々だったこともあり勘違いもあるかと思いますが、感想を書きます。

Play with Mongoid!

Rubyで書かれたODM、Mongoidの紹介。ActiveRecord/DataMapperライクなAPIでMongoDBを扱うことができるので導入しやすいとのこと。こういったことがやり易いのがLLの良いところですね。
validationやrelationshipもお馴染みの形式で書ける上、独自にparanoidやversioningといった機能が提供されていて非常に参考になりました。MongoDBはスキーマレスなので、driverやそのwrapperが各ドキュメントに独自のフィールドをコッソリ追加することができ、それを使って独自の機能を提供しやすいのが面白いところだと思います。

YiiフレームワークとMongoDBを使ったFacebookアプリ開発

PHPのYiiフレームワーク+MongoDBでFacebookアプリを作る話。PHPは全く触ったことがないのですが、コードはActiveRecord風なこともあって普通に読めました。それにしてもPHPフレームワークの多さがすごい。これらのフレームワークが各々独自のdriver wrapperを作っているような印象を受けたのですが、そうだとしたらちょっともったいないかな、と。extensionを作って公開しながら開発を進めているのが羨ましかったです。滅多にPHPの話を聞く機会がないのでちょっとでもPHPの世界が覗けたという意味で貴重な時間でした。

ココロもつながるオンラインゲーム – アットゲームズ – のMongoDB導入事例

ゲームサイトでのMongoDB導入事例。MySQLで扱っていたデータの一部をMongoDBに移した、という話。更新頻度が高く整合性がそれほど重要ではないデータ(あしあと、ゲストブック)をMongoDBで扱うようにしたということで、今回この勉強会に参加した目的もそういったところを狙ってたので、非常に参考になりました。
自動フェイルオーバが行われない理由が、ReplicaSetを構成するサーバが3台以上ないとprimaryが死んだ時に残りのサーバがvoteしても過半数に満たない故であることがセッションの中で明らかに。これは結構やってしまうんじゃないかな…。そんな中でも積極的にshardingを使っている姿勢は素晴らしいと思いました。こういった情熱は大事だな、と改めて思い知ったという意味で、今日は参加して良かったな、と。そう思えるセッションでした。

Not Only NoSQL! RDBの逆襲 – PostgreSQL

それPostgreSQLでもできるよ!という話。MongoDBの勉強会の中で、面白いアプローチでした。主にPostgreSQLのarray typeとXML document機能の紹介。あまりRDBMSでarrayを積極的に使うことはないのですが、PostgreSQLのarray typeは結構色んなサポート機能が付いているということで、array typeを見直すことができたのが収穫でした。「Use Mongo insted of ORM!(or write SQL directly)」は、そもそも何でデータを表形式で保存するか、というところに立ち戻って考えてみたいな、と。
スキーマデザインの話がちょっと出てましたが、MongoDBのようなスキーマレスDBに対してスキーマデザインを検討するということは、アプリケーションのデータモデルを検討するのと同意なのか、それともアプリケーションの外のスコープまで検討するのか、非常に興味深いところです。

ビジネス視点のMongoDB vs RDB 〜MongoDBの導入戦略〜

何故MongoDBを使うのか、というところをビジネス視点で丁寧に考察し、向き/不向きをキッチリ提示するああたり非常に説得力がありました。情報を集めてアウトプットするだけではなく、自分でシッカリ考察して結論を提示するというのが素晴らしく、見習う点が多い発表でした。質問タイムにて、「MongoDBの本当の良い所はアプリケーションとDBの新しい関係性を構築すること」というのは至言だな、と。最初にネタっぽく始まり、途中から真面目モード、(質問に対する回答なので偶発的ではあったものの)最後は至言で締めるという緩急のつけ方も見事でした。

MongoDBとGridFSについて

GridFSをサービスに導入するにあたり、検証した結果を報告という内容でした。結果として、GridFS用に何か特殊な設定をすることなく、shardingやReplicaSetを設定すれは良い、とのこと。GridFSの話が出てくると、昔Oracleが9i頃から出しているOracle Database File Systemを思い出します。Oracleの方はファイルのversioning管理が出来たような(大分古い記憶なので違っているかもしれませんが)。GridFS特有の機能が色々と実装されると面白いのかな。file viewerとか欲しいですね。

[mongodb][java] Java Driverのオブジェクトに変換するmapperを書いてみた

前回JMongoStoreを作った際、HTTP Sessionに格納したオブジェクトをMongoDBに保存する為、JavaのオブジェクトとJava DriverのDBObjectとを相互変換するクラスを書きました。今回、それを別のモジュールとして切り出してmonganezという名前でgithubにアップしました。

特徴

  • Map/Iterable/POJOとDBObjectを相互変換する
  • 事前に変換対象となるクラスの型をmapperに通知しておく必要がない
  • ネストしたオブジェクトも自動的にDBObjectに変換する(←今回はこれがやりたかった)
  • (今のところ)変換対象のクラスにAnnotationを付与する必要がない

ついでにMaven Repositoryを作成してjarをアップしました。JMongoStoremonganezを使うように変更しています。使い方は以下のとおりです。

Java Object -> DBObject
Foo foo = new Foo();
...
DBObjectEncoder encoder = new DBObjectEncoder();
collection.save(encoder.encode(foo));
DBObject -> Java Object
DBObjectDecoder decoder = new DBObjectDecoder();
DBObject dbObject = collection.findOne(query);
Foo foo = (Foo)decoder.toObject(dbObject);

今のところ、MongoDBを操作したりクエリを発行する部分はJava Driverにお任せですが、時間を見つけて書いてみたいと思います。

JMongoStore - a Store class to persistent sessions of Tomcat to MongoDB.

永続化するオブジェクトグラフの構成がある程度固いようであれば、ORMを使ってRDBMSに保存するというアプローチで良いと思いますが、グラフの構成が変動しやすい場合は、変更の都度スキーマ定義を更新することになるので結構面倒です。例えばHTTPSessionのように、アプリケーションのバージョンアップと共に保存される情報が変動していきやすいものは、スキーマレスなKVSの方が扱いやすいと言えるでしょう。
そこで、MongoDBにTomcatのセッション情報を保存するモジュールを書いてみました。JMongoStoreという名前でgithubにアップしています。使い方は以下のとおりです。

1. ${CATALINA_HOME}/libに、mongo-store.jar and mongo-java-driver.jar(MongoDB標準のJavaドライバ)をコピーする
2. Tomcatの任意のContextにPersistentManagerとJMongoStoreの設定をする

<Context antiResourceLocking="false" privileged="true" useHttpOnly="true">
  <Manager className="org.apache.catalina.session.PersistentManager" 
    saveOnRestart=".." 
    maxActiveSessions=".." 
    minIdleSwap=".." 
    maxIdleSwap=".." 
    maxIdleBackup=".."
  >
    <Store className="net.wrap_trap.tomcat.session.MongoStore"
        host="localhost"
        databaseName="session_store"
        collectionName="sessions"
    />
  </Manager> 
</Context>

3. Tomcat起動
4. Sessions Example(http://localhost:8080/examples/servlets/servlet/SessionExample)より、セッション情報を格納
5. mongoシェルを起動し、sessionsコレクションにセッション情報が保存されることを確認する

  • mongoシェルによる実行の結果

JDBCStore等は一旦オブジェクトをJava SerializeしてそのバイナリをDBにそのまま保存しますが、JMongoStoreではセッションのattributesもBSONで格納され、mongoシェルを通じてどんなオブジェクトがどんな値で格納されているか確認することができます。また、MongoDBにはMapReduceの実装が組み込まれているので、例えば何かのアイテムを参照した際にセッションにその情報を格納しておき、後でmap/reduceを使って集計したり、その集計結果を使って効果の高いアイテムをレコメンドする等の使い方があると考えています。ただし、deactiveなセッション情報を集計する為に、MongoDB上のセッション情報はすぐに削除せず、一定期間保持するなどの調整が必要になるでしょう。

セッション情報をMongoDBに保存する部分で、以前使ってみたMungBeanかMorphiaを使おうと考えたのですが、色々と合わない箇所が出てきたので、結局自前で書きました。(BSONをMongoDBに格納する部分はMongoDB標準のJavaドライバに任せています。) この部分はいずれ分離させ、別のモジュールとして管理したいと考えています。

MongoDBとmorphia

mungbeanはPOJOに何も手を加えなくても保存できそうで良かったのですが、残念ながら現状では複数のPOJOを一度に扱うことができないようです。"Third Party Frameworks and Libs"に戻って、mungbeanと同じくPOJO Mappersカテゴリであるmorphiaというライブラリを試してみました。
morphiaの場合、POJOをMongoDBに保存する為にannotation等を追加する必要があります。*1 persistenceの起点となるFooに対して、@Entityと@Id、そしてObjectIdプロパティを追加しました。

何とかannotationの追加だけでいけないか試してみましたが、Morphia#mapメソッドをcallする際に例外が出ました…。仕方なく、FooにObjectIdを追加しています。
Barは@Embeddedを追加しています。こちらはObjectIdは必要なしです。

実際にFooを保存するコードは以下となります。今回はBarのリストも設定しています。

実行後に確認してみたところ、Listも含めてFooが保存されていました。

> db.Foo.find();
{ "_id" : ObjectId("4dc2b9069f14c071cf549dc9"), "className" : "test.entity.Foo",
 "id" : 1, "name" : "hoge", "created" : ISODate("2011-05-05T14:49:42.270Z"), "bars" : [
        {
                "id" : NumberLong(2),
                "list" : [
                        "foo",
                        "bar",
                        "hoge"
                ]
        },
        {
                "id" : NumberLong(3),
                "list" : [
                        "mungbean",
                        "test"
                ]
        }
] }

morphiaのトップページを見ると分かりますが、このmorphiaはannotationによって保存するプロパティ等を調整したり、ロード後の振る舞いを指定したり、色々とできるようです。MongoDBをメインのストレージとするならば、現状はmorphiaの方が良いでしょう。ただ、今回は個人的にMongoDBをサブのストレージとして使う方法を探していたので、mungbeanのアプローチにちょっと手を加えることになるかな…。

*1:それが嫌だったので、mungbeanの方を最初に試しました。

MongoDBとmungbean

sessionに格納するkey-valueのように、保存する要素の変動が多いオブジェクトを簡単に突っ込めるKVSを探していて、MongoDBに行き着きました。が、標準のJava DriverのAPIがイマイチなので、"Third Party Frameworks and Libs"で紹介されているライブラリを使ってみることにしました。これらのライブラリは大きく3つのカテゴリ(POJO Mappers/Code Generation/Misc)が用意されていますが、スキーマレスという特性を活かすのであればPOJO Mappersの方が良いと考え*1mungbeanというライブラリを使ってみました。
取りあえずmongodとmongoシェルを使って、testデータベースのfooコレクションにあらかじめデータを登録しておきます。

> db.foo.find()
{ "_id" : ObjectId("4d8f3719822cd27d3519b47f"), "a" : 1 }

mungbeanのjarを見つけることができなかったので、githubからコードをダウンロードし、mavenでパッケージして使ってます。FooとBarというPOJOを用意して試してみました。まずはFooのみ保存するコードを実行します。

実行後、mongoシェルで確認すると、以下のように保存されていました。

> db.foo.find()
{ "_id" : ObjectId("4d8f3719822cd27d3519b47f"), "a" : 1 }
{ "_id" : ObjectId("4dc192b4f66fd9d355b5f19a"), "id" : 1, "created" : ISODate("2011-05-04T17:53:56.386Z"), "name" : "hoge", "bars" : null }

Fooのみ保存するようにしたのは、createBarListを呼び出している行のコメントアウトを外すと、以下のエラーとなる為です。FooのIdプロパティ(int)にBarを設定しようとしている、ということなのですが、そんなはずもなく。

java.lang.RuntimeException: java.lang.IllegalArgumentException: Can not set int field test.entity.Foo.id to test.entity.Bar
	at mungbean.pojo.FieldDefinition.get(FieldDefinition.java:46)
	at mungbean.pojo.PojoEncoder.encode(PojoEncoder.java:58)
	at mungbean.protocol.bson.BSONCoder.write(BSONCoder.java:52)
	at mungbean.protocol.bson.AbstractBSONArray.encode(AbstractBSONArray.java:57)
	at mungbean.protocol.bson.BSONCoder.write(BSONCoder.java:52)
	at mungbean.protocol.bson.AbstractBSONMap.encode(AbstractBSONMap.java:54)
	at mungbean.pojo.PojoEncoder.encode(PojoEncoder.java:60)
	at mungbean.protocol.bson.BSONCoder.write(BSONCoder.java:69)
	at mungbean.protocol.message.InsertRequest.<init>(InsertRequest.java:19)
	at mungbean.AbstractDBCollection$1.doExecuteWithErrorChecking(AbstractDBCollection.java:77)
	at mungbean.AbstractDBCollection$ErrorCheckingDBConversation.execute(AbstractDBCollection.java:214)
	at mungbean.SingleNodeDbOperationExecutor.execute(SingleNodeDbOperationExecutor.java:53)
	at mungbean.SingleNodeDbOperationExecutor.executeWrite(SingleNodeDbOperationExecutor.java:67)
	at mungbean.AbstractDBCollection.executeWrite(AbstractDBCollection.java:230)
	at mungbean.AbstractDBCollection.save(AbstractDBCollection.java:73)
	at test.MongoTest.testSaveFoo(MongoTest.java:23)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.junit.internal.runners.TestMethodRunner.executeMethodBody(TestMethodRunner.java:99)
	at org.junit.internal.runners.TestMethodRunner.runUnprotected(TestMethodRunner.java:81)
	at org.junit.internal.runners.BeforeAndAfterRunner.runProtected(BeforeAndAfterRunner.java:34)
	at org.junit.internal.runners.TestMethodRunner.runMethod(TestMethodRunner.java:75)
	at org.junit.internal.runners.TestMethodRunner.run(TestMethodRunner.java:45)
	at org.junit.internal.runners.TestClassMethodsRunner.invokeTestMethod(TestClassMethodsRunner.java:66)
	at org.junit.internal.runners.TestClassMethodsRunner.run(TestClassMethodsRunner.java:35)
	at org.junit.internal.runners.TestClassRunner$1.runUnprotected(TestClassRunner.java:42)
	at org.junit.internal.runners.BeforeAndAfterRunner.runProtected(BeforeAndAfterRunner.java:34)
	at org.junit.internal.runners.TestClassRunner.run(TestClassRunner.java:52)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:45)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:460)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:673)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:386)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
Caused by: java.lang.IllegalArgumentException: Can not set int field test.entity.Foo.id to test.entity.Bar
	at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(Unknown Source)
	at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(Unknown Source)
	at sun.reflect.UnsafeFieldAccessorImpl.ensureObj(Unknown Source)
	at sun.reflect.UnsafeIntegerFieldAccessorImpl.getInt(Unknown Source)
	at sun.reflect.UnsafeIntegerFieldAccessorImpl.get(Unknown Source)
	at java.lang.reflect.Field.get(Unknown Source)
	at mungbean.pojo.FieldDefinition.get(FieldDefinition.java:44)
	... 35 more

コレクションを取得する際にFoo.classを指定しているが故なのか、embeddingにするには違うアプローチが必要なのかわかりませんが…。Barを別のコレクションに保存して、FooにはBarのObjectIdだけ持たせる方が良いのかな。

追記: 2011/05/05

FooのインスタンスにBarのインスタンスのリストを設定するとエラーになる件ですが、mungbeanでPOJOを扱うTestCaseを見ても、複数のPOJOを対象としているケースはありませんでした。
コードを調べてみたところ、PojoEncoder#write→PojoEncoder#encode→BSONMap#encode→BSONArray#write→BSONArray#encode→PojoEncoder#write→PojoEncoder#encode、の順序でcallされており、最後のPojoEncoder#encodeでFooのField情報を使ってBarから値を取得しようとして失敗してました。Barのインスタンスを見つけたらPojoEncoderを作ってencodeしてやればBSONに変換できそうですが、現状、encode時にpersistenceの起点となるPOJO以外を見つけてしまうとダメそうです。

*1:Webで標準API以外の方式を探してみる限りprotocol bufferやDSLを使ったCode Generationのアプローチが多いようです

成果物のファイル名にプロファイル名を入れたい

Maven2弱者です。
「mvn -P production package」のようにプロファイルを指定して成果物を生成した際、「hoge-0.0.1-production.war」のように成果物のファイル名に指定したプロファイルの名称が入るようにしたいのですが、良い方法が分かりません。
pom.xml内で、実行時に指定されたプロファイルの名称を取得することができれば良いのですが、${profile}や${profile.id}では取得できずnullになってしまいます。
仕方がないので、pom.xml内でプロファイル毎にプロパティを定義して、finalNameで定義したプロパティを使用するようにしています。

  <profiles>
    ...
    <profile>
      <id>production</id>
      <properties>
        <profile.name>production</profile.name>
      </properties>
    </profile>
  </profiles>

  <build>
    <finalName>${artifactId}-${version}-${profile.name}</finalName>

せっかくprofileという仕組みがあるのだから、自前で定義しなくても取れそうなものですが…。

abstract pointcutとweavingのスイッチ

GAE/Jで永続化層にJDOを使ってきたのですが、GAE/Jを良く知る為にLow Level APIも試してみたい。そこで、AspectJを使って永続化層をうまく切り替えられないものかな、と。ある永続化層を選択した場合のみweavingされるようなadviceを定義できれば、例えばJavaBeanに対する値の取得/設定を、裏でコッソリEntityのget/putに変換してしまうとか。
取りあえずpointcutとadviceを定義し、実行コンテキストによってadviceの内容を実行するかどうか動的に判断するようにweavingすることは簡単です。例えば先ほどのJavaBean(JDOのデータクラス)→Entityの例で言うと、JavaBeanのsetter/getterをpointcutとし、adviceでは実行コンテキストを見て、Low Level APIであればEntityに処理を委譲する、それ以外はスルーする、というやり方ができると思います。
しかし、委譲するかどうか判定するコードは、実行コンテキストが何であれweavingされてしまうことになります。それがちょっと気持ち悪い。考えを進めていくうちに、adviceをweavingするかどうかを、staticにちょっとした操作で切り替えることができないか、と思うようになりました。
その一つの方法がabstract pointcutです。abstract pointcutは、その名のカンジのとおり、pointcutの具体的な位置を定義しません。しかし、abstract pointcutを指定してadviceを定義することができます。

  abstract pointcut metadtaCollect();
  
  after() : metadtaCollect(){
    InitializerSignature initsig =
      (InitializerSignature)thisJoinPoint.getSignature();
    collectMetadata(initsig.getDeclaringType());
  }

上記コードはadviceの実装がinitializer系のpointcutを前提としてしまっているのであまり良い例ではありませんが、abstract pointcutに対してadviceを定義することができる点が重要です。abstract pointcutに対してadviceが定義されてもweavingされません(どこにweavingするか定義してないので当然ですが)。abstract pointcutを持つaspectは、abstract aspectになります。abstract aspectを継承して、abstract pointcutと同じシグネチャのpointcutを定義することにより、abstract aspectで定義したadvice(上記コードだとcollectMetadataメソッドを呼び出しているブロック)がweavingされます。

public aspect GaeModelMetadataCollectorAtClassInit extends GaeModelMetadataCollector{
	pointcut metadtaCollect() : staticinitialization(net.wrap_trap.bitro.model.Model+);
}

このような仕組みを使うことにより、例えば永続化層の各バリエーションに対してabstract aspectを用意し、使用する永続化層が決まったらそれに対応するabstract aspectを継承したaspect、pointcutを定義することで、必要な実装だけをweavingできます。