Как создать схему документа в MongoDB

Автор выбрал Фонд Открытого Интернета/Свободы Слова для получения пожертвования в рамках программы Write for DOnations.

Введение

Если у вас большой опыт работы с реляционными базами данных, может быть трудно выйти за рамки принципов реляционной модели, таких как мышление в терминах таблиц и связей. Документно-ориентированные базы данных типа MongoDB позволяют освободиться от жесткости и ограничений реляционной модели. Однако гибкость и свобода, которая приходит с возможностью хранить самоописывающиеся документы в базе данных, могут привести к другим ловушкам и сложностям.

Эта концептуальная статья очерчивает пять общих рекомендаций, связанных с проектированием схемы в документно-ориентированной базе данных, и подчеркивает различные соображения, которые следует учитывать при моделировании отношений между данными. Также она проведет вас через несколько стратегий, которые можно применять для моделирования таких отношений, включая встраивание документов в массивы и использование дочерних и родительских ссылок, а также когда эти стратегии будут наиболее подходящими для использования.

Правило 1 — Хранение вместе того, что нужно использовать вместе

В типичной реляционной базе данных данные хранятся в таблицах, и каждая таблица состоит из фиксированного списка столбцов, представляющих различные атрибуты, которые образуют сущность, объект или событие. Например, в таблице, представляющей студентов университета, могут быть столбцы, содержащие имя, фамилию, дату рождения и уникальный идентификационный номер каждого студента.

Обычно каждая таблица представляет собой отдельную тему. Если вы хотите хранить информацию о текущих занятиях студента, стипендиях или предыдущем образовании, имеет смысл хранить эти данные в отдельной таблице от той, которая содержит его личную информацию. Затем вы можете связать эти таблицы, чтобы обозначить, что между данными в каждой из них существует связь, указывающая на то, что информация, которую они содержат, имеет значимую связь.

Например, таблица, описывающая статус стипендии каждого студента, может ссылаться на студентов по их идентификационному номеру, но не будет хранить имя или адрес студента напрямую, избегая дублирования данных. В таком случае, для получения информации о любом студенте со всеми данными о его аккаунтах в социальных сетях, предыдущем образовании и стипендиях, потребуется запрос, который будет обращаться к более чем одной таблице одновременно, а затем собирать результаты из разных таблиц в один.

Такой метод описания отношений через ссылки известен как нормализованная модель данных. Хранение данных таким образом — используя несколько отдельных, кратких объектов, связанных друг с другом — также возможно в базах данных, ориентированных на документы. Однако, гибкость модели документов и свобода, которую она даёт для хранения встроенных документов и массивов внутри одного документа, означает, что вы можете моделировать данные иначе, чем в реляционной базе данных.

Основной концепцией для моделирования данных в базе данных, ориентированной на документы, является “хранить вместе то, что будет запрашиваться вместе”. Погрузившись глубже в пример со студентами, предположим, что большинство студентов в этой школе имеют более одного адреса электронной почты. Из-за этого университет хочет иметь возможность хранить несколько адресов электронной почты с каждым студентом контактной информации.

В таком случае, пример документа может иметь структуру следующего вида:

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

Обратите внимание, что в этом примере документ содержит встроенный список адресов электронной почты.

Представление более чем одного предмета внутри одного документа характеризует денормализованную модель данных. Это позволяет приложениям извлекать и манипулировать всеми релевантными данными для данного объекта (здесь, студента) за один раз, без необходимости обращаться к нескольким отдельным объектам и коллекциям. Таким образом также гарантируется атомарность операций над таким документом, без необходимости использования транзакций с несколькими документами для обеспечения целостности.

Хранение вместе того, что должно быть доступно вместе, с использованием встроенных документов, часто является оптимальным способом представления данных в базе данных, ориентированной на документы. В следующих рекомендациях вы узнаете, как различные отношения между объектами, такие как отношения один к одному или один ко многим, могут быть лучше всего смоделированы в базе данных, ориентированной на документы.

Рекомендация 2 — Моделирование отношений один к одному с использованием встроенных документов

Отношение один к одному представляет собой ассоциацию между двумя различными объектами, где один объект связан ровно с одним объектом другого типа.

