在过去的几年里,GraphQL已经成为一个被广泛采用的标准。然而,并不是每个人都在利用该标准的全部功能。在本文中,我将演示创建更具表现力的模式以更好地满足您的数据需求的方法。
让我们从定义一个示例场景开始。我们正在构建一个论坛应用程序,它将拥有几种类型的用户:
管理论坛操作的管理员。例如,处理技术问题、指定版主等。
这样一个站点的架构可能是什么样子的?让我们从一个基本架构开始。此示例将使用Node Express和GraphQL项目。我假设您已经知道如何做到这一点,但稍后我将在本文的底部包含整个项目代码。创建一个typeDefs.ts文件,如下所示。请注意,我们使用的是TypeScript,因为类型注释有助于使代码更清晰。
从";appollo-server-express";导入{gql};const typeDefs=gql`类型StandardUser{id:id!用户名:String!明星:int!}类型版主{id:id!用户名:String!星星:INT!类别:[类别!]}类型管理员{id:id!用户名:字符串!}类型类别{id:id!名称:String!}`;导出默认typeDefs;
您会注意到,我们有三个用户类型,以及一个名为Category的附加类型,它表示我们的帖子将进入的组。然而,很明显,我们的字段之间存在重复(例如,id、用户名)。实际上,可以从三个用户相关类型中的每一个中提取一个“基类型”。让我们尝试通过使用Interfaces更新此架构来实现此目的。
界面只是某种形式的骨架。它是一个声明规则的约定,在本例中是特定的字段,但是它没有实现,因此不能返回数据。只有类型可以。但是,因为它确实提供了结构,所以我们使用接口来定义基本字段,然后让我们的类型实现这些接口。例如,如果我们使用Interfaces更新typeDefs,它可能如下所示。
Const typeDefs=gql`接口可识别{id:id!}接口用户实现可识别{id:id!Username:string!}接口评级{STARS:INT!}类型StandardUser实现可识别的&;用户&;评级{id:id!用户名:String!STARS:INT!}类型版主实现可识别&;用户&;评级{id:id!用户名:String!星星:INT!类别:[类别!]}类型管理员实现可识别的&;用户{id:id!UserName:string!}类型类别实现可识别的{id:id!名称:String!}`;
如果我们从顶部开始,您将看到我们的第一个界面名为“可识别”。因为我们的数据模型中的所有实体都有一个惟一的ID,所以这个接口将是我们所有实体的基础。接下来,我们看到用户界面实现了可识别接口。请注意,用户如何同时拥有id字段和自己的字段(名为username)。当类型或接口实现接口时,它们必须实现其所有字段。
接下来,我们有另一个名为Rating的接口。因为我们的两个类型StandardUser和Master使用STARS字段,而管理员没有,所以我们有这个接口,以便只有使用STARS字段的类型才能实现它-否则,用户模型可以跳过它。
现在,我们终于拥有了实际的StandardUser、Master和Administrator类型,其中每个类型都使用&;符号表示要实现的多个接口,从而仅实现其所需的接口。最后是类别类型。
因此,正如您所看到的,这组类型和层次结构非常类似于大多数现代编程语言,例如TypeScript。
我们现在有了一个合理的模式模型。让我们试着查询一下。通过添加此查询更新typeDefs变量。
如您所见,我们添加了一个函数,该函数检索站点的所有用户,但将他们作为用户数组返回。现在让我们编写解析器,看看这是否可以工作。首先创建您的解析器文件,如下所示:
从";Apollo-server-express";导入{IResolvers};从";../dataService";导入{GqlContext};从";导入{GqlContext}。/GqlContext";;Const解析器:IResolvers={query:{getAllUsers:async(parent,args:null,ctx:GqlContext,info:any):Promise>;=>;{return getAllUsers();},},};导出默认解析器;
我们的解析器唯一的操作是调用getAllUsers函数来检索实际数据。您会注意到,我们的getAllUsers解析器返回一个用户界面数组。这应该有点可疑,但现在让我们看看会发生什么。
现在,我们必须在代码中实现getAllUsers函数。但为了使本文更简单,并将重点放在GraphQL上,我们将创建一个返回硬编码值的函数。创建名为dataService.ts的文件并添加以下代码。
从";uuid";导入{v4};导出接口可标识{id:string;}导出接口用户扩展可标识{username:string;}导出接口评级{STARS:NUMBER;}导出类StandardUser实现user{structor(public id:string,public username:string,public star:number){}}导出类版主实现user{structor(public id:string,public username:string,public star:number,public ategories:[类别]){}}导出类管理员实现user{structor(public id:string,public username:string){}}导出类类别实现可识别的{structor(public id:string,public name:string){}}export函数getAllUsers(){const Users:array=[];Users.ush(new StandardUser(v4(),";dave";,4));users.ush(new版主(v4(),";Ruth";,5,[new Category(v4(),";Cooking";)]);users.ush(new StandardUser(v4(),";Jane";,5));users.ush(new StandardUser(v4(),";tom";,1));Users.ush(new Administrator(v4(),";linda&34;));users.ush(new版主(v4(),";tom";,2,[new Category(v4(),";Programming";)]);users.ush(new Administrator(v4(),";Betty";));返回用户;}。
我们的dataService包含反映我们的GraphQL模式的所有类型脚本类型,以及返回从用户界面继承的对象列表的getAllUsers函数。如您所见,getAllUsers使用所有类型的用户,包括StandardUsers和Administrators,但是由于他们都实现了用户界面(即User是所有用户的父类型),所以函数的返回类型被设置为一组用户类型。现在让我们试着查询一下!
通过打开浏览器访问GraphQL服务器URL(类似于http://localhost:<;port Number&>;/GraphQl),打开您的GraphQL游乐场。然后添加此查询,如下所示。
这是我们对getAllUsers的查询,但是我们看到的不是字段,而是三个句点。我们看到的“…”称为内联片段。这将告诉GraphQL服务我们要查找的数据类型。在本例中,我们指出需要Interface User及其id和username字段。
因此,让我们尝试运行此查询,但当我们这样做时,会显示此错误。
这个错误意味着什么?如果我们回顾一下我们对GraphQL接口的定义,我们会说它们只有声明,没有实现。因此,我们需要将接口替换为接口的实现,即返回数据的类型。让我们看看如何做到这一点。
用户:{__Resolution veType(obj:any,ctx:GqlContext,info:any){if(obj.ategories){return";;版主";;}Else if(obj.star){return";StandardUser&34;;}return";Administrator";;},},
通过在解析器中定义用户界面,我们告诉GraphQL,当它在查询中遇到用户时,应该返回列出的特定类型之一。请注意,如何确定这一点的逻辑完全取决于您。但是,一种明显的潜在方法是使用每种类型唯一的成员字段,就像我们在这里所做的那样。
显然,这是可行的,但是我们没有获得所有相关字段,并且很难判断每个返回的项是哪种类型。让我们稍微更新一下查询以显示这些详细信息。
如果我们遍历此查询,可以看到通过使用__typeName字段,我们可以获得结果集中每一项的类型名称。因此,这在一定程度上肯定有助于澄清一些事情。但是,只有StandardUser类型返回实际字段。显示了其他类型,但具体类型数据为空!我们需要做什么是很清楚的,所以让我们试一试吧。
正如您可能猜到的那样,我们需要添加其他类型及其特定字段。例如,在Master的情况下,我们有一个名为Categories的唯一字段,因为该类别也是它自己的类型,所以我们还从类型中指定了“name”成员。
现在我们已经把一切都准备好了!唉,还有一个小问题。即使在实现了这样的健壮类型之后,我们仍然要多次重复id和username字段。在这种情况下,这无关紧要。但是对于较大的数据模型,拥有几十个共享字段是相当容易的-想象一下重复指定所有这些字段!
我们已经看到了如何使用内联片段来指示我们想要的特定类型的字段,但是我们也可以使用片段来帮助减少重复。让我们像这样再次更新我们的查询。
那好多了。我们能够删除冗余并使我们的代码更干净。现在,让我们再做一次更改,以进一步改善情况。
GraphQL中的UNION是一种可以是几个类型定义之一的类型。例如,我们的模式类型的UNION可能是这样的。
如果我们现在让getAllUsers查询函数返回一个Result Union而不是User Interface,那么我们打算返回哪些特定类型就变得更加清楚了。此外,在较大的模式中,我们可能希望将UNION用于不同的目的,以便一个UNION在我们的模式中只能包含几个类型中的两个,而另一个UNION可能包含几个其他类型。
现在我们已经创建了一个Union,我们还需要为它创建一个解析器。对于我们的模式,解析器实现实际上可以与用户解析器相同。它看起来会是这样的。
结果:{__ResolveType(obj:any,ctx:GqlContext,info:any){if(obj.ategories){return";;版主";;}Else if(obj.star){return";StandardUser&34;;}return";Administrator";;},},
如果您按原样重新运行最后一个查询,它的工作方式应该与以前相同。在我们的样例应用程序中,它恰好是相同的实现。在更完整、更现实的模式中,它可以也将是不同的-我们会有许多不同的类型,在将它们组合成一个UNION的方式上会有所不同。
在本教程中,我们向您介绍了接口、片段和联合。每个特性都可以帮助改进您的模式,使其更精确地符合您的数据需求,并且更易于查询。