MongoDBでドキュメントスキーマを設計する方法

著者は、Write for DOnationsプログラムの一環として、Open Internet/Free Speech Fundに寄付を行いました。

はじめに

リレーショナルデータベースに関する豊富な経験がある場合、テーブルや関係性といったリレーショナルモデルの原則を乗り越えることは難しいかもしれません。MongoDBのようなドキュメント指向データベースは、リレーショナルモデルの硬直性や制限から解放されることを可能にします。しかし、自己記述型ドキュメントをデータベースに保存できる柔軟性と自由度は、他の落とし穴や難題を引き起こす可能性があります。

この概念的な記事では、ドキュメント指向データベースにおけるスキーマ設計に関連する5つの一般的なガイドラインを概説し、データ間の関係をモデル化する際に考慮すべき様々な点を強調します。また、配列内へのドキュメントの埋め込みや子参照・親参照の使用など、関係をモデル化するために採用できるいくつかの戦略を紹介し、それらの戦略がいつ最も適切に使用されるかを説明します。

ガイドライン1 — 一緒にアクセスする必要があるものは一緒に保存する

一般的なリレーショナルデータベースでは、データはテーブルに保持され、各テーブルはエンティティ、オブジェクト、またはイベントを構成するさまざまな属性を表す固定された列リストで構成されています。例えば、大学の学生を表すテーブルでは、各学生のファーストネーム、ラストネーム、生年月日、および固有の識別番号を保持する列が見つかるかもしれません。

通常、各テーブルは単一の主題を表します。学生の現在の学習、奨学金、または以前の教育に関する情報を保存したい場合、そのデータを個人情報を保持するテーブルとは別のテーブルに保持することが理にかなっているかもしれません。その後、これらのテーブルを接続して、各テーブルのデータ間に関係があることを示すことができ、それらが含む情報が意味のある関連性を持っていることを示します。

たとえば、各学生の奨学金ステータスを記述するテーブルは、学生を学生ID番号で参照するかもしれませんが、学生の名前や住所を直接保存することはなく、データの重複を避けます。このような場合、学生のソーシャルメディアアカウント、以前の教育、奨学金に関するすべての情報を取得するためには、一度に複数のテーブルにアクセスし、異なるテーブルからの結果をまとめる必要があります。

このように参照を通じて関係を記述する方法は、正規化されたデータモデルとして知られています。このように、複数の別々の簡潔なオブジェクトを互いに関連付けてデータを保存することは、ドキュメント指向データベースでも可能です。しかし、ドキュメントモデルの柔軟性と、単一のドキュメント内に埋め込みドキュメントや配列を保存する自由度は、リレーショナルデータベースとは異なる方法でデータをモデル化できることを意味します。

ドキュメント指向データベースでデータをモデル化する基本的な概念は、「一緒にアクセスされるものを一緒に保存する」ことです。”学生の例にさらに踏み込むと、この学校のほとんどの学生は複数のメールアドレスを持っているとします。そのため、大学は各学生の連絡先情報とともに複数のメールアドレスを保存する機能を望んでいます。

このような場合、ドキュメントの例は次のような構造になるかもしれません:

{
    "_id": ObjectId("612d1e835ebee16872a109a4"),
    "first_name": "Sammy",
    "last_name": "Shark",
    "emails": [
        {
            "email": "[email protected]",
            "type": "work"
        },
        {
            "email": "[email protected]",
            "type": "home"
        }
    ]
}

この例のドキュメントには、埋め込まれたメールアドレスのリストが含まれていることに注意してください。

単一のドキュメント内に複数の主題を表現することは、非正規化データモデルの特徴です。これにより、アプリケーションは複数の別々のオブジェクトやコレクションにアクセスする必要なく、特定のオブジェクト(ここでは学生)に関連するすべてのデータを一度に取得して操作できます。また、この方法では、マルチドキュメントトランザクションを使用せずに、そのようなドキュメントに対する操作の原子性を保証できます。

埋め込みドキュメントを使用して、一緒にアクセスする必要があるデータをまとめて保存することは、ドキュメント指向データベースでデータを表現するための最適な方法です。以下のガイドラインでは、オブジェクト間のさまざまな関係、例えば一対一や一対多の関係が、ドキュメント指向データベースでどのように最適にモデル化できるかを学びます。

