Como modelar um tipo de entidade que pode ter diferentes conjuntos de atributos?


11

Estou com problemas para recriar um banco de dados com uma relação de um para muitos (1: M) entre usuários e itens .

Isso é bem direto, sim; no entanto, cada item pertence a uma determinada categoria (por exemplo, um carro , um barco ou um avião ) e cada categoria possui um número específico de atributos, por exemplo:

Car estrutura:

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

Boat estrutura:

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

Plane estrutura:

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

Devido a essa diversidade no número de atributos (colunas), inicialmente pensei que seria uma boa ideia criar uma tabela separada para cada categoria , para evitar vários NULLs e, assim, fazer um melhor uso da indexação.

Embora parecesse ótimo no começo, não consegui encontrar uma maneira de criar o relacionamento entre os Itens e as Categorias por meio do banco de dados porque, pelo menos em minha modesta experiência como administrador de banco de dados, ao criar Chaves Estrangeiras, informo explicitamente um banco de dados o nome e a coluna da tabela.

No final, eu gostaria de uma estrutura sólida para armazenar todos os dados, tendo todos os meios para listar todos os atributos de todos os itens que um usuário pode ter com uma consulta.

Eu poderia codificar consultas dinâmicas com o idioma do servidor , mas acho que isso está errado e não é o ideal.

Informação adicional

Estas são minhas respostas aos comentários do MDCCL:

1. Quantas categorias de itens de interesse existem no seu contexto comercial, três (ou seja, carros , barcos e aviões ) ou mais?

De fato, é muito simples: existem apenas cinco categorias no total.

2. O mesmo item sempre pertencerá ao mesmo usuário (ou seja, depois que um determinado item for "atribuído" a um determinado usuário, ele não poderá ser alterado)?

Não, eles podem mudar. No cenário fictício da pergunta, seria como o usuário A vender o item nº 1 para o usuário B , portanto, a propriedade deve ser refletida.

3. Existem atributos compartilhados por algumas ou todas as categorias ?

Não compartilhado, mas, pela memória, posso dizer que pelo menos três atributos estão presentes em todas as categorias .

4. Existe a chance de a cardinalidade do relacionamento entre Usuário e Item ser muitos-para-muitos (M: N) em vez de um-para-muitos (1: M)? Por exemplo, no caso das seguintes regras de negócios: A User owns zero-one-or-many ItemseAn Item is owned by one-to-many Users

Não, porque os itens descreveriam um objeto físico. Os usuários terão uma cópia virtual deles, cada um identificado por um GUID v4 exclusivo

5. Sobre sua resposta a um dos comentários da pergunta:

"No cenário fictício da pergunta, seria como se o usuário A vendesse o item nº 1 para o usuário B , portanto a propriedade deve ser refletida."

Parece que você está planejando acompanhar a evolução da propriedade do item , por assim dizer. Dessa forma, quais atributos você gostaria de armazenar sobre esse fenômeno? Somente a modificação do atributo que indica o Usuário específico que é o Proprietário de um Item específico ?

Não, na verdade não. A propriedade pode mudar, mas não preciso acompanhar o proprietário anterior .

Respostas:


18

De acordo com sua descrição do ambiente de negócios em consideração, existe uma estrutura de supertipo-subtipo que engloba Item - o supertipo - e cada uma de suas categorias , ou seja, carro , barco e avião (juntamente com outras duas que não foram divulgadas) - os subtipos—.

Detalharei abaixo o método que seguiria para gerenciar esse cenário.

Regras do negócio

Para começar a delinear o esquema conceitual relevante , algumas das regras comerciais mais importantes determinadas até o momento (restringindo a análise apenas às três categorias divulgadas , para manter as coisas o mais breve possível) podem ser formuladas da seguinte maneira:

  • Um usuário possui zero-um-ou-muitos itens
  • Um item pertence a exatamente um usuário em um instante específico
  • Um item pode ser de propriedade de um para muitos usuários em momentos distintos
  • Um item é classificado por exatamente uma categoria
  • Um item é, sempre,
    • um carro
    • ou um barco
    • ou um avião

Diagrama ilustrativo do IDEF1X

