В предыдущей серии этого сериала:
Автор постарался описать EAV модель опираясь теперь только на собственный опыт. Привёл живые примеры из жизни, описывая имеющиеся недостатки и иногда заставляя улыбаться читающего.
В этот раз постараюсь предложить схему данных, которая, по моему мнению больше подходит для MVP, но уже с учётом недостатков, которые были описаны ранее. Причем данная схема не является:
- Первичной, так как она отличается от основной модели и является примером выделения наиболее часто запрашиваемых сущностей
- Оригинальной, так как практически такой же подход к распределению данных я на практике встречал несколько раз (пример из практики: при автоматизации ETL-процесса, при сборе данных из 1С на MSSQL в OLAP куб данных).
- Универсальной, так как и в этой схеме таблиц есть слабые места, о которых будет написано позднее.
Предлагаемая схема данных позволит автоматизировать любой процесс, при котором производится какой-то продукт, а так как именно количественные характеристики продуктов(количество, масса и т.д.) будут чаще всего запрашиваться для анализа - выделим сущность Продукт в отдельные таблицы.
Так же необходимо будет ввести типы сущностей, которые необходимо хранить не в magic numbers, а иметь какой-то универсальный способ именования таких сущностей.
Атрибуты будем переиспользовать только в рамках одних и тех же процессов, это позволит более прозрачно отделять одну сущность от другой, а так же выявлять нарушения целостности данных.
Вот так будет выглядеть схема данных целиком
Понимаю, схема большая, сложная. Будем описывать все поля всех таблиц поэтапно:
Таблицы Documents и Products
Корневые родительские сущности, у которых нет структур, которые находятся "выше" в иерархии таблиц. Связь сущностей может быть осуществлена либо с подчинёнными сущностями (Attributes, Lines, DocumentAttributes, ProductAttributes) через хранение идентификатора корневой сущности в подчинённой, либо через связь корневых сущностей между собой (DocumentLinks, ProductLinks).
Кроме стандартных Id (Идентификатор конкретной сущности) и *Type (Тип сущности) каждая корневая сущности имеет свои дополнительные поля, хранение которых в атрибутах будет как усложнять запросы к СУБД, так и увеличивать время их исполнения.
К примеру таблица Documents содержит поля DocNumber, DocDate, DocStatus:
DocNumber, DocDate - т.к. мы все таки описываем какие-то документы, которые можем привязать к понятному нам слову "Документ", то необходимо в таком случае вынести в поля корневой сущности ключевые для обычных людей свойства, а именно № документа и его дату. Причём номер можно генерировать как автоматически, так и получать на уровне UI от оператора. То же самое касается и даты этого документа - в жизни практический каждый документ, который встречается нам имеет какой-то собственный номер и дату
DocStatus - техническое поле. Будем с помощью значений в этом поле управлять доступностью документа. К примеру, если документ является действующим, то ему будет присвоен статус 1 (каждый статус позже будет описан и вы поймете, каким образом в EAV-модели можно избежать magic numbers). А если возникнет необходимость в нашем учёте изменить доступность этого документа или прекратить его действие, то можно изменить статус этой записи, поместив значение 2 в поле DocStatus.
В таблице Products так же имеются свойства, которые вынесены из значений атрибутов непосредственно в таблицы:
ProType, ProPlace - тип продукции и место, где сейчас указанную продукцию можно найти (склад, прилавок, место на парковке, загон и т.д.). Так же будем хранить в этих полях идентификаторы значений, а не само значение в целях упрощения и/или ускорения поиска в СУБД.
ProductioDate, ConsumptionDate - Дата производства и использования соответственно. Т.е. если продукт поступил к нам в магазин или на пастбище какого-то конкретного числа - эта дата будет считаться датой производства, даже если мы к производству этого продукта не имели никакого отношения - продукт появился у нас, а значит должен появиться в наших учётах. В учётах мы его "произвели". Дата использования заполняется по тем же принципам - продукция ушла со склада или была продана или была украдена обезьянами из джунглей - неважно - с учёта указанная продукция должна быть снята как "использованная". Так как продукция на учёте может появиться в любое время, то поле ProductionDate обязательно к заполнению, а так как продукция может находится на складе какое-то время - ConsumptionDate может быть пустой.
ProStatus - по аналогии с DocStatus - в каком статусе продукция сейчас находится.
Таблица Attributes
Идентификатор атрибута(Id) будем использовать для хранения значений этих атрибутов. Таким образом, даже если мы в своих запросах будем использовать magic numbers, то при подключении в запрос описываемой таблицы - сможем получить наименования этих атрибутов.
Имя атрибута (Name) должно быть уникально в рамках всей структуры БД, чтобы у разработчиков и пользователей не возникало разночтений при заполнении атрибута, к примеру атрибут "Имя", который одновременно может являться и строковым и (внезапно) числовым. Для соблюдения этого ограничения можно использовать префиксы при именовании атрибутов. К примеру:
- Атрибут с именем [Продукция]Тип (строка или ссылка) будет описывать тип продукции, с которым ассоциировано значение этого атрибута
- Атрибут с именем [Документ]Тип (строка или ссылка) будет описывать тип документа, с которым ассоциировано значение этого атрибута
Введение такого правила позволит в будущем оценивать , какие сущности требуют выделения в отдельную структуру.
Тип атрибута (Type) наверное единственное свойство, которое будет содержать магические числа, т.к. количество типов атрибутов - это всё таки конечное число. Поэтому определимся следующим образом:
- Строковые значения (Строка) - тип 1
- Числовые значения (целое число или число с плавающей точкой, а так же булевы значения в виде 0/1) - тип 2
- Значения фиксированных значений времени (Дата, Дата и Время, Время) - тип 3
- Ссылка на строку документа (Будет объяснено на примерах) - тип 4
- Ссылка на документ (Будет объяснено на примерах) - тип 5
Да. Пытливый глаз читающего сразу может предположить, что типы атрибутов можно в таком случае смело отразить в виде какого-то перечисления (enum), т.к. количество типов известно заранее и вряд ли когда-нибудь изменится. Здесь я соглашусь с читающим, но отмечу, что это место - единственное в нашей структуре, которое можно обернуть в перечисления без увеличения количества затрат на рефакторинг в случае добавления нового типа данных. Новые типы данных в будущем либо просто не появятся, либо при необходимости можно использовать имеющиеся типы (как с boolean-значениями, которые будут храниться в типе Число).
Ссылка на справочник (DocumentId) - в целях оптимизации хранения различных справочных значений по ходу объяснения я создам первую сущность "Справочник", ссылку на которую я буду хранить тут, в случае необходимости использования.
Подчинённая таблица Lines
Если у нас в системе есть документы, значит в этих документах должны быть и строки этих документов. Описываемая таблица позволяет хранить дополнительные неключевые атрибуты, ссылки на другие документы или продукцию либо дополнительные характеристики. Все характеристики, которые не могут быть связаны с документом напрямую можно будет связать со строками этого документа. К примеру:
У каждого из нас есть какой-то сферический документ в вакууме, которым, выдавшая его нам организация, удостоверяет нашу личность на определённый период. Серия, номер, дата выдачи и дата прекращения действия этого удостоверения являются ключевыми признаками документа.
Исходя из этого утверждения мы уже понимаем, что это удостоверение личности - сущность типа "Документ". А как же быть с указанием организации, которая его выдала? Можно создать атрибут на документе - "Выдан" с типом строка или с типом ссылка на документ, чтобы одна организация могла выдавать удостоверение нескольким людям. Но как быть, если организация в какой-то период выдаёт удостоверения только людям с детьми ,а потом только людям без детей, затем закрывается на ремонт на полгода и не выдаёт совсем? Как определить, что вот это конкретное удостоверение выдано в период, когда оно вообще не работало? Для таких случаев используются строки. В следующих статьях я не раз буду описывать подобные ситуации и постараюсь объяснять, почему я выбираю не атрибут документа, а атрибут подчинённой документу строки.
LineType, LineNumber и LineStatus - по аналогии с таблицами Documents и Products - с целью обозначения каких-то свойств, которые чаще других будут использоваться при запросах к БД.
Таблицы для хранения значений атрибутов DocumentAttributes, LineAttributes и ProductAttributes
В предыдущих статьях уже описывал для чего нужны подобные таблицы и как с ними работать. В этом же разделе кратко опишу названия полей и цели с которым они созданы:
- Id - идентификатор конкретного значения атрибута.
- ParentId - ссылка на родительскую сущность, которую указанного значение описывает с помощью значения атрибута, Для корневых сущностей Document, Line и Product используются соответствующие префиксы.
- AttributeId - ссылка на атрибут, который описывает значение. С помощью этой ссылки мы сможем получить не только имя атрибута, но и его тип, что в будущем позволит практичнее соблюдать целостность данных в таблицах.
- StringValue - поле для хранения строковых значений атрибутов. Для типов атрибутов = 1.
- NumberValue - поле для хранения числовых и булевых значений атрибутов. Для типов атрибутов = 2.
- DateValue - поле для хранения фиксированных значений времени. Для типов атрибутов = 3.
- CodeValue - поле для хранения ссылок на строки документов. Для типов атрибутов = 4.
- DocumentValue - поле для хранения ссылок на документы. Для типов атрибутов = 5.
Так как об индексах, ключах и связях я буду писать много позже - в этот раздел информацию, позволяющую ускорять поиск значений я добавлять не буду - всё должно быть поэтапно.
Таблицы для хранения ссылок между сущностями одного уровня DocumentLinks, LineLinks, ProductLinks и LineProductLinks
Сами таблицы максимально простые - есть идентификатор связи (Id) и идентификаторы родительской и дочерней сущностей. Связи можно реализовать не только 1 к 1, но и многие ко многим с помощью такого приёма. Приём этот известен даже начинающим разработчикам.
В таких таблицах интересно поле LinkType - оно содержит тип связи между сущностями одного уровня. Т.е. к примеру был у нас товар с типом "Целый Арбуз", после обработки которого на кухне (если автоматизируем процессы в ресторане) у нас появилось два товара с типом "Половина арбуза". Связь между дочерними и родительскими товарами будем хранить в этой таблице как 1 к 2, а тип связи, предположим будет = 1, что по справочным значениям будет означать "Резка ножом" или что-то подобное.
Итоги
В общем и целом указанная структура таблиц не является истиной в последней инстанции и в случае необходимости может быть доработана различными способами.
Связь между сущностями может быть представлена различными способами (как напрямую через сущности одного уровня, так и через ссылочные атрибуты). Этот вопрос оговаривается при проектировании автоматизации процесса, который в данный момент у вас в работе.
Выделение конкретных типов сущностей позволяет в будущем увеличить скорость возврата данных, однако, не стоит выделять множество сущностей на первых этапах. Если имеется уже конкретный перечень сущностей, которые необходимо выделить и их количество велико - не стоит прибегать в таком случае к описываемому приёму - это только усложнит разработку, развитие и поддержку вашего учёта. Посмотрите в сторону стандартных подходов - там и читаемость вашего кода будет гораздо выше и скорость возврата данных и в принципе новые разработчики будут быстрее и легче входить в вашу разработку, потому что использовать EAV в действующих системах мало кто решается из-за серьёзных недостатков, описанных мною ранее.
Предыдущая статья | Следующая статья