如何在PostgreSQL中使用单个SQL向父表插入一行,然后向子表插入多行?

8
请看下面的架构图:
CREATE TABLE reps (
  id    SERIAL PRIMARY KEY,
  rep   TEXT NOT NULL UNIQUE
);

CREATE TABLE terms (
  id    SERIAL PRIMARY KEY,
  terms TEXT NOT NULL UNIQUE
);

CREATE TABLE shipVia (
  id        SERIAL PRIMARY KEY,
  ship_via  TEXT NOT NULL UNIQUE
);

CREATE TABLE invoices (
  id                    SERIAL PRIMARY KEY,
  customer              TEXT NOT NULL CONSTRAINT customerNotEmpty CHECK(customer <> ''),
  term_id               INT REFERENCES terms,
  rep_id                INT NOT NULL REFERENCES reps,
  ship_via_id           INT REFERENCES shipVia,
  ...
  item_count            INT NOT NULL,
  modified              TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created               TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  version               INT NOT NULL DEFAULT 0
);

CREATE TABLE invoiceItems (
  id            SERIAL PRIMARY KEY,
  invoice_id    INT NOT NULL REFERENCES invoices ON DELETE CASCADE,
  name          TEXT NOT NULL CONSTRAINT nameNotEmpty CHECK(name <> ''),
  description   TEXT,
  qty           INT NOT NULL CONSTRAINT validQty CHECK (qty > 0),
  price         DOUBLE PRECISION NOT NULL
);

我正在尝试使用可写CTE在一个SQL中插入发票及其发票项目。目前我卡在以下SQL语句上:

WITH new_invoice AS (
    INSERT INTO invoices (id, customer, term_id, ship_via_id, rep_id, ..., item_count)
    SELECT $1, $2, t.id, s.id, r.id, ..., $26
    FROM reps r
    JOIN terms t ON t.terms = $3
    JOIN shipVia s ON s.ship_via = $4
    WHERE r.rep = $5
    RETURNING id
) INSERT INTO invoiceItems (invoice_id, name, qty, price, description) VALUES
 (new_invoice.id,$27,$28,$29,$30)
,(new_invoice.id,$31,$32,$33,$34)
,(new_invoice.id,$35,$36,$37,$38);

当然,这个SQL是错误的,以下是PostgreSQL 9.2对此的解释:
ERROR:  missing FROM-clause entry for table "new_invoice"
LINE 13:  (new_invoice.id,$27,$28,$29,$30)
           ^


********** Error **********

ERROR: missing FROM-clause entry for table "new_invoice"
SQL state: 42P01
Character: 704

这真的可能吗?

编辑1

我正在尝试以下版本:

PREPARE insert_invoice_3 AS WITH 
new_invoice AS (
    INSERT INTO invoices (id, customer, term_id, ship_via_id, rep_id, ..., item_count) 
    SELECT $1, $2, t.id, s.id, r.id, ..., $26
    FROM reps r
    JOIN terms t ON t.terms = $3
    JOIN shipVia s ON s.ship_via = $4
    WHERE r.rep = $5
    RETURNING id
),
v (name, qty, price, description) AS (
    VALUES ($27,$28,$29,$30)
          ,($31,$32,$33,$34)
          ,($35,$36,$37,$38)
) 
 INSERT INTO invoiceItems (invoice_id, name, qty, price, description)
 SELECT new_invoice.id, v.name, v.qty, v.price, v.description
 FROM v, new_invoice;

这是我得到的回应:

ERROR:  column "qty" is of type integer but expression is of type text
LINE 19:  SELECT new_invoice.id, v.name, v.qty, v.price, v.descriptio...
                                         ^
HINT:  You will need to rewrite or cast the expression.

********** Error **********

ERROR: column "qty" is of type integer but expression is of type text
SQL state: 42804
Hint: You will need to rewrite or cast the expression.
Character: 899

我猜 v (name, qty, price, description) 不够,必须指定数据类型。然而,v (name, qty INT, price, description) 无法运行 - 语法错误。

编辑2

接下来,我刚刚尝试了第二个版本:

PREPARE insert_invoice_3 AS WITH 
new_invoice AS (
        INSERT INTO invoices (id, customer, term_id, ship_via_id, rep_id, ..., item_count) 
        SELECT $1, $2, t.id, s.id, r.id, ..., $26
        FROM reps r
        JOIN terms t ON t.terms = $3
        JOIN shipVia s ON s.ship_via = $4
        WHERE r.rep = $5
        RETURNING id
) 
 INSERT INTO invoiceItems (invoice_id, name, qty, price, description)
