资源嵌入

PostgREST 允许在单个 API 调用中包含相关资源。这减少了对多个 API 请求的需求。

外键连接

服务器使用 **外键** 来确定哪些数据库对象可以连接在一起。它支持连接表、视图和表值函数。

  • 对于表,它使用外键列(尊重复合键)生成连接条件。

  • 对于视图,它使用视图的基表外键列生成连接条件。

  • 对于表值函数,它根据返回的表类型的外键列生成连接条件。

重要

关系

例如,考虑一个电影及其奖项的数据库

../../_images/film.png

多对一关系

由于 filmsdirectors 有一个 **外键**,这建立了一个多对一关系。这使我们能够请求所有电影以及每部电影的导演。

curl "http://localhost:3000/films?select=title,directors(id,last_name)"
[
  { "title": "Workers Leaving The Lumière Factory In Lyon",
    "directors": {
      "id": 2,
      "last_name": "Lumière"
    }
  },
  { "title": "The Dickson Experimental Sound Film",
    "directors": {
      "id": 1,
      "last_name": "Dickson"
    }
  },
  { "title": "The Haunted Castle",
    "directors": {
      "id": 3,
      "last_name": "Méliès"
    }
  }
]

请注意,嵌入的 directors 由于“一对一”端而作为 JSON 对象返回。

由于表名是复数,我们可以通过使用别名将其变为单数来更准确。

curl "http://localhost:3000/films?select=title,director:directors(id,last_name)"
[
  { "title": "Workers Leaving The Lumière Factory In Lyon",
    "director": {
      "id": 2,
      "last_name": "Lumière"
    }
  },
  ".."
]

一对多关系

**外键引用** 建立了反向一对多关系。在这种情况下,films 由于“多对一”端而作为 JSON 数组返回。

curl "http://localhost:3000/directors?select=last_name,films(title)"
[
  { "last_name": "Lumière",
    "films": [
      {"title": "Workers Leaving The Lumière Factory In Lyon"}
    ]
  },
  { "last_name": "Dickson",
    "films": [
      {"title": "The Dickson Experimental Sound Film"}
    ]
  },
  { "last_name": "Méliès",
    "films": [
      {"title": "The Haunted Castle"}
    ]
  }
]

多对多关系

联接表确定多对多关系。它必须包含指向其他两个表的外部键,并且它们必须是其复合键的一部分。在 示例电影数据库 中,roles 被视为联接表。

如果复合键包含其他列,也会检测到连接表。

create table roles(
  id int generated always as identity,
, film_id int references films(id)
, actor_id int references actors(id)
, character text,
, primary key(id, film_id, actor_id)
);
curl "http://localhost:3000/actors?select=first_name,last_name,films(title)"
[
  { "first_name": "Willem",
    "last_name": "Dafoe",
    "films": [
      {"title": "The Lighthouse"}
    ]
  },
  ".."
]

一对一关系

一对一关系可以通过两种方式检测到。

  • 当外键是主键时,如 示例电影数据库 中所指定。

  • 当外键具有唯一约束时。

    create table technical_specs(
      film_id int references films(id) unique,
      runtime time,
      camera text,
      sound text
    );
    
curl "http://localhost:3000/films?select=title,technical_specs(camera)"
[
  {
    "title": "Pulp Fiction",
    "technical_specs": {"camera": "Arriflex 35-III"}
  },
  ".."
]

计算关系

您可以使用函数手动定义关系。这对于无法定义外键的数据库对象很有用,例如 外部数据包装器

假设有一个外部表 premieres,我们希望将其与 films 关联。

create foreign table premieres (
  id integer,
  location text,
  "date" date,
  film_id integer
) server import_csv options ( filename '/tmp/directors.csv', format 'csv');

create function film(premieres) returns setof films rows 1 as $$
  select * from films where id = $1.film_id
$$ stable language sql;

上面的函数定义了 premieres(参数)和 films(返回类型)之间的关系。由于存在 rows 1,因此定义了多对一关系。函数名称 film 是任意的,可以用于进行嵌入。

curl "http://localhost:3000/premieres?select=location,film(name)"
[
  {
    "location": "Cannes Film Festival",
    "film": {"name": "Pulp Fiction"}
  },
  ".."
]

