Pergunta

Bem, aqui está o meu problema, tenho três mesas; regiões, países, estados. Os países podem estar dentro das regiões, os estados podem estar dentro das regiões. As regiões são o topo da cadeia alimentar.

Agora estou adicionando uma tabela popular_areas com duas colunas; Region_ID e Popular_Place_Id. É possível fazer popular_cplace_id ser uma chave estrangeira para qualquer um dos países OU estados. Provavelmente vou ter que adicionar uma coluna Popular_place_type para determinar se o ID está descrevendo um país ou estado de qualquer maneira.

Foi útil?

Solução

O que você está descrevendo é chamado de associações polimórficas. Ou seja, a coluna "Chave estrangeira" contém um valor de identificação que deve existir em um de um conjunto de tabelas de destino. Normalmente, as tabelas de destino estão relacionadas de alguma forma, como serem instâncias de alguma superclasse comum de dados. Você também precisaria de outra coluna ao lado da coluna de chave estrangeira, para que, em cada linha, possa designar qual tabela de destino é referenciada.

CREATE TABLE popular_places (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  place_type VARCHAR(10) -- either 'states' or 'countries'
  -- foreign key is not possible
);

Não há como modelar associações polimórficas usando restrições SQL. Uma restrição de chave estrangeira sempre referências 1 Tabela de destino.

Associações polimórficas são apoiadas por estruturas como trilhos e hibernados. Mas eles dizem explicitamente que você deve desativar as restrições do SQL para usar esse recurso. Em vez disso, o aplicativo ou estrutura deve fazer um trabalho equivalente para garantir que a referência seja atendida. Ou seja, o valor na chave estrangeira está presente em uma das tabelas de destino possíveis.

As associações polimórficas são fracas em relação à aplicação da consistência do banco de dados. A integridade dos dados depende de todos os clientes que acessam o banco de dados com a mesma lógica de integridade referencial aplicada e também a aplicação deve ser isenta de bugs.

Aqui estão algumas soluções alternativas que aproveitam a integridade referencial imponente de banco de dados:

Crie uma tabela extra por destino. Por exemplo popular_states e popular_countries, que referência states e countries respectivamente. Cada uma dessas tabelas "populares" também faz referência ao perfil do usuário.

CREATE TABLE popular_states (
  state_id INT NOT NULL,
  user_id  INT NOT NULL,
  PRIMARY KEY(state_id, user_id),
  FOREIGN KEY (state_id) REFERENCES states(state_id),
  FOREIGN KEY (user_id) REFERENCES users(user_id),
);

CREATE TABLE popular_countries (
  country_id INT NOT NULL,
  user_id    INT NOT NULL,
  PRIMARY KEY(country_id, user_id),
  FOREIGN KEY (country_id) REFERENCES countries(country_id),
  FOREIGN KEY (user_id) REFERENCES users(user_id),
);

Isso significa que para obter todos os lugares favoritos de um usuário que você precisa para consultar essas duas tabelas. Mas isso significa que você pode confiar no banco de dados para aplicar a consistência.

Crie um places Tabela como uma supertable. Como Abie menciona, uma segunda alternativa é que seus lugares populares fazem referência a uma tabela como places, que é pai de ambos states e countries. Isto é, estados e países também têm uma chave estrangeira para places (Você pode até fazer essa chave estrangeira também ser a chave principal de states e countries).

CREATE TABLE popular_areas (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  PRIMARY KEY (user_id, place_id),
  FOREIGN KEY (place_id) REFERENCES places(place_id)
);

CREATE TABLE states (
  state_id INT NOT NULL PRIMARY KEY,
  FOREIGN KEY (state_id) REFERENCES places(place_id)
);

CREATE TABLE countries (
  country_id INT NOT NULL PRIMARY KEY,
  FOREIGN KEY (country_id) REFERENCES places(place_id)
);

Use duas colunas. Em vez de uma coluna que possa fazer referência a duas tabelas de destino, use duas colunas. Essas duas colunas podem ser NULL; Na verdade, apenas um deles deve ser nãoNULL.

