Couchbase Lite Net の Unity への導入から簡単な実装まで

覚え書きです。

# Couchbase って？

オールラウンダーなドキュメント指向DB
http://www.couchbase.com/

# Couchbase Lite Net って？

Couchbase を軽量化したもの。
iOS/Android/Unity などで利用可能。
https://github.com/couchbase/couchbase-lite-net

# なぜ Couchbase？

PlayerPrefs では手に余るけど、 Sqlite にするには Asset Store が有象無象すぎるし、かといって自分で実装するのもコスト高いし、っていうかそんなにパフォーマンス良くないし。
というときの逃げ道としてとりあえず調査した。

他には [iBoxDB](http://www.iboxdb.com) などの名前も上がったが、そっちは覚書きが必要なほど複雑ではなかった。


# 開発環境

- OSX 10.11.4
- Unity 5.3.4f1
- Xamarin Studio Community 5.10.3

# 入手

下記から入手。
https://github.com/couchbase/couchbase-lite-net

最新のタグ `1.2.1` では Unity 向けのプロジェクトに含まれるサブプロジェクトの参照が色々とアレだったのでビルド出来なかった。
今回は実機での動作確認やAPI等の仕様を確認することが目的なので、確実に導入できそうな `Couchbase Lite for Unity3D - Beta 3` を利用した。

# プロジェクトの修正

結局、そのままではビルドが通らないので修正した。

`Couchbase.Lite.Unity.Tests.csproj` の `local-test.properties` が存在しない。
テストプロジェクトなので、一旦該当ファイルを削除。

# Unity 向け設定

## iOS 用の公式設定手順

https://github.com/couchbase/couchbase-lite-net/wiki/Unity3D-Usage

- iOS Player Settings > Optimization > Api Compatibility Level
  .NET 2.0 Subset -> .NET 2.0

- iOS Player Settings > Optimization > AOT Compilation Option
  nimt-trampolines=8096,ntrampolines=8096

- iOS 用の SQLitePCL.raw.dll を用意
SqlitePCL.raw.dll は、 Xamarin のプロジェクトで設定されているものだと iOS 実機でランタイム時にエラーが発生する。
そのため、 Unity 上に iOS用の下記 dll を追加する必要がある。
src/Couchbase.Lite.Net35/vendor/SQLitePCL/iOS/SQLitePCL.raw.dll 

Api Compatibility Level を変更しているので、ポインタや fixed array が使えなくなったりなどの副作用がある。
Asset Store などのサードパーティモジュールをバカバカ使ってるような場合は、事前に影響範囲を調べておくと良い。

# Unity 向けビルド

設定を終えた状態で、念のため全てのサブプロジェクトのビルドが個別に通ることを確認し、改めて `Couchbase.Lite.Unity` をビルド。

Xamarin では、下記手順でビルドした。

1. `Couchbase.Lite.Unity` を右クリック
2. ツール > パッケージを作成
3. Archive of Binaries
4. `Couchbase.Lite.Unity` のみをチェック
5. コンフィグレーション:Release, ターゲットプラットフォーム:Windows

作成されたバイナリのうち、 `Couchbase.Lite.dll.mdb` と `UnityEngine.dll` 以外を `Assets` フォルダに追加。
前述の `SQLitePCL.raw.dll` は、iOS以外を対象にビルドするように設定。

# 実装

## メインループのフック

Couchbase で持っている _UnityMainThreadScheduler_ に Unity のメインループをフックさせるために、コンポーネントとして追加する必要がある。
追加しないとデータベースを取得することが出来ない。

```cs:
gameObject.AddComponent<UnityMainThreadScheduler> ();
```

## データベース インスタンス取得

無い場合は新規に作成してくれる。

```cs:
var manager = Manager.SharedInstance;
var db      = manager.GetDatabase(DB_NAME);
```

## データベース削除

```cs:
db.Delete ();
```

## CRUD

### Retrieve

```cs:
Document document = db.GetDocument ("docId");
```

#### View の利用

View インスタンスを用いて独自に Map/Reduce が出来る。

```cs:
string viewName      = "myView";
string mapRedVersion = "1";
View view = db.GetView (mapName);
view.SetMapReduce ((IDictionary<string, object> dict,  EmitDelegate emit) => {
    if ((string)dict["purchasedItem"] == "apple") {
        emit (dict, 10);
    }
    else if ((string)dict["purchasedItem"] == "orange") {
        emit (dict, 15);
    }
}, (IEnumerable<object> keys,  IEnumerable<object> values, bool rereduce) => {
    int cost = 0;

    foreach (int val in values) {
        cost += val;
    }

    return cost;
}, mapRedVersion);

QueryEnumerator enumerator = view.CreateQuery().Run();
enumerator.MoveNext();
int totalCost = enumerator.Current.Value;
```

もちろん、 Map のみも可能。

```cs:
string viewName   = "myView";
string mapVersion = "1";
View view = db.GetView (mapName);
view.SetMap ((IDictionary<string, object> dict,  EmitDelegate emit) => {
    if ((string)dict["purchasedItem"] == "apple") {
        emit (dict, null);
    }
}, mapVersion);

foreach (QueryRow row in view.CreateQuery().Run ()) {
    // do something
}

```

#### Query

Map/Reduce よりも気軽に簡易な条件検索ができる。

```cs:
Dictionary<string, object> record = new Dictionary<string, object> () {
    {"Value", "foo"}
};

for (int k = 1; k <= 10; k++) {
    Document doc = db.GetDocument ("key" + k);
    doc.PutProperties (record);
}

Query query = db.CreateAllDocumentsQuery ();
query.StartKey = "key1";
query.EndKey   = "key3";

foreach (QueryRow row in query.Run()) {
    Debug.Log(row.Key);
}
```

ただし、キーは文字列で評価されるので、上記の例の場合に取れるドキュメントのキーは以下のようになる。

```
key1
key10
key2
key3
```


今回利用したタグでは、 `QueryEnumerator` の中身の `IEnumerator` が暗黙的に `null` になっている場合があり、しかも感知できない。
そのため、 `try/catch` しておいた方がよい。

```cs:
try {
    foreach (QueryRow row in query.Run()) {
        // do something
    }
} catch (ArgumentNullException e) {
    Debug.Log ("No data found.");
}
```

### Create / Update

```cs:
Document document = db.GetDocument ("docId");
document.Update ((UnsavedRevision rev) => {
    rev.Properties["Key"] = "foo";
    return true;
});
```

新規挿入は下記の様にも書ける。
既にキーが存在する場合は `PutProperties` のタイミングでコンフリクトとして処理する事ができる。
これにより、暗黙的な上書きを防止することが出来る。

```cs:
Document document = db.CreateDocument ();
document.Id = "docId";
document.PutProperties (new Dictionary<string, object>(){
    {"Key", "foo"}
});
```

ドキュメントが既に作成されているかどうかは、リビジョンの有無で確認できる。

```cs:
Document document = db.GetDocument ("docId");
if (document.CurrentRevision == null) {
    // process new document
}
```

### Delete

```cs:
Document document = db.GetDocument ("docId");
document.Delete();
```

# 所感

- ドキュメント指向楽しい
- MapReduce楽しい
- StartKey/EndKey や View 内での条件一致は RegEx 系操作、パフォーマンス直結
- NoSQL を謳っているが、 SQlitePCL に依存していてモヤモヤする
- パフォーマンスが、言うほど良くない気がする

正直な所、ありきたりなゲームデータやマスターデータのセーブ/ロードに、あえてこれを使う理由は無いんじゃないか、という感想です。
クライアント単体で複雑なデータを処理させたいのであれば一考の価値あり。
iOS ビルドの Player Settings による影響範囲だけ注意。