A Figura 1 exibe um diagrama IDEF1X 1 que eu criei para agrupar as formulações anteriores, juntamente com outras regras de negócios que parecem pertinentes:

Figura 1 - Estrutura de supertipo-subtipo de itens e categorias

Supertipo

Por um lado, Item , o supertipo, apresenta as propriedades ou atributos comuns a todas as categorias , ou seja,

  • CategoryCode - especificado como uma FOREIGN KEY (FK) que faz referência a Category.CategoryCode e funciona como um discriminador de subtipo , ou seja, indica a categoria exata do subtipo ao qual um determinado item deve ser conectado -,
  • OwnerId - distinto como um FK que aponta para User.UserId , mas atribuí a ele um nome de função 2 para refletir suas implicações especiais com mais precisão -,
  • Foo ,
  • Bar ,
  • Baz e
  • CreatedDateTime .

Subtipos

Por outro lado, as propriedades que pertencem a cada categoria em particular , ou seja,

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

são mostrados na caixa de subtipo correspondente.

Identificadores

Em seguida, a Item.ItemId PRIMARY KEY (PK) migrou 3 para os subtipos com diferentes nomes de função, ou seja,

  • CarId ,
  • BoatId e
  • PlaneId .

Associações mutuamente exclusivas

Como representado, há uma associação ou relação de cardinalidade um a um (1: 1) entre (a) cada ocorrência de supertipo e (b) sua instância complementar de subtipo.

O símbolo do subtipo exclusivo retrata o fato de que os subtipos são mutuamente exclusivos, ou seja, uma ocorrência concreta de item pode ser complementada apenas por uma única instância de subtipo: um carro , um avião ou um barco (nunca por dois ou mais).

, Empreguei nomes de espaços reservados clássicos para dar direito a algumas das propriedades do tipo de entidade, pois suas denominações reais não foram fornecidas na pergunta.

Layout de nível lógico expositivo

Conseqüentemente, para discutir um design lógico expositivo, derivamos as seguintes instruções SQL-DDL com base no diagrama IDEF1X exibido e descrito acima:

-- 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)  
);

Como demonstrado, o tipo de super-entidade e cada um dos tipos de sub-entidade são representados pela tabela base correspondente .

As colunas CarId, BoatIde PlaneId, restritas como as PKs das tabelas apropriadas, ajuda na representando o nível de conceptual associação de um-para-um a título de limitações FK § que aponte para a ItemIdcoluna, a qual é constrangida como o PK da Itemtabela. Isso significa que, em um "par" real, as linhas de supertipo e subtipo são identificadas pelo mesmo valor de PK; portanto, é mais do que oportuno mencionar que

  • (a) anexar uma coluna extra para armazenar valores substitutos controlados pelo sistema para (b) as tabelas que representam os subtipos são (c) inteiramente supérfluas .

§ Para evitar problemas e erros relacionados às definições de restrição KEY (particularmente ESTRANGEIRAS) - situação a que você se refere nos comentários -, é muito importante levar em conta a dependência da existência que ocorre entre as diferentes tabelas em mãos, como exemplificado em a ordem de declaração das tabelas na estrutura DDL expositiva, que também forneci neste SQL Fiddle .

Eg, anexando uma coluna adicional com o AUTO_INCREMENT propriedade de uma tabela de uma base de dados construída em MySQL.

Considerações sobre integridade e consistência

É essencial ressaltar que, em seu ambiente de negócios, você deve (1) garantir que cada linha do "supertipo" seja sempre complementada pelo correspondente correspondente do "subtipo" e, por sua vez, (2) garantir que A linha "subtipo" é compatível com o valor contido na coluna "discriminador" da linha "supertipo".

Seria muito elegante aplicar essas circunstâncias de maneira declarativa , mas, infelizmente, nenhuma das principais plataformas SQL forneceu os mecanismos adequados para isso, tanto quanto eu sei. Portanto, recorrer ao código de procedimento dentro de ACID TRANSACTIONS é bastante conveniente, para que essas condições sejam sempre atendidas no banco de dados. Outra opção seria empregar TRIGGERS, mas eles tendem a tornar as coisas desarrumadas, por assim dizer.

