Как смоделировать тип объекта, который может иметь разные наборы атрибутов?

11

У меня возникли проблемы при воссоздании базы данных с отношением один-ко-многим (1: M) между пользователями и элементами .

Это довольно просто, да; тем не менее, каждый элемент относится к определенной категории (например, автомобиль , лодка или самолет ), и каждая категория имеет определенное количество атрибутов, например:

Car структура:

+----+--------------+--------------+
| PK | Attribute #1 | Attribute #2 |
+----+--------------+--------------+

Boat структура:

+----+--------------+--------------+--------------+
| PK | Attribute #1 | Attribute #2 | Attribute #3 |
+----+--------------+--------------+--------------+

Plane структура:

+----+--------------+--------------+--------------+--------------+
| PK | Attribute #1 | Attribute #2 | Attribute #3 | Attribute #4 |
+----+--------------+--------------+--------------+--------------+

Из-за такого разнообразия в количестве атрибутов (столбцов) я изначально думал, что было бы неплохо создать одну отдельную таблицу для каждой категории , поэтому я бы избегал нескольких NULL и, таким образом, лучше использовал индексирование.

Хотя поначалу это выглядело великолепно, я не смог найти способ создать отношения между Предметами и Категориями через базу данных, потому что, по крайней мере, в моем скромном опыте работы в качестве администратора базы данных, при создании внешних ключей я сообщаю явно базу данных имя таблицы и столбец.

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

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

Дополнительная информация

Вот мои ответы на комментарии MDCCL:

1. Сколько интересующих категорий товаров существует в вашем бизнесе, три (например, автомобили , лодки и самолеты ) или больше?

На самом деле все очень просто: всего пять категорий .

2. Будет ли один и тот же Предмет всегда принадлежать одному и тому же Пользователю (то есть после того, как данный Предмет был «назначен» определенному Пользователю, его нельзя изменить)?

Нет, они могут измениться. В вымышленном сценарии вопроса это будет похоже на то, как Пользователь А продает Элемент № 1 за Пользователя Б , поэтому право собственности должно быть отражено.

3. Существуют ли атрибуты, которые являются общими для некоторых или всех категорий ?

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

4. Есть ли вероятность того, что кардинальность отношений между Пользователем и Предметом будет «многие ко многим» (M: N) вместо «один ко многим» (1: M)? Например, в случае следующих бизнес-правил: A User owns zero-one-or-many ItemsиAn Item is owned by one-to-many Users

Нет, потому что Предметы описывают физический объект. У пользователей будет их виртуальная копия, каждая из которых идентифицируется уникальным GUID v4.

5. Относительно вашего следующего ответа на один из комментариев к вопросу:

«В вымышленном сценарии вопроса это будет похоже на то, как Пользователь А продает Элемент № 1 за Пользователя Б , поэтому право собственности должно быть отражено».

Похоже, вы планируете отслеживать эволюцию владения предметами , так сказать. Таким образом, какие атрибуты вы хотели бы хранить о таком явлении? Только модификация атрибута, который указывает конкретного пользователя, который является владельцем определенного элемента ?

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

user5613506
источник

Ответы:

18

Согласно вашему описанию рассматриваемой бизнес-среды, существует структура супертип-подтип, которая включает Item - супертип - и каждую из его категорий , т. Е. Автомобиль , катер и самолет (наряду с еще двумя, которые не были известны) - подтипы

Я опишу ниже метод, которым я буду следовать, чтобы управлять таким сценарием.

Бизнес правила

Чтобы начать разграничение соответствующей концептуальной схемы, некоторые из наиболее важных бизнес-правил, определенных до сих пор (ограничив анализ только тремя раскрытыми категориями , чтобы сделать вещи как можно более краткими), можно сформулировать следующим образом:

  • Пользователь имеет нулевой один или много- товары
  • Пункт принадлежит точно-один пользователь в определенный момент времени
  • Пункт может принадлежать один-ко-многим пользователям в различных точках во времени
  • Пункт классифицируется ровно одной категории
  • Пункт есть, во все времена,
    • либо автомобиль
    • или лодка
    • или самолет

