駆け出しのエンジニア日記

プログラミング言語勉強中の奮闘日記

MongoDBのリレーション:1対たくさん

〜MongoDBのリレーション〜

今回は1対たくさんの場合を見ていきます。

例えば、商品の生産管理でどの農場でどの商品が作られているかの情報を提供したい時

のケースで記述していきます。

早速書いていきます。

・1対たくさん(One to Many)

//1対たくさんのリレーション
const mongoose = require('mongoose');
const { Schema } = mongoose;
mongoose.connect('mongodb://localhost:27017/relationshipDemo',
{ useNewUrlParser: true, useUnifiedTopology: true })
.then(() => {
console.log('MongoDBコネクションOK!!');
})
.catch(err => {
console.log('MongoDBコネクションエラー!!!');
console.log(err);
});

const productSchema = new Schema({
name: String,
price: Number,
season: {
type: String,
enum: ['spring', 'summer', 'fall', 'winter']
}
})
const farmSchema = new Schema({
name: String,
city: String,
// 商品の紐付けは「ID」 productSchemaと紐付けるための定義
//refを定義することで、Productモデルと紐づいていることが確認できる
products: [{ type: Schema.Types.ObjectId, ref: 'Product' }]
})
//モデル作成
const Product = mongoose.model('Product', productSchema);
const Farm = mongoose.model('Farm', farmSchema);

まずはスキーマ定義とモデル作成から行います。

mongoose の場合、スキーマ定義の部分で「 ref 」を使うことでリレーションを表現す

ることができます。
mongodb は コレクションに ドキュメント が作成された際に一意なObjectIdが生成され

るため、これを外部キーのように利用してリレーションを表現することが可能です。

次に農場を新規作成する関数を作成します。

const makeFarm = async () => {
const farm = new Farm({ name: 'まったり牧場', city: '淡路市' });
const melon = await Product.findOne({ name: 'メロン' });
farm.products.push(melon);
await farm.save();
}

makeFarm();

実行すると、「まったり牧場」という農場が作成されます。

商品(オブジェクト)としても「メロン・498・夏」の情報になります。

※メロンに関しては先に下記データを挿入しています。

Product.insertMany([
{ name: 'メロン', price: 498, season: 'summer' },
{ name: 'スイカ', price: 498, season: 'summer' },
{ name: 'アスパラガス', price: 298, season: 'spring' }
]);

 

牧場は一つの商品だけということは少なく、複数(たくさん)の商品を扱うことが前提

となるので、これが「1対たくさん」の例に挙げております。

データサイズが巨大になる可能性がある場合や1対数個の直接記述するよりは、

柔軟に使えると感じます。

MongoDBのリレーション:1対数個

〜MongoDBのリレーション〜

ここで言うリレーションはデータの関連付けを指すことになります。

簡単に説明すると、あるSNSに対して、あるツーザーが投稿します。コメントがついた

り、お気に入りができたり、リツートができたりと一つのデータに対して関連性がある

ということです。

 

早速リレーションについて見ていきましょう。

・1対数個

これが一番シンプルです。

例えば一人のユーザーに対して複数の住所を管理してみます。(Amazonみたいな)

//スキーマ作成
const userSchema = new mongoose.Schema({
first: String,
last: String,
//住所を複数管理
addresses: [{
_id: { id: false },
country: String,
prefecture: String,
address1: String,
address2: String,
}]
});
 
//モデル作成
const User = mongoose.model('User', userSchema);

//ユーザーのインスタンス作成
const makeUser = async () => {
const u = new User({
first: '太郎',
last: '山田'
});
// u に対してアドレスをpushメソッドで追加する
u.addresses.push({
country: '日本',
prefecture: '北海道',
address1: '札幌市',
address2: '0丁目1番地'
});
const res = await u.save();
console.log(res);
}

これで追加することができました。

ここから既存のユーザーに住所の追加が可能です。

//既存ユーザーに対して住所の追加 idを引数で受け取る
const addAddress = async (id) => {
const user = await User.findById(id);
user.addresses.push({
country: '日本',
prefecture: '青森県',
address1: '青森市',
address2: '0丁目1番地'
});
const res = await user.save();
console.log(res);
}
addAddress('63568a540ae51fdca7fdd307');

住所の追加ができました。

数個くらいなら直接ドキュメントにいてれも大丈夫でしょみたいなニュアンスです。

asyncなエラーハンドラ

非同期処理のところでエラーが起きた場合どうなるのでしょうか。

asyncなエラーハンドラについて完成させていきます。

・AppError.js

// extendsキーワードで継承して新しいクラスを作成
class AppError extends Error {
constructor(message, status) {
super();
this.message = message;
this.status = status;
}
}
module.exports = AppError;

・index.js

const express = require('express');
const app = express();
const path = require('path');
const mongoose = require('mongoose');
const methodOverride = require('method-override');
const AppError = require('./AppError');
const Product = require('./models/product');
 