CREATE TABLE popular_areas (
  place_id SERIAL PRIMARY KEY,
  user_id INT NOT NULL,
  state_id INT,
  country_id INT,
  CONSTRAINT UNIQUE (user_id, state_id, country_id), -- UNIQUE permits NULLs
  CONSTRAINT CHECK (state_id IS NOT NULL OR country_id IS NOT NULL),
  FOREIGN KEY (state_id) REFERENCES places(place_id),
  FOREIGN KEY (country_id) REFERENCES places(place_id)
);

Em termos de teoria relacional, as associações polimórficas violam Primeira forma normal, porque o popular_place_id está em vigor uma coluna com dois significados: é um estado ou um país. Você não armazenaria uma pessoa age e deles phone_number em uma única coluna e pelo mesmo motivo, você não deve armazenar os dois state_id e country_id em uma única coluna. O fato de esses dois atributos terem tipos de dados compatíveis é coincidência; Eles ainda significam diferentes entidades lógicas.

Associações polimórficas também viola Terceira forma normal, porque o significado da coluna depende da coluna extra que nomeia a tabela à qual a chave estrangeira se refere. Na terceira forma normal, um atributo em uma tabela deve depender apenas da chave primária dessa tabela.


RE comentar de @savasvedova:

Não tenho certeza se sigo sua descrição sem ver as definições da tabela ou uma consulta de exemplo, mas parece que você simplesmente tem múltiplo Filters mesas, cada uma contendo uma chave estrangeira que faz referência a um centro Products tabela.

CREATE TABLE Products (
  product_id INT PRIMARY KEY
);

CREATE TABLE FiltersType1 (
  filter_id INT PRIMARY KEY,
  product_id INT NOT NULL,
  FOREIGN KEY (product_id) REFERENCES Products(product_id)
);

CREATE TABLE FiltersType2 (
  filter_id INT  PRIMARY KEY,
  product_id INT NOT NULL,
  FOREIGN KEY (product_id) REFERENCES Products(product_id)
);

...and other filter tables...

Juntar -se aos produtos em um tipo específico de filtro é fácil se você souber para qual tipo deseja participar:

SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)

Se você deseja que o tipo de filtro seja dinâmico, deve gravar código do aplicativo para construir a consulta SQL. O SQL exige que a tabela seja especificada e corrigida no momento em que você escreve a consulta. Você não pode fazer com que a mesa unida seja escolhida dinamicamente com os valores encontrados em linhas individuais de Products.

A única outra opção é se juntar a tudo Tabelas de filtro usando junções externas. Aqueles que não possuem produtos correspondentes_id serão devolvidos apenas como uma única linha de nulas. Mas você ainda tem que hardcode tudo As tabelas unidas e, se você adicionar novas tabelas de filtro, precisará atualizar seu código.

SELECT * FROM Products
LEFT OUTER JOIN FiltersType1 USING (product_id)
LEFT OUTER JOIN FiltersType2 USING (product_id)
LEFT OUTER JOIN FiltersType3 USING (product_id)
...

Outra maneira de participar de todas as mesas de filtro é fazê -lo em série:

SELECT * FROM Product
INNER JOIN FiltersType1 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType3 USING (product_id)
...

Mas esse formato ainda exige que você escreva referências a todas as tabelas. Não há como contornar isso.

Outras dicas

Esta não é a solução mais elegante do mundo, mas você pode usar herança da mesa de concreto para fazer isso funcionar.

Conceitualmente, você está propondo uma noção de uma classe de "coisas que podem ser áreas populares" das quais seus três tipos de lugares herdam. Você pode representar isso como uma tabela chamada, por exemplo, places onde cada linha tem um relacionamento individual com uma fila em regions, countries, ou states. (Atributos compartilhados entre regiões, países ou estados, se houver, podem ser empurrados para esta mesa de lugares.) Seu popular_place_id seria então uma referência chave estrangeira a uma fileira na tabela de lugares que o levaria a uma região, país ou estado.

A solução que você propõe com uma segunda coluna para descrever o tipo de associação é como o Rails lida com associações polimórficas, mas não sou fã disso em geral. Bill explica em excelentes detalhes por que as associações polimórficas não são seus amigos.

