Momy: MongoのビューにMySQLを使ったっていいじゃないか


最近、改めて(というか3周くらいして)、DBは混ぜて使ったほうが良いんじゃないかという話もちらほら。そんな高尚な話はともかく、Mongo始めたSQLerの「精神衛生のために」「いざとなったらSQLで」くらいのノリで作ってみた件です。

![Artboard 1.png](https://qiita-image-store.s3.amazonaws.com/0/1858/ff85da46-c732-5794-b7b0-1f9d3fb9d231.png "Artboard 1.png")

コードだけ見たい方は、[Momy](https://github.com/cognitom/momy)リポジトリをどうぞ。

## はじめに

動機はいくつかあって、MongoDBに対して、

- がしがし集計したい
- さくっとテーブル間のJOINをしたい
- Microsoft Accessや、Libre Office Baseから接続したい

など。よくある話の割に、MEANやらSPAやらの文脈で考えたときに、結構面倒なんですよね。最後のやつも意外と **鬼門です**。

- *注1:* ここでは、一応、MongoDB→MySQL同期の方向に話を持って行きますが、もっといい方法があったら是非教えてください。
- *注2:* ちなみにMEANの「A」はanythingの略ですよ?

## ソリューションを探して

### ODBC?

一応オープンなものが存在していますが、「実験的」なまま3年経過しているので、あくまで参考まで。あー、もう、MySQLなら簡単につなげるのに...! やっぱりNoSQLに直接繋ぎに行くって無理があるのか。

- [sp13_10g](https://github.com/NYUITP/sp13_10g) - mongoDb ODBC Driver *EXPERIMENTAL*

あとは、商用であれば[ここ](http://www.simba.com/connectors/mongodb-odbc)とか、[ここ](http://www.easysoft.com/products/data_access/odbc-mongodb-driver/)とかからも、ドライバが出ているようですが、「お高いんでしょ?」で深追いせず。


### MoSQL?

[Stripe](https://stripe.com/about)が「[MoSQL](https://github.com/stripe/mosql)」というオープンソースツールを出しています。そのままズバリMongoDBからSQLの世界に同期をかけるツールで、Rubyで書かれています。GitHubのスターが1Kくらい。ただし、 **PostgreSQLにのみ対応** です。[MySQLに対応させる動き](https://github.com/stripe/mosql/issues/30)も一瞬あったんですが、なかったことになってるっぽい。残念。

- https://github.com/stripe/mosql

とはいえ、PostgreSQLに転向可能であれば選択肢のひとつです。MEANの文脈だと扱いづらいのは確かですが...

### Nodeの世界にないんですか?

ここまで候補を見てきたものの、今一歩。なんとか、Nodeの世界で解決できないものか? しらみ潰しに探してみると...、それっぽいものが。こ、これが正解か!?

- [MongoDB-to-MySQL](https://github.com/doubaokun/MongoDB-to-MySQL)

[npmにも公開](https://www.npmjs.com/package/mongo2mysql)されているのですが、どうも作りかけで放置した感がパない。実際、使ってみると...動かなくて途方に暮れます。やはり、これも「3年前」で止まっていて、皆もうMongoに飽きたんだな...とか分かって歴史を感じます。

## 仕方がない、オレオレで

途方に暮れつつも、コードを読むと、MoSQLにしろMongoDB-to-MySQLにしろ、やっていることはシンプル。

- MongoDB間のReplication機能をうまく流用する
- Tailable Cursor(追尾カーソル?)でMongoDBの変更を監視
- 変更箇所のログを取ってきて、SQLなデータベースに反映

以上。ほとんど、MongoDB自体の機能でなんとかなるかも。これだけなら作れそうだよ?

### Momy

作りました。

当初、MongoDB-to-MySQLを改造する方向で始めたんですが、結果どこにも元のコードが残ってないところまで来たので、別名にしました。 **Mo**ngo to **My**SQLで「[Momy](https://github.com/cognitom/momy)」です。フランスにそんな地名があるらしいです。

![momy.png](https://qiita-image-store.s3.amazonaws.com/0/1858/47f51473-00cf-183f-1ad6-cd479fad9bf3.png "momy.png")


### レプリカセット用のログを流用する

コードの要点は、次の部分でしょうか。(若干デフォルメ)

```javascript
tail () {
  const
    filters = {
      // 前回のタイムスタンプより新しいログを監視
      ts: { $gt: Timestamp.fromNumber(ts) }
    },
    curOpts = {
      // 追尾モードを設定
      tailable: true,
      awaitdata: true
    }
  mongodb.connect(url)
    .then(db => {
      db.collection('oplog.rs')
        .find(filters, curOpts)
        // 検索結果はストリームとして取得
        .stream()
        .on('data', log => { /* MySQLに保存 */ })
    })
}
```

単体で使ってる時は存在しないのですが、MongoDBにリプリケーション関連の設定をすると、`local`という名のデータベースが追加されます。(GUIアプリだと不可視になっていることもあります)

最近のMongoDBだと基本的には[レプリカセット](https://docs.mongodb.org/manual/core/replication/)(replica set)を使って、複数台でのレプリケーション/フェールオーバーを実現します。この、本来はMongoDB同士の同期に利用されるログ情報は、先ほどの`local`内の`oplog.rs`コレクションに保存されています。これを監視しつつ、ストリームとして取得すると、さっきのようなコードになるわけです。便利。MongoDBのAPI便利すぎ。

### データベースの下ごしらえ

まずは、空のmongodを立ち上げましょう。その際に、`--replSet`と`--oplogSize`のオプションを加えます。前者はリプリカセットの「グループ名」みたいなもの、後者はログのサイズを設定するものです(MB単位)。

- *注:* すでに手元で運用中のmongodがある場合は、別ポート`--port`かつ別ディレクトリ`--dbpath`にしたほうが良いかもしれません。

```bash
$ mongod --replSet "rs0" --oplogSize 100
```

別ウィンドウのターミナルで、mongoシェルを開きます。

```bash
$ mongo
....
> rs.initiate()
```

このように、`rs.initiate()`コマンドを実行すると、必要なデータベースとコレクションを生成してくれます。

後は、普段通りに、

- MongoDB: アプリケーション用のデータベースを作成
- MySQL: 受入れ用のデータベースを作成

すれば準備完了。データベース名は任意ですが、以降の説明ではどちらも「myapp」とします。

### スキーマ的な何か

MongoDB上の全フィールドをコピーしても構いませんが、「必要なところだけ」にするのも手です。「統計に使う数値は取り込み、備考欄とかはスキップする」などが考えられますね。

スキーマレスなNoSQLとはいえ、実際にはアプリケーション側である程度の構造を持たせているはず。主要項目、解析に必要な項目に限定した方が、MongoDB側のスキーマ変更に逐一追従せずに済み、現実的かも。考え方としては、

- ドキュメント (帳票データとか) - MongoDB
- メタデータ (共通項目、集計項目) - MySQL

とするとわかりやすいでしょうか? 前者を「ファイル」、後者を「ファイルシステム」に例えても良いですね。

Momyでは、次のようなjsonファイルを用意して、スキーマを定義します。ファイル名は`momyfile.json`を推奨しています。

```json
{
  "src": "mongodb://localhost:27017/myapp",
  "dist": "mysql://root@localhost:3306/myapp",
  "prefix": "t_",
  "collections": {
    "collection1": {
      "_id": "number",
      "field1": "number",
      "field2": "string",
      "field3": "boolean"
    },
    "collection2": {
      "_id": "number",
      "field1": "number",
      "field2": "string",
      "field3": "boolean"
    }
  }
}
```

- `src`: MongoDBサーバのURL
- `dist`: MySQLサーバのURL
- `prefix`: MySQLのデータベース名に共通の接頭辞をつけたい場合に設定 (省略可)
- `collections`: 同期するコレクションごとのフィールド定義

フィールド定義には、シンプルに数値(number)、文字列(string)、真偽値(boolean)を指定するようにしています。

```json
"<コレクション名>": {
  "<フィールド名>": "<フィールド型>"
}
```

### 動かしてみる

インストールは、npmからどぞ。

```bash
$ npm install -g momy
```

MongoDB側には、すでに何かしらのデータが入っているものとします。初回実行時は、そのデータをまとめてインポートしてから、監視を始めます。`--import`オプションを指定します。

```bash
$ momy --config momyfile.json --import
```

データの件数次第では時間がかかります。MySQL側にデータがコピーされているか確認しましょう。止めるときは、`Ctrl + C`で。以降は、差分だけで良いはずなので、次のようにオプション無しでOKです。

```bash
$ momy --config momyfile.json
```

あるいは、`forever`を使うのもあり。

```bash
$ forever momy --config momyfile.json
```

- *注:* `--config`オプションを外すと、ワーキングディレクトリの`momyfile.json`を使います。

### APIから

APIからも使えるよう作っています (たぶん)。

## まとめ

BaaSが当たり前になっていく中、こうやってデータベースで遊べるのもあと数年の話でしょうか。はやく[Relay](https://facebook.github.io/relay/)的な世界に移行しますように。

とはいえ、現場レベルだとMicrosoft Accessはまだ幅を利かせているので、そこからBaaSにどうやって繋げるの? とかはまだ不透明だったりします。ログの解析ツールはオンラインで色々ありますが、社内での解析方法自体が秘伝のタレ化し、オンライン化不可能案件になっていることもしばしば。ビッグデータになったとたん、Accessだとどうしようもなくなるという話はそれはそれ。

[MongoDB自体での集計機能](https://docs.mongodb.org/manual/aggregation/)も、ここ数年でだいぶ良くなってきました。それを使いこなすのも筋ですが、

- SQL慣れしているほど、覚えるのがめんどくさい
- 多機能になった集計機能(aggregation)をどのようにAPIに晒すか

とか考えてしまいます。後者は結構面倒な問題で、

- 定型の集計以外
- 設計時には想定外のテーブルと複雑にJOINさせつつ、傾向を探るとか

API越しにどうすれば良いのか、着地点がイマイチ見えてません(私が)。

こういった話に答えが出るまでは、メイン機能はMongoDBで柔軟に作って、集計とか裏方用に、読出し専用のMySQL(ほかのRDS)を置くというのは、リーズナブルかなと思う次第でした。