Meteorのあんちょこ꒰｡･ω･`;꒱ データベースアーキテクチャ

# 本流

- [Meteorのあんちょこ꒰｡･ω･`;꒱](http://qiita.com/shoko_kb/items/786524ce3a7f20c464ad)

----

# 参考

- [第7回　クライアントとサーバのコード共有を促進するデータベースアーキテクチャ:体感！JavaScriptで超速アプリケーション開発 －Meteor完全解説](http://gihyo.jp/dev/serial/01/meteor/0007)

# DB接続先

- 127.0.0.1:3001 DB名=meteor(Robomongoでアクセスできた）

# 概要

- サーバとクライアントと同じコードでDBにアクセスできる。
- Meteorのクライアントはサーバデータの**キャッシュ**を持っている。
- クライアントでデータを変更すると、クライアントのキャッシュが更新され、即時にサーバのDBデータも更新される。（別のクライアントの変更が瞬時に伝わるため、共同編集が可能となる。）

# DB操作

## コレクションの生成（コンストラクタ）

> new Meteor.Collection(name, [options])

- DBのコレクション名をnameに指定すると、サーバ・クライアント間での同期が行われれるコレクションとなる。
- DBのコレクション名をnameに指定しないと、サーバ・クライアント間での同期が行われないコレクションとなる。

## コレクション内を検索

### find 
> find(selector, options)

```js:sample.js

var Employees = new Meteor.Collection('employees');

// IDで検索
Employees.find({ _id: '...' }); 

// 複数条件で検索（name='...'、かつ、age='...'の項目を検索）
Employees.find({ name: '...', age: '...' });　
// ageの値が10以上
Employees.find({ age: { $gt: 10 } });
// nameの値が'しゅんぺい'以外のレコード
Employees.find({ name: { $not: 'しゅんぺい' } });

// 部分一致
const name = '太郎';
Employees.find({name : new RegExp(name, 'i')});

// 全検索
Employees.find({});

```

### sort（結果をソートする）

> name（昇順）、age（降順）で検索
> Employees.find({}, {**sort**: [['name', 'asc'], ['age', 'desc']]});


### skip（結果をスキップする件数）

> ５件スキップ
> Employees.find({}, {**skip**: 5});


### limit（最大の取得結果数）

> ５件のみ取得
> Employees.find({}, {**limit**: 5});

### fields（フィールド有効・無効）

- 結果に含めるプロパティには1
- 結果に含めないプロパティには0

> // ageプロパティを除外する
> Employees.find({}, {fields: {age: 0}})

### reactive（結果をリアクティブ採否）

- デフォルトはtrue（リアクティビティについては，後の連載記事をお待ちください）

### forEach（先頭から逐次実行）

```js:sample.js
var cursor = Employees.find();
cursor.forEach(function(employee) {
  ...
});
```

### map

```js:sample.js
// 従業員IDのみで構成される
var idList = cursor.map(function(employee) {
  return employee._id;
});
```

### fetch

### count（結果総数）

### rewind（カーソル初期化）

### observe（カーソルの状態を監視）

```js:sample.js
cursor.observe({
    // 検索結果が増やされた
    added: function(document, beforeIndex) {
        console.log('added(追加された位置:' + beforeIndex + ')');
    },
    // 検索結果が変更された
    changed: function(newDocument, atIndex, oldDocument) {
        console.log('changed(位置:' + atIndex + ')');
    },
    // 検索結果内でオブジェクトのインデックスが変わった
    moved: function(document, oldIndex, newIndex) {
        console.log('moved(前の位置:' + oldIndex + ' 後の位置:' + newIndex + ')');
    },
    // 検索結果からオブジェクトが減った
    removed: function(oldDocument, atIndex) {
        console.log('removed(位置:' + atIndex + ')');
    }
});
```

### findOne(selector, options)

```js:sample
var employee = Employees.findOne({_id: selected_employee_id});
```

### insert（データ挿入）

```js:sample.js
'click #addButton': function(e, template) {
    // (5) コレクションに新たなデータを追加
    Employees.insert({
        name: nameElem.value,
        age: ageElem.valueAsNumber
    }, function(error, result) {
        if (error) {
            alert('エラーが発生しました');
        }
    });
},
```

### update（データ更新）

- update(selector, modifier, options, callback)

```js:sample.js

// 年齢に40歳を指定する。
Employees.update({ _id: id }, { $set: { age: 40 }};

// ageプロパティの値を2増加させる。
Employees.update({ _id: id }, { $inc: { age: 2 }};

// コレクションの要素を更新する（完全入替）
Employees.update(
    { _id: selectedIdElem.value },
    {
        name: nameElem.value,
        age: ageElem.valueAsNumber
    }
);

```

- 第三引数の**options**
- multiというプロパティにtrueを設定すると、マッチしたドキュメント全てを更新する
- falseを設定すると一件のみ更新する。デフォルトは{ multi: false }

- 最後の引数**callback**
- 処理が終了した際のコールバックを指定する。
- サーバ上で実行する際には、コールバックを省略すると同期型のメソッドとなる。
- 処理が終了するまでブロックします。それ以外の場合はブロックしない。

### remove（データ更新）

- remove(selector, callback)

- 引数**selector**に合致したドキュメントを全て削除する。

- 最後の引数callbackには，処理が終了した際のコールバックを指定する。
- サーバ上で実行する際には、コールバックを省略すると同期型のメソッドとなり、処理が終了するまでブロックする。それ以外の場合はブロックしない。

サンプルでは，削除ボタンをクリックされた際に，チェックされた行のレコードを全て削除するためにremoveメソッドを使用しています。$inセレクタを用いて，削除対象となるドキュメントを絞り込んでいます。

```js:sample.js
// 削除ボタンをクリックされた際の処理
'click #removeButton': function(e, template) {
    if (confirm('選択されているレコードを削除してもよろしいですか')) {
        // チェックされているチェックボックスを全て取得
        var selected = template.findAll('[name="selectedEmployees"]:checked');
        // IDの配列に変換
        var ids = selected.map(function(id) { return id.value; });
        // コレクションの要素を削除する
        Employees.remove({_id: { $in: ids }});
        // 入力フォームをクリア
        selectedIdElem.value = '';
        nameElem.value = '';
        ageElem.value = '';
    }
},

```

# サンプルコード

- importでJavascriptファイルを分割できる。
- DB操作については、[MongoDB 公式リファレンス](https://docs.mongodb.com/manual/reference/)を参照。

- DB操作（API）

```js:imports/api/employees.js
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';

export const Employees = new Meteor.Collection('employees');

Meteor.methods({
    // 初期化
    'employees.init'() {
        // Employees.remove({});

        // // 決め打ちでデータを登録する。
        // var data = [
        //     { name: 'しゅんぺい', age: '34' },
        //     { name: 'たえこ', age: '33' },
        //     { name: 'こうたろう', age: '4' },
        //     { name: 'ちほ', age: '2' }
        // ];
        // // データをMongoDBに挿入する。
        // data.forEach(function (emp) {
        //     Employees.insert(emp);
        // })
    },
    // 追加
    'employees.insert'(name, age) {
		check(name, String);
		check(age, String);
		//console.log(name, age);
		Employees.insert({name: name, age :age});
    },
    // 編集
    'employees.update' (id, name, age) {
    	const employee = Employees.findOne(id);
    	console.log(employee)
    	console.log('employees.update', id, name, age);
	    Employees.update(id, { $set: { name: name, age: age } });

    },
    // 削除
    'employees.remove'(employeeId) {
        check(employeeId, String);
        const employee = Employees.findOne(employeeId);
        Employees.remove(employeeId);
    },
});
```

- 見かけ(UI)

```js:imports/ui/employee.js
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';

import './employee.html';

Template.employee.events({
    'click .edit'(event) {
    	const target = event.target;
    	const name = $(target).parents('tr').find('.name').val();
    		// this.nameではDBに登録された値が取得される。
    	const age = $(target).parents('tr').find('.age').val();
        Meteor.call('employees.update', this._id, name, age);
    },
    'click .del'() {
        if (confirm('削除しますか?')) {
            Meteor.call('employees.remove', this._id);
        }
    },
});
```

```html:imports/ui/employee.html
<template name="employee">
  <tr>
    <td class="id">{{_id}}</td>
    <td><input type="text" class="name" value="{{name}}"/></td>
    <td><input type="text" class="age" value="{{age}}" /></td>
    <td><button class="edit">編集</button></td>
    <td><button class="del">削除</button></td>
  </tr>
</template>
```

- body部

```js:imports/ui/body.js
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';

import { Employees } from '../api/employees.js';

import './body.html';
import './employee.js';

Template.employees.helpers({
	employees() {
		return Employees.find();
	},
});

Template.employees.events({
	'click .add'(event) {

	event.preventDefault();

	const target = event.target;
	const name = $(target).parents('tr').find('.name').val();
	const age = $(target).parents('tr').find('.age').val();
	// console.log(name, age);

	Meteor.call('employees.insert', name, age);

	$(target).parents('tr').find('.name').val('');
	$(target).parents('tr').find('.age').val('');

	// Meteor.call('employees.remove', this._id);
    },
});

```

```html:imports/ui/body.html
<body>
  <h1>Database</h1>
  {{> employees}}
</body>

<template name="employees">
  <table id="employees">
    <thead>
      <th>System ID</th>
      <th>名前</th>
      <th>年齢</th>
      <th>編集</th>
      <th>削除</th>
    </thead>
    <tbody>
      <tr>
        <td>&nbsp;</td>
        <td><input class="name" type="text" value="" placeholder="名前"></td>
        <td><input class="age" type="text" value="" placeholder="年齢"></td>
        <td>&nbsp;</td>
        <td><button class="add">追加</button></td>
      </tr>
      {{#each employees}}
        {{ > employee }}
      {{/each}}
    </tbody>
  </table>
</template>

```

- サーバメイン

```js:server/main.js
import { Employees } from '../imports/api/employees.js';

Meteor.startup(() => {
	Meteor.call('employees.init');
});

```


- クライアントメイン

```js:client/main.js
import '../imports/ui/body.js';
```

```html:client/main.html
<head>
  <title>database</title>
</head>
```

# DBアクセス

- [第9回　データベースに関するその他の話題 @ 体感！JavaScriptで超速アプリケーション開発 －Meteor完全解説](http://gihyo.jp/dev/serial/01/meteor/0009) 

- 「**meteor mongo**」で確認する。

```bash:console
$ meteor mongo -h

Options:
  --url, -U  return a Mongo database URL

$ meteor mongo -U
mongodb://127.0.0.1:3001/meteor

$ meteor mongo
MongoDB shell version: 2.6.7                  
connecting to: 127.0.0.1:3001/meteor
meteor:PRIMARY> db.employees.find()
{ "_id" : "jzqxufPYG3bDCCzfh", "name" : "しゅんぺい", "age" : "34" }
{ "_id" : "BEGgx8mJm93yqoGuu", "name" : "たえこ", "age" : "33" }
{ "_id" : "aB7PPJXsvzB3fYPuP", "name" : "こうたろう", "age" : "4" }
{ "_id" : "2KFbPkBrKsjLANog4", "name" : "ちほ", "age" : "2" }
meteor:PRIMARY> exit
bye

# mongodb標準のmongoクライアントツールからアクセスする
$ mongo 127.0.0.1:3001/meteor
MongoDB shell version: 3.2.8
connecting to: 127.0.0.1:3001/meteor
meteor:PRIMARY> show dbs
local   0.063GB
meteor  0.031GB
meteor:PRIMARY> exit
bye

```

- ローカルで実行する際、DBは一律「127.0.0.1:3001/meteor」になるようだ。。。

----

# セキュリティ向上

- ここまでの設定の場合、DB全データがクライアントにキャッシュされる。
- データが膨大になるなどの場合に対応できない。
- 全クライアントから、全データにアクセスできてしまう。 

```bash:console
$ meteor remove autopublish insecure
                                              
Changes to your project's package version selections:
                                              
autopublish  removed from your project        
insecure     removed from your project

autopublish: removed dependency               
insecure: removed dependency    
```

- クライアントサイドのキャッシュに全DBデータを展開すると膨大となる。

```js:api/employees.js

export const Employees = new Meteor.Collection('employees');

if (Meteor.isServer) {
    // 全データを公開する。
    Meteor.publish('all-employees', function () {
      return Employees.find();
    });
}

```

- autopublish解除により、展開できるデータをすぼめる。

```js:ui/body.js
Template.body.onCreated(function bodyOnCreated() {
	Meteor.subscribe('all-employees');
});
```

- クライアントサイドでDBのCRUD操作が可能では危ない。
- insecureを解除すると、クライアントから直接挿入はできなくなる。
- Meteor.callによるサーバー関数の呼び出しによって実施する。

```js:body.js
Template.employees.events({
	'click .add button.add'(event) {

		event.preventDefault();

		const target = event.target;
		const name = $(target).parents('tr').find('.name').val();
		const age = $(target).parents('tr').find('.age').val();

		if (name && age) {
			// Employees.insert({name : name, age : age}, function () { alert('error'); });

			//　↓直接、データ挿入せず、サーバ側の関数を経由する
			Meteor.call('employees.insert', name, age);
			$(target).parents('tr').find('.name').val('');
			$(target).parents('tr').find('.age').val('');
		}
	},
});
```

# バックアップ＆復元

- [meteor: how can I backup my mongo database @ stack overflow](http://stackoverflow.com/questions/16816860/meteor-how-can-i-backup-my-mongo-database)

- ~~DBコンソールを起動。~~

> ~~meteor mongo~~


## バックアップ

```bash:console
$ mongodump -h 127.0.0.1 --port 3001 -d meteor
```

- dumpフォルダが出来上がるので、それがバックアップファイル(jsonとbsonのデータ）となる

## 復元

- meteorで空のDBを作成しておいて、そこに下記のコマンドを流すとリストアが正常終了した。
- RobomongoでDB作成して、そのまま、下記のコマンドを流してもエラーとなった（詳細不明）。

```bash:console
$ mongorestore -h 127.0.0.1 --port 3001 -d meteor dump/meteor(path to dump)
```