Иллюстративная схема IDEF1X

На рисунке 1 показана диаграмма IDEF1X 1 , которую я создал для группировки предыдущих формулировок вместе с другими бизнес-правилами, которые кажутся уместными:

Рисунок 1 - Структура подтипа подтипов и категорий

Supertype

С одной стороны, Item , супертип, представляет свойства или атрибуты, которые являются общими для всех категорий , т. Е.

  • CategoryCode -specified в качестве внешнего ключа (FK) , что ссылки Category.CategoryCode и функции в качестве подтипа дискриминатора , то есть, это указывает на точную категорию подтипа , с которой данный Пункт должен быть визит-,
  • OwnerId - выделяется как FK, который указывает на User.UserId , но я назначил ему имя роли 2 , чтобы точнее отразить его особые последствия -,
  • Фу ,
  • Бар ,
  • Баз и
  • CreatedDateTime .

Подтипы

С другой стороны, свойства ‡, которые относятся к каждой конкретной категории , т. Е.

  • Qux и Corge ;
  • Grault , Garply и Plugh ;
  • Xyzzy , Thud , Wibble и Flob ;

показаны в соответствующем поле подтипа.

Идентификаторы

Затем первичный ключ Item.ItemId (PK) перенес 3 в подтипы с разными именами ролей, т.е.

  • CarId ,
  • BoatId и
  • PlaneId .

Взаимоисключающие ассоциации

Как изображено, существует связь или взаимосвязь кардинальности один к одному (1: 1) между (a) каждым вхождением супертипа и (b) его дополнительным экземпляром подтипа.

Эксклюзивный подтип символ изображает тот факт , что подтипы являются взаимоисключающими, т.е. конкретный Item явление может быть дополнен только одним экземпляром подтипа: либо один автомобиль , или один Plane , или одна лодка (никогда на два или более).

, Я использовал классические имена местозаполнителей для обозначения некоторых свойств типа сущности, поскольку их фактические наименования не были указаны в вопросе.

Описательное расположение логического уровня

Следовательно, чтобы обсудить объяснительный логический дизайн, я вывел следующие операторы SQL-DDL на основе отображенной и описанной выше диаграммы IDEF1X:

-- You should determine which are the most fitting 
-- data types and sizes for all your table columns 
-- depending on your business context characteristics.

-- Also, you should make accurate tests to define the 
-- most convenient INDEX strategies based on the exact 
-- data manipulation tendencies of your business context.

-- As one would expect, you are free to utilize 
-- your preferred (or required) naming conventions. 

CREATE TABLE UserProfile (
    UserId          INT      NOT NULL,
    FirstName       CHAR(30) NOT NULL,
    LastName        CHAR(30) NOT NULL,
    BirthDate       DATE     NOT NULL,
    GenderCode      CHAR(3)  NOT NULL,
    Username        CHAR(20) NOT NULL,
    CreatedDateTime DATETIME NOT NULL,
    --
    CONSTRAINT UserProfile_PK  PRIMARY KEY (UserId),
    CONSTRAINT UserProfile_AK1 UNIQUE ( -- Composite ALTERNATE KEY.
        FirstName,
        LastName,
        GenderCode,
        BirthDate
    ),
    CONSTRAINT UserProfile_AK2 UNIQUE (Username) -- ALTERNATE KEY.
);

CREATE TABLE Category (
    CategoryCode     CHAR(1)  NOT NULL, -- Meant to contain meaningful, short and stable values, e.g.; 'C' for 'Car'; 'B' for 'Boat'; 'P' for 'Plane'.
    Name             CHAR(30) NOT NULL,
    --
    CONSTRAINT Category_PK PRIMARY KEY (CategoryCode),
    CONSTRAINT Category_AK UNIQUE      (Name) -- ALTERNATE KEY.
);

