Ответ 1
@Bill Karwin описывает три модели наследования в своей книге "Антипаттерны SQL ", предлагая решения для антипаттерна SQL Entity-Attribute-Value. Это краткий обзор:
Наследование в одной таблице (или Наследование таблиц в иерархии):
Использование одной таблицы, как в первом варианте, вероятно, самый простой дизайн. Как вы упомянули, многим атрибутам, относящимся к подтипу, должно быть присвоено значение NULL
в строках, где эти атрибуты не применяются. В этой модели у вас будет одна таблица политик, которая будет выглядеть примерно так:
+------+---------------------+----------+----------------+------------------+
| id | date_issued | type | vehicle_reg_no | property_address |
+------+---------------------+----------+----------------+------------------+
| 1 | 2010-08-20 12:00:00 | MOTOR | 01-A-04004 | NULL |
| 2 | 2010-08-20 13:00:00 | MOTOR | 02-B-01010 | NULL |
| 3 | 2010-08-20 14:00:00 | PROPERTY | NULL | Oxford Street |
| 4 | 2010-08-20 15:00:00 | MOTOR | 03-C-02020 | NULL |
+------+---------------------+----------+----------------+------------------+
\------ COMMON FIELDS -------/ \----- SUBTYPE SPECIFIC FIELDS -----/
Простота дизайна - это плюс, но основные проблемы с этим подходом заключаются в следующем:
Когда дело доходит до добавления новых подтипов, вам придется изменить таблицу, чтобы разместить атрибуты, которые описывают эти новые объекты. Это может быстро стать проблематичным, если у вас много подтипов или вы планируете регулярно добавлять подтипы.
База данных не сможет принудительно установить, какие атрибуты применяются, а какие нет, поскольку нет метаданных, которые бы определяли, какие атрибуты принадлежат каким подтипам.
Вы также не можете применить
NOT NULL
к атрибутам подтипа, которые должны быть обязательными. Вам придется справиться с этим в вашем приложении, что в целом не идеально.
Наследование бетонного стола:
Другой подход к решению проблемы наследования - создать новую таблицу для каждого подтипа, повторяя все общие атрибуты в каждой таблице. Например:
--// Table: policies_motor
+------+---------------------+----------------+
| id | date_issued | vehicle_reg_no |
+------+---------------------+----------------+
| 1 | 2010-08-20 12:00:00 | 01-A-04004 |
| 2 | 2010-08-20 13:00:00 | 02-B-01010 |
| 3 | 2010-08-20 15:00:00 | 03-C-02020 |
+------+---------------------+----------------+
--// Table: policies_property
+------+---------------------+------------------+
| id | date_issued | property_address |
+------+---------------------+------------------+
| 1 | 2010-08-20 14:00:00 | Oxford Street |
+------+---------------------+------------------+
Этот дизайн в основном решит проблемы, определенные для метода с одной таблицей:
Обязательные атрибуты теперь могут быть применены с помощью
NOT NULL
.Добавление нового подтипа требует добавления новой таблицы вместо добавления столбцов к существующей.
Также нет риска, что для определенного подтипа будет установлен неподходящий атрибут, такой как поле
vehicle_reg_no
для политики свойств.Нет необходимости в атрибуте
type
, как в методе с одной таблицей. Тип теперь определяется метаданными: именем таблицы.
Однако эта модель также имеет ряд недостатков:
Общие атрибуты смешиваются с определенными атрибутами подтипа, и нет простого способа их идентифицировать. База данных тоже не будет знать.
При определении таблиц вам придется повторять общие атрибуты для каждой таблицы подтипов. Это определенно не СУХОЙ.
Поиск всех политик вне зависимости от подтипа становится трудным, и для этого потребуется куча
UNION
.
Вот как вы должны были бы запрашивать все политики независимо от типа:
SELECT date_issued, other_common_fields, 'MOTOR' AS type
FROM policies_motor
UNION ALL
SELECT date_issued, other_common_fields, 'PROPERTY' AS type
FROM policies_property;
Обратите внимание, что добавление новых подтипов потребовало бы модификации вышеуказанного запроса с дополнительным UNION ALL
для каждого подтипа. Это может легко привести к ошибкам в вашем приложении, если эта операция будет забыта.
Наследование таблиц классов (aka Table Inheritance):
Это решение, которое @David упоминает в другом ответе. Вы создаете одну таблицу для вашего базового класса, которая включает в себя все общие атрибуты. Затем вы должны создать конкретные таблицы для каждого подтипа, первичный ключ которого также служит внешним ключом для базовой таблицы. Пример:
CREATE TABLE policies (
policy_id int,
date_issued datetime,
-- // other common attributes ...
);
CREATE TABLE policy_motor (
policy_id int,
vehicle_reg_no varchar(20),
-- // other attributes specific to motor insurance ...
FOREIGN KEY (policy_id) REFERENCES policies (policy_id)
);
CREATE TABLE policy_property (
policy_id int,
property_address varchar(20),
-- // other attributes specific to property insurance ...
FOREIGN KEY (policy_id) REFERENCES policies (policy_id)
);
Это решение решает проблемы, выявленные в двух других проектах:
Обязательные атрибуты могут быть применены с помощью
NOT NULL
.Добавление нового подтипа требует добавления новой таблицы вместо добавления столбцов к существующей.
Нет риска, что для определенного подтипа установлен неподходящий атрибут.
Нет необходимости в атрибуте
type
.Теперь общие атрибуты больше не смешиваются с определенными атрибутами подтипа.
Мы можем остаться сухими, наконец. При создании таблиц нет необходимости повторять общие атрибуты для каждой таблицы подтипов.
Управление автоматическим приращением
id
для политик становится проще, поскольку это может быть обработано базовой таблицей, а не каждой таблицей подтипов, генерирующей их независимо.Поиск всех политик независимо от подтипа теперь становится очень простым: не нужно
UNION
- простоSELECT * FROM policies
.
Я считаю, что подход с таблицами классов является наиболее подходящим в большинстве ситуаций.
Названия этих трех моделей взяты из Мартина Фаулера книги Шаблоны архитектуры корпоративных приложений.