覚え書きです。
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 などの名前も上がったが、そっちは覚書きが必要なほど複雑ではなかった。
開発環境
- 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 用の公式設定手順
-
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 では、下記手順でビルドした。
-
Couchbase.Lite.Unity
を右クリック - ツール > パッケージを作成
- Archive of Binaries
-
Couchbase.Lite.Unity
のみをチェック - コンフィグレーション:Release, ターゲットプラットフォーム:Windows
作成されたバイナリのうち、 Couchbase.Lite.dll.mdb
と UnityEngine.dll
以外を Assets
フォルダに追加。
前述の SQLitePCL.raw.dll
は、iOS以外を対象にビルドするように設定。
実装
メインループのフック
Couchbase で持っている UnityMainThreadScheduler に Unity のメインループをフックさせるために、コンポーネントとして追加する必要がある。
追加しないとデータベースを取得することが出来ない。
gameObject.AddComponent<UnityMainThreadScheduler> ();
データベース インスタンス取得
無い場合は新規に作成してくれる。
var manager = Manager.SharedInstance;
var db = manager.GetDatabase(DB_NAME);
データベース削除
db.Delete ();
CRUD
Retrieve
Document document = db.GetDocument ("docId");
View の利用
View インスタンスを用いて独自に Map/Reduce が出来る。
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 のみも可能。
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 よりも気軽に簡易な条件検索ができる。
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
しておいた方がよい。
try {
foreach (QueryRow row in query.Run()) {
// do something
}
} catch (ArgumentNullException e) {
Debug.Log ("No data found.");
}
Create / Update
Document document = db.GetDocument ("docId");
document.Update ((UnsavedRevision rev) => {
rev.Properties["Key"] = "foo";
return true;
});
新規挿入は下記の様にも書ける。
既にキーが存在する場合は PutProperties
のタイミングでコンフリクトとして処理する事ができる。
これにより、暗黙的な上書きを防止することが出来る。
Document document = db.CreateDocument ();
document.Id = "docId";
document.PutProperties (new Dictionary<string, object>(){
{"Key", "foo"}
});
ドキュメントが既に作成されているかどうかは、リビジョンの有無で確認できる。
Document document = db.GetDocument ("docId");
if (document.CurrentRevision == null) {
// process new document
}
Delete
Document document = db.GetDocument ("docId");
document.Delete();
所感
- ドキュメント指向楽しい
- MapReduce楽しい
- StartKey/EndKey や View 内での条件一致は RegEx 系操作、パフォーマンス直結
- NoSQL を謳っているが、 SQlitePCL に依存していてモヤモヤする
- パフォーマンスが、言うほど良くない気がする
正直な所、ありきたりなゲームデータやマスターデータのセーブ/ロードに、あえてこれを使う理由は無いんじゃないか、という感想です。
クライアント単体で複雑なデータを処理させたいのであれば一考の価値あり。
iOS ビルドの Player Settings による影響範囲だけ注意。