(
 SELECT i.id, $27, $28, $29, $30 FROM new_invoice i
 UNION ALL
 SELECT i.id, $31, $32, $33, $34 FROM new_invoice i
 UNION ALL
 SELECT i.id, $35, $36, $37, $38 FROM new_invoice i
);

这是我得到的内容:
ERROR:  column "qty" is of type integer but expression is of type text
LINE 15:  SELECT i.id, $27, $28, $29, $30 FROM new_invoice i
                            ^
HINT:  You will need to rewrite or cast the expression.

********** Error **********

ERROR: column "qty" is of type integer but expression is of type text
SQL state: 42804
Hint: You will need to rewrite or cast the expression.
Character: 759

看起来是同样的错误。有趣的是,如果我删除所有的UNION ALL,只留下一个SELECT语句 - 它就可以工作了!

编辑3

为什么我需要转换参数?是否可以在CTE中指定列的类型?


错误信息已经说明了一切:您正在为 $28(或 $32 或 $36)提供文本参数,而查询期望它是整数。提供正确的参数类型,或在查询内将它们转换为整数。 - wildplasser
这是一个预处理语句 - 我不传递任何参数。另外,我该如何指定类型?我尝试了最明显的选项,但它未通过语法检查。如果您知道如何操作,请发布答案。 - mark
这里可以使用VALUES ($27,$28::integer,$29::integer,$30),但会出现以下错误:ERROR: could not determine data type of parameter $6。$6似乎未被使用。顺便说一下,我认为你有很多参数,而27到38似乎也是一个重复的组。 - wildplasser
我没有发布整个SQL,因为发票创建需要26个参数。第27-38个参数对应于与同一发票相关联的三个发票项目。我可能有多达100个发票项目,意味着总共426个参数。你想说一个包含426个参数的SQL语句比在一个事务中有101个SQL语句,总共具有相同的426个参数更糟糕吗? - mark
个人而言,我不会在值列表中使用$N占位符。我已经+1了此评论关于您之前的问题,建议将值作为文字传递,并进行适当的转义。如何确切地执行取决于客户端语言/驱动程序。 - Daniel Vérité
显示剩余2条评论
3个回答

10

PostgreSQL对VALUES子句有着广泛的解释,因此它可以作为一个独立的子查询使用。

因此,您可以以这种形式表达您的查询:

WITH new_invoice AS (
    INSERT INTO ...
    RETURNING id
),
v(a,b,c,d) AS (values
  ($27,$28,$29,$30),
  ($31,$32,$33,$34),
  ...
)
INSERT INTO invoiceItems (invoice_id, name, qty, price, description)
 SELECT new_invoice.id, a,b,c,d FROM v, new_invoice;

假设您想要插入new_invoice和这些值的笛卡尔积,这在new_invoice实际上是单行值时才有意义。


请查看编辑1 - mark
1
目前我只是将$28转换为int类型,接下来的行默认假定相同的转换。我想知道是否可以在CTE中声明类型。 - mark
@epox的回答中的方法似乎更加简洁 (https://dev59.com/sHjZa4cB1Zd3GeqPbkD-#65517141)。它的效果与此处给出的答案有何不同? - Rich Apodaca
1
@RichApodaca:当new_invoice中有多行时,列表达式(select id from new_invoice)将失败。实际上,这可能并不重要,因为您很可能一次只想插入一个发票。 - Daniel Vérité

3
WITH new_invoice AS (
    INSERT INTO invoices ...
    RETURNING id
)

INSERT INTO invoiceItems (invoice_id, name, qty, price, description)
VALUES ((select id from new_invoice), $27 , $28, $29,   $30),
       ((select id from new_invoice), $31 , $32, $33,   $34),
       ((select id from new_invoice), $35 , $36, $37,   $38);

1
以比其他答案更少的复杂性准确地执行问题所要求的操作。 - Rich Apodaca

1

不要使用插入...值...,而是使用插入...选择...:

) INSERT INTO invoiceItems (invoice_id, name, qty, price, description)
SELECT new_invoice.id,$27,$28,$29,$30 FROM new_invoice
UNION ALL
...

请参见 EDIT 2 - mark
将第一行或所有行转换为:($27)::text,($28)::int等。 - Denis de Bernardy
这是由于Postgres强制类型的方式造成的。使用单个select语句时,它将根据语句的插入部分推断类型,而使用union时,它将根据联合的第一行推断类型,并因缺乏提示而回退到文本。 - Denis de Bernardy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接