CREATE TABLE Item ( -- Stands for the supertype.
    ItemId           INT      NOT NULL,
    OwnerId          INT      NOT NULL,
    CategoryCode     CHAR(1)  NOT NULL, -- Denotes the subtype discriminator.
    Foo              CHAR(30) NOT NULL,
    Bar              CHAR(30) NOT NULL,
    Baz              CHAR(30) NOT NULL,  
    CreatedDateTime  DATETIME NOT NULL,
    --
    CONSTRAINT Item_PK             PRIMARY KEY (ItemId),
    CONSTRAINT Item_to_Category_FK FOREIGN KEY (CategoryCode)
        REFERENCES Category    (CategoryCode),
    CONSTRAINT Item_to_User_FK     FOREIGN KEY (OwnerId)
        REFERENCES UserProfile (UserId)  
);

CREATE TABLE Car ( -- Represents one of the subtypes.
    CarId INT      NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
    Qux   CHAR(30) NOT NULL,
    Corge CHAR(30) NOT NULL,   
    --
    CONSTRAINT Car_PK         PRIMARY KEY (CarId),
    CONSTRAINT Car_to_Item_FK FOREIGN KEY (CarId)
        REFERENCES Item (ItemId)  
);

CREATE TABLE Boat ( -- Stands for one of the subtypes.
    BoatId INT      NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
    Grault CHAR(30) NOT NULL,
    Garply CHAR(30) NOT NULL,   
    Plugh  CHAR(30) NOT NULL, 
    --
    CONSTRAINT Boat_PK         PRIMARY KEY (BoatId),
    CONSTRAINT Boat_to_Item_FK FOREIGN KEY (BoatId)
        REFERENCES Item (ItemId)  
);

CREATE TABLE Plane ( -- Denotes one of the subtypes.
    PlaneId INT      NOT NULL, -- Must be constrained as (a) the PRIMARY KEY and (b) a FOREIGN KEY.
    Xyzzy   CHAR(30) NOT NULL,
    Thud    CHAR(30) NOT NULL,  
    Wibble  CHAR(30) NOT NULL,
    Flob    CHAR(30) NOT NULL,  
    --
    CONSTRAINT Plane_PK         PRIMARY KEY (PlaneId),
    CONSTRAINT Plane_to_Item_PK FOREIGN KEY (PlaneId)
        REFERENCES Item (ItemId)  
);

Как продемонстрировано, тип сверхчувствительности и каждый из типов подчиненности представлены соответствующей базовой таблицей.

Столбцы CarId, BoatIdи PlaneId, сдерживаются как ПКС соответствующих таблиц, помощь в представлении концептуального уровня один-к-одному ассоциации путем ограничения FK § , что указывает на ItemIdколонку, которая стесненного как PK в Itemтаблице. Это означает, что в фактической «паре» строки как супертипа, так и подтипа идентифицируются одним и тем же значением PK; таким образом, более чем уместно упомянуть, что

  • (а) прикрепление дополнительной колонки , чтобы держать под контролем система-суррогатных значения к (б) таблицам , стоящие для подтипов (с) полностью излишним .

§ Чтобы предотвратить проблемы и ошибки, касающиеся (в частности, FOREIGN) определений ограничений KEY - ситуации, на которую вы ссылались в комментариях, - очень важно учитывать зависимость существования, которая имеет место между различными таблицами, как показано в примере порядок объявления таблиц в описательной структуре DDL, который я также предоставил в этой скрипте SQL .

Например, добавление дополнительного столбца со свойством AUTO_INCREMENT к таблице базы данных, построенной на MySQL.

Целостность и последовательность соображений

Важно отметить, что в вашей бизнес-среде вы должны (1) убедиться, что каждая строка «супертипа» всегда дополняется соответствующим аналогом «подтипа», и, в свою очередь, (2) гарантировать, что Строка «подтип» совместима со значением, содержащимся в столбце «дискриминатор» строки «супертип».