Продолжая пример со студентом из предыдущего раздела, каждый студент имеет только одну действительную студенческую карточку в любой момент времени. Одна карта никогда не принадлежит нескольким студентам, и ни один студент не может иметь несколько идентификационных карт. Если бы вы хранили все эти данные в реляционной базе данных, вероятно, имело бы смысл моделировать связь между студентами и их студенческими карточками, храня записи студентов и записи студенческих карточек в отдельных таблицах, связанных через ссылки.

Один из распространенных методов представления таких связей в базе данных документов заключается в использовании встроенных документов. Например, следующий документ описывает студента по имени Сэмми и его студенческую карточку:

{
    "_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, датой выдачи карты и датой ее истечения. Идентификационная карта по сути становится частью документа, описывающего студента Сэмми, хотя в реальной жизни это отдельный объект. Обычно структурирование схемы документа таким образом, чтобы можно было получить всю связанную информацию с помощью одного запроса, является разумным выбором.

Вещи становятся менее очевидными, если вы сталкиваетесь со связями, соединяющими один объект одного типа с множеством объектов другого типа, такими как электронные адреса студента, курсы, которые они посещают, или сообщения, которые они публикуют на доске объявлений студенческого совета. В следующих нескольких руководствах вы будете использовать эти примеры данных, чтобы изучить различные подходы к работе с отношениями один-ко-многим и многие-ко-многим.

Правило 3 — Моделирование одно-ко-многим отношений с помощью встроенных документов

Когда объект одного типа связан с несколькими объектами другого типа, это можно описать как один-ко-многим отношение. Студент может иметь несколько адресов электронной почты, автомобиль может состоять из многочисленных деталей или заказ на покупку может включать несколько товаров. Каждый из этих примеров представляет собой один-ко-многим отношение.

Хотя наиболее распространенный способ представления один-к-одному отношения в базе данных документов — это встроенный документ, существует несколько способов моделирования один-ко-многим отношений в схеме документов. При рассмотрении вариантов того, как лучше всего моделировать эти отношения, следует учитывать три свойства данного отношения:

  • Кардинальность: Кардинальность — это мера количества отдельных элементов в заданном множестве. Например, если в классе 30 студентов, можно сказать, что у этого класса кардинальность равна 30. В один-ко-многим отношении кардинальность может быть разной в каждом случае. У студента может быть один адрес электронной почты или несколько. Он может быть зарегистрирован только на несколько занятий или у него может быть полностью заполненное расписание. В один-ко-многим отношении размер “многих” повлияет на то, как вы могли бы моделировать данные.
  • Независимый доступ: Некоторые связанные данные будут редко, если вообще, доступны отдельно от основного объекта. Например, может быть необычным получать адрес электронной почты одного студента без других деталей студента. С другой стороны, курсы университета могут нуждаться в индивидуальном доступе и обновлении, независимо от студента или студентов, которые зарегистрированы для участия в них. Будет ли когда-либо доступ к связанному документу в одиночку также повлияет на то, как вы могли бы моделировать данные.
  • Является ли связь между данными строго один-ко-многим: Рассмотрим курсы, которые посещает примерный студент в университете. С точки зрения студента, он может участвовать в нескольких курсах. На первый взгляд, это может показаться один-ко-многим отношением. Однако университетские курсы редко посещаются одним студентом; чаще, несколько студентов будут посещать один и тот же класс. В таких случаях, рассматриваемое отношение на самом деле не один-ко-многим, а многие-ко-многим, и, следовательно, вы бы приняли другой подход к моделированию этого отношения, чем при один-ко-многим отношении.

Представьте, что вы решаете, как хранить адреса электронной почты студентов. Каждый студент может иметь несколько адресов электронной почты, таких как один для работы, один для личного использования и один, предоставленный университетом. Документ, представляющий один адрес электронной почты, может принять форму, подобную этой:

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

С точки зрения количества, для каждого студента будет лишь несколько адресов электронной почты, поскольку маловероятно, что у студента будет десятки — не говоря уже о сотнях — адресов электронной почты. Таким образом, эту связь можно охарактеризовать как один-ко-нескольким, что является убедительным аргументом для прямого встраивания адресов электронной почты в документ студента и совместного хранения. Вы не рискуете, что список адресов электронной почты будет расти бесконечно, что сделает документ большим и неэффективным для использования.

Примечание: Имейте в виду, что хранение данных в массивах связано с определенными подводными камнями. Например, один документ MongoDB не может превышать 16 МБ. Хотя встраивание нескольких документов с использованием полей массива возможно и распространено, если список объектов растет бесконтрольно, документ может быстро достичь этого предела размера. Кроме того, хранение большого количества данных внутри встроенных массивов оказывает значительное влияние на производительность запросов.

Встраивание нескольких документов в поле массива, вероятно, будет подходящим в многих ситуациях, но знайте, что это может и не всегда быть лучшим решением.

Что касается независимого доступа, адреса электронной почты, вероятно, не будут использоваться отдельно от студента. Таким образом, нет четкого стимула хранить их как отдельные документы в отдельной коллекции. Это еще один убедительный аргумент для встраивания их внутрь документа студента.

Последнее, что следует учитывать, это то, действительно ли это отношение является один-ко-многим, а не многие-ко-многим. Поскольку адрес электронной почты принадлежит одному человеку, разумно описывать это отношение как один-ко-многим (или, возможно, более точно, один-к-нескольким) вместо многие-ко-многим.

Эти три предположения указывают на то, что встраивание различных адресов электронной почты студентов в те же документы, которые описывают самих студентов, было бы хорошим выбором для хранения такого рода данных. Пример документа студента с встроенными адресами электронной почты может выглядеть следующим образом:

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

Используя эту структуру, каждый раз, когда вы извлекаете документ студента, вы также извлекаете встроенные адреса электронной почты в одной операции чтения.

Если вы моделируете отношение вида один-к-нескольким, где связанные документы не требуется обращаться независимо, встраивание документов напрямую обычно желательно, так как это может упростить схему.

Как упоминалось ранее, однако, встраивание документов таким образом не всегда является оптимальным решением. В следующем разделе приводятся более подробные сведения о том, почему это может быть так в некоторых сценариях, и описывается, как использовать дочерние ссылки в качестве альтернативного способа представления отношений в базе данных документов.

Правило 4 — Моделирование отношений “один ко многим” и “многие ко многим” с использованием дочерних ссылок

Природа отношений между студентами и их электронными адресами определяет, как эти отношения лучше всего могут быть смоделированы в документной базе данных. Существуют некоторые различия между этим и отношениями между студентами и курсами, которые они посещают, поэтому способ моделирования отношений между студентами и их курсами будет другим.

Документ, описывающий один курс, который посещает студент, может следовать структуре, подобной этой:

{
    "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 и мог бы хорошо выполнять свою задачу, но рассмотрите три свойства отношений, о которых вы узнали в предыдущем правиле.

Первое из них — кардинальность. Студент, вероятно, будет иметь всего несколько электронных адресов, но он может посещать множество курсов в течение своего обучения. После нескольких лет посещения может быть десятки курсов, в которых студент принимал участие. Кроме того, он будет посещать эти курсы вместе с множеством других студентов, которые также посещают свои собственные курсы в течение своих лет обучения.

Если бы вы решили встроить каждый курс, как в предыдущем примере, документ студента быстро стал бы громоздким. При более высокой кардинальности подход с встраиванием документов становится менее привлекательным.

Второе соображение – независимый доступ. В отличие от адресов электронной почты, можно предположить, что будут случаи, когда информация о университетских курсах потребуется извлекать отдельно. Например, предположим, что кто-то нуждается в информации о доступных курсах для подготовки маркетингового буклета. Кроме того, курсы, вероятно, потребуется обновлять со временем: преподаватель, ведущий курс, может измениться, его расписание может колебаться, или могут потребоваться обновления предварительных условий.

Если бы вы хранили курсы как документы, встроенные в документы студентов, извлечение списка всех курсов, предлагаемых университетом, стало бы проблематичным. Кроме того, каждый раз, когда курс требует обновления, вам пришлось бы пройтись по всем записям студентов и обновить информацию о курсе везде. И то, и другое являются хорошими причинами для хранения курсов отдельно и не встраивать их полностью.

Третье, что следует учитывать, – это то, является ли связь между студентом и университетским курсом фактически один-ко-многим или, наоборот, многие-ко-многим. В данном случае это последнее, так как более одного студента может посещать каждый курс. Кардинальность этой связи и аспекты независимого доступа предполагают против встраивания каждого документа курса, в первую очередь по практическим причинам, таким как удобство доступа и обновления. Учитывая множественно-множественный характер связи между курсами и студентами, может иметь смысл хранить документы курсов в отдельной коллекции с собственными уникальными идентификаторами.

Документы, представляющие классы в этой отдельной коллекции, могут иметь структуру, подобную этим примерам:

{
    "_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")
}

Вы могли бы использовать любой из двух рассмотренных ранее подходов — встраивание и ссылки на дочерние элементы — для моделирования этого отношения. Если бы вы решили использовать встраивание, документ студента мог бы выглядеть следующим образом:

{
    "_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")
        },
        . . .
    ]
}