现在让我们定义相反的一对多关系。

create function premieres(films) returns setof premieres as $$
  select * from premieres where film_id = $1.id
$$ stable language sql;

在这种情况下,PostgreSQL 定义了一个隐式的 ROWS 1000在此 PostgreSQL 文档中搜索“result_rows”)。我们认为任何大于 1 的值都是“多”,因此这定义了一对多关系。

curl "http://localhost:3000/films?select=name,premieres(name)"
[
  {
    "name": "Pulp Ficiton",
    "premieres": [{"location": "Cannes Festival"}]
  },
  ".."
]

覆盖关系

计算关系还允许您覆盖 PostgREST 自动检测的关系。

例如,要覆盖 filmsdirectors 之间的 多对一关系

create function directors(films) returns setof directors rows 1 as $$
  select * from directors where id = $1.director_id
$$ stable language sql;

由于函数重载,您可以对不同的参数使用相同的函数名。因此,从其他表/视图定义到导演的关系。

create function directors(film_schools) returns setof directors as $$
  select * from directors where film_school_id = $1.id
$$ stable language sql;

计算关系具有良好的性能,因为它们的设计意图允许 函数内联

警告

  • 在创建计算关系时始终使用 SETOF。函数可以在不使用 SETOF 的情况下返回一个表,但请记住,PostgreSQL 不会内联它们。

  • 确保正确标记关系的 to-one 部分。当使用 ROWS 1 估计时,PostgREST 会期望返回单行。如果不是这种情况,它将展开嵌入并为顶级资源返回重复的值。

多个外键关系上的外键连接

当表之间存在多个外键时,外键连接 需要消除歧义以确定要用于连接的外键列。为此,您可以使用 !<fk> 语法指定外键。

多个多对一

例如,假设您有以下 ordersaddresses

../../_images/orders.png

由于 orders 表有两个指向 addresses 表的外键,因此外键连接存在歧义,PostgREST 将返回错误

curl "http://localhost:3000/orders?select=*,addresses(*)" -i
HTTP/1.1 300 Multiple Choices
{
  "code": "PGRST201",
  "details": [
    {
      "cardinality": "many-to-one",
      "embedding": "orders with addresses",
      "relationship": "billing using orders(billing_address_id) and addresses(id)"
    },
    {
      "cardinality": "many-to-one",
      "embedding": "orders with addresses",
      "relationship": "shipping using orders(shipping_address_id) and addresses(id)"
    }
  ],
  "hint": "Try changing 'addresses' to one of the following: 'addresses!billing', 'addresses!shipping'. Find the desired relationship in the 'details' key.",
  "message": "Could not embed because more than one relationship was found for 'orders' and 'addresses'"
}

要成功将 ordersaddresses 连接,我们可以遵循错误 hint,它告诉我们将外键名称添加为 !billing!shipping。请注意,外键已在 上面的 SQL 定义 中显式命名。为了使结果更清晰,我们还将对表进行别名

# curl "http://localhost:3000/orders?select=name,billing_address:addresses!billing(name),shipping_address:addresses!shipping(name)"

curl --get "http://localhost:3000/orders" \
  -d "select=name,billing_address:addresses!billing(name),shipping_address:addresses!shipping(name)"
[
  {
    "name": "Personal Water Filter",
    "billing_address": {
      "name": "32 Glenlake Dr.Dearborn, MI 48124"
    },
    "shipping_address": {
      "name": "30 Glenlake Dr.Dearborn, MI 48124"
    }
  }
]

多个一对多

让我们从 多个多对一 中获取表。要获得相反的一对多关系,我们也可以指定外键名称

# curl "http://localhost:3000/addresses?select=name,billing_orders:orders!billing(name),shipping_orders!shipping(name)&id=eq.1"

curl --get "http://localhost:3000/addresses" \
  -d "select=name,billing_orders:orders!billing(name),shipping_orders!shipping(name)" \
  -d "id=eq.1"
[
  {
    "name": "32 Glenlake Dr.Dearborn, MI 48124",
    "billing_orders": [
      { "name": "Personal Water Filter" },
      { "name": "Coffee Machine" }
    ],
    "shipping_orders": [
      { "name": "Coffee Machine" }
    ]
  }
]

