[Groovy] MongoDBを簡単に扱えるイケてる言語Groovy -基本的なCRUD-

これはG* Advent Calendar 2015の7日目の記事です。
昨日は [yukungsさんの「Groovy で寒い冬を乗り越える話 #gadvent」](http://yukung.hatenablog.com/entry/2015/12/06/080000)でした。
こうやってハードウェアを扱える人は本当にかっこいいですね！

# はじめに
G*のアドベントカレンダーではあるのですが、今後MongoDBに興味のある人がGoogleなどから辿り着いたときを想定して、以下の文章を書いています。
G*な人からするとちょっと違和感のある書き方かもしれませんがご了承ください。
２部構成で、今回はGroovyからJavaの`mongo-java-driver`を用いてMongoDBを扱う基本、明日はさらにGroovyのライブラリやその文法からより簡単便利にMongoDBが扱えることを示します。

# Groovyとは？
正式には[Apache Groovy](http://www.groovy-lang.org/)です。
JVM上で動く動的型付け言語で、Javaを更に便利にした言語です。
Groovyを動かすためにはJava8が必要です。逆に言えばJava8があれば動きます！
Java8がすでにインストールされているのであれば、[SDKMAN](http://sdkman.io/)をインストールしましょう。
SDKMANはGroovyに関連するプロダクトに限らず、様々なJavaプラットフォームで動くプロダクトを簡単にインストール、管理できるツールです。
[SDKMAN自体のインストールはとても簡単です。](http://sdkman.io/install.html)
SDKMANをインストールしたあとに、`sdk intall groovy`を実行すれば、Groovyの最新バージョンがインストールされます。
Windowsユーザの方は[posh-gvm](https://github.com/flofreud/posh-gvm)を参照してください。
よくわからん！という方は以下の記事を参照しつつGroovy自体のインストーラを使ってインストールすることも出来ます。
[インストールと実行（プログラミング入門 with Groovy）](http://qiita.com/saba1024/items/66c7154608ed52363e8f)


# GroovyとMongoDB
すでに述べましたが、GroovyはJavaを便利にした言語です。大胆に言い換えれば **GroovyはJavaです**。
つまり、JavaのMongoDB用ライブラリである`mongo-java-driver`をGroovyからもそのまま利用できます。
また、最近の`mongo-java-driver`だと、ちょっと前のバージョンの`mongo-java-driver`からは比べ物にならないくらい簡単にMongoDBが扱えるようになっています。
また、Groovyを用いることでさらに冗長な記述を減らすことが可能になります。
**JavaでMongoDB使ったことがあるけど、最新のMongoDBドライバはまだ触ってない**、
**MongoDBは色々触ってるけど、まだプログラミング言語からはMongoDBを扱ったことがない**、
という方にも参考になると思います。
そして、GroovyにはMongoDB用のサードパーティライブラリの[GMongo](https://github.com/poiati/gmongo)がありますが、最新の`mongo-java-driver`は、それ自体でかなり簡潔な書き方が出来ます。GMongo以外は使ったことがないというGroovyユーザの方にも参考になるのではと思います。

# 前提条件
今回、コードを実際に動かした環境は

|Name|Value|
|----|----|
|OS|Linux Mint 17
|MongogDB|3.0.7|
|Java|1.8.0_60|

となっています。
一番上で述べましたが、事前にGroovyの最新版をインストールしておいてください。
また、今回のサンプルコードをサクサク実行するために、`Groovyコンソール`の利用をおすすめします。
利用方法は[こちらを参照してください。](http://qiita.com/saba1024/items/66c7154608ed52363e8f#%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E5%AE%9F%E8%A1%8C)

Groovyの文法の話はここでは触れません。
ただ、それでもGroovyからMongoDBがとても簡単に扱えるということが分かると思います。

# コーディング開始
さて！それではいよいよ始めていきましょう。

## 準備
まず、以下のコードを記述します。

```groovy
@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
```
この記述だけで、`mongo-java-driver`のダウンロードやパスの設定、今回サンプルで利用する必要なクラスのimportが完了します。

## MongoDBに接続
それでは、さらに上記のコードの下部に以下を追記します。
**なお、今後すべてのコードはどんどん既存のコードの下側に追記していってください。**

```groovy
// MongoDB本体に接続
MongoClient mongoClient = new MongoClient("localhost", 27017)
// 利用するデータベースに接続（データベース名はgroovy）
MongoDatabase db = mongoClient.getDatabase("groovy")
// 利用するコレクションに接続（コレクション名はtest）
MongoCollection<Document> collection = db.getCollection("test")
```
コレだけです！MongoDBの特徴として、特にデータベースとかコレクションが事前に存在していなくても大丈夫です。
また、Groovyは変数を宣言する際に型を指定する必要は無いので、以下のように`defキーワード`を使って更にシンプルに書くことも出来ます。

```groovy
def mongoClient = new MongoClient("localhost", 27017)
def db = mongoClient.getDatabase("groovy")
def collection = db.getCollection("test")
```
なお、この時点ではまだ **groovyデータベース** も **testコレクション** もMongoDB上には作成されていません。
コレクションに値を保存した段階で自動的にコレクションも作成されます。

## ドキュメントの削除
いきなりですが、今後何度もサンプルコードを実行していくと、どんどんデータがMongoDB蓄積されて面倒です。
そのため、毎回データを削除するコードを書いておきましょう。

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

これで、`testコレクション`にドキュメントがある場合は、毎回削除されるようになります。
上記の内容はまだ今一わからないと思いますが、以下どんどん進めていけば分かってきますので気にせず先に進みましょう。

## ドキュメントの保存
それではいよいよドキュメントを保存していきましょう！


```groovy
// 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)

// 複数件一気に登録する
List<Document> documents = (1..10).collect { new Document("i", it).append("name", it) }
collection.insertMany(documents)
```

ドキュメントの登録だけでなく、更新、削除にも、 **1件だけ** 処理する為のメソッドと、 **複数件一気に** 処理するためのメソッドが用意されています。
上記のコードを見れば一目瞭然ですが、`insertOne()`が一件だけ、`insertMany()`が、一気にドキュメントを登録するメソッドになります。
MongoDBに登録するドキュメントを生成するために、`Document`というクラスのインスタンスを生成します。
使い方はとても簡単で、第1引数にキー名、第2引数に値を渡します。インスタンス生成時のコンストラクタへ渡す場合でも、その後返される自身のインスタンスへ値を追加する`appendメソッド`へも同様に値を渡していけば、MongoDB用のドキュメントが生成されます。
ドキュメントが入れ子になる場合は、上記の`info`の部分のように、値にさらに新しいDocumentオブジェクトを指定することで可能になります。
上記の`doc`の中身は

```json
{
    "name" : "MongoDB", 
    "type" : "database", 
    "count" : 1,
    "info" : {
        "x" : 203,
        "y" : 102 
    }
}
```
という感じのJSONになっています。
このJSON（Documentオブジェクト）を`insertOne`に渡すか、Listに格納してそのリストを`insertMany`に渡せば、MongoDBにドキュメントが保存されます。
この時点で、**groovyデータベース** も **testコレクション** がMongoDBに保存されました。

## ドキュメントの取得

ドキュメントの取得、SQLで言う所のSELECT文ですね。
基本的にCollectionオブジェクトのメソッドである`find`メソッドを実行するだけです。
`find`を実行すると、Documentが格納されたリストが返されます。
さらに`first`を実行すると、そのリストに格納されたDocumentから最初のDocumentオブジェクトを取得できます。

```groovy
// 全件取得
assert 11 == collection.find().size()

// 最初のレコードを取得
Document myDoc = collection.find().first()
assert "MongoDB" == myDoc.name
```

簡単ですね。
では、検索する際に条件を指定するにはどうしたらいいでしょうか。
それも簡単で、以下のようにfindに条件をそのまま記述するだけです。
まるでMongoDBをmongoシェルから直接いじっているような流れるようなお手軽さですね！

```groovy
myDoc = collection.find(eq('i', 5)).first()
assert 5 == myDoc.i
assert 45 == collection.find(gte('i',5)).sum{it.i} // シンプルな取得条件
assert 30 == collection.find( and(gt('i',5),lt('i',10)) ).sum{it.i} // ANDで複数の条件を指定
```

`.sum{it.i}`という記述がありますが、コレはGroovyのメソッドです。
リストに格納されている各要素から、`i`の合計を求める、という処理になっています。
MongoDBからGroovyへ流れるように自然に式が記述できるのが解ると思います。
Groovyを使えば合計を求めたりすることもこんなに簡単になります！
利用できる`gt`や`lt`といった比較用演算子については[Filters](http://mongodb.github.io/mongo-java-driver/3.1/builders/filters/)を参照してください。

次に、指定した値でソートして最初のドキュメントのiの値を取得する、というサンプルを以下に記載します。

```groovy
// iというフィールドを持つドキュメントを、降順で取得する。そして、最初の値を取得。
// なお、sortとdescendingは、com.mongodb.client.model.Sortsクラスにある。
assert 10 == collection.find( exists('i') ).sort(descending('i')).first().i
```

ドキュメント指向なMongoDBなので、RDBMSと違ってドキュメントには特定のフィールドが合ったりなかったりします。
そこで、上記の例では、`exists`でまず`i`というフィールドを持つドキュメントを抜き出して、さらにiの降順に並び替え、そこから最初のドキュメントのiの値を取得しています。
sortの詳細については[Sorts](http://mongodb.github.io/mongo-java-driver/3.1/builders/sorts/)を参照してください。

## ドキュメントの更新

続いてドキュメントの更新です。SQLで言うところの`UPDATE`です。


```groovy
// UpdateOneの第1引数に条件式、第2引数にUPDATEで実行する式を与える。
// 存在しないフィールドを与えれば該当するドキュメントにそのフィールド追加される。以下はiフィールドが追加される
// フィールドが存在する場合は、単純にそのフィールドの値を上書きする。
collection.updateOne( eq('name', 'MongoDB'), set('i', 1) )

// UPDATEで複数のフィールドをアップデートしたい場合は、combineの中に記述する。
// nameフィールドがMongoDBというドキュメントを1件更新する。
// iフィールドと、countフィールドがそれぞれ1インクリメントされる。
collection.updateOne( eq('name', 'MongoDB'), combine( inc('i',1), inc('count',1) ) )

// 複数のドキュメントを一気にUPDATE
// iフィールドの値が8より大きいドキュメントのiフィールドの値を10インクリメントする。
UpdateResult ur = collection.updateMany(gt('i',8), inc('i',10)) // このサンプルではiが9と10のものは、iの値に10を加算する。

// 更新されたか確認してみる。なお、collectとsumはGroovyの便利なメソッド！
assert 39 == collection.find(gt('i',8)).sum{it.i}
assert 2 == collection.find(eq('name', 'MongoDB')).first().i
assert 2 == ur.matchedCount
```

上記のソースとコメントを見てもらえればなんとなく解ると思います。
ドキュメントを1件だけ更新したい場合は`updateOne`を、複数件一気に更新したい場合は`updateMany`を使用します。
それぞれ **第1引数に更新対象のドキュメントの条件** 、 **第2引数に更新する内容** を渡します。
利用できる`inc`や`set`といった更新系メソッドの一覧は[Updates](http://mongodb.github.io/mongo-java-driver/3.1/builders/updates/)を参照してください。
SQLだと`UPDATE TABLE_NAME SET i=1 WHERE i > 8`みたいな感じに、最初に更新内容、その後のWHEREで条件を指定するので逆になった感じですね。

## ドキュメントの削除
すでにプログラムの一番上最初に全件削除のコードを書きましたね？
以下は、検索して該当するレコードのうち1件のみ削除する方法

```groovy
// 1件削除
collection.deleteOne(gt('i', 8))
```

削除もドキュメントの作成や更新と同様に、1件だけの削除する場合は`deleteOne`、該当するドキュメントをすべて削除する場合は`deleteMany`となります。

# その他
ドキュメントを更新、削除するときに条件を指定するのは別にいいけど、全件が対象の場合はどうなるの？という話ですが、一番最初の`deleteMany`のように、第1引数の条件のどころに`new Document()`を渡してあげれば、無条件ですべてのドキュメントが処理対象となります。


#まとめ
いかがだったでしょうか？
G*な人やJavaな人からは最新のJavaDriverを使えばかなりシンプルに処理が記述できる、MongoDBユーザの人からもGroovyを使えばかなりお手軽にプログラムからMongoDBを扱えることが伺えるのではないでしょうか？
しかしGroovyの便利さはコレだけにはとどまりません！明日はさらにGroovyの機能を使ったサンプルを投稿します！

最後に、現在のコードの全体を以下に記載しておきます。

```groovy
@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)

// 複数件一気に登録する
List<Document> documents = (1..10).collect { new Document("i", it).append("name", it) }
collection.insertMany(documents)

assert 11 == collection.find().size()

// 最初のレコードを取得
Document myDoc = collection.find().first()
assert "MongoDB" == myDoc.name

myDoc = collection.find(eq('i', 5)).first()
assert 5 == myDoc.i
assert 45 == collection.find(gte('i',5)).sum{it.i} // シンプルな取得条件
assert 30 == collection.find( and(gt('i',5),lt('i',10)) ).sum{it.i} // ANDで複数の条件を指定

// iというフィールドを持つドキュメントを、降順で取得する。そして、最初の値を取得。
// なお、sortとdescendingは、com.mongodb.client.model.Sortsクラスにある。
assert 10 == collection.find( exists('i') ).sort(descending('i')).first().i

// UpdateOneの第1引数に条件式、第2引数にUPDATEで実行する式を与える。
// 存在しないフィールドを与えれば該当するドキュメントにそのフィールド追加される。以下はiフィールドが追加される
// フィールドが存在する場合は、単純にそのフィールドの値を上書きする。
collection.updateOne( eq('name', 'MongoDB'), set('i', 1) )

// UPDATEで複数のフィールドをアップデートしたい場合は、combineの中に記述する。
// nameフィールドがMongoDBというドキュメントを1件更新する。
// iフィールドと、countフィールドがそれぞれ1インクリメントされる。
collection.updateOne( eq('name', 'MongoDB'), combine( inc('i',1), inc('count',1) ) )

// 複数のドキュメントを一気にUPDATE
// iフィールドの値が8より大きいドキュメントのiフィールドの値を10インクリメントする。
UpdateResult ur = collection.updateMany(gt('i',8), inc('i',10)) // このサンプルではiが9と10のものは、iの値に10を加算する。

// 更新されたか確認してみる。なお、collectとsumはGroovyの便利なメソッド！
assert 39 == collection.find(gt('i',8)).sum{it.i}
assert 2 == collection.find(eq('name', 'MongoDB')).first().i
assert 2 == ur.matchedCount

// 1件削除
collection.deleteOne(gt('i', 8))
```


# 参考
[Getting Started with MongoDB (Java Edition)](https://docs.mongodb.org/getting-started/java/)
[MongoDB Java Driver](http://mongodb.github.io/mongo-java-driver/)
[Apache Groovy](http://www.groovy-lang.org/)
[SDKMAN](http://sdkman.io/)
[Filters](http://mongodb.github.io/mongo-java-driver/3.1/builders/filters/)
[Sorts](http://mongodb.github.io/mongo-java-driver/3.1/builders/sorts/)
[Updates](http://mongodb.github.io/mongo-java-driver/3.1/builders/updates/)