Однако, если студент активно пишет сообщения, его документ быстро станет чрезвычайно длинным и легко может превысить лимит в 16 МБ, поэтому высокая кардинальность этого отношения говорит против встраивания. Кроме того, сообщения могут нуждаться в отдельном доступе от студента, что может быть актуально, если страница доски объявлений предназначена для отображения последних сообщений, оставленных студентами. Это также указывает на то, что встраивание не является лучшим выбором для этого сценария.

Примечание: Также следует учитывать, насколько часто обращаются к сообщениям доски объявлений при получении документа студента. Если нет, наличие их всех встроенными в этот документ приведет к снижению производительности при его получении и манипуляциях, даже если список сообщений используется редко. Редкое обращение к связанным данным часто является еще одним признаком того, что не следует встраивать документы.

Теперь рассмотрим использование дочерних ссылок вместо встраивания полных документов, как в предыдущем примере. Отдельные сообщения будут храниться в отдельной коллекции, и документ студента может иметь следующую структуру:

{
    "_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. Однако изменение подхода решает только одну из проблем, упомянутых ранее, поскольку теперь становится возможным независимое доступ к сообщениям. Однако, хотя размер документа студента будет увеличиваться медленнее при использовании подхода с дочерними ссылками, коллекция идентификаторов объектов также может стать неудобной из-за неограниченной кардинальности этой связи. В конце концов, студент может легко написать тысячи сообщений за четыре года обучения.

В таких сценариях обычным способом связи одного объекта с другим являются родительские ссылки. В отличие от описанных ранее дочерних ссылок, теперь не документ студента ссылается на отдельные сообщения, а ссылка в документе сообщения указывает на студента, который его написал.

Для использования родительских ссылок необходимо изменить схему документа сообщения, чтобы она содержала ссылку на студента, который написал сообщение:

{
    "_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. Наличие их в отдельной коллекции делает безопасным рост списка сообщений, не влияя на какие-либо документы студента.

Примечание: При использовании родительских ссылок создание индекса на поле, ссылающееся на родительский документ, может значительно повысить производительность запросов при каждом фильтрации по идентификатору родительского документа.

Если вы моделируете отношение один-ко-многим, где количество связанных документов не ограничено, независимо от того, требуется ли доступ к документам независимо друг от друга, обычно рекомендуется хранить связанные документы отдельно и использовать родительские ссылки для их подключения к родительскому документу.

Заключение

Благодаря гибкости документо-ориентированных баз данных, определение наилучшего способа моделирования отношений в документальных базах данных менее строгая наука, чем в реляционных базах данных. Прочитав эту статью, вы ознакомились с встраиванием документов и использованием дочерних и родительских ссылок для хранения связанных данных. Вы узнали о необходимости учитывать кардинальность отношений и избегать неограниченных массивов, а также принимать во внимание, будет ли документ доступен отдельно или часто.

Это лишь несколько рекомендаций, которые могут помочь вам моделировать типичные отношения в MongoDB, но моделирование схемы базы данных не является универсальным решением. Всегда учитывайте ваше приложение и то, как оно использует и обновляет данные при проектировании схемы.

Чтобы узнать больше о проектировании схем и общих шаблонах для хранения различных типов данных в MongoDB, мы рекомендуем вам ознакомиться с официальной документацией MongoDB по этой теме.

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