Было бы очень элегантно применять такие обстоятельства декларативным образом, но, к сожалению, ни одна из основных платформ SQL, насколько я знаю, не предоставила надлежащих механизмов для этого. Поэтому, прибегая к процедурному коду в рамках ACID TRANSACTIONS, довольно удобно, чтобы эти условия всегда выполнялись в вашей базе данных. Другим вариантом будет использование TRIGGERS, но они, как говорится, делают вещи неопрятными.

Объявление полезных просмотров

Имея логический дизайн, подобный описанному выше, было бы очень полезно создать одно или несколько представлений, то есть производных таблиц, которые содержат столбцы, которые принадлежат двум или более соответствующим базовым таблицам. Таким образом, вы можете, например, ВЫБРАТЬ напрямую из этих представлений без необходимости записывать все СОЕДИНЕНИЯ каждый раз, когда вам нужно получить «объединенную» информацию.

Пример данных

В этом отношении предположим, что базовые таблицы «заполнены» приведенными ниже примерами данных:

--

INSERT INTO UserProfile 
    (UserId, FirstName, LastName, BirthDate, GenderCode, Username, CreatedDateTime)
VALUES
    (1, 'Edgar', 'Codd', '1923-08-19', 'M', 'ted.codd', CURDATE()),
    (2, 'Michelangelo', 'Buonarroti', '1475-03-06', 'M', 'michelangelo', CURDATE()),
    (3, 'Diego', 'Velázquez', '1599-06-06', 'M', 'd.velazquez', CURDATE());

INSERT INTO Category 
    (CategoryCode, Name)
VALUES
    ('C', 'Car'), ('B', 'Boat'), ('P', 'Plane');

-- 1. ‘Full’ Car INSERTion

