具有一对多关系的 GraphQL 中的多态性



我的架构中有以下实体。

  • 用户
  • 链接帖子
  • 普通帖子

如何对UserPost类型之间的一对多关系进行建模,其中Post可以是两种类型LinkPostNormalPost

我会使用 GraphQL 中存在的多态类型之一对这些模式进行建模。这种方法将使你将来在查询和扩展方面具有最大的灵活性。

架构设计

对于User来说,拥有这样的Posts数组非常方便:

type User {
id: Int!
email: String!
posts: [Posts!]
username: String!
}

这意味着Post必须是interfaceunion类型。这两种类型中的任何一种都允许我们利用NormalPostLinkedPost仍然是一种Post的事实。它还将允许我们在同一位置查询它们,就像上面的user.posts一样。

接口

interface类型在行为上与 OOP 中的interface非常相似。它定义了比任何实现类型还必须具有的基本字段集。例如,所有Post对象可能如下所示:

interface Post {
id: Int!
author: String!
dateCreated: String!
dateUpdated: String!
title: String!
}

任何implementsPost接口的类型,除了特定于该类型的任何字段外,还必须实现字段idauthordateCreateddateUpdatedtitle。因此,使用interfaceNormalPostLinkedPost可能如下所示:

type NormalPost implements Post {
id: Int!
author: String!
body: String!
dateCreated: String!
dateUpdated: String!
title: String!
}
type LinkedPost implements Post {
id: Int!
author: String!
dateCreated: String!
dateUpdated: String!
link: String!
title: String!
}

工会

union类型允许一起返回不需要实现相似字段的不同类型。union类型的结构与使用接口的结构不同,因为union类型不指定任何字段。union架构定义中定义的唯一内容是由|分隔的联合类型。

该规则的唯一警告是union中定义了相同字段名称的任何类型都需要具有相同的可空性(即。因为NormalPost.title是不可为空的(title: String!),那么LinkedPost.title也必须是不可为空的。

union Post = NormalPost | LinkedPost
type NormalPost implements Post {
id: Int!
author: String!
body: String!
dateCreated: String!
dateUpdated: String!
title: String!
}
type LinkedPost implements Post {
id: Int!
author: String!
dateCreated: String!
dateUpdated: String!
link: String!
title: String!
}

查询

上面介绍了在查询LinkedPostNormalPost时如何区分user.posts的问题。在这两种情况下,您都需要使用条件片段。

条件片段允许从interfaceunion类型查询一组特定的字段。它们看起来与常规查询片段相同,因为它们是使用查询正文中的... on <Type>语法定义的。对于interfaceunion类型的条件片段的结构方式略有不同。

除了使用条件片段进行查询之外,将__typename元字段添加到多态查询中往往很有用,这样查询的使用者就可以更好地识别代码中结果对象的类型。

接口

由于interface定义了一组特定的字段,所有实现类型都具有共同的字段,因此可以像对类型进行任何其他普通查询一样查询这些字段。不同之处在于,当interface类型具有特定于其类型的不同字段时,例如NormalPost.bodyLinkedPost.link。选择整个Post接口,然后选择NormalPost.bodyLinkedPost.link的查询如下所示:

query getUsersNormalAndLinkedPosts {
user(id: 123) {
id
name
username
posts {
__typename
id
author
dateCreated
dateUpdated
title
... on NormalPost {
body
}
... on LinkedPost {
link
}
}
}
}

工会

由于union不会在其类型之间定义任何公共字段,因此要选择的所有字段都必须存在于每个条件片段中。这是查询interfaceunion之间的唯一区别。查询union类型如下所示:

query getUsersNormalAndLinkedPosts {
user(id: 123) {
id
name
username
posts {
__typename
... on NormalPost {  
id
author
body
dateCreated
dateUpdated
title
}
... on LinkedPost {
id
author
dateCreated
dateUpdated
link
title
}
}
}
}

结论

这两种多态类型都有优点和缺点,由您决定哪种最适合您的用例。我在构建 GraphQL 模式时已经使用了两者,当我使用interfaceunion时的具体区别在于实现类型之间是否有公共字段。

当实现类型之间的唯一区别只是一小部分字段而其余字段在它们之间共享时,接口非常有意义。这会导致查询更小,并且可能需要更少的条件片段。

当您有一种类型是多个不同类型的掩码时,工会真的会发光,这些类型不相关但又重新组合在一起,就像在一组搜索结果中一样。根据它返回的搜索类型,可能会返回许多看起来完全不一样的不同类型。例如,在CMS上进行搜索,可以同时产生用户和帖子。在这种情况下,具有以下类型是有意义的:

union SearchResult = User | Post.

然后可以从带有签名的查询中返回此信息

search(phrase: String!): [SearchResult!]

在这个特定问题的上下文中,我会采用interface方法,因为它从关系和查询的角度来看最有意义。

您是否考虑过使用枚举类型使用单个Post实体并定义帖子类型?

用户

type User {
id: ID!
posts: [Post!]!
}

发布

type Post {
id: ID!
title: String!
createdBy: User!
type: PostType!
}

以及Post的枚举类型

后类型

enum PostType {
NORMAL
LINK
}

-----更新!-----

如果要拥有单独的实体,可以这样做:

用户

type User {
id: ID!
linkPosts: [LinkPost!]!
normalPosts: [NormalPost]
}

链接帖子类型

type LinkPost {
description: String!
url: String!
createdBy: User!
}

正常开机自检类型

type NormalPost {
title: String!
description: String!
createdBy: User!
}

让我知道这是否适合您,

问候

最新更新