在 PostgreSQL 中实现多对多关系的最佳方式是什么?

What is the best way to implement a many-to-many relationship in PostgreSQL?

我正在尝试在 PostgreSQL 中实现多对多关系。 这是我数据库中的 tables:

CREATE TABLE products (
   product_id   serial PRIMARY KEY
 , product_name varchar NOT NULL
);

CREATE TABLE orders (
   order_id   serial PRIMARY KEY
 , order_name varchar NOT NULL
);

CREATE TABLE product_order (
   product_id  int REFERENCES products
 , order_id    int REFERENCES orders
 , PRIMARY KEY (product_id, order_id)
);

产品和订单 table 中不会有任何 UPDATE 或 DELETE,因此不需要 ON DELETE 和 ON UPDATE 语句。

我还创建了两个哈希索引,因此我可以搜索订单和产品名称并将它们的 ID 存储在 table:

CREATE INDEX product_index ON products USING hash(product_name);
CREATE INDEX order_index ON orders USING hash(order_name);

这是我正在尝试做的事情:

  1. 我想插入产品 table 和 return 插入行的 ID。
  2. 我想在订单 table 和 return 中插入插入行的 ID。
  3. 我想将 product_id 和 order_id 都插入 product_order table。

有一个边缘情况:

如果我要插入的产品已经在产品 table 中,那么我不想创建具有不同 ID 的另一行。在这种情况下,我想检索已经在 table.

中的 product_id

这种边缘情况与订单相同。

为了完成所有这些,我创建了一个 SQL 函数:

CREATE OR REPLACE FUNCTION add_product_order(myproduct varchar, myorder varchar)
RETURNS VOID
LANGUAGE sql
AS
$$
    WITH pro AS (
        WITH p as (
            SELECT product_id FROM products WHERE product_name = myproduct -- check if product is already in the table
        )
        INSERT into products (product_name) -- insert into products and get the product_id only if myproduct was not found
            SELECT (myproduct)
            WHERE NOT EXISTS (
                SELECT product_id FROM p
            )
        RETURNING product_id
    ),

    ord AS (
        WITH o as(
            SELECT order_id FROM orders WHERE order_name = myorder -- check if order is already in the table
        )
        INSERT into orders (order_name) -- insert into orders and get the order_id only if myorder was not found
            SELECT (myorder)
            WHERE NOT EXISTS (
                SELECT order_id FROM o
            )
        RETURNING order_id
    )
    INSERT INTO product_order (product_id, order_id) -- insert both FK ids into the product_order table
        SELECT pro.product_id, ord.order_id FROM pro, ord;
$$;

创建函数后,我对 运行 执行以下 SQL 查询:

select add_product_order('product1','order1');

一切似乎都很好,但是 只有当我尝试插入的产品不在 table.

中时它才有效

如果产品已经在table,第一个SELECTreturns在product_id临时ptable。但是我不知道如何在最后的 INSERT INTO product_order.

中获取 p.product_id

看到我不能做得太过分,我也尝试了 plpgsql FUNCTION:

CREATE OR REPLACE FUNCTION add_product_order(myproduct varchar, myorder varchar)
RETURNS VOID
LANGUAGE plpgsql
AS
$$
DECLARE
   id_product integer;
   id_order integer;
BEGIN
    SELECT product_id INTO id_product FROM products WHERE product_name = myproduct; -- check if product is already in the table

    IF NOT FOUND THEN
        RAISE INFO 'product % not found', myproduct;

        INSERT INTO products (product_name) VALUES (myproduct) RETURNING product_id; -- product not found, so insert it and get the id
        id_product := product_id; -- Tried also with SELECT product_id INTO id_product;
    END IF;

    SELECT order_id INTO id_order FROM orders WHERE order_name = myorder; -- check if order is already in the table

    IF NOT FOUND THEN
        RAISE INFO 'order % not found', myorder;

        INSERT INTO orders (order_name) VALUES (myorder) RETURNING order_id; -- order not found, so insert it and get the id
        id_order := order_id;
    END IF;

    INSERT INTO product_order (product_id, order_id) VALUES (id_product, id_order); -- insert both ids into the product_order table
END;
$$;

这个 plpgsql FUNCTION 应该可以解决我在上面的 SQL 函数中遇到的问题。

但它给我一个错误:查询没有结果数据的目的地

完成此任务的正确方法是什么?

PS。在发布这个问题之前,我搜索并阅读了多个答案:

您只需插入 Product NameOrder name 让 Postgres 处理重复即可得到您想要的。您将它们的索引提升到 unique constraints(不需要散列),然后让 ON CONFLICT 子句处理重复。这就是 select, if not found insert 逻辑无论如何都在尝试的。唯一的问题是 Postgres 不返回重复的 ID。为了克服 Product_Orders 的插入从适当的名称中检索 ID。这可以在带有几个 CTE 的单个语句中完成,但是返回子句是必需的,并且返回的任何内容在名称已经存在的情况下都是空的。所以无论如何它都会被忽略。但是 SQL function/procedure 可以有多个语句,所以(恕我直言)3 个语句更清楚。 (参见 example here

create or replace 
procedure gen_product_order(
          myproduct varchar
        , myorder   varchar
        )
  language sql
as $$
  insert into products(product_name) 
       values (myproduct) 
           on conflict (product_name) do nothing; 
 
  insert into orders (order_name)  
      values (myorder)
          on conflict (order_name) do nothing;      
 
    insert into product_orders (product_id, order_id) -- insert both fk ids into the product_order table
       select product_id, order_id 
         from (select product_id 
                 from products 
                where product_name = myproduct
              ) prd
            , (select order_id 
                 from orders
                where order_name = myorder
              ) ord  
        on conflict (product_id, order_id) do nothing; 
$$; 

注意:我使用了一个过程而不是一个返回 void 的函数,但是如果你的 Postgres 版本不支持一个函数也可以。