Meteor + Blaze で匿名質問サービスをつくろう：第5夜 DBコレクションと Pub/Sub

[Meteor + Blaze で匿名質問サービスをつくろう：第4夜 UI作成 前編](https://qiita.com/plageoj/items/bf7d47f374abd432db33) から続きです。

# Meteor と MongoDB
Meteor の空恐ろしいところは、サーバー側の DB の一部をコピーして
クライアントにも持っているということです。

初期状態では、クライアントで DB に加えた編集がサーバー側にも適用される
ようになっていますので、例えば他人に寄せられた質問を読んだり、
編集・削除したりできてしまうんですよね。

これではセキュリティ的に非常にまずいので、なんとかします。

## パッケージを削除
~~なんでプレインストールされているんだろう、とぼやきながら~~
DB の同期を行っているパッケージを削除します。
`(For prototyping only)` がついているふたつですね。

```bash
$ meteor list

accounts-twitter       1.4.1  Login service for Twitter accounts
accounts-ui            1.3.0  Simple templates to add login widgets to an app
autopublish            1.0.7  (For prototyping only) Publish the entire database to all clients
blaze-html-templates   1.1.2  Compile HTML templates into reactive UI with Meteor Blaze
ecmascript             0.10.0* Compiler plugin that supports ES2015+ in all .js files
es5-shim               4.7.3  Shims and polyfills to improve ECMAScript 5 support
insecure               1.0.7  (For prototyping only) Allow all database writes from the client
iron:router            1.1.2  Routing specifically designed for Meteor
meteor-base            1.3.0  Packages that every Meteor app needs
mobile-experience      1.0.5  Packages for a great mobile user experience
mongo                  1.4.2* Adaptor for using MongoDB and Minimongo over DDP
reactive-var           1.0.11  Reactive variable
shell-server           0.3.1  Server-side component of the `meteor shell` command.
standard-minifier-css  1.4.0* Standard css minifier used with Meteor apps by default.
standard-minifier-js   2.3.1* Standard javascript minifiers used with Meteor apps by default.
tap:i18n               1.8.2  A comprehensive internationalization solution for Meteor
tracker                1.1.3  Dependency tracker to allow reactive callbacks
twitter-config-ui      1.0.0  Blaze configuration templates for Twitter OAuth.


* New versions of these packages are available! Run 'meteor update' to try to update those packages to their
  latest versions. If your packages cannot be updated further, try typing `meteor add <package>@<newVersion>`
  to see more information.


$ meteor remove insecure autopublish

Changes to your project's package version selections:

autopublish  removed from your project
insecure     removed from your project

insecure: removed dependency
autopublish: removed dependency
```

これで、DBの同期は止まりました。
ついでに、更新のきているパッケージのアップデートもしておきます。

```bash
$ meteor update
This project is already at Meteor 1.6.1, the latest release.

Changes to your project's package version selections from updating package versions:

babel-compiler             upgraded from 7.0.0 to 7.0.5
base64                     upgraded from 1.0.10 to 1.0.11
ecmascript                 upgraded from 0.10.0 to 0.10.5
ecmascript-runtime-client  upgraded from 0.6.0 to 0.6.2
jquery                     upgraded from 1.11.10 to 1.11.11
meteor                     upgraded from 1.8.2 to 1.8.3
minifier-css               upgraded from 1.3.0 to 1.3.1
minifier-js                upgraded from 2.3.1 to 2.3.3
modules                    upgraded from 0.11.3 to 0.11.5
mongo                      upgraded from 1.4.2 to 1.4.4
promise                    upgraded from 0.10.1 to 0.10.2
rate-limit                 upgraded from 1.0.8 to 1.0.9
standard-minifier-css      upgraded from 1.4.0 to 1.4.1
standard-minifier-js       upgraded from 2.3.1 to 2.3.2
```

## コレクションの準備
MongoDB コレクションを指定します。RDB ではテーブルに相当します。
サーバー・クライアントの双方からアクセスできるように、
`shared` ディレクトリに新しくファイルを作成します。

```javascript:shared/Database.js
Questions = new Mongo.Collection('questions');
```

Development 環境では DB の設定は Meteor がやってくれますので、
特に何もする必要はありません。

また、`accounts-ui` パッケージを導入していれば、`users` コレクションが自動的に
作成されていますので、明示的な宣言は不要です。

この記事ではMongoDB 自体については詳しく扱いません。
詳しく知りたい方は適宜検索してください。

## スキーマの定義 (Optional)
MongoDB にスキーマという概念はありませんが、この時点で DB に格納する
データのスキーマを決めておくことは悪くないはずです。

### パッケージの追加
```bash
$ meteor add aldeed:simple-schema

aldeed:simple-schema: A simple schema validation object with reactivity. Used by collection2 and autoform.
```

`autoform` パッケージを入れるとついてくるのですが、今回は
`autoform` を使わないので手動でインストールします。

### 定義
さきほどの `Database.js` に追記します。

```javascript:shared/Database.jsに追記
Questions.schema = new SimpleSchema({
    userId: { type: String, regEx: SimpleSchema.RegEx.Id },
    content: { type: String },
    isPublic: { type: Boolean, defaultValue: false },
    isAnswered: { type: Boolean, defaultValue: false },
    isDeclined: { type: Boolean, defaultValue: false },
    isReported: { type: Boolean, defaultValue: false },
    createdAt: { type: Date }
});
```

このスキーマを適用すると、データベースに格納されるデータはこんなイメージになるでしょう。

```javascript:sample_data
{
    _id: "0e1b8263277ee31c8b3f2691",    // 質問ID。ユニーク。
    content: "進捗どうですか？",         // 質問の内容。
    userId: "7f34348aa6fcfd21f499f27e", // 質問相手の user._id。
    isPublic: false,                    // true なら大喜利モード。誰でも回答できる
    isAnswered: false,                  // true なら回答済。
    isDeclined: false,                  // 回答をスキップすると true にする。
    isReported: false,                  // 運営に通報すると true にする。
    createdAt: new Date()               // 投稿日時。
}
```

ユーザーがコンテンツを送信できるサービスは、必ず事務局に通報するボタンがあるのが最近の流行りです。
このことをすっかり忘れていました。入れておきます。

## Publish / Subscribe の設定

さきほど `autopublish` `insecure` を削除したので、
DB とクライアントの同期については明示しないといけません。

Meteor では、 publish / subscription という仕組みで同期を実現します。

+ サーバー側

サーバーでは、DB のコレクションに名前をつけて publish します。
この名前はコレクション自体の名前とは無関係です。
中身は DB コレクションを返す関数なので、引数を受け取って特定のフィルターをかけることもできます。

+ クライアント側

クライアントでは、先程の名前を使って subscribe します。
サーバー上で関数を実行して返り値をもらうようなイメージですが、
subscribe では DB が変更されたときに、クライアントから関数を呼ばなくても自動的にデータがプッシュされてきます。
Reactive programming というやつですね。

この機能が不要で、ただ単に関数を実行したいときは methods / call という別の仕組みを使いますが、この説明はのちほど。

それでは、サーバーが questions コレクションを publish する宣言をします。

```js:server/main.jsに追記
Meteor.publish('Questions', (type, query, limit) => {
    if (!limit) limit = 30;
    switch (type) {
        case 'public':
            return Questions.find({ isPublic: true }, { limit: limit });
        case 'specific':
            return Questions.find({ _id: query }, { limit: limit });
        case 'users':
            return Questions.find({ userId: query }, { limit: limit });
    }
});
```

必要な情報だけ絞って publish することで、表示速度とセキュリティの向上をはかります。
面倒でもコレクション全体を publish しないようにしましょう。

次にクライアントが subscribe する宣言を。

```diff:shared/Routing.js
Router.route('/', function() {
+   this.subscribe('Questions', 'public');
    this.render('Home');
});
Router.route('/user/:userid', function() {
+   this.subscribe('Questions', 'users', this.params.userid);
    this.render('User');
});
Router.route('/user/:userid/q', function() {
    this.render('Enqueue');
});
Router.route('/q/:qid', function() {
+   this.subscribe('Questions', 'specific', this.params.qid);
    this.render('Question');
});
```

パスパラメーターは `this.params.hoge` で取得できます。

## ごめんなさい
前回、

> 第5夜ではモックアップと DB を結びつけていきます。

こんなことを書いたんですが、疲れたのでこのへんで……

[ソースをGitHubで見る](https://github.com/plageoj/queuestion/tree/696dad7bd476102769daa2e7c027a248790a16b5)