重构函数,以便它们可以在CTE中使用

时间:2018-01-08 13:23:31

标签: sql postgresql refactoring

我有一个parentchild表格如下:

create table parent
(
  identifier serial primary key,
  name text
);
create table child
(
  identifier serial primary key,
  name text, parent_identifier integer references parent
);

我创建了两个实用程序函数来将parentchild行格式化为JSON对象:

create function format(child) returns json
  as $$
  select json_build_object('identifier', $1.identifier, 'name', $1.name)
  $$ language sql stable;

create function format(parent) returns json
  as $$
  select json_build_object('identifier', $1.identifier, 'name', $1.name, 
                           'children', array(select format(child) from child where parent_identifier = $1.identifier))
  $$ language sql stable;

让我们测试一下:

insert into parent(name) values('first parent');
insert into parent(name) values('second parent');

insert into child(name, parent_identifier) values('first child first parent', (select identifier from parent where name = 'first parent'));
insert into child(name, parent_identifier) values('second child first parent', (select identifier from parent where name = 'first parent'));
insert into child(name, parent_identifier) values('first child second parent', (select identifier from parent where name = 'second parent'));

select format(parent) from parent;

这将返回以下JSON对象:

{
   "identifier":5,
   "name":"first parent",
   "children":[
      {
        "identifier":7,
        "name":"first child first parent"
      },
      {
        "identifier":8,
        "name":"second child first parent"
      }
   ]
}
{
   "identifier":6,
   "name":"second parent",
   "children":[
      {
        "identifier":9,
        "name":"first child second parent"
      }
   ]
}

大!但是,这有一个大问题:如果另一个事务在我们的insertselect查询之间进行了一些更改,则select查询会完全返回我们刚插入的内容。我们可以通过将事务隔离级别设置为repeatable read来解决此问题,但这会产生性能成本和其他缺点(我们可能需要重试)。

所以我想在一个CTE中重写上面的查询。如果我没有弄错的话,这不会受到这种并发问题的影响。我开始如下:

with
parents as
(
  insert into parent(name)
  select 'first parent'
  union all
  select 'second parent'
  returning parent.identifier, parent.name
),
children as
(
  insert into child(name, parent_identifier)
  select 'first child first parent', identifier from parents where name = 'first parent'
  union all
  select 'second child first parent', identifier from parents where name = 'first parent'
  union all
  select 'first child second parent', identifier from parents where name = 'second parent'
)
select format(parents::parent) from parents;

这不能按预期工作。它返回以下JSON对象:

{
   "identifier":7,
   "name":"first parent",
   "children":[]
}
{
   "identifier":8,
   "name":"second parent",
   "children":[]
}

如您所见,不包括儿童。经过一些阅读,我明白了发生了什么。 CTE适用于在查询开始之前创建的快照。在format(parent)中,我们正在执行select format(child) from child where parent_identifier = $1.identifier),但这不会产生任何子行,因为子行不在快照中。所以我的问题与此无关,正如我所理解的那样。

当然,如果我在主查询中简单地执行json_build_object内容,与format函数完全相同,我可以轻松解决这个问题,但后来我复制了代码。我在其他查询中也使用这些format函数,与此问题无关。理想情况下,我想在我的解决方案中避免代码重复。所以我想继续使用它们,可能需要先重构它们才能在这个问题的场景中使用它们。

我现在很困惑。我真的想继续使用CTE(因此我可以避免将事务隔离级别设置为repeatable read),但我无法找到重新计算format(parent)和{{1}的因素的方法函数和/或CTE,所以我不会在所有地方重复代码重复。是否有一个聪明的灵魂与一些聪明的想法?

请注意,我使用的是PostgreSQL 10.1。请在这里找到一个小提琴:http://sqlfiddle.com/#!17/a251d/2

关于Laurenz Albe回答的

更新

背景:https://stackoverflow.com/revisions/48152380/1

在上面的问题中,我确实简化了我的情况。让我更仔细地解释真实场景,而不会涉及太多令人困惑的细节。

在该方案中,用户正在为特定日期范围(例如2018年1月)提供数据(= format(child)及其对应的parents)。另外,我不只是在插入,我实际上正在做孤立行的upserts和删除。因此,场景很简单:客户端正在替换给定日期范围内的所有数据。

如果我在upserts和deletes之后执行children,则其他一些客户端可能会更改其间的重叠日期范围。在这种情况下,我返回客户端提供的不同结果,如果客户端未正确实现,可能会引入错误。因此,这就是为什么我认为插入和选择需要成为事务隔离级别设置为select format(parent) from parent where <parent is in date range as provided>的同一事务的一部分。

然而,我开始思考一个单一的,肥胖的CTE,因此我的问题。

我希望这可以澄清这种情况。

1 个答案:

答案 0 :(得分:1)

正如您所注意到的那样,您无法在主SELECT中看到CTE中修改的行。这is documented

  

WITH中的子语句彼此同时执行   并与主要查询。因此,在使用数据修改语句时   在WITH中,指定更新实际发生的顺序是   不可预知的。所有语句都使用相同的快照执行   (见Chapter 13),所以他们不能“看到”彼此   对目标表的影响。这减轻了影响   行更新的实际顺序的不可预测性,并且意味着   RETURNING数据是在不同之间传递变化的唯一方式   WITH子语句和主要查询。

所以你应该使用RETURNING

我想最简单的方法是不使用函数,而是在主查询中执行json_build_object并使其在CTE parentschildren上运行。