БД NoSQL сейчас очень популярны среди стартапов и крупных компаний. Тем не менее, все еще существует значительное количество разработчиков, которым сложно понять, как это работает и может повлиять на архитектурные решения проекта. В результате я создал путешествие, показывающее, как база данных может полностью изменить способ решения наших проблем разработки, таких как структура данных, целостность данных, микросервисы, производительность и запросы. Идея состоит в том, чтобы дать представление о том, как тип БД может также повлиять на дизайн вашего кода.
Как работает база данных SQL
Прежде чем мы поговорим о нереляционной БД, давайте потратим немного времени на понимание концепции реляционной БД и того, чем отличается от нее модель нереляционной базы данных. Рассмотрим DRM, в котором есть 2 объекта: клиент и страна.
Отношения довольно просты: у клиента может быть 1 страна, а у страны может быть много клиентов. Чтобы запросить клиента, использующего эту модель, вам нужно будет сделать что-то вроде этого:
select * from customer cu, country co where cu.CountryId == co.CountryId
В приведенном выше примере каждый раз при запросе клиента читаются 2 таблицы: заказчик и страна. Это связано с тем, что они связаны, и подробная информация о стране указана в таблице Country. А теперь давайте забудем о технической перспективе и рассмотрим эту модель под другим углом: как часто кто-то переезжает в другую страну в течение своей жизни? Я считаю, что очень немногие люди скажут больше четырех, и я думаю, что к настоящему времени вы поняли мою точку зрения. Зачем мне читать из двух таблиц, если значение практически не меняется?
Если вы думаете, что решением было бы добавить CountryName в качестве независимого поля в таблице Customer, я считаю, что вы все еще не поняли сути. Несмотря на то, что это могло быть решением (рассмотренным позже) вышеупомянутой проблемы, к сожалению, это могло привести к несогласованности данных. Если вы создадите открытое поле, чтобы запросить отчет по странам, вы, скорее всего, найдете «Австралия», «Австралия», «Австралия», «АВСТРАЛИЯ» и так далее. Определенно не то, что мы хотим. Более того, приведенный выше пример носит чисто академический характер. В реальной жизни DRM клиента будет выглядеть примерно так:
Может показаться, что я переоценил DRM, описанный выше, но поверьте мне, есть и похуже. А теперь представьте, сколько стоит запрос всех этих таблиц только для того, чтобы получить профиль клиента. Так работает реляционная БД, и это одна из причин, по которой была создана нереляционная БД: для снижения затрат на запросы деталей из разных таблиц во время выполнения кода. С другой стороны, реляционная БД обеспечивает лучшую поддержку для создания бизнес-отчетов и сложных запросов.
Как бы эта архитектура выглядела в нереляционной БД?
В модели noSQL у нас есть коллекции вместо таблиц, и они не связаны. Какие? 😱. Да никаких отношений. Они просто бродят сами по себе независимо. Вдобавок ко всему, документы коллекции не должны поддерживать идентичные структуры данных, что означает большую гибкость. Основное отличие состоит в том, что одна коллекция будет содержать всю информацию, необходимую для извлечения объекта. Это было бы примерно так:
{ "_id": "b28f459c-8ad3-11ea-bc55-0242ac130003", "name": "Test Name", "country": { "_id": "b28f459c-8ad3-11ea-bc55-0242ac130004", "name": "Australia" } }
Хотя у нас по-прежнему будет две коллекции (Клиент и Страна), новая запись о Клиенте будет содержать не только идентификатор страны, но и имя. Таким образом, вы можете легко узнать название страны, не запрашивая ее коллекцию.
Но что, если название страны клиента изменится?
Теперь это становится интересным, поскольку в БД нет функции зеркалирования данных. Когда дело доходит до нереляционной БД, есть несколько способов решить эту проблему (если это вообще проблема), и вот несколько вариантов, которые можно использовать в зависимости от бизнес-решения:
- Вместо того, чтобы позволять клиенту изменять название страны, создайте сценарий, который обновит Country и Customer за один раз. Бывший:
Customer.update( { "_id": "b28f459c-8ad3–11ea-bc55–0242ac130003" }, { "country.name": "new_country_name" } ) Country.update( { "_id": "b28f459c-8ad3–11ea-bc55–0242ac130004" }, { "name": "new_country_name" } )
- Выполните задачу (cronjob, лямбда или очередь сообщений), которая проверит измененную страну и обновит всех клиентов с этой страной. Это сохранит согласованность, но требует постоянного обновления данных.
- Храните старые данные, пока кто-нибудь не обновит клиента. Это решение не только сохраняет согласованность, но и имеет очень низкую стоимость. Другими словами, пользовательский интерфейс будет иметь новое имя, а когда клиент обновится, у него будет новое правильное название страны.
- Та же функция кода, которая обновляет название страны, также обновит клиентов. Это сохранит согласованность, но требует больших затрат на UX, поскольку пользователю / клиенту нужно будет ждать завершения всей серверной транзакции, а также это решение, которое сложно поддерживать.
- Игнорируйте данные в БД и изменяйте их только на клиенте. Например, если DB возвращает «AUS» или «AU», покажите «Australia». Это решение не сохранит единообразия и будет дорого обходиться, если придется менять много клиентов.
Идея состоит не в том, чтобы рассказать, как решить проблему, а в том, чтобы подчеркнуть, как меняется подход в зависимости от типа БД. Каждый вариант может применяться к разным ситуациям. Например, вариант 3 может быть лучшим выбором, поскольку в 1949 году страна Сиам стала Таиландом, и не похоже, чтобы все принимали новое название с первого дня. полагают, что даже старожилы все еще будут использовать прежнее название, так зачем тратить время и силы на изменение так скоро?
Хорошо. Клиент x Страна - это просто, но как насчет второго сложного примера, когда у клиента есть несколько отношений?
В действительности в нереляционной архитектуре из-за гибкой структуры данных эти сущности вряд ли будут независимым документом в БД. В большинстве случаев это просто перечисление с несколькими предопределенными значениями. Поскольку документ в базе данных noSQL может иметь любую структуру, очень часто его схему определяют на уровне кода. Естественно, это неприменимо, если вы хотите предоставить своему клиенту гибкость для управления самими данными, а это означает, что в этом случае база данных SQL может быть лучшим выбором. Если мы рассмотрим сложный пример, ODM будет выглядеть примерно так:
var customer = new Schema({ name: { type: String }, DOB: { type: Date, required: true }, country: new Schema({ name: { type: String } }), membershipType: { type: String, enum: ['Gold', 'Silver', 'Premium'] }, employmentStatus: { type: String, enum: ['Casual', 'Full-time', 'Part-time', 'Retired'] }, maritalStatus: { type: String, enum: ['Married', 'Divorced', 'DeFacto', 'Single'] } });
В этом случае, поскольку параметры предопределены, будет выполняться проверка любой попытки ввести значение. Представьте, что нам нужно изменить Де-факто на Де-факто. Запрос на обновление может быть выполнен для уже существующих записей и обновлен код.
Поскольку nonSQL является гибким, схема данных может быть определена на уровне кода и легко изменена, проверена и повторно использована. Другими словами, никаких изменений БД.
Что касается сведений о стране в приведенном выше примере, с точки зрения сбора данных Заказчиком не имеет значения, поступают ли эти данные из другой коллекции или из любого другого источника. Важно то, что он ожидает код (неявно созданный некоторым ODM при создании новой схемы) и имя. Фактически, он может иметь CreationDate, updateDate, customersLanguage, previousCountry и т. Д. Существует гибкость для создания всего, что вы хотите.
Придется ли мне всегда обновлять свои коллекции, чтобы данные были единообразными?
До сих пор мы говорили о разных сущностях и о том, как поддерживать согласованность их изменений. Но что, если согласованность на самом деле означает отсутствие изменения данных? 😕. Хорошо, позволь мне объяснить. Рассмотрим классический товарный заказ, в котором заказ может содержать несколько товаров. В реляционной БД он будет содержать: Product, Customer, Order, ProductOrderList и т. Д. Однако, поскольку мы не будем связывать данные, в нереляционной БД заказ будет примерно таким:
var Order = new Schema({ customer: { customerId: UUID, membershipType: String, maritalStatus: String //or a list of predefined values }, date: { type: Date, required: true }, total: { type: Number, required: true }, Products: [{ productId: UUID, name: String, price: Number, discount: Number, }], });
Если вы присмотритесь, в приведенном выше примере я выделил некоторые атрибуты, потому что на самом деле это информация, которую мы, возможно, не захотим изменять в случае изменения исходного источника. Что 🤪? да. Представьте, что продавец предлагает скидку на основании членства, а иногда и семейного положения. В этом случае мы не хотим, чтобы эти данные дублировались с данными клиента, потому что заказ представляет собой снимок клиента на момент совершения покупки, и это информация о членстве, которую мы хотим знать. Таким образом, если клиент обновит свое членство, мы узнаем, что у него был «базовый» тип в предыдущем порядке.
Удивительно, но для выполнения того же запроса с реляционной БД можно было бы использовать несколько объединений, содержащих:
- Клиент с memberStatus и с maritalStatus,
- и клиент с заказом,
- и Order с membersStatus и maritalStatus (да, мы должны продублировать эти отношения, потому что они неизменны от клиента)
- и так далее
И это только для того, чтобы вытащить данные о клиенте из заказа. Запрос продолжается и продолжается для других атрибутов, если нам также нужно запросить их.
Но что, если я хочу запросить все сведения о клиенте?
Веря в эффективность моего письма, на данный момент мы понимаем, что цель - снизить стоимость запросов к БД. Другими словами: производительность. В идеале данные, которые должны быть получены, должны быть проверены во время архитектурного проектирования, но если вывод заключался в сборе данных от двух разных объектов по отдельности, тогда решение будет заключаться в выполнении 2 или более запросов. Однако то, как выполнить запрос, будет зависеть от архитектуры вашего кода. Давайте посмотрим на несколько примеров:
- Микросервис: если объект не принадлежит микросервису, тогда пользовательскому интерфейсу клиента потребуется получить данные из каждой микросервиса.
- Монолитный: если это будет очень часто используемый запрос и все данные потребляются, тогда можно создать единую конечную точку для получения всех данных. Если он не будет использоваться очень часто и существует риск или частичное использование сведений о клиенте в разных случаях, тогда для каждого случая может быть создана отдельная конечная точка.
Более того, даже несмотря на то, что база данных noSQL не имеет реляционной концепции, в частности, Mongoose имеет функцию, называемую заполнение, которая позволяет выполнять соединение с той же концепцией, что и в базе данных SQL. Однако, несмотря на то, что эта функция может быть очень удобной, есть люди, которые снова ее используют, поскольку это не то, как следует использовать нереляционную БД.
Как тип БД влияет на архитектуру микросервисов?
Один из принципов микросервисов - быть слабосвязанными, что означает, что каждый микросервис должен быть полностью независимым и изолированным. Я не собираюсь вдаваться в подробности об этом шаблоне (возможно, мне стоит написать на эту тему целую статью), но я думаю, вы можете увидеть, к чему я иду. У каждой службы должна быть своя собственная БД, которая не должна быть связана с другими БД, поэтому мы пришли к выводу, что БД, отличная от SQL, будет лучшим выбором для микросервисов. Я никогда не использовал базу данных SQL с микросервисами, но мне интересно, что дублировать таблицы, относящиеся к более чем одной таблице, - неинтересно. Более того, использование одной и той же БД для всех микросервисов было бы антипатном. Если вы использовали микросервисы с реляционной БД, оставьте, пожалуйста, комментарий о своем опыте.
Слабо связана с другими службами: позволяет команде работать независимо большую часть времени над своими услугами, не подвергаясь влиянию изменений в других службах и не влияя на другие службы.
Не так давно я написал статью Что вам следует знать перед подключением функции AWS Lambda к базе данных, в которой я выделил причины, по которым я выбираю MongoDB вместо DynamoDB для бессерверного приложения. Однако по тем же причинам DynamoDB будет лучшим выбором для микросервисов, учитывая, что каждая таблица считается полностью независимой сущностью. Это упростило бы сохранение модели поддержки разных БД (в DynamoDB это таблицы) для каждой службы.
Подводя итоги
Когда дело доходит до создания приложения, есть несколько вариантов баз данных, которые в основном делятся на NoSQL и SQL. Несмотря на то, что NoSQL завоевывает рынок, очень важно понимать, как он меняет способ разработки наших приложений, решения повседневных проблем и покрывает ли это бизнес-потребности проекта.
Вот также таблица с обзором основных областей, затронутых в этой статье:
Глоссарий
DRM → Реляционная модель данных
ODM → Object Document Mapper
Ссылки
Https://mongoosejs.com/docs/populate.html
https://microservices.io/patterns/microservices.html