Aqui está uma correção para a abordagem "Supertable" de Bill Karwin, usando uma chave composta ( place_type, place_id ) Para resolver as violações de forma normal percebida:

CREATE TABLE places (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) NOT NULL
     CHECK ( place_type = 'state', 'country' ),
  UNIQUE ( place_type, place_id )
);

CREATE TABLE states (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'state' NOT NULL
     CHECK ( place_type = 'state' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to states go here
);

CREATE TABLE countries (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'country' NOT NULL
     CHECK ( place_type = 'country' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to country go here
);

CREATE TABLE popular_areas (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  UNIQUE ( user_id, place_id ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
);

O que esse design não pode garantir que para cada linha em places Existe uma fila em states ou countries (mas não ambos). Esta é uma limitações de chaves estrangeiras no SQL. Em um DBMS completo dos padrões SQL-92, você pode definir restrições entre tabelas diferíveis que permitiriam que você alcançar o mesmo, mas é desajeitado, envolve transação e esse DBMS ainda não chegou ao mercado.

Percebo que esse tópico é velho, mas vi isso e uma solução veio à mente e pensei em jogar lá fora.

Regiões, países e estados são locais geográficos que vivem em uma hierarquia.

Você pode evitar completamente seu problema criando uma tabela de domínio chamada geográfica_location_type, que você preencheria com três linhas (região, país, estado).

Em seguida, em vez das três tabelas de localização, crie uma única tabela geográfica que tenha uma chave estrangeira de geográfico_location_type_id (para que você saiba se a instância é uma região, país ou estado).

Modele a hierarquia, fazendo com que essa tabela referenciando para que uma instância do estado mantenha o FKEY em sua instância do país-mãe, que por sua vez mantém o FKEY em sua instância da região pai. As instâncias da região se manteriam nulas naquele fkey. Isso não é diferente do que você teria feito com as três mesas (você teria 1 - muitas relações entre região e país e entre país e estado), exceto agora que está tudo em uma mesa.

A tabela Popular_User_Location seria uma tabela de resolução de escopo entre o usuário e o georraphical_location (muitos usuários poderiam gostar de muitos lugares).

Tããão ...

enter image description here

CREATE TABLE [geographical_location_type] (
    [geographical_location_type_id] INTEGER NOT NULL,
    [name] VARCHAR(25) NOT NULL,
    CONSTRAINT [PK_geographical_location_type] PRIMARY KEY ([geographical_location_type_id])
)

-- Add 'Region', 'Country' and 'State' instances to the above table


CREATE TABLE [geographical_location] (
   [geographical_location_id] BIGINT IDENTITY(0,1) NOT NULL,
    [name] VARCHAR(1024) NOT NULL,
    [geographical_location_type_id] INTEGER NOT NULL,
    [geographical_location_parent] BIGINT,  -- self referencing; can be null for top-level instances
    CONSTRAINT [PK_geographical_location] PRIMARY KEY ([geographical_location_id])
)

CREATE TABLE [user] (
    [user_id] BIGINT NOT NULL,
    [login_id] VARCHAR(30) NOT NULL,
    [password] VARCHAR(512) NOT NULL,
    CONSTRAINT [PK_user] PRIMARY KEY ([user_id])
)


CREATE TABLE [popular_user_location] (
    [popular_user_location_id] BIGINT NOT NULL,
    [user_id] BIGINT NOT NULL,
    [geographical_location_id] BIGINT NOT NULL,
    CONSTRAINT [PK_popular_user_location] PRIMARY KEY ([popular_user_location_id])
)

ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_type_geographical_location] 
    FOREIGN KEY ([geographical_location_type_id]) REFERENCES [geographical_location_type] ([geographical_location_type_id])



ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_geographical_location] 
    FOREIGN KEY ([geographical_location_parent]) REFERENCES [geographical_location] ([geographical_location_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [user_popular_user_location] 
    FOREIGN KEY ([user_id]) REFERENCES [user] ([user_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [geographical_location_popular_user_location] 
    FOREIGN KEY ([geographical_location_id]) REFERENCES [geographical_location] ([geographical_location_id])

Não tinha certeza de qual era o banco de dados alvo; O acima é o MS SQL Server.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top