-- 1.1 
INSERT INTO Item
    (ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
    (1, 1, 'C', 'This datum', 'That datum', 'Other datum', CURDATE());

 -- 1.2
INSERT INTO Car
    (CarId, Qux, Corge)
VALUES
    (1, 'Fantastic Car', 'Powerful engine pre-update!');

-- 2. ‘Full’ Boat INSERTion

-- 2.1
INSERT INTO Item
  (ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
  (2, 2, 'B', 'This datum', 'That datum', 'Other datum', CURDATE());

-- 2.2
INSERT INTO Boat
    (BoatId, Grault, Garply, Plugh)
VALUES
    (2, 'Excellent boat', 'Use it to sail', 'Everyday!');

-- 3 ‘Full’ Plane INSERTion

-- 3.1
INSERT INTO Item
  (ItemId, OwnerId, CategoryCode, Foo, Bar, Baz, CreatedDateTime)
VALUES
  (3, 3, 'P', 'This datum', 'That datum', 'Other datum', CURDATE());

-- 3.2
INSERT INTO Plane
    (PlaneId, Xyzzy, Thud, Wibble, Flob)
VALUES
    (3, 'Extraordinary plane', 'Traverses the sky', 'Free', 'Like a bird!');

--

Тогда предпочтительным видом является тот, который собирает столбцы из Item, Carи UserProfile:

--

CREATE VIEW CarAndOwner AS
    SELECT C.CarId,
           I.Foo,
           I.Bar,
           I.Baz,
           C.Qux,
           C.Corge,           
           U.FirstName AS OwnerFirstName,
           U.LastName  AS OwnerLastName
        FROM Item I
        JOIN Car C
          ON C.CarId = I.ItemId
        JOIN UserProfile U
          ON U.UserId = I.OwnerId;

--

Естественно, можно использовать аналогичный подход, так что вы также можете ВЫБРАТЬ «полную» Boatи Planeинформацию прямо из одной таблицы (производной, в этих случаях).

После этого -Если вы не возражаете о наличии NULL знаков в результате sets- со следующим определением VIEW, вы можете, например, «собирать» столбцы из таблиц Item, Car, Boat, Planeи UserProfile:

--

CREATE VIEW FullItemAndOwner AS
    SELECT I.ItemId,
           I.Foo, -- Common to all Categories.
           I.Bar, -- Common to all Categories.
           I.Baz, -- Common to all Categories.
          IC.Name      AS Category,
           C.Qux,    -- Applies to Cars only.
           C.Corge,  -- Applies to Cars only.
           --
           B.Grault, -- Applies to Boats only.
           B.Garply, -- Applies to Boats only.
           B.Plugh,  -- Applies to Boats only.
           --
           P.Xyzzy,  -- Applies to Planes only.
           P.Thud,   -- Applies to Planes only.
           P.Wibble, -- Applies to Planes only.
           P.Flob,   -- Applies to Planes only.
           U.FirstName AS OwnerFirstName,
           U.LastName  AS OwnerLastName
        FROM Item I
        JOIN Category IC
          ON I.CategoryCode = IC.CategoryCode
   LEFT JOIN Car C
          ON C.CarId = I.ItemId
   LEFT JOIN Boat B
          ON B.BoatId = I.ItemId
   LEFT JOIN Plane P
          ON P.PlaneId = I.ItemId               
        JOIN UserProfile U
          ON U.UserId = I.OwnerId;

--

Код представленных здесь представлений является только иллюстративным. Конечно, выполнение некоторых тестовых упражнений и модификаций может помочь ускорить (физическое) выполнение запросов. Кроме того, вам может потребоваться удалить или добавить столбцы к указанным представлениям в соответствии с потребностями бизнеса.

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

Обработка данных: псевдонимы кода и столбцов прикладных программ

Использование кода прикладной программы (программ) (если вы это подразумеваете под «специфичным для сервера кодом») и псевдонимы столбцов - это другие важные моменты, которые вы затронули в следующих комментариях:

  • Мне удалось обойти проблему [JOIN] с серверным кодом, но я действительно не хочу этого делать - И добавление псевдонимов ко всем столбцам может быть «напряженным».

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

Уместно указать, что, хотя использование прикладного программного кода является очень подходящим ресурсом для обработки особенностей представления (или графических) наборов результатов, исключение извлечения данных построчно является первостепенным для предотвращения проблем со скоростью выполнения. Задача должна состоять в том, чтобы «извлечь» соответствующие наборы данных в совокупности с помощью надежных инструментов манипулирования данными, предоставляемых (точно) механизмом установки платформы SQL, чтобы вы могли оптимизировать поведение вашей системы.

Кроме того, использование псевдонимов для переименования одного или нескольких столбцов в определенной области может показаться напряженным, но лично я считаю такой ресурс очень мощным инструментом, который помогает (i) контекстуализировать и (ii) устранять неоднозначность значения и намерения, приписываемых колонны; следовательно, это аспект, который следует тщательно обдумать в отношении манипулирования интересующими данными.

Подобные сценарии

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

Я также предложил решение для бизнес-среды, включающее кластер супертип-подтип, где подтипы не являются взаимоисключающими в этом (более новом) ответе .


Сноски

1 Определение интеграции для информационного моделирования ( IDEF1X ) - это очень рекомендуемый метод моделирования данных, который был установлен в качестве стандарта в декабре 1993 года Национальным институтом стандартов и технологий США (NIST). Оно прочно основано на (а) некоторые из теоретических работавтором которого является единственным оригинатора в реляционной модели , т.е. д - р Ф. Кодда ; (б) взгляд на сущность-отношение , разработанный доктором П.П. Ченом ; а также о (c) методике проектирования логических баз данных, созданной Робертом Г. Брауном.

2 В IDEF1X имя роли - это отличительная метка, назначенная свойству (или атрибуту) FK, чтобы выразить значение, которое оно содержит в области видимости соответствующего типа объекта.

3 Стандарт IDEF1X определяет миграцию ключей как «процесс моделирования размещения первичного ключа родительского или общего объекта в его дочернем объекте или объекте категории в качестве внешнего ключа».

MDCCL
источник
1
Я не уверен, что понимаю ваш запрос, но, как показано на схеме DDL, Itemтаблица содержит CategoryCodeстолбец. Как упомянуто в разделе, озаглавленном «Соображения целостности и согласованности»:
MDCCL
1
Важно отметить, что в вашей бизнес-среде вы должны (1) убедиться, что каждая строка «супертипа» всегда дополняется соответствующим аналогом «подтипа», и, в свою очередь, (2) гарантировать, что Строка «подтип» совместима со значением, содержащимся в столбце «дискриминатор» строки «супертип».
MDCCL
1
Было бы очень элегантно применять такие обстоятельства декларативным образом, но, к сожалению, ни одна из основных платформ SQL, насколько я знаю, не предоставила надлежащих механизмов для этого. Поэтому, прибегая к процедурному коду в рамках ACID TRANSACTIONS, довольно удобно, чтобы эти условия всегда выполнялись в вашей базе данных. Другим вариантом будет использование TRIGGERS, но они, как говорится, делают вещи неопрятными.
MDCCL
1
Суть дела в том, что ни одна реализация SQL (включая диалект MySQL) не обеспечивает надлежащей поддержки ASSERTIONS, мощных и элегантных декларативных инструментов, которые помогут избежать обращения к процедурным подходам (TRANSACTIONS или TRIGGERS), или обходной работы избыточным способом, таким как например, без необходимости повторять CategoryColumnтаблицы, обозначающие подтипы (со всеми последствиями для логических [например, аномалий модификации] и физических уровней абстракции [например, дополнительные индексы, большие структуры и т. д.]).
MDCCL
2
До тех пор, пока кто-либо из поставщиков / разработчиков систем управления базами данных не предоставит ASSERTIONS - подходящий инструмент для этой задачи - я предпочитаю (a) процедурные подходы - будь то TRANSACTIONS или TRIGGERS - более (b) избыточный курс действий, хотя (b) Это возможность, которую я лично не рекомендую. Конечно, администратор баз данных должен тщательно управлять разрешениями относительно допустимых операций манипулирования данными, которые могут быть выполнены в релевантной базе данных, что, безусловно, очень помогает в поддержании целостности данных.
MDCCL
0

Позволяет назвать основной стол Продукты. Здесь размещены общие атрибуты. Тогда давайте скажем, что у нас есть Стол для автомобилей, Стол для самолетов и Стол для лодок. Эти три таблицы будут иметь ключ ProductID с ограничением FK в строке идентификатора таблицы Product. Если вы хотите их всех - присоединяйтесь к ним. Если вам нужны только автомобили, присоединяйтесь слева к Cars with Products (или объединяйте товары и автомобили справа, но я предпочитаю всегда использовать левосторонние объединения).

Это называется иерархической моделью данных. Для небольшого количества вложенных таблиц это может иметь смысл в длинной таблице (миллионы продуктов).

neManiac
источник
А потом я присоединяюсь к пользователям с продуктами?
user5613506
1
Как правило, вам не нужна информация о пользователях, когда вы возвращаете список продуктов в ваш интерфейс, вам нужна информация о продукте. Не имеет смысла присоединяться к Пользователям и Продуктам и возвращать идентичные пользовательские данные для каждой возвращенной строки продукта. Итак, сначала вы фильтруете по типу продукта, объединяя таблицу продуктов и соответствующую вложенную таблицу (Car, Boat ...), а затем вы фильтруете по User, используя предложение WHERE. Обычно вы хотите иметь OwnerID в таблице «Продукты» (FK в столбце «ID» таблицы «Пользователь»). Таким образом, вы бы добавили WHERE Owner = [Request.User].
neManiac