É aceitável ter referências circulares de chave estrangeira \ Como evitá-las?

29

É aceitável ter uma referência circular entre duas tabelas no campo de chave estrangeira?

Caso contrário, como essas situações podem ser evitadas?

Se sim, como os dados podem ser inseridos?

Abaixo está um exemplo de onde (na minha opinião) uma referência circular seria aceitável:

CREATE TABLE Account
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50)
)

CREATE TABLE Contact
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50),
    AccountID INT FOREIGN KEY REFERENCES Account(ID)
)

ALTER TABLE Account ADD PrimaryContactID INT FOREIGN KEY REFERENCES Contact(ID)
KidCode
fonte
2
" Se sim, como os dados podem ser inseridos " - depende do DBMS que está sendo usado. Postgres, Oracle, SQLite e Apache Derby, por exemplo, permitem restrições adiaveis que tornariam isso possível. Com outros DBMS você está fora de sorte (Mas eu ainda iria disputar a necessidade de tal restrição a em primeiro lugar)
a_horse_with_no_name

Respostas:

12

Como você está usando campos anuláveis ​​para as chaves estrangeiras, é possível construir um sistema que funcione corretamente da maneira que você o imagina. Para inserir linhas na tabela Contas, você precisa ter uma linha presente na tabela Contatos, a menos que permita inserções nas Contas com um PrimaryContactID nulo. Para criar uma linha de contato sem já ter uma linha de Conta presente, você deve permitir que a coluna Código da Conta na tabela Contatos seja anulável. Isso permite que as contas não tenham contatos e permite que os contatos não tenham conta. Talvez isso seja desejável, talvez não.

Dito isto, minha preferência pessoal seria ter a seguinte configuração:

CREATE TABLE dbo.Accounts
(
    AccountID INT NOT NULL
        CONSTRAINT PK_Accounts
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , AccountName VARCHAR(255)
);

CREATE TABLE dbo.Contacts
(
    ContactID INT NOT NULL
        CONSTRAINT PK_Contacts
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , ContactName VARCHAR(255)
);

CREATE TABLE dbo.AccountsContactsXRef
(
    AccountsContactsXRefID INT NOT NULL
        CONSTRAINT PK_AccountsContactsXRef
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , AccountID INT NOT NULL
        CONSTRAINT FK_AccountsContactsXRef_AccountID
        FOREIGN KEY REFERENCES dbo.Accounts(AccountID)
    , ContactID INT NOT NULL
        CONSTRAINT FK_AccountsContactsXRef_ContactID
        FOREIGN KEY REFERENCES dbo.Contacts(ContactID)
    , IsPrimary BIT NOT NULL 
        CONSTRAINT DF_AccountsContactsXRef
        DEFAULT ((0))
    , CONSTRAINT UQ_AccountsContactsXRef_AccountIDContactID
        UNIQUE (AccountID, ContactID)
);

CREATE UNIQUE INDEX IX_AccountsContactsXRef_Primary
ON dbo.AccountsContactsXRef(AccountID, IsPrimary)
WHERE IsPrimary = 1;

Isso fornece a capacidade de:

  1. Delineie claramente os relacionamentos entre contatos e contas por meio de uma tabela de referência cruzada da maneira como Pieter recomenda em sua resposta
  2. Mantenha a integridade referencial de maneira não circular.
  3. Forneça uma lista altamente sustentável de contatos principais por meio do IX_AccountsContactsXRef_Primaryíndice. Este índice contém um filtro, portanto, ele funcionará apenas em plataformas que os suportem. Como esse índice é especificado com a UNIQUEopção, só pode haver um único contato principal para cada conta.

Por exemplo, se você deseja exibir uma lista de todos os contatos, com uma coluna indicando o status "primário", mostrando os contatos principais no topo da lista de cada conta, você pode:

SELECT A.AccountName
    , C.ContactName
    , XR.IsPrimary
FROM dbo.Accounts A
    INNER JOIN dbo.AccountsContactsXRef XR ON A.AccountID = XR.AccountID
    INNER JOIN dbo.Contacts C ON XR.ContactID = C.ContactID
ORDER BY A.AccountName
    , XR.IsPrimary DESC
    , C.ContactName;