Declarando visualizações úteis

Tendo um design lógico como o explicado acima, seria muito prático criar uma ou mais visualizações, isto é, tabelas derivadas que compreendem colunas que pertencem a duas ou mais das tabelas base relevantes . Dessa maneira, você pode, por exemplo, SELECIONAR diretamente a partir dessas visualizações sem precisar escrever todos os JOINs toda vez que precisar recuperar informações "combinadas".

Dados de amostra

A esse respeito, digamos que as tabelas base sejam "preenchidas" com os dados de amostra mostrados abaixo:

--

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!');

--

Em seguida, uma vista vantajoso é um que reúne de colunas Item, Care 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;

--

Naturalmente, uma abordagem semelhante pode ser seguida para que você possa SELECIONAR as informações "completas" Boate Planediretamente de uma única tabela (uma derivada, nesses casos).

Depois disso -Se você não se importa sobre a presença de marcas NULL no resultado conjuntos- com a seguinte definição de visualização, você pode, por exemplo, “a cobrar” colunas das tabelas Item, Car, Boat, Planee 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;

--

O código das visualizações aqui mostradas é apenas ilustrativo. Obviamente, fazer alguns exercícios e modificações de teste pode ajudar a acelerar a execução (física) das consultas em questão. Além disso, pode ser necessário remover ou adicionar colunas às visualizações, conforme as necessidades da empresa.

Os dados de amostra e todas as definições de visualização são incorporadas neste SQL Fiddle para que possam ser observadas "em ação".

Manipulação de dados: aliases de código e coluna de programa (s) de aplicativo

O uso do código do (s) aplicativo (s) (se é isso que você quer dizer com “código específico do servidor”) e os aliases da coluna são outros pontos significativos que você mencionou nos próximos comentários:

  • Eu consegui solucionar um problema [JOIN] com código específico do servidor, mas eu realmente não quero fazer isso - e - adicionar aliases a todas as colunas pode ser "estressante".

  • Muito bem explicado, muito obrigado. No entanto, como suspeitava, terei que manipular o conjunto de resultados ao listar todos os dados devido às semelhanças com algumas colunas, pois não quero usar vários aliases para manter a instrução mais limpa.

É oportuno indicar que, embora o código do programa de aplicação seja um recurso muito adequado para lidar com os recursos de apresentação (ou gráficos) dos conjuntos de resultados, é fundamental evitar a recuperação de dados linha por linha, para evitar problemas de velocidade de execução. O objetivo deve ser “buscar” os conjuntos de dados pertinentes in toto por meio dos robustos instrumentos de manipulação de dados fornecidos pelo mecanismo (precisamente) do conjunto da plataforma SQL, para que você possa otimizar o comportamento do seu sistema.

Além disso, a utilização de aliases para renomear uma ou mais colunas dentro de um certo escopo pode parecer estressante, mas, pessoalmente, vejo esse recurso como uma ferramenta muito poderosa que ajuda a (i) contextualizar e (ii) desambiguar o significado e a intenção atribuídos à colunas; portanto, esse é um aspecto que deve ser cuidadosamente ponderado com relação à manipulação dos dados de interesse.

Cenários semelhantes

Você também pode ajudar nesta série de postagens e neste grupo de postagens que contêm minha opinião sobre outros dois casos que incluem associações de supertipo-subtipo com subtipos mutuamente exclusivos.

Também propus uma solução para um ambiente de negócios que envolve um cluster de supertipos e subtipos, onde os subtipos não são mutuamente exclusivos nesta resposta (mais recente) .


Notas finais

1 Definição de Integração para Modelagem de Informações ( IDEF1X ) é uma técnica de modelagem de dados altamente recomendável que foi estabelecida como padrão em dezembro de 1993 pelo Instituto Nacional de Padrões e Tecnologia dos EUA (NIST). É solidamente baseado em (a) alguns dos trabalhos teóricos de autoria do único autordo modelo relacional , ou seja, Dr. EF Codd ; (b) a visão de entidade-relacionamento , desenvolvida pelo Dr. PP Chen ; e também (c) a Logical Database Design Technique, criada por Robert G. Brown.

