[Groovy] MongoDBを簡単に扱えるイケてる言語Groovy -Groovyの応用編-

  • 3
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

これはG* Advent Calendar 2015の8日目の記事です。

はじめに

昨日の記事でGroovyからMongoDBを扱う基本を示しました。
今回は、さらにGroovyの機能を使ってよりGroovyな感じを掴んでみましょう!
なお、一番最後にコード全体を記載していますので、時間のない方はそちらを見て頂いてもOKです。

ドキュメントの生成をよりシンプルに

さて、昨日の記事でドキュメントを生成する際に、以下のコードを書きました。

// 1件登録する
Document doc = new Document("name", "MongoDB")
                .append("type", "database")
                .append("count", 1)
                .append("info", new Document("x", 203).append("y", 102))
collection.insertOne(doc)

その結果生成されるドキュメント(JSON)は以下のようになります。

{
    "name" : "MongoDB", 
    "type" : "database", 
    "count" : 1,
    "info" : {
        "x" : 203,
        "y" : 102 
    }
}

いいねー!シンプルシンプル!
でも、よく見ると毎回フィールドごとにappend書かなきゃイケないのが冗長ですね。
さらに入れ子の場合にはまたDocumentオブジェクト生成しないといけないし。。。
はい!あります!対応策あります!

と言ってもそんな難しい話ではなくて、実はDocumentクラスはコンストラクタでMapを受け付けてくれます。
さらに!Groovyの場合はMapがとてもお手軽に生成できます。
では、上記のコードと等価なGroovyのMapバージョンを見てみましょう!

def doc2 = [
    "name": "MongoDB",
    "type": "database",
    "count": 1,
    "info": ["x": 203, "y": 102]
]
collection.insertOne(new Document(doc2))

これです!これだけです!このコードは最初のappendだらけのコードと全く同じです!
新しいMongoDBの公式JavaDriverと、Groovyの機能を組み合わせるとこんなにコードがシンプルになります!

JSON(テキスト)を保存する

GroovyのGStringを使えば、簡単にJSONをテキストとして表現できます。
GStringを利用するのには別に特別は宣言などは必要ありません。また、3つのダブルクォーテーションで囲んであげれば、改行やエスケープが簡単に記述できます。
そして、Documentクラスはスタティックメソッドとしてテキスト形式のJSONをパースするparseメソッドがあるので、JSONテキストを渡してあげれまこれまたお手軽にMongoDBに保存できます。

String jsonAsText = """
{
    "name": "Koji", 
    "age": 30, 
    "address":{
        "city": "Tokyo", 
        "country": "Japan"
    },
    "married": true,
    "favorites": ["Groovy", "Grails", "MongoDB"]
}
"""
collection.insertOne(Document.parse(jsonAsText))

通常のJSONをテキスト(String)として変数jsonAsTextに格納して、Document.parse()に渡しているだけです。
これで、Mapを使ったDocumentインスタンスの生成と同じことが実現できています。

JSON(オブジェクト)を保存する

さて、コレは一体なんのことでしょう?
基本的に上記のテキストでJSONを表現する方法で、Stringの中で変数も扱えるので特に問題はないのですが、GroovyにはJsonBuilderという簡単にJSONオブジェクトを生成するクラスがあります。今度はこのJsonBuilderを使って同じことをしてみましょう。

// JsonBuilder経由
// Groovy上で、複雑なJSONを生成する場合、JsonBuilderがとっても便利。
def builder = new groovy.json.JsonBuilder()
builder {
        name 'Koji'
        age 30
        address(
            city: 'Tokyo',
            country: 'Japan'
        )
        married true
        favorites 'Groovy', 'Grails', 'MongoDB'
}
collection.insertOne(Document.parse(builder.toPrettyString()))

実行結果は上記の「JSON(テキスト)を保存する」と全く同じです。
JsonBuilderのインスタンスを生成して値をその中に記述していくだけです。
ちょっと書き方が独特で、通常JSONは "KEY: VALUE"の形式ですが、上記のJsonBuilderの書き方だとキーとバリューの間のコロンが必要なくなります。(が、addressの部分のようにそうでもない例外もあります。。。)
そしてDocument.parseにtoStringした値を渡すだけでOKです。
ちなみに、JsonBuilderにはbuilder.toPrettyStringというメソッドが用意されていて、buiderの値を綺麗に表示することが出来ます。

// println builder.toPrettyString()を実行した結果
{
    "name": "Koji",
    "age": 30,
    "address": {
        "city": "Tokyo",
        "country": "Japan"
    },
    "married": true,
    "favorites": [
        "Groovy",
        "Grails",
        "MongoDB"
    ]
}

JSON(テキスト)を編集して保存する

さて、最初の「JSON(テキスト)を保存する」の応用編です。
すでにString型変数jsonAsTextにJSONが格納されています。
この状態で値を変更するにはどうすればいいでしょうか?
テキストなので当然正規表現で置換することも出来ます。が、そんな面倒臭いことしなく無いですよね?
Groovyならそれを簡単に実現できます!
GroovyにはテキストのJSONをMapライクなオブジェクトに変換してくれるJsonSlurperというクラスがあるのでそれを利用して実現できます。
ではコードを見てみましょう。

// JsonSlurper経由
// JsonSlurperを使うと、テキストで受け取ったJSONでも、通常のJSONオブジェクトのようにその中身を編集することができる
def slurper = new groovy.json.JsonSlurper().parseText(jsonAsText)
slurper.name = "JsonSlurper of Groovy!"
slurper.hoge = "piyo" // 存在しないフィールの追加もなんのその!
slurper.married = false
collection.insertOne(new Document(slurper))

