Как вы можете представить наследование в базе данных? - PullRequest
194 голосов
/ 27 августа 2010

Я думаю о том, как представить сложную структуру в базе данных SQL Server.

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

Это легко реализовать в C # и т. Д., Так как вы можете создать Политику с набором Разделов, где Раздел наследуется, как требуется для различных типов покрытия. Однако, реляционные базы данных, кажется, не позволяют это легко.

Я вижу, что есть два основных варианта:

  1. Создайте таблицу политик, затем таблицу разделов со всеми необходимыми полями для всех возможных вариантов, большинство из которых будут нулевыми.

  2. Создайте таблицу правил и множество таблиц секций, по одной для каждого вида покрытия.

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

Какова лучшая практика для этого сценария?

Ответы [ 7 ]

381 голосов
/ 27 августа 2010

@ Билл Карвин описывает три модели наследования в своей книге SQL Antipatterns , предлагая решения для SQL Entity-Attribute-Value antipattern. Это краткий обзор:

Наследование в одной таблице (он же таблица на иерархическое наследование):

Использование одной таблицы, как в вашем первом варианте, вероятно, самый простой дизайн. Как вы упомянули, многим атрибутам, относящимся к подтипу, нужно будет присвоить значение 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 не требуется, как в методе с одной таблицей. Тип теперь определяется метаданными: имя таблицы.

Однако эта модель также имеет несколько недостатков:

  • Общие атрибуты смешиваются с атрибутами, относящимися к подтипу, и простого способа их идентификации нет. База данных также не будет знать.

  • При определении таблиц вам придется повторять общие атрибуты для каждой таблицы подтипов. Это точно не DRY .

  • Поиск всех политик независимо от подтипа становится трудным, и для этого потребуется куча 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 для каждого подтипа. Это может легко привести к ошибкам в вашем приложении, если эта операция будет забыта.

Наследование таблиц классов (или Наследование таблиц типов):

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

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 s - просто SELECT * FROM policies.

Я считаю подход таблицы классов наиболее подходящим в большинстве ситуаций.


Названия этих трех моделей взяты из Книги Мартина Фаулера Шаблоны архитектуры корпоративных приложений .

12 голосов
/ 27 августа 2010

Третий вариант - создать таблицу «Policy», а затем таблицу «SectionsMain», в которой хранятся все поля, общие для типов разделов. Затем создайте другие таблицы для каждого типа раздела, которые содержат только поля, которые не являются общими.

Выбор наилучшего зависит в основном от того, сколько у вас полей и как вы хотите написать свой SQL. Они все будут работать. Если у вас есть только несколько полей, то я бы, вероятно, пошел с # 1. С «множеством» полей я бы склонялся к # 2 или # 3.

9 голосов
/ 27 августа 2010

С учетом предоставленной информации я бы смоделировал базу данных следующим образом:

POLICIES

  • POLICY_ID (первичный ключ)

LIABILITIES

  • LIABILITY_ID (первичный ключ)
  • POLICY_ID (внешний ключ)

СВОЙСТВА

  • PROPERTY_ID (первичный ключ)
  • POLICY_ID (внешний ключ)

... и т. Д., Поскольку я ожидаю, что с каждым разделом политики будут связаны различные атрибуты.В противном случае может быть одна таблица SECTIONS, и в дополнение к policy_id будет section_type_code ...

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

Я не понимаю, что вас не устраивает в этом подходе - это то, как вы храните данные, сохраняя ссылочную целостность и не дублируя данные.Термин «нормализован» ...

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

6 голосов
/ 04 декабря 2013

Другой способ сделать это - использовать компонент INHERITS. Например:

CREATE TABLE person (
    id int ,
    name varchar(20),
    CONSTRAINT pessoa_pkey PRIMARY KEY (id)
);

CREATE TABLE natural_person (
    social_security_number varchar(11),
    CONSTRAINT pessoaf_pkey PRIMARY KEY (id)
) INHERITS (person);


CREATE TABLE juridical_person (
    tin_number varchar(14),
    CONSTRAINT pessoaj_pkey PRIMARY KEY (id)
) INHERITS (person);

Таким образом, можно определить наследование между таблицами.

2 голосов
/ 01 сентября 2017

Кроме того, в решении Daniel Vassallo, если вы используете SQL Server 2016, есть еще одно решение, которое я использовал в некоторых случаях без значительной потери производительности.

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

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

0 голосов
/ 27 августа 2010

Я склоняюсь к методу № 1 (унифицированная таблица разделов), чтобы эффективно извлекать целые политики со всеми их разделами (что, как я полагаю, ваша система будет делать много).

Далее яНе знаю, какую версию SQL Server вы используете, но в 2008+ Разреженные столбцы помогают оптимизировать производительность в ситуациях, когда многие значения в столбце будут равны NULL.

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

0 голосов
/ 27 августа 2010
...