概要
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 ファイルでデータを以下のとおりに作成。
[
{ "_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.
尚、MongoDB Compass の導入方法は、「こちらの記事」を参照のこと。
MongoDB 接続部作成
DB 接続基底クラス作成
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);
}
}
モデル作成
モデルを定義するファイルを作成する
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);
処理クラス作成
モデルを利用して実処理を行う部分を作成する
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);
}
}
コントローラを修正
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.
初期表示画面
ブラウザ初期表示状態
検索(全検索)
条件を指定せずに検索ボタンを押すと、DB の Sample ドキュメントの全データを表示する。
検索(条件検索)
条件を適用に指定して検索ボタンを押すと、DB の Sample ドキュメントを AND 検索する。
(画像では年齢で検索している)
存在しない条件を指定すると、結果表示は空になる。
新規作成
作成に成功すると、成功したデータが表示される。(ID は自動発行するロジックに今回している)
更新
作成したデータを一部更新してみる。指定したところだけが更新される。
(今回は、名前を「新しいデータ」を「更新したデータ」にしてみる)
削除
作成したデータを 1 件削除してみる(今回は、論理削除でなく物理削除を実装)
削除したデータが一覧から消えている。
以上で、MEAN スタックの Web アプリケーションが完成した。
あとは、node-config や forever 、log4js などのモジュールを入れて補強してください。