MongoDBは典型的な NoSQL データベースであるため、JOIN 操作は提供されません。しかし、それでも他のコレクション内のドキュメントを参照したい場合があります。最近MongoDBのジョイントテーブルクエリを使ってみたのでまとめておきます。
まず、リレーショナル データベースでは、結合演算子を使用して複数のテーブルに対する結合クエリを実行できることがわかっています。非リレーショナル データベースの特徴は、データがドキュメント型であること、テーブル内にスキーマが存在しないこと、およびテーブル間の関係が弱いことです。その特性により、複数のコレクションの関連付けを処理することは当然ながら困難です。リレーショナル データベースのリレーション機能に非常に優れたテーブルです。
NoSQL の代表格である MongoDB は非リレーショナル データベースですが、最もリレーショナル データベースに近く、そのような問題を解決するいくつかの方法を提供するのです。
モデリングと関係
MongoDB は、柔軟なスキーマを備えた非リレーショナル データベースです。 MongoDB は埋め込みオブジェクトと配列タイプをサポートしているため、MongoDB モデリングは 2 つの方法で行うこともできます。1 つは埋め込み、もう 1 つは参照です。
一方、リレーションシップは複数のドキュメント間の論理的な相互関係を表し、1:1、1:N、N:1)、N:Nの4種類があります。 ドキュメントは埋め込みと参照でリンクすることができ、これはMongoDBの2つのモデリング方法に対応しています。
次に、ユーザーとユーザーの住所の紐付けを例にとってみましょう。 ユーザは複数の住所を持つことができるので、一対多の関係です。
埋め込み式の紐付け
埋め込みモデリングを使用すると、ユーザーのドキュメントにユーザー アドレスのデータを埋め込むことができます。
{
"_id":ObjectId("a4aca2a0a7955605bb0012ee"),
"number": "111111",
"name": "Tanaka",
"address": [
{
"building": "Tokyo Building",
"code": 222222,
"city": "Tokyo",
"area": "Marunouchi"
},
{
"building": "Abeno Harukas",
"code": 333333,
"city": "Osaka",
"area": "Abeno"
}]
}
上記のデータは 1 つのドキュメントに保存されるため、データの取得と管理が容易になります。このデータ構造の欠点は、ユーザーとユーザー アドレスが増加し続け、データ量が増加し続けると、読み取りおよび書き込みのパフォーマンスに影響することです。
参照式の紐付け
参照式の紐付けは、データベースを設計するときによく使用される方法です。この方法は、ユーザー データ ドキュメントとユーザー アドレス データ ドキュメントを分離し、ドキュメント内のフィールドを参照することによって関係を確立します。
マニュアル参照
{
"_id":ObjectId("a4aca2a0a7955605bb0012ee"),
"number": "111111",
"name": "Tanaka",
"address_ids": [
ObjectId("a4955605bb0012ee13f41d1d"),
ObjectId("a2a0a7955605bb0012ee13f4")
]
}
上の例では、ユーザー ドキュメントのフィールドには、ユーザーのアドレスのオブジェクト ID 配列が含まれています。これらのユーザー アドレスのオブジェクト ID を読み取ることで、ユーザーの詳細なアドレス情報を取得できます。このメソッドでは 2 つのクエリが必要です。1 回目はユーザーのアドレスのオブジェクト ID をクエリし、2 回目はクエリ ID を通じてユーザーの詳細なアドレス情報を取得します。
DBRefs
異なるアドレスを異なるコレクションに格納するシナリオを考えてみましょう。 こ の よ う に、 異な る 住所のデータを取得際に も コ レ ク シ ョ ン を指定す る 必要があ り 、 1 つの文書が複数の コ レ ク シ ョ ンか ら その文書を参照す る 場合は、 DBRefs を使用す る 必要があ り ます。
DBRef の形式:
{ $ref : , $id : , $db : }
3 つのフィールドの意味は次のとおりです。
- $ref:コレクション名
- $id:参照ID
- $db:データベース名、オプションのパラメータ
次の例では、ユーザー データ ドキュメントは DBRef、フィールド address 使用します。
{
"_id":ObjectId("7955605955605bb00955605bb"),
"address": {
"$ref": "address",
"$id": ObjectId("ee13f41012ee13f41605bb00"),
"$db": "testdb"
},
"contact": "123456",
"name": "Tanaka"
}
以下のコードでは、$refパラメータを指定して、コレクション内の指定されたidを持つユーザーの住所情報を検索しています:
const user = db.users.findOne({"name":"Tanaka"})
const dbRef = user.address
db[dbRef.$ref].findOne({"_id":(dbRef.$id)})
上の例は、addressコレクション内の住所データを返します。
要約すると、複雑すぎるフィールドや大量のデータが含まれるフィールドの場合は、インライン化の代わりに参照を使用します。そして、大まかに言えば、これらは標準化されたデータ モデルです。
$lookup
MongoDb 3.2 より前は、DBRef を使用して参照を設定する必要がありました。 MongoDB 3.2 では $lookup が追加され、Aggregation などの強力なパイプライン分析フレームワークに配置されました。
MongoDB の集計は主にデータを処理し、計算されたデータ結果を返すために使用されます。 SQL ステートメントの count(*) に似ています。 MongoDB の集約パイプラインは、1 つのパイプラインが処理された後、MongoDB ドキュメントの結果を次のパイプラインに渡し、パイプライン操作を繰り返すことができます。例えば:
db.getCollection('workers').aggregate([{
$lookup: {
from: "another_table",
as: "test_field",
localField: "id",
foreignField: "test_id",
},
},{
$match: {
primary_id: 'f9609553-aq59-4da6-85bf-de1179556059',
}
}])
上記のステートメントは、workers テーブルに基づいており、another_table テーブルの結合クエリを主キーが id、外部キーが test_id としてマージし、 合する Primary_id フィールドが追加されます。 MongoDB はリレーショナル データベースではありませんが、上記では実際に $lookup を使用してLeft Join操作を実装しました。
populate
Mongoose は、node.js でエレガントな mongodb オブジェクト モデリング ソリューションを提供します。 MongoDB のバージョン 3.2 以上には、結合に似た $lookup 集計演算子があります。 Mongoose には、populate() と呼ばれるより強力な代替手段があり、これを使用すると、他のコレクション内のドキュメントを参照できます。
ポピュレーションとは、ドキュメント内の指定されたパスを、別のコレクションのドキュメントに自動的に置き換える処理のことです。 単一のドキュメント、複数のドキュメント、共通オブジェクト、複数の共通オブジェクト、クエリから返されたすべてのオブジェクトをポピュレーションすることができます。 いくつかの例を見てみましょう。
外部キー参照
Schema フィールドの定義では、ref 属性を追加して別の Schema を指すようにすることができます。 この ref 属性は、その後に入力されるときに Mongoose に読み込まれます。以下は Book と Story の Schema 定義です。
const mongoose = require('mongoose'), Schema = mongoose.Schema
const bookSchema = Schema({
_id: Number,
name: String
});
const storySchema = Schema({
inBook : { type: Number, ref: 'Book' },
title : String,
// ネストされたプロパティで外部キー参照を定義できる
fans : [{ type: Number, ref: 'Person' }]
});
const Story = mongoose.model('Story', storySchema);
const Book = mongoose.model('Book', bookSchema);
外部キーのタイプは、ObjectId、Number、String、および Buffer のいずれかにすることができます。割り当てとpopulateする際に一貫性が保たれている必要があります。
保存とポピュレーション
StoryはPersonオブジェクトの_idを保存し、Queryの.populate()を呼び出して元のフィールドをPersonのドキュメントに置き換えます。
const grimms = new Book({ _id: 0, name: 'Grimms' Fairy Tales'});
const story = new Story({ title: 'Cinderella', inBook: grimms._id });
Story.findOne({title: 'Cinderella'})
.populate('inBook')
.exec(function(err, story){
if(err) throw err;
console.log(story.inBook.name);
});
動的ポピュレーション
p>上記の .populate() を呼び出す前に条件があります。入力されるフィールドが ref オプションで設定されているということです。 Mongoose は、ref で指定されたコレクション内で対応する ID を検索します。 それが動的フィールドの場合はどうなるでしょうか?その参照はポピュレーション操作で指定できます。const storySchema = new Schema({
_id: Number,
name: String,
inBook: Number
});
Story.
findOne({ title: 'Cinderella' }).
populate({
path: 'inBook',
model: 'Book' // ブック コレクションで ID を見つける
})