O índice filtrado impede a inserção de mais de um único contato principal por conta, ao mesmo tempo em que fornece um método rápido de retornar uma lista de contatos principais. Pode-se imaginar facilmente outra coluna, IsActivecom um índice filtrado não exclusivo para manter um histórico de contatos por conta, mesmo depois que esse contato não estiver mais associado à conta:

ALTER TABLE dbo.AccountsContactsXRef
ADD IsActive BIT NOT NULL
CONSTRAINT DF_AccountsContactsXRef_IsActive
DEFAULT ((1));

CREATE INDEX IX_AccountsContactsXRef_IsActive
ON dbo.AccountsContactsXRef(IsActive)
WHERE IsActive = 1;
Max Vernon
fonte
11
você diria, em geral, que referências circulares devem ser evitadas? Sou da opinião de que eles não são ruins e os utilizamos para realizar projetos eficazes. Eles tornam as exclusões um pouco mais complicadas, pois exigem e atualizam para NULL na entidade que seria o único pai, mas acho que esse é um preço baixo a pagar pela conveniência. Eu os uso no Postgres, onde o campo FK é anulável, então eu crio uma linha com ele NULL e, em seguida, atualizo o campo FK para o PK a partir da tabela filho para realizar a mesma função descrita no OP
anfibiente
Não gosto de referências circulares simplesmente porque elas tendem a complicar desnecessariamente o design e, na maioria das vezes , não oferecem nenhum benefício significativo de desempenho que valha a pena. Sou fã do Navalha de Occam e, como resultado, tendem a encontrar a solução mais simples para um determinado problema.
Max Vernon
11
Sou a favor da navalha de Occam, mas o design descrito acima me permitiu evitar algumas consultas ou junções, sem violar necessariamente a terceira forma normal. Agradeço o seu feedback
anfíbio
6

Não, não é aceitável ter referências circulares de chaves estrangeiras. Não apenas porque seria impossível inserir dados sem eliminar e recriar constantemente a restrição. mas porque é um modelo fundamentalmente defeituoso de todo e qualquer domínio em que posso pensar. No seu exemplo, não consigo pensar em nenhum domínio no qual o relacionamento entre Conta e Contato não seja NN, exigindo uma tabela de junção com referências FK de volta para Conta e Contato.

CREATE TABLE Account
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50)
)

CREATE TABLE Contact
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50),
)

CREATE TABLE AccountContact
(
    AccountID INT FOREIGN KEY REFERENCES Account(ID),
    ContactID INT FOREIGN KEY REFERENCES Contact(ID),

    primary key(AccountID,ContactID)
)
Pieter Geerkens
fonte
5
" seria impossível inserir dados " - não, não seria impossível. Apenas declare as restrições como adiadas. Mas eu concordo: em quase todos os casos, as referências circulares são um projeto ruim.
A_horse_with_no_name 31/05
3
@a_horse - não é possível definir uma referência adiada no SQL Server ... Eu sei que você pode no Oracle, só queria apontar a discrepância.
Max Vernon
2
@ MaxVernon: a questão não é apenas sobre o SQL Server e há mais DBMS do que apenas o Oracle que oferece suporte a restrições deferíveis - mas como eu disse: eu concordo com Pieter que o design em si está errado (e sua solução faz muito mais sentido)
a_horse_with_no_name
4
Deixando de lado as especificidades de qualquer exemplo, em termos gerais, não há nada necessariamente errado ou "defeituoso" em ter restrições de integridade referencial recíprocas (isto é, "circulares"). Este é, na verdade, apenas um exemplo de uma dependência de associação. As dependências de junção são boas, em princípio, se o seu DBMS permitir implementá-las. Apenas nos DBMSs do SQL não é muito fácil implementar dependências complexas entre tabelas.
Nvogel
6
@ Pieter, 1-1 não é o único exemplo de dependência de junção e nem é um caso particularmente especial. Há casos em que as restrições de dependência de junção fazem todo o sentido.
Nvogel
1

Você pode fazer com que seu objeto externo aponte para o contato principal e não para a conta. Seus dados ficariam assim:

CREATE TABLE Account
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50)
)

CREATE TABLE Contact
(
    ID INT PRIMARY KEY IDENTITY,
    Name VARCHAR(50),
    AccountID INT FOREIGN KEY REFERENCES Account(ID)
)

CREATE TABLE AccountOwner (
    Other Stuff Here . . .
    PrimaryContactID INT FOREIGN KEY REFERENCES Contact(ID)
)
William Jockusch
fonte