ガイドライン2 — 埋め込みドキュメントを使用した一対一関係のモデリング

一対一関係は、2つの異なるオブジェクト間の関連を表し、一方のオブジェクトが他方の種類の正確に1つのオブジェクトと接続されていることを意味します。

前のセクションの学生の例を引き続き考えると、各学生はどの時点でも有効な学生証を1枚しか持つことができません。1枚のカードが複数の学生に属することはなく、また学生が複数の身分証明カードを持つこともできません。このデータをリレーショナルデータベースに保存する場合、学生と学生証の関係をモデル化するために、学生レコードと学生証レコードを別々のテーブルに保存し、参照によって結びつけるのが理にかなっています。

ドキュメントデータベースでこのような関係を表現する一般的な方法は、埋め込みドキュメントを使用することです。例として、以下のドキュメントはサミーという学生と彼の学生証を記述しています:

{
    "_id": ObjectId("612d1e835ebee16872a109a4"),
    "first_name": "Sammy",
    "last_name": "Shark",
    "id_card": {
        "number": "123-1234-123",
        "issued_on": ISODate("2020-01-23"),
        "expires_on": ISODate("2020-01-23")
    }
}

この例のドキュメントでは、id_cardフィールドが単一の値ではなく、学生の身分証明カードを表す埋め込みドキュメントを保持していることに注意してください。このドキュメントは、ID番号、カードの発行日、および有効期限によって記述されています。身分証明カードは、実生活では別個のオブジェクトですが、サミーという学生を記述するドキュメントの一部として扱われます。通常、関連するすべての情報を1回のクエリで取得できるように、ドキュメントスキーマをこのように構造化することは賢明な選択です。

学生のメールアドレス、出席するコース、学生会の掲示板に投稿するメッセージなど、ある種のオブジェクトを他の種類の多くのオブジェクトと結びつける関係に遭遇した場合、物事はより複雑になります。次のいくつかのガイドラインでは、これらのデータ例を使用して、1対多および多対多の関係を扱うためのさまざまなアプローチを学びます。

ガイドライン3 — 埋め込みドキュメントによる一対少数の関係のモデリング

あるタイプのオブジェクトが別のタイプの複数のオブジェクトに関連している場合、それは一対多の関係として説明できます。学生は複数のメールアドレスを持つことができ、車は多数の部品を持つことができ、ショッピングの注文は複数のアイテムで構成されることがあります。これらの例はそれぞれ、一対多の関係を表しています。

ドキュメントデータベースで一対一の関係を表す最も一般的な方法は埋め込みドキュメントを通じてですが、ドキュメントスキーマで一対多の関係をモデル化する方法はいくつかあります。しかし、これらを最適にモデル化する方法を検討する際には、与えられた関係の三つの特性を考慮する必要があります:

  • カーディナリティカーディナリティは、与えられた集合内の個々の要素の数の測定値です。例えば、あるクラスに30人の学生がいる場合、そのクラスのカーディナリティは30であると言えます。一対多の関係では、カーディナリティはそれぞれのケースで異なる可能性があります。学生は一つのメールアドレスを持つかもしれないし、複数持つかもしれません。少数のクラスに登録されているかもしれないし、完全にフルスケジュールであるかもしれません。一対多の関係では、「多」のサイズがデータのモデル化方法に影響を与えます。
  • 独立アクセス: メインオブジェクトから独立して関連データにアクセスすることは、ほとんどないか、まったくない場合があります。例えば、他の学生の詳細情報なしに、一人の学生のメールアドレスを取得することは珍しいかもしれません。一方で、大学のコースは、登録されている学生や学生に関係なく、個別にアクセスして更新する必要があるかもしれません。関連ドキュメントに単独でアクセスするかどうかは、データのモデリング方法にも影響を与えます。
  • データ間の関係が厳密に一対多の関係であるかどうか: 例えば、大学で学生が受講するコースを考えてみましょう。学生の視点から見ると、複数のコースに参加できます。一見すると、これは一対多の関係のように見えるかもしれません。しかし、大学のコースはほとんどの場合、一人の学生だけが受講することはありません。むしろ、複数の学生が同じクラスに出席することが多いです。このような場合、問題の関係は実際には一対多の関係ではなく、多対多の関係であり、一対多の関係とは異なるアプローチでこの関係をモデル化することになります。