mongoose.connect('mongodb://localhost:27017/farmStand2',
{ useNewUrlParser: true, useUnifiedTopology: true })
.then*1;
}
res.render('products/edit', { product});
});
//エラーハンドラ
app.use((err, req, res, next) => {
const { status = 500, message = '問題が発生しました' } = err;
res.status(status).send(message);
});

第三引数にnextを渡し、next()の中にエラーを入れることで改善されます。

*1:) => {

console.log('MongoDBコネクションOK!!');
})
.catch(err => {
console.log('MongoDBコネクションエラー!!!');
console.log(err);
});
 
app.get('/products/:id', async (req, res) => {
const { id } = req.params;
const product = await Product.findById(id);
if (!product) {
throw new AppError('商品が見つかりません', 404);
}
res.render('products/show', { product });
});
//エラーハンドラ
app.use((err, req, res, next) => {
const { status = 500, message = '問題が発生しました' } = err;
res.status(status).send(message);
});

例えば商品の詳細ページに遷移した際に、存在しないIDでリクエストを投げた場合、

「product」には中身がなくnullになるため、

throw new AppError('商品が見つかりません', 404);」が通り、エラーが返ってくる

想定です。

しかしエラーは起きているのですが、うまくリクエストが投げられていませんでした。

asyncの関数内でエラーが発生した際は、next()関数に渡す必要があります。

問題箇所を修正してみます。

