如何在 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。见:
我强烈建议这样做,因为产品的名称几乎不是唯一的(不是一个好的“自然键”)。此外,使用 4 字节 integer
(甚至是 8 字节 bigint
)在外键中强制唯一性和引用列通常比使用存储为 text
或 varchar
的字符串便宜。
不要使用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_id
或bill_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)
上添加另一个索引。见:
阅读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 中实现多对多关系?的主要内容,如果未能解决你的问题,请参考以下文章
Sonata Admin 编辑表单多对多不起作用 - symfony2.1.6