递归关系

为了消除递归关系的歧义,PostgREST 需要使用 计算关系

递归一对一

../../_images/presidents.png

要获取递归一对一关系的任一端,请创建以下函数

create or replace function predecessor(presidents) returns setof presidents rows 1 as $$
  select * from presidents where id = $1.predecessor_id
$$ stable language sql;

create or replace function successor(presidents) returns setof presidents rows 1 as $$
  select * from presidents where predecessor_id = $1.id
$$ stable language sql;

现在,要查询总统及其前任和继任者

# curl "http://localhost:3000/presidents?select=last_name,predecessor(last_name),successor(last_name)&id=eq.2"

curl --get "http://localhost:3000/presidents" \
  -d "select=last_name,predecessor(last_name),successor(last_name)" \
  -d "id=eq.2"
[
  {
    "last_name": "Adams",
    "predecessor": {
      "last_name": "Washington"
    },
    "successor": {
      "last_name": "Jefferson"
    }
  }
]

递归一对多

../../_images/employees.png

要获取一对多嵌入,即主管及其下属,请创建类似于以下的函数

create or replace function supervisees(employees) returns setof employees as $$
  select * from employees where supervisor_id = $1.id
$$ stable language sql;

现在,查询将是

# curl "http://localhost:3000/employees?select=last_name,supervisees(last_name)&id=eq.1"

curl --get "http://localhost:3000/employees" \
  -d "select=last_name,supervisees(last_name)" \
  -d "id=eq.1"
[
  {
    "name": "Taylor",
    "supervisees": [
      { "name": "Johnson" },
      { "name": "Miller" }
    ]
  }
]

递归多对一

让我们使用 递归一对多 中的相同 employees 表。要获取多对一关系,即员工及其各自的主管,您需要创建类似于以下的函数

create or replace function supervisor(employees) returns setof employees rows 1 as $$
  select * from employees where id = $1.supervisor_id
$$ stable language sql;

然后,查询将是

# curl "http://localhost:3000/employees?select=last_name,supervisor(last_name)&id=eq.3"

curl --get "http://localhost:3000/employees" \
  -d "select=last_name,supervisor(last_name)" \
  -d "id=eq.3"
[
  {
    "last_name": "Miller",
    "supervisor": {
      "last_name": "Taylor"
    }
  }
]

递归多对多

../../_images/users.png

要获取用户的全部订阅者以及他们正在关注的用户,请定义以下函数

create or replace function subscribers(users) returns setof users as $$
  select u.*
  from users u,
       subscriptions s
  where s.subscriber_id = u.id and
        s.subscribed_id = $1.id
$$ stable language sql;

create or replace function following(users) returns setof users as $$
  select u.*
  from users u,
       subscriptions s
  where s.subscribed_id = u.id and
        s.subscriber_id = $1.id
$$ stable language sql;

然后,请求将是

# curl "http://localhost:3000/users?select=username,subscribers(username),following(username)&id=eq.4"

curl --get "http://localhost:3000/users" \
  -d "select=username,subscribers(username),following(username)" \
  -d "id=eq.4"
[
  {
    "username": "the_top_artist",
    "subscribers": [
      { "username": "patrick109" },
      { "username": "alicia_smith" }
    ],
    "following": [
      { "username": "top_streamer" }
    ]
  }
]

分区表上的外键连接

外键连接也可以在 分区表 和其他表之间进行。

例如,让我们创建 box_office 分区表,其中包含电影的每日总收入

../../_images/boxoffice.png

由于它包含 films_id 外键,因此可以连接 box_officefilms

# curl "http://localhost:3000/box_office?select=bo_date,gross_revenue,films(title)&gross_revenue=gte.1000000"

curl --get "http://localhost:3000/box_office" \
  -d "select=bo_date,gross_revenue,films(title)" \
  -d "gross_revenue=gte.1000000"

注意

  • 不允许对分区进行外键连接,因为这会导致它们与其父分区表之间的歧义错误(请参阅 多个外键关系上的外键连接)。如果需要,可以使用 计算关系

  • 分区表可以从 PostgreSQL 11 开始引用其他表,但只能从 PostgreSQL 12 开始被其他任何表引用。