app.get('/products/:id', async (req, res, next) => {
const { id } = req.params;
const product = await Product.findById(id);
if (!product) {
 return next(new AppError('商品が見つかりません', 404

カスタムなエラークラスを作成

エラーハンドリングでよく採用されるやり方があります。

一つの例ですが、自分たちのエラークラスを作成するやり方です。

なぜ、クラスを作成するかというとステータスコードだけでエラーの概要を伝えること

ができると便利だからです。

言葉だけでは難しいと思うので、シンプルなエラークラスを作成していきます。

 

・AppError.js

// extendsキーワードで継承して新しいクラスを作成
class AppError extends Error {
constructor(message, status) {
super();
this.message = message;
this.status = status;
}
}
module.exports = AppError;

〜継承を簡単に解説〜

・継承とは、クラスの構造や機能を引き継いだ新しいクラスを定義すること

・extendsキーワードを使って既存のクラスを継承した新しいクラスを定義できる

・継承されるクラスを親クラス、親クラスを継承するクラスを子クラスと呼ぶ

 

継承した子クラスで親クラスのコンストラクタ(初期化処理)を super() とすることで呼

び出せます。

また自分のプロパティとしてメッセージとステータスを設定しておきます。

これで簡単ではありますが、自分たちの情報を入れられるカスタムなエラークラスの作

成が完了です。

 

早速、これを使ってみます。

・index.js

const express = require('express');
const app = express();
const morgan = require('morgan');

const AppError = require('./AppError');

app.use(morgan('tiny'));
 
app.get('/admin', (req, res) => {
throw new AppError('管理者しかアクセスできません!', 403);
});
 
app.use((err, req, res, next) => {
const { status = 500, message = '何かエラーが起きました' } = err;
res.status(status).send(message);
});
 
app.listen(3000, () => {
console.log('locahost:3000で待受中...');
});

index.js で使うために、AppErrorを require しておきます。(下線部)

例えば管理者しかアクセスできないパスにアクセスしてしまったとします。

/admin のパスに行くと、、

AppErrorで設定した、メッセージとステータスコードがしっかりと表示されています。

自分たちで作成したエラークラスが問題なく継承されています。

メッセージ等を指定しなければ、

{ status = 500, message = '何かエラーが起きました' }

が表示されるようになっています。

ちなみに403エラー・・・

閲覧禁止を示すエラーメッセージです。

アクセスしているページはきちんと存在していますが、何らかの理由によりユーザーのアクセスが禁止されて

いるため、ページが見られないエラーです。

カスタムのエラーハンドラを作成する

実用的ではないですが、理解を深めるために簡単なカスタムエラーハンドラを作っていきます。

 

エラー処理ミドルウェア関数は、その他のミドルウェア関数と同じ方法で定義します。

エラー処理関数の引数は3つではなく、4つ (err、req、res、next) となります。

expressjs.com

 

これを踏まえて記述してみましょう。

const express = require('express');
const app = express();
const morgan = require('morgan');
app.use(morgan('tiny'));
 
app.get('/error', (req, res) => {
hoge.moge();
});

app.use((err, req, res, next) => {
console.log('********************************');
console.log('**************エラー**************');
console.log('********************************');
});
 
app.listen(3000, () => {
console.log('locahost:3000で待受中...');
});

太字の部分ですね。

ここで、「error」のパスに行くと、レスポンスが返ってこなくなり、

ログも確認取れます。

自分たちが作成したエラーハンドラが実行されていることになります。

 

ここで、

res.status(500).send('エラーが発生しました!!!');

を追加します。

問題なく返ってきています。

 

エラー用のミドルウェアを呼びたいのであれば、「err」を渡す必要があります。

next(err);

まとめるとこのようになります。

app.use((err, req, res, next) => {
console.log('********************************');
console.log('**************エラー**************');
console.log('********************************');
console.log(err);
next(err);
});

・ログ上・・・コンソールの中身も確認できています。

・画面上・・・上記で設定した「hoge」のエラーがレスポンスとして返ってきます。

あんまり意味がないエラーハンドラですが、自分が作成したエラーが確認でき、複数のエラーも呼ぶことも可能です。

404エラーのルーティング定義

ルーティングを設定していない場所にリクエストが投げられた時、404エラーを返すよ

うにルーティング設定を行います。

404エラー とは、、、

404 not found 」:存在しないページにアクセスしようとした時に起こるエラー

 

他にもHTTPステータスコードが存在するので簡単に紹介します。

もしステータスコードの壁に当たった時に参考にしてみてください。

・100番台 Informational(情報レスポンス)

クライアントからのリクエストを受け入れ可能で、継続して処理されている状態

リクエストとレスポンスは一瞬の出来事のため人間には体感できない程の速さなのでほとんどわからない

・200番台 Success (成功レスポンス)

クライアントからリクエストがサーバに送られ理解されて受理された状態です。成功した後はリクエストに応じてサーバの方で処理をしクライアントにレスポンスを返します。

・300番台 Redirection(リダイレクション)

・400番台 Client Error(クライアントエラー)

ユーザー側で操作や入力に不備があった際に出てしまうエラー

・500番台 Server Error(サーバーエラー)

サーバー側に不備ある場合に起こるエラー。システムが復旧するまで何もできない

 

早速404エラーを返すルーティングを記述していきます。

一通りのルーティング設定が終わった最後に追加します。

app.get('/', (req, res) => {
console.log(`リクエスト時間:${req.requestTime}`);
res.send('ホームページ');
})
app.get('/dogs', (req, res) => {
console.log(`リクエスト時間:${req.requestTime}`);
res.send('わんわん');
})

//該当するパスが見つからない時の404エラー
app.use((req, res) => {
res.send('404エラー ページが見つかりません');
})

関係のないパスにリクエストを送った際( "/" "/dogs" 以外)に最終的に404エラー

のルーティングに行き着く流れとなります。

実際に  "/cats" というパスにリクエストを送ると、

上記のように記述した404エラーが確認取れます。

自作のミドルウェアを作ってみる

簡単なものですが、自作のミドルウェアを作成してみましょう。

expressjs.com

Expressのガイドにも記載がありますが、

「 req , res 」のオブジェクト以外にも第三引数に「 next 」を渡すことができます。

この「 next 」が次のミドルウェアを指す関数となっています。

文字だけではわかりづらいので実際に記述してみます。(_の部分)

const express = require('express');
const app = express();
const morgan = require('morgan');
app.use(morgan('tiny'));
// 自作のミドルウェア
app.use*1;

// 自作のミドルウェア
app.use((req, res, next) => {
console.log('初めてのミドルウェア!!!');
//returnすることで、next()以降の処理は無視される
return next();
   console.log('初めてのミドルウェアのnextのあとの処理!!!');
});
app.use((req, res, next) => {
console.log('2個目のミドルウェア!!!');
return next();
});
app.use((req, res, next) => {
console.log('3個目のミドルウェア!!!');
return next();
});

app.get('/', (req, res) => {
console.log(`リクエスト時間:${req.requestTime}`);
res.send('ホームページ');
})
app.get('/dogs', (req, res) => {
console.log(`リクエスト時間:${req.requestTime}`);
res.send('わんわん');
})
app.listen(3000, () => {
console.log('localhost:3000で待機');
})

連続して出力されています。次の関数が続く限り、呼ばれています。

そして「 next() 」の前に「 return 」を記述することで、「 next() 」以降の処理を無視

するような意味になります。

なので「console.log('初めてのミドルウェアのnextのあとの処理!!!');」というコ

ンソールはログに表示されていません。

誤って、「 next() 」の後ろに何かしらの処理を記述し、それに気づかないと思わぬバグ

になってしまうこともあるので、「 return 」を記述することでそれを防ぐようになって

います。

*1:req, res, next) => {

console.log('初めてのミドルウェア!!!');
return next();
});
app.get('/', (req, res) => {
console.log(`リクエスト時間:${req.requestTime}`);
res.send('ホームページ');
})
app.get('/dogs', (req, res) => {
console.log(`リクエスト時間:${req.requestTime}`);
res.send('わんわん');
})
app.listen(3000, () => {
console.log('localhost:3000で待機');
})

ログで確認取れました。

app.use() 」を使うことで全てのリクエストで処理が実行されるようになっていま

す。(GETやPOST、DELETEなど)

 

また「 next() 」を記述することで次の関数を呼ぶことになるので、下記のように記述

することも可能です。

const express = require('express');
const app = express();
const morgan = require('morgan');
app.use(morgan('tiny'