Angular7 で俺式 MEAN スタック Webアプリを構築する、ほぼ全手順（4）

## 概要
Angular7 で俺式 MEAN スタックを作るための備忘録。
今回は「DB接続部のロジック作成」を行う。

### 前提
2019年1月1日時点の情報です。また、以下の環境になっている前提です。

- Angular CLI： 7.0.6
- Node.js： 10.15.0
- npm： 6.4.1

また、「[Angular7 で俺式 MEAN スタック Webアプリを構築する、ほぼ全手順（3）](https://qiita.com/tomy0610/items/1bf3c347df6a5b7d6806)」が完了していること。

## 事前準備

### MongoDB を導入

今回は使用している環境が Mac なので Homebrew で入れます。導入方法は「[こちらの Qiita 記事](https://qiita.com/tomy0610/items/4a882c0dda75b2710ba3)」を参照のこと。

もしくは、「[MongoDBの公式サイト](https://www.mongodb.com/download-center/community)」からバイナリデータをダウンロードしてください。無料で落とせます。

### ディレクトリ作成

以下の通りに、DB 操作用のディレクトリを作成しておく。

```bash
［ルートディレクトリ］
  :
  └─ server
     ├─ accessor # 新規作成： 外部接続用ディレクトリ
     │   └─ db
     │       ├─ model
     │       │   └─ sample.model.ts
     │       ├─ accessors
     │       │   └─ sample.accessor.ts
     │       └─ db.accessor.ts
     :
```

ゆくゆく Web API や、ファイルなどの他の外部接続を行うことも想定して、DB用に「db」というディレクトリで分けておく。

### mongoose を導入

MongoDB を操作するパッケージをインストールする

```bash
# 本体パッケージ
npm i mongoose

# 型定義ファイル
npm i -D @types/mongoose
```

## MongoDB のテストデータ投入

### 投入データの準備

DB: sample の特定のコレクションに投入するデータを事前に定義。
インポートに使用するのは `mongoimport` というコマンド。このコマンドがサポートしているファイル形式は「JSON」「CSV」「TSV」なので、今回は JSON ファイルでデータを以下のとおりに作成。

```json:db-inputs/sample.json
[
  { "_id": 1, "name": "あああ", "age": 21 },
  { "_id": 2, "name": "いいい", "age": 22 },
  { "_id": 3, "name": "ううう", "age": 23 },
  { "_id": 4, "name": "えええ", "age": 24 },
  { "_id": 5, "name": "おおお", "age": 25 }
]
```

尚、投入データの順番は保証されないので、実行結果はドキュメントの順序がバラバラになる場合がある。

### 投入コマンド実行

準備ができたら、以下のコマンドでデータを投入する。

ポートはデフォルトの MongoDB ポート（27017） をリッスンしていると想定。
今回は、既存の該当するコレクション（RDB で言うとテーブル）を一回削除し、指定の JSON ファイルからデータを新規作成するコマンド。

また、認証設定が有効済みの想定なので、`-u`, `-p`, `--authenticationDatabase` オプションを末尾に付けている。（認証無効なら不要）

```bash:ルートディレクトリ直下で実行
mongoimport -h localhost:27017 --db sample --collection sample --drop --jsonArray --file db-inputs/sample.json -u admin -p Zaq12wsx --authenticationDatabase admin
```

オプションの詳細や他のコマンドは `mongoimport --help` 見れば書いてあリます。

### 結果確認

Mongo Compass で結果を見てみた。
以下のようになっていればOK.

<img width="1392" alt="スクリーンショット 2019-02-11 0.09.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/199602/7f86cfd1-2567-be73-4573-0d04f054fee3.png">

尚、MongoDB Compass の導入方法は、「[こちらの記事](https://qiita.com/tomy0610/items/81391f74b51706b5c171)」を参照のこと。

## MongoDB 接続部作成

### DB 接続基底クラス作成

```ts:server/accessor/db/db.accessor.ts
import { ConnectionOptions, createConnection, Connection } from 'mongoose';

/** DB アクセサ 基底クラス */
export class DbAccessor {

  /** DB 接続 */
  protected async createConnection(): Promise<Connection> {
    // TODO: 後々 config ファイルなどに移行
    const URI = 'mongodb://localhost:27017';
    const OPTIONS = {
      useNewUrlParser: true, // 推奨のパーサ設定記述
      dbName: 'sample', // 接続する DB
      user: 'admin', // 認証ユーザ ID
      pass: 'Zaq12wsx', // 認証パスワード
      auth: { authdb: 'admin'}, // 認証用 DB 指定
    } as ConnectionOptions;

    return await createConnection(URI, OPTIONS);

  }

}

```

### モデル作成

モデルを定義するファイルを作成する

```ts:server/accessors/db/models/sample.model.ts
import { Document, Model, Schema, Connection } from 'mongoose';

const collectionName = 'sample';

/** サンプル ドキュメント */
export interface SampleDocument extends Document {
  // DB コレクション 型定義 (必ずスキーマと型を適合させる)
  _id: number;
  name: string;
  age: number;
}

const schema = new Schema(
  // スキーマ定義
  {
    _id: Number,
    name: { type: String },
    age: { type: Number }
  },
  // コレクション名を指定 (しないと mongoose が勝手に複数形に変えてしまう)
  { collection: collectionName }
);

/** サンプル モデル */
export const sampleModel = (con: Connection): Model<SampleDocument> => con.model(collectionName, schema);

```

### 処理クラス作成

モデルを利用して実処理を行う部分を作成する

```ts:server/accessor/db/accessors/sample.accessor.ts
import { DbAccessor } from '../db.accessor';
import { sampleModel, SampleDocument } from '../models/sample.model';

/** サンプル アクセサ */
export class SampleAccessor extends DbAccessor {

  /** 参照 */
  public async select(request: SampleDocument): Promise<SampleDocument[]> {

    const connection = await this.createConnection();
    const model = sampleModel(connection);

    // WHERE 句
    const whereCluse: SampleDocument = {} as SampleDocument;
    if (request.id) { whereCluse._id = request.id; }
    if (request.name) { whereCluse.name = request.name; }
    if (request.age) { whereCluse.age = request.age; }

    // ドキュメント取得
    return await model.find(whereCluse);
  }

  /** 挿入 */
  public async insert(request: SampleDocument): Promise<any> {

    const connection = await this.createConnection();
    const model = sampleModel(connection);

    // _id で降順に並び替え、最大値を取得
    const lastDocument = await model.findOne().select('_id').sort({ '_id': -1 });
    request._id = lastDocument ? lastDocument._id + 1 : 1;

    // 新 id でドキュメント作成
    return await model.create(request);
  }

  /** 更新 */
  public async update(id: number, request: SampleDocument): Promise<any> {

    const connection = await this.createConnection();
    const model = sampleModel(connection);

    // SET 句
    const setCluse: SampleDocument = {} as SampleDocument;
    if (request.name) { setCluse.name = request.name; }
    if (request.age) { setCluse.age = request.age; }

    return await model.findOneAndUpdate({_id: id}, setCluse);
  }

  /** 物理削除 */
  public async delete(id: number): Promise<any> {

    const connection = await this.createConnection();
    const model = sampleModel(connection);

    return await model.findByIdAndDelete(id);
  }
}

```

### コントローラを修正

```ts:server/controllers/sample.controller.ts
import { SampleAccessor } from '../accessor/db/accessors/sample.accessor';
import { SampleDocument } from '../accessor/db/models/sample.model';
import { Sample } from '../../common/entities/sample/sample.entity';
import { ISamplePathParams, ISampleRequest, ISampleResponse } from '../../common/apis/sample/sample.api';

/** サンプル コントローラ */
export class SampleController {

  public async getUsers(request: ISampleRequest, params?: ISamplePathParams): Promise<ISampleResponse> {
    // ドキュメント検索 実行
    const dbRequest = { id: params.id, name: request.name, age: request.age } as SampleDocument;
    const dbResponse = await new SampleAccessor().select(dbRequest);

    // レスポンス生成
    const users: Sample[] = [];
    for (const element of dbResponse) {
      users.push({ id: element.id, name: element.name, age: element.age });
    }
    const response: ISampleResponse = { users: users };

    return response;
  }

  public async createUser(request: ISampleRequest): Promise<ISampleResponse> {
    // ドキュメント挿入 実行
    const dbRequest = { name: request.name, age: request.age } as SampleDocument;
    const dbResponse = await new SampleAccessor().insert(dbRequest); // 挿入されたドキュメントを返却

    // レスポンス生成
    const response: ISampleResponse = { users: [] };

    return response;
  }

  public async updateUser(request: ISampleRequest, params: ISamplePathParams): Promise<ISampleResponse> {
    // ドキュメント更新 実行
    const dbRequest = { name: request.name, age: request.age } as SampleDocument;
    const dbResponse = await new SampleAccessor().update(params.id, dbRequest); // 更新前のドキュメントを返却

    // レスポンス生成
    const response: ISampleResponse = { users: [] };

    return response;
  }

  public async deleteUser(request: ISampleRequest, params: ISamplePathParams): Promise<ISampleResponse> {
    // ドキュメント削除 実行
    const dbResponse = await new SampleAccessor().delete(params.id); // 削除されたドキュメントを返却

    // レスポンス生成
    const response: ISampleResponse = { users: [] };

    return response;
  }

}

```

## 確認

- ブラウザで `http://localhost:3000` にアクセスし、画面位表示されたボタンをクリック
- 画面の動作結果が以下のとおりになっていればOK.

#### 初期表示画面

ブラウザ初期表示状態

<img width="1552" alt="スクリーンショット 2019-02-11 0.12.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/199602/97058385-4960-2bed-37ae-6e9a6ca22f1c.png">

#### 検索（全検索）

条件を指定せずに検索ボタンを押すと、DB の Sample ドキュメントの全データを表示する。

<img width="1552" alt="スクリーンショット 2019-02-11 0.14.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/199602/9657923e-e127-1e1a-713f-d2265956f617.png">

#### 検索（条件検索）

条件を適用に指定して検索ボタンを押すと、DB の Sample ドキュメントを AND 検索する。
（画像では年齢で検索している）

<img width="1552" alt="スクリーンショット 2019-02-11 0.15.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/199602/cbe126ca-0599-fcc5-6b3a-319405a5f6ca.png">

存在しない条件を指定すると、結果表示は空になる。

#### 新規作成

作成に成功すると、成功したデータが表示される。（ID は自動発行するロジックに今回している）

<img width="1552" alt="スクリーンショット 2019-02-11 0.17.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/199602/91173d77-a9ea-150f-6c2d-cf6ea7dd9ac0.png">

#### 更新

作成したデータを一部更新してみる。指定したところだけが更新される。
（今回は、名前を「新しいデータ」を「更新したデータ」にしてみる）

<img width="1552" alt="スクリーンショット 2019-02-11 0.19.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/199602/b9b357b3-4227-ce22-3c38-f569437a3655.png">

#### 削除

作成したデータを 1 件削除してみる（今回は、論理削除でなく物理削除を実装）

<img width="1552" alt="スクリーンショット 2019-02-11 0.14.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/199602/9657923e-e127-1e1a-713f-d2265956f617.png">

削除したデータが一覧から消えている。

以上で、MEAN スタックの Web アプリケーションが完成した。

あとは、node-config や forever 、log4js などのモジュールを入れて補強してください。