@dataplan/pg 中的多态性
PostgreSQL 模式中的多态性可以有多种形式。@dataplan/pg
提供了两种主要方法来处理这种多态性:pgSelect
(只要所有数据都来自单个表,或者单个表左连接到其他表,它就具有多态性能力),以及 pgUnionAll
(它允许您通过 SQL UNION ALL
结构从多个不同的(独立的)数据库表中提取数据)。这两个步骤类在很多方面相似,但 pgUnionAll
的限制要多得多,以便在处理复杂设置时也能保持性能。
继续阅读以了解这些示例。
支持的多态性类型
在数据库中建模多态性有很多方法,它们各有优劣。@dataplan/pg
目前支持以下方法,但如果您在数据库中使用其他方法来建模多态性,请与我们联系 - 也许我们可以添加对该方法的支持!
单个表
在这种多态性形式中,我们有一个包含“类型”列的单个数据库表,该列指示行的类型(它不必称为“类型”,并且可以具有任何数据类型 - 我们在下面使用枚举,但这并不是必需的)。该表还包含接口或联合1中所有不同类型所需的所有字段。例如
create type item_type as enum (
'TOPIC',
'POST',
'DIVIDER',
'CHECKLIST',
'CHECKLIST_ITEM'
);
create table items (
id serial primary key,
type item_type not null default 'POST'::item_type,
-- Shared attributes:
parent_id int references items on delete cascade,
author_id int not null references people on delete cascade,
position bigint not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
is_explicitly_archived bool not null default false,
archived_at timestamptz,
-- Attributes that may be used by one or more item subtypes.
title text,
description text,
note text,
color text
);
items
表包含我们 GraphQL 类型 Topic
、Post
、Divider
、Checklist
和 ChecklistItem
所需的所有信息。
这种多态性风格可以使用 pgSelect
,就像您使用常规行选择一样,但是您的 pgSelect
使用的源上的编解码器必须将 polymorphic
配置选项设置为 mode: "single"
才能正常工作。类似于
itemResource.codec.polymorphism = {
mode: "single",
typeAttributes: ["type"],
types: {
TOPIC: {
name: "Topic",
},
POST: {
name: "Post",
},
DIVIDER: {
name: "Divider",
},
CHECKLIST: {
name: "Checklist",
},
CHECKLIST_ITEM: {
name: "ChecklistItem",
},
},
};
或者,如果您不想更改您的源/编解码器...
如果您不想更改您的源/编解码器,则可以使用 pgSingleTablePolymorphic
// Map the SQL 'type' values to their GraphQL equivalents
const sqlTypeToGraphQLType = (type) =>
({
TOPIC: "Topic",
POST: "Post",
DIVIDER: "Divider",
CHECKLIST: "Checklist",
CHECKLIST_ITEM: "ChecklistItem",
})[type] ?? null;
// Or: `const sqlTypeToGraphQLType = pascalCase;`
/******/
const plans = {
Comment: {
item($comment) {
// Get the 'item' related to this comment
const $item = $comment.singleRelation("item");
// Get the 'type' column from the item
const $type = $item.get("type");
// Convert the 'type' value into the name of a GraphQL type
const $typeName = lambda($type, sqlTypeToGraphQLType);
// Return the polymorphic step representing this item
return pgSingleTablePolymorphic($typeName, $item);
},
},
};
也可以使用 pgPolymorphic
来规划这种多态性风格。
pgPolymorphic
来规划这种多态性风格。所有 plan
方法都直接返回 $item
,因为 $item
代表所有可能的类型。
const itemsTypeMap = {
Topic: {
match: (t) => t === "TOPIC",
plan: (_, $item) => $item,
},
Post: {
match: (t) => t === "POST",
plan: (_, $item) => $item,
},
Divider: {
match: (t) => t === "DIVIDER",
plan: (_, $item) => $item,
},
Checklist: {
match: (t) => t === "CHECKLIST",
plan: (_, $item) => $item,
},
ChecklistItem: {
match: (t) => t === "CHECKLIST_ITEM",
plan: (_, $item) => $item,
},
};
/******/
const plans = {
Comment: {
item($comment) {
const $item = $comment.singleRelation("item");
const $type = $item.get("type");
return pgPolymorphic($item, $type, itemsTypeMap);
},
},
};
关系表
与上面的单表示例类似,关系表有一个中心表,其中包含一个“类型”列;但是每个类型(不共享)的字段位于单独的表中,这些表可以根据需要进行联接。这些关系表与中心表共享相同的主键,而中心表上的类型指示应联接哪个表。例如
create type item_type as enum (
'TOPIC',
'POST',
'DIVIDER',
'CHECKLIST',
'CHECKLIST_ITEM'
);
-- Central table
create table items (
id serial primary key,
type item_type not null default 'POST'::item_type,
-- Shared attributes:
parent_id int references items on delete cascade,
author_id int not null references people on delete cascade,
position bigint not null default 0,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
is_explicitly_archived bool not null default false,
archived_at timestamptz
);
-- Tables for each of the subtypes
create table topics (
id int primary key references items,
title text not null
);
create table posts (
id int primary key references items,
title text not null,
description text default '-- Enter description here --',
note text
);
create table dividers (
id int primary key references items,
title text,
color text
);
create table checklists (
id int primary key references items,
title text not null
);
create table checklist_items (
id int primary key references items,
description text not null,
note text
);
在构建这些表的 PgResource
时,子表应该在 columns
中为所有共享列添加条目,并使用 via: "item"
(将 "item"
替换为需要遍历以访问中心表的关联关系的名称)来指示这些列来自“item”关系。
这种模式也可以用来表示联合,在这种情况下,中心表可能只包含主键和类型字段。但是,如果您使用这种模式来表示联合,那么这可能表明您的 GraphQL 数据建模存在问题,您实际上想要的是一个接口。
这种类型的多态性可以使用 pgSelect
,就像您使用常规行选择一样,但是您的 pgSelect
使用的源的编解码器必须将 polymorphic
配置选项设置为 mode: "relational"
才能正常工作。类似于
itemResource.codec.polymorphic = {
mode: "relational",
typeAttributes: ["type"],
types: {
TOPIC: {
name: "Topic",
relationName: "topic",
},
POST: {
name: "Post",
relationName: "post",
},
DIVIDER: {
name: "Divider",
relationName: "divider",
},
CHECKLIST: {
name: "Checklist",
relationName: "checklist",
},
CHECKLIST_ITEM: {
name: "ChecklistItem",
relationName: "checklistItem",
},
},
};
上面的配置中的 relationName
是中心源具有的关联关系的名称,该关联关系链接到包含此类型附加数据的相关表。
或者,如果您不想更改编解码器...
这种类型的多态性可以通过pgPolymorphic
进行规划(注意plan
方法返回一个代表来自相关底层表的行的步骤)
const itemsTypeMap = {
Topic: {
match: (t) => t === "TOPIC",
plan: (_, $item) => $item.singleRelation("topic"),
},
Post: {
match: (t) => t === "POST",
plan: (_, $item) => $item.singleRelation("post"),
},
Divider: {
match: (t) => t === "DIVIDER",
plan: (_, $item) => $item.singleRelation("divider"),
},
Checklist: {
match: (t) => t === "CHECKLIST",
plan: (_, $item) => $item.singleRelation("checklist"),
},
ChecklistItem: {
match: (t) => t === "CHECKLIST_ITEM",
plan: (_, $item) => $item.singleRelation("checklistItem"),
},
};
const plans = {
Comment: {
item($comment) {
const $item = $comment.singleRelation("item");
const $type = $item.get("type");
return pgPolymorphic($item, $type, itemsTypeMap);
},
},
};
复合类型联合
指示联合的一种方法是使用复合类型,该类型为每种可能的类型提供一个属性,引用给定类型的主键;一次只有一个属性应该是非空的。例如
create table people (...);
create table posts (...);
create table comments (...);
create type entity as (
person_id int,
post_id int,
comment_id int
);
然后,此类型可以用作函数的返回结果,或用作列的类型以指示多态关系。
这种类型的多态性可以通过pgPolymorphic
进行规划(注意我们已经将规范器建模为元组)
const entityTypeMap = {
Person: {
match: (specifier) => specifier[0] != null,
plan: ($specifier) => personResource.get({ person_id: $specifier.at(0) }),
},
Post: {
match: (specifier) => specifier[1] != null,
plan: ($specifier) => postResource.get({ post_id: $specifier.at(1) }),
},
Comment: {
match: (specifier) => specifier[2] != null,
plan: ($specifier) => commentResource.get({ comment_id: $specifier.at(2) }),
},
};
const plans = {
PersonBookmark: {
bookmarkedEntity($bookmark) {
const $item = $bookmark.get("bookmarked_entity");
const $specifier = list([
$item.get("person_id"),
$item.get("post_id"),
$item.get("comment_id"),
]);
return pgPolymorphic($item, $specifier, entityTypeMap);
},
},
};
基于列的联合
指示联合关系的另一种方法是在表中添加一组列以指向联合中的所有可能实体,并添加一条规则,即其中只有一个必须设置。
考虑以下 GraphQL 架构,其中一个人可能有多个不同类型的收藏实体
union PersonFavouriteEntity = Person | Post | Comment
type PersonFavourite {
id: ID!
person: Person!
entity: PersonFavouriteEntity!
}
type Person {
# ...
favourites: [PersonFavourite!]!
}
type Query {
person: Person
}
此架构可能具有以下底层数据库表
create table person_favourites (
id serial primary key,
person_id int not null references people on delete cascade,
liked_person_id int references people on delete cascade,
liked_post_id int references posts on delete cascade,
liked_comment_id int references comments on delete cascade
);
在这里,我们可能设置一条规则,即liked_person_id
、liked_post_id
和liked_comment_id
中只有一个必须是非空的,而哪个是非空的将指示PersonFavouriteEntity
代表哪个具体类型。
我们可以使用pgUnionAll
来规划它
const plans = {
Person: {
favourites($person) {
const $favourites = personFavouritesResource.find({
person_id: $person.get("id"),
});
return each($favourites, ($favourite) => {
const $list = pgUnionAll({
attributes: {},
resourceByTypeName: {
Person: personResource,
Post: postResource,
Comment: pommentResource,
},
members: [
{
typeName: "Person",
resource: personResource,
match: {
id: $favourite.get("liked_person_id"),
},
},
{
typeName: "Post",
resource: postResource,
match: {
id: $favourite.get("liked_post_id"),
},
},
{
typeName: "Comment",
resource: commentResource,
match: {
id: $favourite.get("liked_comment_id"),
},
},
],
});
return $list.single();
});
},
},
};
或者,您可以使用pgPolymorphic
规划此操作可能与上面的复合类型联合非常相似
const personFavouriteEntityTypeMap = {
Person: {
match: (specifier) => specifier[0] != null,
plan: ($specifier) => personResource.get({ person_id: $specifier.at(0) }),
},
Post: {
match: (specifier) => specifier[1] != null,
plan: ($specifier) => postResource.get({ post_id: $specifier.at(1) }),
},
Comment: {
match: (specifier) => specifier[2] != null,
plan: ($specifier) => commentResource.get({ comment_id: $specifier.at(2) }),
},
};
const plans = {
Person: {
favourites($person) {
const $favourites = personFavouritesResource.find({
person_id: $person.get("id"),
});
return each($favourites, ($favourite) => {
const $specifier = list([
$favourite.get("liked_person_id"),
$favourite.get("liked_post_id"),
$favourite.get("liked_comment_id"),
]);
return pgPolymorphic(
$favourite,
$specifier,
personFavouriteEntityTypeMap,
);
});
},
},
};
完全独立的表
如果您有两个完全不同的表(例如users
和organizations
),并且您希望它们参与 GraphQL 接口或联合,您可以使用pgUnionAll来规划它们。
const plans = {
Query: {
allPeopleAndOrganizations() {
const $list = pgUnionAll({
resourceByTypeName: {
Person: personResource,
Organization: organizationResource,
},
});
return $list;
},
},
};
- 以这种方式对联合进行建模可能表明您的 GraphQL 设计存在问题 - 也许您应该使用接口?↩