视图上的外键连接

PostgREST 将使用视图的基础表推断视图的外键。基础表是在视图定义的 FROMJOIN 子句中引用的表。外键的列必须出现在视图的顶部 SELECT 子句中才能使此方法起作用。

例如,以下视图具有 nominationsfilmscompetitions 作为基础表

CREATE VIEW nominations_view AS
  SELECT
     films.title as film_title
   , competitions.name as competition_name
   , nominations.rank
   , nominations.film_id as nominations_film_id
   , films.id as film_id
  FROM nominations
  JOIN films ON films.id = nominations.film_id
  JOIN competitions ON competitions.id = nominations.competition_id;

由于此视图包含 nominations.film_id,它与 films 表具有 **外键** 关系,因此我们可以连接 films 表。类似地,因为视图包含 films.id,所以我们也可以连接 rolesactors 表(最后一个是多对多关系)。

# curl "http://localhost:3000/nominations_view?select=film_title,films(language),roles(character),actors(last_name,first_name)&rank=eq.5"

curl --get "http://localhost:3000/nominations_view" \
  -d "select=film_title,films(language),roles(character),actors(last_name,first_name)" \
  -d "rank=eq.5"

也可以对 物化视图 进行外键连接。

重要

  • 不能保证外键连接对所有类型的视图都有效。特别是,外键连接不适用于包含 UNION 的视图。

    • 为什么?PostgREST 通过查询和解析 pg_rewrite 来检测视图中的基表外键。这可能会根据视图的复杂性而失败。

    • 作为解决方法,您可以使用 计算关系 为视图定义手动关系。

  • 如果视图定义发生更改,您必须刷新 PostgREST 的模式缓存才能使其正常工作。请参阅 模式缓存重新加载 部分。

视图链上的外键连接

视图也可以依赖于其他视图,而这些视图又依赖于实际的基表。为了让 PostgREST 递归地拾取这些链到任何深度,所有视图都必须在搜索路径中,因此要么在公开的模式 (db-schemas) 中,要么在 db-extra-search-path 中设置的某个模式中。这并不适用于基表,基表也可以在私有模式中。有关更多详细信息,请参阅 模式隔离

表值函数上的外键连接

如果您有一个 函数 返回表类型,您可以在结果上进行外键连接。

这是一个示例函数(注意 RETURNS SETOF films)。

CREATE FUNCTION getallfilms() RETURNS SETOF films AS $$
  SELECT * FROM films;
$$ LANGUAGE SQL STABLE;

包含 directors 的请求

# curl "http://localhost:3000/rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers*"

curl --get "http://localhost:3000/rpc/getallfilms" \
  -d "select=title,directors(id,last_name)" \
  -d "title=like.*Workers*"
[
  { "title": "Workers Leaving The Lumière Factory In Lyon",
    "directors": {
      "id": 2,
      "last_name": "Lumière"
    }
  }
]

写入上的外键连接

在执行 插入更新删除 操作后,您可以将相关的数据库对象连接起来。

例如,您想插入一部电影,然后获取其部分属性并连接其导演

curl "http://localhost:3000/films?select=title,year,director:directors(first_name,last_name)" \
  -H "Prefer: return=representation" \
  -d @- << EOF
  {
    "id": 100,
    "director_id": 40,
    "title": "127 hours",
    "year": 2010,
    "rating": 7.6,
    "language": "english"
  }
EOF

响应

{
 "title": "127 hours",
 "year": 2010,
 "director": {
   "first_name": "Danny",
   "last_name": "Boyle"
 }
}

嵌套嵌入

如果您想通过连接表进行嵌入,但需要对中间资源有更多控制,您可以进行嵌套嵌入。例如,您可以请求演员、他们的角色以及这些角色的电影。

curl "http://localhost:3000/actors?select=roles(character,films(title,year))"

嵌入式过滤器

嵌入式资源的塑造方式类似于其顶层资源。为此,请在查询参数前加上嵌入式资源的名称。例如,要对每部电影中的演员进行排序

# curl "http://localhost:3000/films?select=*,actors(*)&actors.order=last_name,first_name"

curl --get "http://localhost:3000/films" \
  -d "select=*,actors(*)" \
  -d "actors.order=last_name,first_name"