2 No IDEF1X, um nome de função é um rótulo distinto atribuído a uma propriedade (ou atributo) de FK para expressar o significado que ela possui no escopo de seu respectivo tipo de entidade.

3 O padrão IDEF1X define migração de chave como “O processo de modelagem de colocar a chave primária de uma entidade pai ou genérica em seu filho ou entidade de categoria como uma chave estrangeira”.


1
Não sei se entendi sua pergunta, mas, como ilustrado no layout DDL, a Itemtabela inclui uma CategoryCodecoluna. Conforme mencionado na seção intitulada “Considerações sobre integridade e consistência”:
MDCCL

1
É essencial ressaltar que, em seu ambiente de negócios, você deve (1) garantir que cada linha do "supertipo" seja sempre complementada pelo correspondente correspondente do "subtipo" e, por sua vez, (2) garantir que A linha "subtipo" é compatível com o valor contido na coluna "discriminador" da linha "supertipo".
MDCCL

1
Seria muito elegante aplicar essas circunstâncias de maneira declarativa, mas, infelizmente, nenhuma das principais plataformas SQL forneceu os mecanismos adequados para isso, tanto quanto eu sei. Portanto, recorrer ao código de procedimento dentro de ACID TRANSACTIONS é bastante conveniente, para que essas condições sejam sempre atendidas em seu banco de dados. Outra opção seria empregar TRIGGERS, mas eles tendem a tornar as coisas desarrumadas, por assim dizer.
MDCCL

1
O cerne da questão é que nenhuma implementação SQL (incluindo o dialeto MySQL) fornece suporte adequado para ASSERTIONS, ferramentas declarativas poderosas e elegantes que ajudariam a evitar o recurso a abordagens procedimentais (TRANSACTIONS ou TRIGGERS), ou a trabalhar de maneira redundante como , por exemplo, repetindo desnecessariamente as CategoryColumntabelas que representam os subtipos (com todas as implicações nos níveis lógicos [por exemplo, anomalias de modificação] e físicos da abstração (por exemplo, índices extras, estruturas maiores etc.)).
MDCCL

2
Até que qualquer fornecedor / desenvolvedor de sistema de gerenciamento de banco de dados forneça ASSERTIONS - a ferramenta apropriada para esta tarefa -, prefiro (a) as abordagens processuais - sejam TRANSACTIONS ou TRIGGERS - sobre (b) o curso de ação redundante, embora (b) é uma possibilidade - que eu pessoalmente não recomendo -. Obviamente, o DBA precisa gerenciar cuidadosamente as permissões referentes às operações válidas de manipulação de dados que podem ser executadas no banco de dados relevante, o que decididamente ajuda bastante na manutenção da integridade dos dados.
MDCCL

0

Vamos chamar a tabela principal de produtos. Isso hospeda os atributos compartilhados. Então vamos dizer que temos uma mesa de carro, uma mesa de avião e uma mesa de barco. Essas três tabelas teriam uma chave ProductID com uma restrição FK na linha de ID da tabela Product. Se você quiser todos eles - junte-se a eles. Se você quiser apenas os carros, junte à esquerda Carros com produtos (ou junte à direita produtos e carros, mas eu prefiro sempre usar junções do lado esquerdo).

Isso é chamado de modelo de dados hiearchical. Para um número baixo de sub-tabelas, pode fazer sentido em uma tabela longa (milhões de produtos).


E então eu ingresso Usuários com Produtos?
user5613506

1
Normalmente, você não precisa de informações do usuário ao retornar uma lista de produtos ao seu front end; precisa das informações do produto. Não faz sentido associar Usuários e Produtos e retornar as informações idênticas de usuário para cada linha de produto retornada. Então, primeiro você filtra por tipo de produto, ingressando na tabela de produtos e na sub-tabela apropriada (Carro, Barco ...) e depois filtra por Usuário usando uma cláusula WHERE. Geralmente, você deseja ter um OwnerID na tabela Products (um FK na coluna ID da tabela User). Então você adicionaria um WHERE Owner = [Request.User].
NeManiac
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.