1
2

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-10

概要

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

前提

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

  • Angular CLI: 7.0.6
  • Node.js: 10.15.0
  • npm: 6.4.1

また、「Angular7 で俺式 MEAN スタック Webアプリを構築する、ほぼ全手順(3)」が完了していること。

事前準備

MongoDB を導入

今回は使用している環境が Mac なので Homebrew で入れます。導入方法は「こちらの Qiita 記事」を参照のこと。

もしくは、「MongoDBの公式サイト」からバイナリデータをダウンロードしてください。無料で落とせます。

ディレクトリ作成

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

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

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

mongoose を導入

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

# 本体パッケージ
npm i mongoose

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

MongoDB のテストデータ投入

投入データの準備

DB: sample の特定のコレクションに投入するデータを事前に定義。
インポートに使用するのは mongoimport というコマンド。このコマンドがサポートしているファイル形式は「JSON」「CSV」「TSV」なので、今回は 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 オプションを末尾に付けている。(認証無効なら不要)

ルートディレクトリ直下で実行
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.

スクリーンショット 2019-02-11 0.09.55.png

尚、MongoDB Compass の導入方法は、「こちらの記事」を参照のこと。

MongoDB 接続部作成

DB 接続基底クラス作成

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);

  }

}

モデル作成

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

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);

処理クラス作成

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

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);
  }
}

コントローラを修正

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.

初期表示画面

ブラウザ初期表示状態

スクリーンショット 2019-02-11 0.12.23.png

検索(全検索)

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

スクリーンショット 2019-02-11 0.14.18.png

検索(条件検索)

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

スクリーンショット 2019-02-11 0.15.31.png

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

新規作成

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

スクリーンショット 2019-02-11 0.17.30.png

更新

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

スクリーンショット 2019-02-11 0.19.44.png

削除

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

スクリーンショット 2019-02-11 0.14.18.png

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

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

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

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2