Всем привет.
Я люблю базы данных, люблю строить запросы, люблю проектировать БД. Раскладывать по полочка, систематизировать это моё любимое занятие. Конечно первые годы я проектировал таблицы БД как меня научили в ВУЗе - каждому свойству отдельная колонка.
Но этот подход отстал от жизни, то есть он удобен если ваша бизнес логика работает на хранимых процедурах. Тогда действительно, записи таблицы это готовые объекты вашей бизнес логики.
Но актуальных подход это разделение ответственности за обработку данных между сервером баз данных и сервером приложений. Сервер баз данных предоставляет данные, сервер приложений их обрабатывает.
Речь конечно об Online Transaction Processing (OLTP). Когда нам надо получить данные по одной сущности, например, показать профиль пользователя, или показать товарные позиции определённого заказа.
Ниже я расскажу о продвинутом способе хранения данных.
Продвинутый формат хранения данных
Хотя и принято говорить о хранении данных, но на самом деле никому не надо хранить данные, всем надо читать данные, главное назначение сервера баз данных это предоставление данных по запросу.
То есть при проектировании БД мы должны исходить из требования быстрой выдачи необходимого объёма данных. Мы должны обеспечить быстрый поиск по заданным условиям.
Что помогает поиску ? Индексы. То есть наша таблица должна иметь индекс для быстрого поиска и собственно данные. Таблица должна состоять из колонок индекса и колонки с собственно данными. Для согласованности данных можно добавить колонки внешнего ключа.
Пример реализации
Допустим перед нами стоит задача обрабатывать некие документы. В документе важен его статус, для быстрого поиска мы делаем колонку статуса, конечно статусы у нас могут быть не какие попало, возможные значения ограничены справочником, поэтому колонка будет внешним ключом на справочник статусов, создаём колонку status_id.
Каждый документ имеет уникальный номер, создаём колонку doc_num, делаем по ней уникальный индекс.
Для данных документа делаем колонку data, тип данных строка, можно использовать специализированны формат СУБД.
Что у нас получилось
create table document
(
id integer not null
constraint document_pk
primary key,
doc_num text not null,
status_id integer
constraint document_status_id_fk
references status,
data jsonb
);
create unique index document_doc_num_uindex
on public.document (doc_num);
create index document_status_index
on public.document (status);
Откуда растут уши
К такой схеме хранения данных я пришёл не сам, эту идею я взял у Филиппа Дельгядо. Вы можете посмотреть его доклады на конференциях с тайм кодами: раз, два.
Или весь доклад целиком:
В видео идея подробно излагается со всеми нюансами граблей на которые вы можете наступить, я в своей статье излагаю только основные моменты.
Способ работы с СУБД
Как правило, вам не надо делать выборки с условием отбора по языку интерфейса в профиле пользователя, вам надо только отобразить интерфейс с учётом настроек профиля. Или как правило, вам не надо делать поиск по заказам пользователя, что бы показать заказы в которых было определённое количество определённого товара, вам надо показать список заказов.
Запросы с выборками пользователей по языку интерфейса или со средним чеком по заказу делают для аналитики в системах Online Analytical Processing (OLAP), это их хлеб.
Как правило Online Transaction Processing (OLTP) занимается атомарной обработкой одной записи. Вам надо получить данные от пользователя, записать их в таблицу. На следующем этапе вы эти данные прочитаете, измените и снова запишите в эту же таблицу и скорей всего создадите связанные записи в других таблицах. Такая работа с записью будет продолжаться до тех пор пока бизнес процесс не будет полностью выполнен.
После этого вы ни когда к этой записи не вернётесь, вы выгрузите её в OLAP и забудете о ней навсегда.
При работе с СУБД через ORM происходит чтение всех колонок, ни кто не пишет в коде "дай мне колонку номер документа и дату документа", объект считывается целиком. Поэтому нет смысла разбивать свойства сущности по отдельным колонкам. Всё равно они будут прочитаны все. При разбиении информации на отдельные колонки и при записи в одну единственную колонку объем данных переданных от СУБД к приложению не измениться.
Получить JSON и превратить его в объект это элементарная задача. Обратная задача, получить JSON представление для объекта, это тоже элементарная задача.
Появляется нюанс с записью колонок индекса, когда мы сериализировали наш объект для записи в БД, мы также должны записать новые значения для колонок индекса, то есть если мы создали индекс со статусом документа, то при сохранении документа мы не только записываем новое значение в колонку data, но так же мы записываем новое значение в колонку status.
Всё это автоматизируется на уровне ORM.
Наследуемся от класса модели данных, добавляем абстрактные методы для сериализации и десериализации, теперь все классы наших моделей наследуем от нашего кастомного класса.
Для каждой нашей модели пишем реализацию сериализации с обновлением значений в колонках индекса. Это методы на несколько строк кода, ни чего сложного.
Преимущества
Какие преимущества даёт подход с единой колонкой для всех данных ?
При добавлении нового свойства у сущности, вам не надо изменять схему данных, вам не надо писать миграцию.
Теперь миграции нужны только для создания таблицы, и этот код упрощается до пары колонок.
Меньше миграций - проще деплой.
Ни кому в голову не придёт строить аналитические отчёты на СУБД Приложения, для этого будет использоваться OLAP.
Недостатки
Миграции по прежнему нужны для создания новых индексов и внешних ключей.
Для каждой модели надо писать код актуализации значений в колонках индекса.
Когда следует использовать такой подход ?
Мой ответ - всегда.
Всё что для этого надо - это преодолеть инертность своего мышления. Упрощения деплоя и сокращение количества миграций это серьёзное преимущество для по настоящему промышленных систем.
Если вы пишите домашний проект или проверяете гипотезу, то конечно вам не надо тратить время на пляски с бубном вокруг ORM, пусть всё мапиться один в один.
Если вы используете автогенерацию фронт-энда из схемы БД (пример Craftable — Laravel CRUD generator), то такой вариант с записью данных в одну колонку вам помешает.
Во всех других случаях, когда вы ведёте разработку не один год и не одной командой разработчиков - такой подход к работе с данными - облегчит жизнь вам и вашим разработчикам.
Пользуйтесь с удовольствием.
PS
Проходит год, на Хабре публикуют перевод статьи: "Пока-пока, MongoDB: почему компании переходят на PostgreSQL"
Что и требовалось доказать.
Всем безмозглым истеричкам привет! Продолжайте минусить всех здравых людей, которые способны по новому взглянуть на старую проблему.