postgresql 如何传递一个元组数组作为批量UPSERT的参数?

envsm3lx  于 2023-06-29  发布在  PostgreSQL
关注(0)|答案(1)|浏览(106)

使用此模式:

CREATE TABLE tag (
  tag_id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  tag_slug text UNIQUE NOT NULL,
  tag_name text NOT NULL
);

我目前正在使用JavaScript生成一个批量UPSERT命令,如下所示(它使用knex.raw):

knex.raw(
  `
    INSERT INTO tag (tag_slug, tag_name)
    VALUES ${tags.map(() => `(?, ?)`).join(", ")}
    ON CONFLICT (tag_slug)
    DO UPDATE SET tag_name = excluded.tag_name
    RETURNING *
  `,
  tags.map((t) => [t.slug, t.text]).flat()
)

但我想将其转换为一个名为upsert_tags的存储过程,可以像这样调用(请随意将函数签名调整为同样或更符合人体工程学的东西):

call upsert_tags(
  [
    ('first-tag', 'First Tag'),
    ('second-tag', 'Second Tag')
  ]
);

我该怎么做?
这是我迄今为止最好的尝试,但它肯定不起作用!

CREATE PROCEDURE upsert_tags(tags array)
LANGUAGE SQL
AS $$
  INSERT INTO tag (tag_slug, tag_name) 
  VALUES (unnest(tags))
  ON CONFLICT (tag_slug)
  DO UPDATE SET tag_name = excluded.tag_name
  RETURNING *
$$

返回值只需要包含tag_id,因为我使用它将数据输入到直通表中,以记录many:many关系。

gfttwv5a

gfttwv5a1#

FUNCTION对比PROCEDURE

RETURNING *表示您希望返回一组行-准确地说是SETOF tagThe manual:
过程不返回函数值;因此CREATE PROCEDURE缺少RETURNS子句。但是,过程可以通过输出参数将数据返回给调用方。
输出参数不会削减它。PROCEDURE是一个错误的选择。使用FUNCTION代替。参见:

  • 如何从存储过程(而不是函数)返回值?

实现

许多可能的方式如何格式化输入数据。
有很多种方法可以正确地处理冲突。
返回数据的方法有很多种。
我们几乎可以做任何事情。我们甚至可以使它的大部分是动态的和/或通用的,以处理不同的表/列。(考虑将来可能对表进行的更改...)请参阅:

  • 重构PL/pgSQL函数以返回各种SELECT查询的输出

最好的解决方案取决于你需要什么 * 确切 *。演示两个实现。

实现一:传递两个独立的Postgres数组

对于两列,传递两个单独的数组可能比较方便。Postgres有一个专用的unnest()变体来并行解嵌套数组。参见:

  • 并行解嵌套多个阵列

所以:

CREATE OR REPLACE FUNCTION public.f_upsert_tags1(_tag_slugs text[], _tag_names text[])
  RETURNS SETOF public.tag
  LANGUAGE sql AS
$func$
INSERT INTO public.tag AS t (tag_slug, tag_name) 
SELECT *
FROM   unnest($1, $2)
ON     CONFLICT (tag_slug) DO UPDATE
SET    tag_name = EXCLUDED.tag_name
-- WHERE  t.tag_name <> EXCLUDED.tag_name  -- see below
RETURNING *
$func$;

电话:

SELECT *
FROM   public.f_upsert_tags1('{first-tag, second-tag}'
                           , '{First Tag, Second Tag}');

返回SETOF public.tag将返回与原始行相同的完整结果行。它引入了对表的行类型的依赖性,这有利有弊……
关于将数组传递给函数:

  • 将数组文字传递给PostgreSQL函数
  • PostgreSQL:向过程传递数组时出现问题
  • 向函数传递多个值集或数组

实现二:传递对象的JSON数组

您提到了JavaScript,所以传递一个JSON对象数组可能会很方便,然后我们用json_populate_recordset()分解它。(还有其他选项,如json_to_recordset()...)

CREATE OR REPLACE FUNCTION public.f_upsert_tags2(_tags json)
  RETURNS SETOF public.tag
  LANGUAGE sql AS
$func$
INSERT INTO public.tag AS t (tag_slug, tag_name) 
SELECT i.tag_slug, i.tag_name
FROM   json_populate_recordset(null::public.tag, $1) i
ON     CONFLICT (tag_slug) DO UPDATE
SET    tag_name = EXCLUDED.tag_name
-- WHERE  t.tag_name <> EXCLUDED.tag_name  -- see below
RETURNING *
$func$;

电话:

SELECT *
FROM   public.f_upsert_tags2('[{"tag_slug":"first-tag", "tag_name":"First Tag2"}
                             , {"tag_slug":"second-tag", "tag_name":"Second Tag2"}]');

fiddle

并发?表演?

如果可以从多个事务并发地调用函数(或者以任何方式在同一表上并发地竞争写入),则存在复杂的竞争条件。
这两种解决方案(包括您的原始解决方案)都会覆盖冲突的行,即使tag_name没有更改,这也会增加不做任何有用操作的成本。如果它发生了很多,这很重要。我们可以跳过这一步,但您可能仍然希望获得与输入匹配的完整输出行集?
对于这些问题,请参见:

*如何在PostgreSQL中使用RETURNING with ON CONFLICT?

如果您还想知道是否插入或更新了每一行,请参阅:

  • 检测行是否已更新或插入

最终实现

基于您在评论中添加的解决方案。对于您的特定情况,这种实现是有意义的。

  • “许多冲突但很少实际更新”
  • 并发写入实际上可以竞争相同的行
  • “返回值只需要tag_id”,以便后续通过表写入以记录多:多关系
CREATE FUNCTION upsert_tags(_tags json)
   RETURNS TABLE (tag_slug text, tag_id text)
   LANGUAGE sql AS
$func$
INSERT INTO tag AS t (tag_slug, tag_name)
SELECT i.tag_slug, i.tag_name
FROM   json_populate_recordset(null::public.tag, _tags) i
ON     CONFLICT (tag_slug) DO UPDATE
SET    tag_name = EXCLUDED.tag_name
WHERE  t.tag_name <> EXCLUDED.tag_name;  -- omit empty updates

-- New statement to include previously invisible, concurrently written rows
SELECT t.tag_slug, t.tag_id
FROM   json_populate_recordset(NULL::public.tag, _tags)
JOIN   public.tag t USING (tag_slug);  -- JOIN beats IN without need for removing dupes
$func$;

fiddle
我在结果行中添加了tag_slug以允许反向链接。
所有这些都假设您的输入中**没有重复项 *。否则你需要做更多。相关内容:

由于输入中没有重复,因此普通的JOIN在最终的SELECT中的性能优于ININ也会尝试删除右侧的重复项...
最后,关于对直通表后续写入:您可以将其集成到同一函数中以优化性能。相关内容:

  • 使用Postgres一次在3个表中插入数据

相关问题