JsonSlurperのインスタンスを生成して、parseTextメソッドにすでに存在しているテキスト形式のJSONを渡すだけです。
あとは通常のGroovyのMapのように扱えます!
そしてこのJsonSlurperインスタンスをnew Documentに渡してあげればちゃんとMongoDBに保存されます。

おまけ1(特定ドキュメントのUPDATE)

updateOneupdateManyでは、MongoDB上で対象ドキュメントの検索と更新が行われます。
では、該当ドキュメントの情報をGroovy上で加工して、さらにそのドキュメンをUPDATEするにはどうすればいいのでしょうか?
実はまだよく理解できていません。。。
一応、以下のコードでドキュメントを検索して、それぞれのtypeフィールドの内容を、それぞれの_idフィールドの値を付加してUPDATEすることが出来ます。恐らく別の良いアプローチがあるとは思いますが、とりあえずコレでも問題はないはずです。

collection.find(eq("name", "MongoDB")).collect {
    ["_id":it._id, "newType": "NoSQL(ObjectId = ${it._id})"]
}.each {
    collection.updateOne(eq("_id", it._id), set("type", it.newType.toString()))
}

おまけ2(JsonBuilderのComposite)

どういうことか?単純にJsonBuilderインスタンスの中で、別のJsonBuilderインスタンスを扱う、というだけです。
しかし、ちょっとクセがあって、JsonBuilderの中で利用される別のJsonBuilderは、.contentでその値を取得しなければならない、という点です。
なんの事だかワケ分かんないですよね。ソースを見てみましょう!

def builder2 = new groovy.json.JsonBuilder()
builder2 {
    abc 'def'
    fromOtherJsonBuilder builder.content
}
println builder2.toPrettyString()
collection.insertOne(Document.parse(builder2.toPrettyString()))

これで、1件新しいドキュメントがMongoDBに保存されます。
println builder2.toPrettyString()で表示されるJSONは以下のとおりです。

{
    "abc": "def",
    "fromOtherJsonBuilder": {
        "name": "Koji",
        "age": 30,
        "address": {
            "city": "Tokyo",
            "country": "Japan"
        },
        "married": true,
        "favorites": [
            "Groovy",
            "Grails",
            "MongoDB"
        ]
    }
}

特に難しいことはないですよね。元々あったJsonBuilderオブジェクトを、別のJsonBuilderで包んだ形です。
fromOtherJsonBuilder builder.contentを見て分かるとおり、単純に別のJsonBuilderインスタンスから.contentでその中身を持ってくるだけでOKです。
元々あるJSONを別のJSONで包む今回のような例だと、テキストベースでも当然実現できますがコードがゴチャゴチャしてしまいがちです。
そういった際に、JsonBuilderが力を発揮します。

まとめ

いかがだったでしょうか。
Groovyを使えば、MongoDB用の公式Java用ドライバを使って、さらに便利、シンプルにMongoDBを扱えることがわかったのではないでしょうか。
最後に、今回のコードの全体を記述して終わりにさせていただきます。
ありがとうございました。

明日は@h1romas4さんです!

@Grab(group='org.mongodb', module='mongo-java-driver', version='3.1.1')

import com.mongodb.MongoClient
import com.mongodb.client.MongoDatabase
import com.mongodb.client.MongoCollection
import org.bson.Document
import static com.mongodb.client.model.Filters.*
import static com.mongodb.client.model.Sorts.*
import static com.mongodb.client.model.Updates.*
import com.mongodb.client.result.UpdateResult

// MongoDB本体に接続
MongoClient mongoClient = new MongoClient("localhost", 27017)
// 利用するデータベースに接続(データベース名はgroovy)
MongoDatabase db = mongoClient.getDatabase("groovy")
// 利用するコレクションに接続(コレクション名はtest)
MongoCollection<Document> collection = db.getCollection("test")

// DELETE ALL EXISTING DOCUMENTS
if (collection.count() > 0) {
    collection.deleteMany(new Document())
}

// 1件登録する
Document doc = new Document("name", "MongoDB")
                .append("type", "database")
                .append("count", 1)
                .append("info", new Document("x", 203).append("y", 102))
collection.insertOne(doc)

// 上の例と等価
def doc2 = [
    "name": "MongoDB",
    "type": "database",
    "count": 1,
    "info": ["x": 203, "y": 102]
]
collection.insertOne(new Document(doc2))

// テキストなJSONを保存する
String jsonAsText = """
{
    "name": "Koji", 
    "age": 30, 
    "address":{
        "city": "Tokyo", 
        "country": "Japan"
    },
    "married": true,
    "favorites": ["Groovy", "Grails", "MongoDB"]
}
"""
collection.insertOne(Document.parse(jsonAsText))

// JsonBuilder経由
// Groovy上で、複雑なJSONを生成する場合、JsonBuilderがとっても便利。
// ちょっと書き方が独特で、通常JSONは "KEY: VALUE"の形式だけど、間のコロンが必要なくなる。
def builder = new groovy.json.JsonBuilder()
builder {
        name 'Koji'
        age 30
        address(
            city: 'Tokyo',
            country: 'Japan',
        )
        married true
        favorites 'Groovy', 'Grails', 'MongoDB'
}
collection.insertOne(Document.parse(builder.toString()))


// JsonSlurper経由
// JsonSlurperを使うと、テキストで受け取ったJSONでも、通常のJSONオブジェクトのようにその中編みを編集することができる
def slurper = new groovy.json.JsonSlurper().parseText(jsonAsText)
slurper.name = "JsonSlurper of Groovy!"
slurper.hoge = "piyo" // 存在しないフィールの追加もなんのその!
slurper.married = false
collection.insertOne(new Document(slurper))