这会对每部电影中的演员列表进行排序,但不会改变电影本身的顺序。要过滤每部电影返回的角色

# curl "http://localhost:3000/films?select=*,roles(*)&roles.character=in.(Chico,Harpo,Groucho)"

curl --get "http://localhost:3000/films" \
  -d "select=*,roles(*)" \
  -d "roles.character=in.(Chico,Harpo,Groucho)"

同样,这会将包含的角色限制在某些角色,但不会以任何方式过滤电影。没有这些角色的电影将与空角色列表一起包含。

可以使用 or 过滤器进行类似的操作

# curl "http://localhost:3000/films?select=*,roles(*)&roles.or=(character.eq.Gummo,character.eq.Zeppo)"

curl --get "http://localhost:3000/films" \
 -d "select=*,roles(*)" \
 -d "roles.or=(character.eq.Gummo,character.eq.Zeppo)"

但是,这仅适用于 roles 内的列。请参阅 如何在多个资源中使用“或”

限制和偏移操作是可能的

# curl "http://localhost:3000/films?select=*,actors(*)&actors.limit=10&actors.offset=2"

curl --get "http://localhost:3000/films" \
  -d "select=*,actors(*)" \
  -d "actors.limit=10" \
  -d "actors.offset=2"

嵌入式资源可以被别名化,并且可以对这些别名应用过滤器

# curl "http://localhost:3000/films?select=*,actors(*)&actors.limit=10&actors.offset=2"

curl --get "http://localhost:3000/films" \
  -d "select=*,90_comps:competitions(name),91_comps:competitions(name)" \
  -d "90_comps.year=eq.1990" \
  -d "91_comps.year=eq.1991"

过滤器也可以应用于嵌套的嵌入式资源

curl "http://localhost:3000/films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom*"

结果将显示名为 Tom 的嵌套演员,并按姓氏排序。别名也可以用作资源名称来过滤嵌套表。

顶层过滤

默认情况下,嵌入式过滤器 不会更改顶层资源 (films) 行

# curl "http://localhost:3000/films?select=title,actors(first_name,last_name)&actors.first_name=eq.Jehanne

curl --get "http://localhost:3000/films" \
  -d "select=title,actors(first_name,last_name)" \
  -d "actors.first_name=eq.Jehanne"
[
  {
    "title": "Workers Leaving The Lumière Factory In Lyon",
    "actors": []
  },
  {
    "title": "The Dickson Experimental Sound Film",
    "actors": []
  },
  {
    "title": "The Haunted Castle",
    "actors": [
      {
        "first_name": "Jehanne",
        "last_name": "d'Alcy"
      }
    ]
  }
]

为了过滤顶层行,您需要在嵌入式资源中添加 !inner。例如,要获取包含名为 Jehanne 的演员的电影

# curl "http://localhost:3000/films?select=title,actors!inner(first_name,last_name)&actors.first_name=eq.Jehanne"

curl --get "http://localhost:3000/films" \
  -d "select=title,actors!inner(first_name,last_name)" \
  -d "actors.first_name=eq.Jehanne"
[
  {
    "title": "The Haunted Castle",
    "actors": [
      {
        "first_name": "Jehanne",
        "last_name": "d'Alcy"
      }
    ]
  }
]

嵌入资源的空值过滤

嵌入资源的空值过滤可以与 !inner 的行为相同,但更灵活。

例如,执行 actors=not.is.null 返回的结果与 actors!inner(*) 相同。

# curl "http://localhost:3000/films?select=title,actors(*)&actors=not.is.null"

curl --get "http://localhost:3000/films" \
  -d "select=title,actors(*)" \
  -d "actors=not.is.null"

在嵌入资源中可以使用 is.null 过滤器执行反连接。获取所有没有提名的电影。

# curl "http://localhost:3000/films?select=title,nominations()&nominations=is.null"

curl --get "http://localhost:3000/films" \
  -d "select=title,nominations()" \
  -d "nominations=is.null"

is.nullnot.is.null 都可以包含在 or 运算符中。例如,要获取没有演员或导演注册的电影。

# curl "http://localhost:3000/films?select=title,nominations()&nominations=is.null"

