Como implementar um relacionamento muitos para muitos no PostgreSQL?

95

Eu acredito que o título é autoexplicativo. Como você cria a estrutura da tabela no PostgreSQL para fazer um relacionamento muitos para muitos.

Meu exemplo:

Product(name, price);
Bill(name, date, Products);
Radu Gheorghiu
fonte
2
remova produtos da tabela de faturamento, crie uma nova tabela chamada "bill_products" com dois campos: um apontando para produtos e outro apontando para fatura. torne esses dois campos a chave primária desta nova tabela.
Marc B de
Então bill_products (bill, products); ? E ambos PK?
Radu Gheorghiu
1
sim. eles seriam individualmente um FK apontando para suas respectivas mesas e, juntos, seriam o PK da nova mesa.
Marc B de
Então, bill_product (referências de produto product.name, bill referências bill.name, (product, bill) chave primária)?
Radu Gheorghiu
Eles apontariam para o que seriam os campos PK das tabelas Produto e Bill.
Marc B de

Respostas:

298

As instruções SQL DDL (linguagem de definição de dados) podem ter a seguinte aparência:

CREATE TABLE product (
  product_id serial PRIMARY KEY  -- implicit primary key constraint
, product    text NOT NULL
, price      numeric NOT NULL DEFAULT 0
);

CREATE TABLE bill (
  bill_id  serial PRIMARY KEY
, bill     text NOT NULL
, billdate date NOT NULL DEFAULT CURRENT_DATE
);

CREATE TABLE bill_product (
  bill_id    int REFERENCES bill (bill_id) ON UPDATE CASCADE ON DELETE CASCADE
, product_id int REFERENCES product (product_id) ON UPDATE CASCADE
, amount     numeric NOT NULL DEFAULT 1
, CONSTRAINT bill_product_pkey PRIMARY KEY (bill_id, product_id)  -- explicit pk
);

Fiz alguns ajustes:

  • O relacionamento n: m é normalmente implementado por uma tabela separada - bill_productneste caso.

  • Eu adicionei serialcolunas como chaves primárias substitutas . No Postgres 10 ou posterior, considere uma IDENTITYcoluna . Vejo:

    Eu recomendo fortemente, porque o nome de um produto dificilmente é único (não é uma boa "chave natural"). Além disso, impor exclusividade e referenciar a coluna em chaves estrangeiras é normalmente mais barato com um código de 4 bytesinteger (ou mesmo 8 bytes bigint) do que com uma string armazenada como textou varchar.

  • Não use nomes de tipos de dados básicos datecomo identificadores . Embora isso seja possível, é um estilo incorreto e leva a erros e mensagens de erro confusos. Use identificadores legais, minúsculas e sem aspas . Nunca use palavras reservadas e evite identificadores de maiúsculas e minúsculas entre aspas, se possível.

  • "nome" não é um bom nome. Mudei o nome da coluna da tabela productpara product(ou product_nameou similar). Essa é uma convenção de nomenclatura melhor . Caso contrário, quando você junta algumas tabelas em uma consulta - o que você faz muito em um banco de dados relacional - você acaba com várias colunas chamadas "nome" e tem que usar aliases de coluna para resolver a bagunça. Isso não ajuda. Outro antipadrão amplamente difundido seria apenas "id" como nome da coluna.
    Não tenho certeza de qual seria o nome de a bill. bill_idprovavelmente será suficiente neste caso.

  • priceé do tipo de dadosnumeric para armazenar números fracionários precisamente conforme inseridos (tipo de precisão arbitrária em vez de tipo de ponto flutuante). Se você lida exclusivamente com números inteiros, faça com queinteger . Por exemplo, você pode economizar preços em centavos .

  • O amount( "Products"na sua pergunta) vai para a tabela de ligação bill_producte também é do tipo numeric. Novamente, integerse você lidar exclusivamente com números inteiros.

  • Você vê as chaves estrangeiras em bill_product? Eu criei tanto a mudanças em cascata: ON UPDATE CASCADE. Se um product_idoubill_id deve ser alterado, a alteração é enviada em cascata para todas as entradas dependentes em bill_producte nada é interrompido. Essas são apenas referências sem significado próprio.
    Eu também usei ON DELETE CASCADEparabill_id : Se uma conta for excluída, seus detalhes morrem com ela.
    Não é assim para produtos: você não deseja excluir um produto que é usado em uma fatura. O Postgres irá gerar um erro se você tentar isso. Você adicionaria outra coluna para productmarcar linhas obsoletas ("exclusão reversível").

  • Todas as colunas neste exemplo básico acabam sendo NOT NULL, portanto, os NULLvalores não são permitidos. (Sim, todas as colunas - colunas de chave primária são definidas UNIQUE NOT NULLautomaticamente.) Isso porque os NULLvalores não fariam sentido em nenhuma das colunas. Facilita a vida de um iniciante. Mas você não vai escapar tão facilmente, você precisa entender o NULLmanuseio de qualquer maneira. Colunas adicionais podem permitir NULLvalores, funções e junções podem introduzir NULLvalores em consultas etc.

  • Leia o capítulo no CREATE TABLEmanual .

  • As chaves primárias são implementadas com um índice exclusivo nas colunas de chave, que agiliza as consultas com condições na (s) coluna (s) PK. No entanto, a sequência de colunas-chave é relevante em chaves com várias colunas. Uma vez que o PK ativado bill_productestá ativado (bill_id, product_id)no meu exemplo, você pode querer adicionar outro índice apenasproduct_id ou (product_id, bill_id)se tiver consultas procurando por um dado product_ide não bill_id. Vejo:

  • Leia o capítulo sobre índices no manual .

Erwin Brandstetter
fonte
Como posso criar um índice para a tabela de mapeamento bill_product? Normalmente ele deve se parece com: CREATE INDEX idx_bill_product_id ON booked_rates(bill_id, product_id). Isto está certo?
codyLine
1
@codyLine: Este índice é criado automaticamente pelo PK.
Erwin Brandstetter,
1
@ErwinBrandstetter: Não deve ser criado um índice em bill_product para a coluna product_id?
Christian
2
@ ChristianB.Almeida: Isso é útil em muitos casos, sim. Eu adicionei um pouco sobre indexação.
Erwin Brandstetter,
1
@Jakov: Existe apenas 1 linha para cada conta na tabela bill. Precisamos do valor por item adicionado em bill_product.
Erwin Brandstetter,