学生のメールアドレスをどのように保存するかを決定していると想像してみてください。各学生は、仕事用、個人用、大学提供のメールアドレスなど、複数のメールアドレスを持つことができます。単一のメールアドレスを表すドキュメントは、次のような形式になるかもしれません:

{
    "email": "[email protected]",
    "type": "work"
}

カーディナリティの観点から見ると、学生一人あたりのメールアドレスはごく少数になるでしょう。学生が数十、それこそ数百ものメールアドレスを持つことは考えにくいからです。したがって、この関係は一対少数の関係として特徴づけられ、メールアドレスを学生のドキュメントに直接埋め込み、一緒に保存する強い理由となります。メールアドレスのリストが無制限に増大し、ドキュメントが大きくなり使いづらくなるリスクはありません。

注意:配列にデータを保存することには特定の落とし穴があることに注意してください。例えば、単一のMongoDBドキュメントは16MBを超えることはできません。配列フィールドを使用して複数のドキュメントを埋め込むことは可能であり、一般的ですが、オブジェクトのリストが制御不能になると、ドキュメントはすぐにこのサイズ制限に達する可能性があります。また、埋め込み配列内に大量のデータを保存すると、クエリのパフォーマンスに大きな影響を与えます。

配列フィールドに複数のドキュメントを埋め込むことは多くの状況で適しているかもしれませんが、常に最良の解決策であるとは限りません。

独立したアクセスに関しては、メールアドレスは学生から独立してアクセスされることはほとんどないでしょう。そのため、別のコレクションに別々のドキュメントとして保存する明確なインセンティブはありません。これは、学生のドキュメント内に埋め込むもう一つの強い理由です。

考慮すべき最後の点は、この関係が本当に多対多の関係ではなく、一対多の関係であるかどうかです。メールアドレスは一人の人に属するものであるため、この関係を多対多の関係ではなく、一対多の関係(より正確には、一対少の関係)として表現するのが妥当です。

これら三つの仮定から、学生自身を記述する同じ文書内に学生の様々なメールアドレスを埋め込むことが、この種のデータを保存するのに適した選択であると示唆されます。メールアドレスが埋め込まれた学生の文書の例は、次のような形をとるかもしれません:

{
    "_id": ObjectId("612d1e835ebee16872a109a4"),
    "first_name": "Sammy",
    "last_name": "Shark",
    "emails": [
        {
            "email": "[email protected]",
            "type": "work"
        },
        {
            "email": "[email protected]",
            "type": "home"
        }
    ]
}

この構造を使用すると、学生の文書を取得するたびに、同じ読み取り操作で埋め込まれたメールアドレスも取得されます。

一対少の関係をモデル化する場合、関連する文書が独立してアクセスされる必要がない場合、このように直接文書を埋め込むことは通常望ましいです。スキーマの複雑さを軽減できるためです。

しかし、前述のように、このように文書を埋め込むことが常に最適な解決策であるとは限りません。次のセクションでは、なぜこれが一部のシナリオで最適な解決策でない可能性があるのかについて詳しく説明し、ドキュメントデータベースにおける関係を表現する代替方法としての子参照の使用方法を概説します。

ガイドライン4 — 子参照を用いた1対多および多対多の関係のモデリング

学生とそのメールアドレスの関係の性質は、その関係をドキュメントデータベースで最適にモデル化する方法を示唆しています。これは、学生と出席するコースの関係とはいくつかの違いがあるため、学生とコースの関係をモデル化する方法も異なります。

学生が出席する単一のコースを記述するドキュメントは、次のような構造に従うことができます:

{
    "name": "Physics 101",
    "department": "Department of Physics",
    "points": 7
}

例えば、各学生のコースに関する情報を埋め込みドキュメントを使用して保存することを最初から決定したとします:

{
    "_id": ObjectId("612d1e835ebee16872a109a4"),
    "first_name": "Sammy",
    "last_name": "Shark",
    "emails": [
        {
            "email": "[email protected]",
            "type": "work"
        },
        {
            "email": "[email protected]",
            "type": "home"
        }
    ],
    "courses": [
        {
            "name": "Physics 101",
            "department": "Department of Physics",
            "points": 7
        },
        {
            "name": "Introduction to Cloud Computing",
            "department": "Department of Computer Science",
            "points": 4
        }
    ]
}

これは完全に有効なMongoDBドキュメントであり、その目的に十分に役立つかもしれませんが、前のガイドラインで学んだ3つの関係の特性を考慮してください。

最初のものは基数です。学生はおそらく少数のメールアドレスしか維持しませんが、複数のコースに出席することができます。数年間の出席を経て、学生が参加したコースは数十になる可能性があります。さらに、彼らはこれらのコースを、同様に数年間にわたって自分のコースセットに出席している他の多くの学生と共に出席します。

もし、前の例のように各コースを埋め込むことを決めた場合、学生のドキュメントはすぐに扱いにくくなるでしょう。カーディナリティが高くなるほど、埋め込みドキュメントのアプローチはあまり魅力的ではなくなります。

次に考慮すべき点は独立したアクセスです。メールアドレスとは異なり、大学のコースに関する情報が単独で取得される必要があるケースがあると想定するのは妥当です。例えば、マーケティングブローシュレットを準備するために利用可能なコースの情報が必要になるかもしれません。さらに、コースは時間とともに更新が必要になる可能性が高いです:コースを担当する教授が変わるかもしれませんし、スケジュールが変動するかもしれませんし、前提条件が更新される必要があるかもしれません。

もし学生のドキュメント内にコースをドキュメントとして保存すると、大学が提供するすべてのコースのリストを取得することが面倒になります。また、コースの更新が必要になるたびに、すべての学生の記録を通じてコース情報をすべて更新する必要があります。これらは、コースを別々に保存し、完全に埋め込まないのが良い理由です。

3つ目に考慮すべき点は、学生と大学のコースの関係が実際には一対多なのか、それとも多対多なのかということです。この場合、後者です。複数の学生が各コースに参加できるためです。この関係のカーディナリティと独立したアクセスの側面から、主にアクセスと更新の容易さといった実用的な理由から、各コースのドキュメントを埋め込むことに反対する根拠があります。コースと学生の関係が多対多であることを考えると、コースのドキュメントを独自の識別子を持つ別のコレクションに保存するのが理にかなっているかもしれません。

この別コレクション内のクラスを表すドキュメントは、以下の例のような構造になっているかもしれません:

{
    "_id": ObjectId("61741c9cbc9ec583c836170a"),
    "name": "Physics 101",
    "department": "Department of Physics",
    "points": 7
},
{
    "_id": ObjectId("61741c9cbc9ec583c836170b"),
    "name": "Introduction to Cloud Computing",
    "department": "Department of Computer Science",
    "points": 4
}

もしこのようにコース情報を保存することを決定した場合、学生とこれらのコースをつなぐ方法を見つける必要があります。そうすれば、どの学生がどのコースに出席しているかがわかります。このような場合、特に多対多の関係で、関連するオブジェクトの数が極端に多くない場合、一般的な方法の一つは子参照を使用することです。

子参照を使用すると、学生のドキュメントは、学生が出席するコースのオブジェクト識別子を埋め込み配列で参照します。以下の例のように:

{
    "_id": ObjectId("612d1e835ebee16872a109a4"),
    "first_name": "Sammy",
    "last_name": "Shark",
    "emails": [
        {
            "email": "[email protected]",
            "type": "work"
        },
        {
            "email": "[email protected]",
            "type": "home"
        }
    ],
    "courses": [
        ObjectId("61741c9cbc9ec583c836170a"),
        ObjectId("61741c9cbc9ec583c836170b")
    ]
}

この例のドキュメントには依然としてcoursesフィールドがあり、これも配列ですが、以前の例のように完全なコースドキュメントを埋め込むのではなく、別コレクション内のコースドキュメントを参照する識別子のみが埋め込まれています。これにより、学生ドキュメントを取得する際にはコース情報は即座に利用できず、別途クエリが必要になります。一方で、どのコースを取得すべきかは即座にわかります。また、コースの詳細を更新する必要がある場合、コースドキュメント自体のみを変更すればよく、学生とコースの間のすべての参照は有効なままです。