curl --get "http://localhost:3000/films" \
  -d select=title,actors(*),directors(*)" \
  -d "or=(actors.is.null,directors.is.null)"

跨嵌入资源的 OR 过滤

您也可以使用 not.is.null 在多个资源上进行 or 过滤。例如,要显示演员或导演名为 John 的电影。

# curl "http://localhost:3000/films?select=title,actors(),directors()&directors.first_name=eq.John&actors.first_name=eq.John&or=(directors.not.is.null,actors.not.is.null)"

curl --get "http://localhost:3000/films" \
  -d "select=title,actors(),directors()" \
  -d "directors.first_name=eq.John" \
  -d "actors.first_name=eq.John" \
  -d "or=(directors.not.is.null,actors.not.is.null)"
[
  { "title": "Pulp Fiction" },
  { "title": "The Thing" },
  ".."
]

这里,我们使用 空嵌入,因为检索其信息将受到过滤器的限制。例如,如果 directors 嵌入的 first_name 不是 John,则它将返回 null。要解决此问题,您需要添加额外的嵌入资源,并使用空嵌入资源进行过滤。从上面的例子来看

# curl "http://localhost:3000/films?select=title,act:actors(),dir:directors(),actors(first_name),directors(first_name)&dir.first_name=eq.John&act.first_name=eq.John&or=(dir.not.is.null,act.not.is.null)"

curl --get "http://localhost:3000/films" \
  # We need to use aliases like "act" and "dir" to filter the empty embeds
  -d "select=title,act:actors(),dir:directors(),actors(first_name),directors(first_name)" \
  -d "dir.first_name=eq.John" \
  -d "act.first_name=eq.John" \
  -d "or=(dir.not.is.null,act.not.is.null)"
[
  {
    "title": "Pulp Fiction",
    "actors": [
      { "first_name": "John" },
      { "first_name": "Samuel" },
      { "first_name": "Uma" },
      ".."
    ]
    "directors": {
      "first_name": "Quentin"
    }
  },
  ".."
]

空嵌入

您可以将嵌入资源留空,这在某些情况下有助于过滤。

按演员过滤电影,但不包含它们。

# curl "http://localhost:3000/films?select=title,actors()&actors.first_name=eq.Jehanne&actors=not.is.null"

curl --get "http://localhost:3000/films" \
  -d "select=title,actors()" \
  -d "actors.first_name=eq.Jehanne" \
  -d "actors=not.is.null"
[
  {
    "title": "The Haunted Castle",
  }
]

顶层排序

多对一一对一 关系中,您可以使用“一对一”端的列对顶层进行排序。

例如,使用导演的姓氏按降序排列电影。

# curl "http://localhost:3000/films?select=title,directors(last_name)&order=directors(last_name).desc"

curl --get "http://localhost:3000/films" \
  -d "select=title,directors(last_name)" \
  -d "order=directors(last_name).desc"

扩展嵌入资源

在多对一和一对一关系中,您可以“扩展”嵌入资源。也就是说,删除嵌入资源列周围的 JSON 对象。

 # curl "http://localhost:3000/films?select=title,...directors(director_last_name:last_name)&title=like.*Workers*"

curl --get "http://localhost:3000/films" \
  -d "select=title,...directors(director_last_name:last_name)" \
  -d "title=like.*Workers*"
[
  {
    "title": "Workers Leaving The Lumière Factory In Lyon",
    "director_last_name": "Lumière"
  }
]

请注意,没有 "directors" 对象。此外,嵌入列可以像平常一样被别名化。

您可以使用它来获取多对多关系中联接表的列。例如,要获取电影及其演员,但包括角色表中的 character

 # curl "http://localhost:3000/films?select=title,actors:roles(character,...actors(first_name,last_name))&title=like.*Lighthouse*"

curl --get "http://localhost:3000/films" \
  -d "select=title,actors:roles(character,...actors(first_name,last_name))" \
  -d "title=like.*Lighthouse*"
[
  {
    "title": "The Lighthouse",
    "actors": [
       {
         "character": "Thomas Wake",
         "first_name": "Willem",
         "last_name": "Dafoe"
       }
    ]
  }
]

注意

扩展运算符 ... 来自 Javascript 的 扩展语法