Como ter um relacionamento um-para-muitos com um filho privilegiado?

22

Quero ter um relacionamento um-para-muitos, no qual, para cada pai / mãe, um ou zero dos filhos seja marcado como "favorito". No entanto, nem todos os pais terão um filho. (Pense nos pais como perguntas neste site, nos filhos como respostas e nos favoritos como a resposta aceita.) Por exemplo,

TableA
    Id            INT PRIMARY KEY

TableB
    Id            INT PRIMARY KEY
    Parent        INT NOT NULL FOREIGN KEY REFERENCES TableA.Id

Do jeito que eu vejo, posso adicionar a seguinte coluna à TabelaA:

    FavoriteChild INT NULL FOREIGN KEY REFERENCES TableB.Id

ou a seguinte coluna na Tabela B:

    IsFavorite    BIT NOT NULL

O problema com a primeira abordagem é que ela introduz uma chave estrangeira anulável, que, eu entendo, não está na forma normalizada. O problema com a segunda abordagem é que mais trabalho precisa ser feito para garantir que no máximo uma criança seja a favorita.

Que tipo de critério devo usar para determinar qual abordagem usar? Ou existem outras abordagens que não estou considerando?

Estou usando o SQL Server 2012.

cubetwo1729
fonte

Respostas:

19

Outra maneira (sem Nulos e sem ciclos nos FOREIGN KEYrelacionamentos) é ter uma terceira tabela para armazenar os "filhos favoritos". Na maioria dos DBMS, você precisará de uma UNIQUErestrição adicional TableB.

O @Aaron foi mais rápido ao identificar que a convenção de nomenclatura acima é bastante complicada e pode levar a erros. Geralmente é melhor (e manterá você saudável) se você não tiver Idcolunas em todas as suas tabelas e se as colunas (que estão unidas) tiverem o mesmo nome nas muitas tabelas que aparecerem. Então, aqui está uma renomeação:

Parent
    ParentID        INT NOT NULL PRIMARY KEY

Child
    ChildID         INT NOT NULL PRIMARY KEY
    ParentID        INT NOT NULL FOREIGN KEY REFERENCES Parent (ParentID)
    UNIQUE (ParentID, ChildID)

FavoriteChild
    ParentID        INT NOT NULL PRIMARY KEY
    ChildID         INT NOT NULL 
    FOREIGN KEY (ParentID, ChildID) 
        REFERENCES Child (ParentID, ChildID)

No SQL-Server (que você está usando), você também tem a opção da IsFavoritecoluna de bits mencionada. O filho favorito único por pai / mãe pode ser realizado por meio de um Índice Único filtrado:

Parent
    ParentID        INT NOT NULL PRIMARY KEY

Child
    ChildID         INT NOT NULL PRIMARY KEY
    ParentID        INT NOT NULL FOREIGN KEY REFERENCES Parent (ParentID)
    IsFavorite      BIT NOT NULL

CREATE UNIQUE INDEX is_FavoriteChild
  ON Child (ParentID)
  WHERE IsFavorite = 1 ;

E o principal motivo pelo qual sua opção 1 não é recomendada, pelo menos não no SQL-Server, é que o padrão de caminhos circulares nas referências de chave estrangeira apresenta alguns problemas.

Leia um artigo bastante antigo: SQL por design: a referência circular

Ao inserir ou excluir linhas das duas tabelas, você encontrará o problema "galinha e ovo". Qual tabela devo inserir primeiro - sem violar nenhuma restrição?

Para resolver isso, é necessário definir pelo menos uma coluna anulável. (OK, tecnicamente não é necessário, você pode ter todas as colunas, NOT NULLmas apenas no DBMS, como Postgres e Oracle, que implementaram restrições adiadas. Consulte a resposta de Erwin em uma pergunta semelhante: Restrição de chave estrangeira complexa no SQLAlchemy sobre como isso pode ser feito no Postgres). Ainda assim, essa configuração parece patinar em gelo fino.

Verifique também uma pergunta quase idêntica no SO (mas no MySQL). No SQL, é correto que duas tabelas se refiram? onde minha resposta é praticamente a mesma. O MySQL não possui índices parciais, portanto, as únicas opções viáveis ​​são o FK anulável e a solução extra de tabela.

ypercubeᵀᴹ
fonte
9

Depende da sua prioridade. Deseja evitar o trabalho ou deseja aderir às regras mais estritas da normalização?

Pessoalmente, acho que é melhor ter IsFavoritena mesa filho e estaria disposto a trabalhar para garantir que no máximo um filho para cada pai seja o favorito dele. O principal motivo não tem nada a ver com a chave nula estrangeira: não gosto da ideia de todas as chaves estrangeiras apontadas nas duas direções.

A sugestão de @ ypercube também é um bom compromisso.

Como um aparte, por favor, por favor, não coloque seu esquema com nomes de colunas sem sentido como Id. Eu preferiria ver o Idnome de uma maneira significativa em todo o esquema. Identifica um autor? Ok, chame AuthorID. Representa um produto? Ok ProductID. É um funcionário e, em alguns casos, faz referência a um gerente? Ok, EmployeeIDe ManagerIDfaz mais sentido para mim do que IDe Parent. Embora possa parecer lógico deixar isso de fora (e redundante para colocá-lo), quando você começar a escrever junções complexas (ou postar consultas aqui), você definitivamente sentirá alguma maldição ao tentar fazer engenharia reversa de várias junções nesse ponto. para colunas como a.Parent = b.ID... blecch.

Aaron Bertrand
fonte
1

Os dados pertencem à tabela filho. Mantemos isso correto com um gatilho na mesa para garantir que um e único registro seja marcado como favorito (ou, no nosso caso, como o endereço preferido).

No entanto, a idéia de @ ypercube de uma tabela separada também é boa.

HLGEM
fonte