如何在 PostgreSQL 中实现多对多关系?

Posted

技术标签:

【中文标题】如何在 PostgreSQL 中实现多对多关系?【英文标题】:How to implement a many-to-many relationship in PostgreSQL? 【发布时间】:2012-04-05 02:14:21 【问题描述】:

我相信标题是不言自明的。如何在 PostgreSQL 中创建表结构以建立多对多关系。

我的例子:

Product(name, price);
Bill(name, date, Products);

【问题讨论】:

从账单表中删除产品,创建一个名为“bill_products”的新表,其中包含两个字段:一个指向产品,一个指向账单。将这两个字段设为新表的主键。 所以 bill_products(bill, products); ?并且两人PK? 是的。他们将单独成为指向各自桌子的 FK,并且他们将共同成为新桌子的 PK。 那么,bill_product(product references product.name, bill references bill.name, (product, bill) 主键) ? 他们会指向 Product 和 Bill 表的 PK 字段。 【参考方案1】:

SQL DDL(数据定义语言)语句可能如下所示:

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

我做了一些调整:

n:m 关系通常由单独的表实现 - 在这种情况下为 bill_product

我添加了serial 列作为代理主键。在 Postgres 10 或更高版本中,请考虑使用 IDENTITY column。见:

Safely rename tables using serial primary key columns Auto increment table column https://www.2ndquadrant.com/en/blog/postgresql-10-identity-columns/

我强烈建议这样做,因为产品的名称几乎不是唯一的(不是一个好的“自然键”)。此外,使用 4 字节 integer(甚至是 8 字节 bigint)在外键中强制唯一性和引用列通常比使用存储为 textvarchar 的字符串便宜。

不要使用date 等基本数据类型的名称作为标识符。虽然这是可能的,但这是一种糟糕的风格,会导致令人困惑的错误和错误消息。使用legal, lower case, unquoted identifiers。切勿使用reserved words,并尽可能避免使用双引号混合大小写标识符。

“名字”不是一个好名字。我将表product 的列重命名为product(或product_name 或类似名称)。这是一个更好的命名约定。否则,当您在查询中加入几个表时 - 您在关系数据库中做了很多很多 - 您最终会得到多个名为“name”的列,并且必须使用列别名来整理混乱。那没有帮助。另一个广泛使用的反模式将“id”作为列名。 我不确定bill 的名称是什么。在这种情况下,bill_id 可能就足够了。

price 属于数据类型 numeric,用于存储精确输入的小数(任意精度类型而不是浮点类型)。如果您专门处理整数,请使用integer。例如,您可以将价格保存为美分

amount(您的问题中的"Products")进入链接表bill_product,并且也是numeric 类型。同样,integer 如果您只处理整数。

您看到bill_product 中的外键了吗?我创建了两者以级联更改:ON UPDATE CASCADE。如果product_idbill_id 应该更改,则更改将级联到bill_product 中的所有相关条目,并且不会中断。这些只是参考,没有任何意义。 我还将ON DELETE CASCADE 用于bill_id:如果账单被删除,其详细信息也会随之消失。 产品并非如此:您不想删除账单中使用的产品。如果你尝试这个,Postgres 会抛出一个错误。您可以向product 添加另一列来标记过时的行(“软删除”)。

此基本示例中的所有列最终都是 NOT NULL,因此不允许使用 NULL 值。 (是的,所有 列 - 主键列是自动定义的 UNIQUE NOT NULL。)这是因为 NULL 值在任何列中都没有意义。它使初学者的生活更轻松。但是你不会那么容易逃脱的,无论如何你需要了解NULL handling。附加列可能允许NULL 值,函数和连接可以在查询等中引入NULL 值。

阅读CREATE TABLE in the manual的章节。

主键是通过键列上的唯一索引实现的,这使得在 PK 列上具有条件的查询快速。但是,键列的顺序与多列键相关。由于bill_product 上的PK 在我的示例中位于(bill_id, product_id) 上,如果您有查询查找给定的product_id 而没有bill_id,您可能只想在product_id(product_id, bill_id) 上添加另一个索引。见:

PostgreSQL composite primary key Is a composite index also good for queries on the first field? Working of indexes in PostgreSQL

阅读chapter on indexes in the manual。

【讨论】:

如何为映射表bill_product创建索引?通常它应该看起来像:CREATE INDEX idx_bill_product_id ON booked_rates(bill_id, product_id)。对吗? @codyLine:这个索引是PK自动创建的。 @ErwinBrandstetter:不应该在 bill_product 上为 product_id 列创建索引吗? @ChristianB.Almeida:这在很多情况下都很有用,是的。我添加了一些关于索引的内容。 @Jakov:bill 表中的每个账单只有 1 行。我们需要bill_product 中每个添加项目的金额。

以上是关于如何在 PostgreSQL 中实现多对多关系?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Laravel 5.5 中实现多个多对多关系?

如何在 .NET EF Core 中实现自引用多对多关系

Postgresql 多对多关系插入

Sonata Admin 编辑表单多对多不起作用 - symfony2.1.6

在 PostgreSQL 中存储一对多或多对多关系的最佳方式是啥?

在 laravel 中使用多对多关系同步:PostgreSQL 数据透视表不更新