注意:この方法で子参照を埋め込む際の関係のカーディナリティがいつ大きすぎるかについての厳密なルールはありません。問題のアプリケーションに最も適していると判断した場合、カーディナリティが低いか高いかに応じて別のアプローチを選択することがあります。結局のところ、アプリケーションがデータをクエリおよび更新する方法に合わせてデータ構造を設計することを常に望むでしょう。

一対多の関係をモデル化する際、関連するドキュメントの量が適度な範囲内であり、それらのドキュメントが独立してアクセスされる必要がある場合は、関連するドキュメントを別々に保存し、子参照を埋め込んでそれらに接続することをお勧めします。

異なるタイプのデータ間の関係を示すために子参照を使用する方法を学んだので、このガイドでは逆の概念である親参照について概説します。

ガイドライン5 — 親参照を使用した無制限の一対多関係のモデリング

子参照は、関連するオブジェクトが多すぎて親ドキュメント内に直接埋め込むことができないが、その量は依然として既知の範囲内にある場合にうまく機能します。しかし、関連するドキュメントの数が無制限であり、時間とともに増加し続ける場合もあります。

例として、大学の学生会が掲示板を持っており、どの学生もコースに関する質問、旅行の話、求人情報、学習資料、またはただの雑談など、好きなメッセージを投稿できると想像してください。この例のサンプルメッセージは、件名とメッセージ本文で構成されています。

{
    "_id": ObjectId("61741c9cbc9ec583c836174c"),
    "subject": "Books on kinematics and dynamics",
    "message": "Hello! Could you recommend good introductory books covering the topics of kinematics and dynamics? Thanks!",
    "posted_on": ISODate("2021-07-23T16:03:21Z")
}

この関係をモデル化するために、前述の2つのアプローチ(埋め込みと子参照)のいずれかを使用することができます。埋め込みを選択すると決定した場合、学生のドキュメントは次のような形になるかもしれません:

{
    "_id": ObjectId("612d1e835ebee16872a109a4"),
    "first_name": "Sammy",
    "last_name": "Shark",
    "emails": [
        {
            "email": "[email protected]",
            "type": "work"
        },
        {
            "email": "[email protected]",
            "type": "home"
        }
    ],
    "courses": [
        ObjectId("61741c9cbc9ec583c836170a"),
        ObjectId("61741c9cbc9ec583c836170b")
    ],
    "message_board_messages": [
        {
            "subject": "Books on kinematics and dynamics",
            "message": "Hello! Could you recommend good introductory books covering the topics of kinematics and dynamics? Thanks!",
            "posted_on": ISODate("2021-07-23T16:03:21Z")
        },
        . . .
    ]
}

しかし、学生がメッセージを多く書く場合、その文書はすぐに非常に長くなり、16MBのサイズ制限を簡単に超えてしまう可能性があるため、この関係の濃度は埋め込みに反対するものとなります。また、メッセージは学生とは別にアクセスする必要があるかもしれません。例えば、メッセージボードのページが学生が投稿した最新のメッセージを表示するように設計されている場合などです。これもまた、このシナリオでは埋め込みが最適な選択ではないことを示唆しています。

注意: 学生の文書を取得する際に、メッセージボードのメッセージが頻繁にアクセスされるかどうかも考慮する必要があります。そうでない場合、それらすべてを文書内に埋め込んでしまうと、メッセージのリストが頻繁に使用されない場合でも、この文書の取得と操作にパフォーマンスのペナルティが発生します。関連データへのアクセスが稀であることは、多くの場合、文書を埋め込まないべき別の手がかりとなります。

次に、前の例のように完全な文書を埋め込む代わりに、子参照を使用することを検討してみましょう。個々のメッセージは別のコレクションに保存され、学生の文書は次のような構造を持つことができます:

{
    "_id": ObjectId("612d1e835ebee16872a109a4"),
    "first_name": "Sammy",
    "last_name": "Shark",
    "emails": [
        {
            "email": "[email protected]",
            "type": "work"
        },
        {
            "email": "[email protected]",
            "type": "home"
        }
    ],
    "courses": [
        ObjectId("61741c9cbc9ec583c836170a"),
        ObjectId("61741c9cbc9ec583c836170b")
    ],
    "message_board_messages": [
        ObjectId("61741c9cbc9ec583c836174c"),
        . . .
    ]
}

この例では、message_board_messagesフィールドには、Sammyが書いたすべてのメッセージへの子参照が格納されるようになりました。しかし、このアプローチを変更することで、前述の問題のうち1つだけが解決され、独立してメッセージにアクセスできるようになりました。しかし、子参照を使用することで学生のドキュメントサイズはより緩やかに増加しますが、この関係の無制限のカーディナリティにより、オブジェクト識別子のコレクションも扱いにくくなる可能性があります。結局のところ、学生は4年間の勉強中に簡単に何千ものメッセージを書くことができます。

このようなシナリオでは、あるオブジェクトを別のオブジェクトに接続する一般的な方法は、親参照を通じて行うことです。先に説明した子参照とは異なり、今度は学生のドキュメントが個々のメッセージを参照するのではなく、メッセージのドキュメントに学生を指す参照が含まれるようになります。

親参照を使用するには、メッセージドキュメントのスキーマを変更して、メッセージを作成した学生への参照を含める必要があります:

{
    "_id": ObjectId("61741c9cbc9ec583c836174c"),
    "subject": "Books on kinematics and dynamics",
    "message": "Hello! Could you recommend a good introductory books covering the topics of kinematics and dynamics? Thanks!",
    "posted_on": ISODate("2021-07-23T16:03:21Z"),
    "posted_by": ObjectId("612d1e835ebee16872a109a4")
}

新しいposted_byフィールドには、学生のドキュメントのオブジェクト識別子が含まれていることに注意してください。これで、学生のドキュメントには、投稿したメッセージに関する情報は含まれなくなります:

{
    "_id": ObjectId("612d1e835ebee16872a109a4"),
    "first_name": "Sammy",
    "last_name": "Shark",
    "emails": [
        {
            "email": "[email protected]",
            "type": "work"
        },
        {
            "email": "[email protected]",
            "type": "home"
        }
    ],
    "courses": [
        ObjectId("61741c9cbc9ec583c836170a"),
        ObjectId("61741c9cbc9ec583c836170b")
    ]
}

学生が書いたメッセージのリストを取得するには、メッセージコレクションに対してクエリを実行し、posted_byフィールドに対してフィルタリングします。メッセージを別のコレクションに分けておくことで、学生のドキュメントに影響を与えることなく、メッセージのリストが増えることを安全に許容できます。

注意: 親参照を使用する際、親ドキュメントを参照するフィールドにインデックスを作成することで、親ドキュメントの識別子に対してフィルタリングを行うたびにクエリのパフォーマンスを大幅に向上させることができます。

1対多の関係をモデル化する場合、関連するドキュメントの量が無制限であり、ドキュメントが独立してアクセスされる必要があるかどうかに関係なく、一般的に関連するドキュメントを別々に保存し、親参照を使用して親ドキュメントに接続することが推奨されます。

結論

ドキュメント指向データベースの柔軟性により、リレーショナルデータベースに比べて、ドキュメントデータベースでの関係のモデリングは厳密な科学ではなくなっています。この記事を読むことで、ドキュメントの埋め込みと、子および親参照を使用して関連データを保存する方法について理解しました。関係のカーディナリティを考慮し、無制限の配列を避けること、およびドキュメントが個別にまたは頻繁にアクセスされるかどうかを考慮することを学びました。

これらはMongoDBで一般的な関係をモデル化するためのいくつかのガイドラインに過ぎませんが、データベーススキーマのモデリングは万能ではありません。スキーマ設計時には常に、アプリケーションとそのデータの使用および更新方法を考慮してください。

MongoDBにおけるスキーマ設計やさまざまな種類のデータを保存するための一般的なパターンについてもっと学ぶために、そのトピックに関する公式MongoDBドキュメントをチェックすることをお勧めします。

Source:
https://www.digitalocean.com/community/tutorials/how-to-design-a-document-schema-in-mongodb