“ GraphQL是API的查询语言,是用于使用现有数据完成这些查询的运行时。 GraphQL为您的API中的数据提供了一个完整且易于理解的描述,使客户能够准确地询问他们所需的内容,仅此而已,使随着时间的推移更容易开发API并启用强大的开发人员工具。
–来自https://graphql.org/
任何已经构建了可供多个消费者使用的REST服务的人,例如其他服务,网站或移动设备,都将知道很难构建满足所有需求的完美端点。 对于所有这些特殊情况,您通常最终都会获得相同服务的变体:)
现在,我们都知道我们应该只使用HATEOAS …,它在我的TODO列表中(承诺!),直到我偶然发现GraphQL为止 。
因此,在此博客文章中,我将解释如何轻松地将GraphQL添加到现有的JAX-RS应用程序中。
示例项目
Github中提供了示例项目,并且非常容易上手。
git clone https://github.com/phillip-kruger/membership.git
cd membership
mvn clean install
这将使用示例应用程序http:// localhost:8080 / membership /来启动fatjar wildfly-swarm
高水平
该示例是基本的成员资格服务,您可以在其中获得所有成员或特定成员。 您可以添加,编辑和删除成员。
该应用程序是典型的JAX-RS,CDI,EJB,JPA,Bean验证Java EE应用程序,并且我们正在添加一个新的GraphQL端点。
GraphQL部分使用以下库:
- graphql-java
- graphql-java-servlet
- graphQL-spqr
- 石墨烯
我添加来将现有的JAX-RS公开为GraphQL的唯一Java类:
- MembershipGraphQLListener –注册“ / graphql” Servlet侦听器。
- MembershipGraphQLApi – GraphQL端点。 仅包装现有的
@Stateless
服务。 - MembershipErrorHandler –处理异常。
使用graphQL-spqr的注释, MembershipGraphQLApi
类实际上只是描述和包装了现有的@Stateless
服务:
@RequestScopedpublic class MembershipGraphQLApi {@Injectprivate MembershipService membershipService;// ...@GraphQLQuery(name = "memberships")public List<Membership> getAllMemberships(Optional<MembershipFilter> filter,@GraphQLArgument(name = "skip") Optional<Integer> skip,@GraphQLArgument(name = "first") Optional<Integer> first) {return membershipService.getAllMemberships(filter, skip, first); }// ...}
我的希望–我们很快将在Java EE(或Jakarta EE或MicroProfile)中提供一个JAX-QL(或其他东西),以使其变得更加简单!
首先是一些REST
我正在使用MicroProfile OpenAPI和Swagger UI为REST端点创建Open API定义。
您可以使用http:// localhost:8080 / membership / rest / openapi-ui /测试一些查询
示例 –获取所有成员资格:
GET http://localhost:8080/membership/rest
这将返回:
[{"membershipId": 1,"owner": {"id": 1,"names": ["Natus","Phillip"],"surname": "Kruger"},"type": "FULL"},{"membershipId": 2,"owner": {"id": 2,"names": ["Charmaine","Juliet"],"surname": "Kruger"},"type": "FULL"},{"membershipId": 3,"owner": {"id": 3,"names": ["Koos"],"surname": "van der Merwe"},"type": "FULL"},{"membershipId": 4,"owner": {"id": 4,"names": ["Minki"],"surname": "van der Westhuizen"},"type": "FREE"}]
示例 –获得一定的成员资格(1):
GET http://localhost:8080/membership/rest/1
这将返回:
{"membershipId": 1,"owner": {"id": 1,"names": ["Natus","Phillip"],"surname": "Kruger"},"type": "FULL"}
现在让我们看一下GraphQL
该应用程序包括GraphiQL UI(作为Webjar),可以轻松测试一些GraphQL查询
您可以使用http:// localhost:8080 / membership / graph / graphiql /测试一些查询
因此,让我们看看GraphQL是否兑现了“不再需要过度提取和提取不足”的承诺。
获取所有成员资格和所有字段(与REST相同)
query Memberships {memberships{...fullMembership}}fragment fullMembership on Membership {membershipIdowner{...owner}type}fragment owner on Person {idnamessurname }
这将返回所有值,但是,现在很容易定义应包括哪些字段…
获取所有会员资格,但仅包括id字段
query Memberships {memberships{...membershipIdentifiers}}fragment membershipIdentifiers on Membership {membershipId}
现在产生的有效负载要小得多:
{"data": {"memberships": [{"membershipId": 1},{"membershipId": 2},{"membershipId": 3},{"membershipId": 4}]}}
现在,仅获取特定类型的会员资格(因此,获取所有免费会员资格)
query FilteredMemberships {memberships(filter:{type:FREE}){...fullMembership}}fragment fullMembership on Membership {membershipIdowner{...owner}type}fragment owner on Person {idnamessurname }
这将仅返回免费会员资格。 好酷!
甚至更好,所有姓氏的成员都以“ Kru”开头
query FilteredMemberships {memberships(filter:{surnameContains: "Kru"}){...fullMembership}}fragment fullMembership on Membership {membershipIdowner{...owner}type}fragment owner on Person {idnamessurname }
太好了! 我们找到了两个人:
{"data": {"memberships": [{"membershipId": 1,"owner": {"id": 1,"names": ["Natus","Phillip"],"surname": "Kruger"},"type": "FULL"},{"membershipId": 2,"owner": {"id": 2,"names": ["Charmaine","Juliet"],"surname": "Kruger"},"type": "FULL"}]}}
使用客户端上的变量获取特定的成员资格:
query Membership($id:Int!) {membership(membershipId:$id){...fullMembership}}fragment fullMembership on Membership {membershipIdowner{...owner}type}fragment owner on Person {idnamessurname }
变量:
{"id":1}
在特定条件下包括字段:
query Membership($id:Int!,$withOwner: Boolean!) {membership(membershipId:$id){...fullMembership}}fragment fullMembership on Membership {membershipIdowner @include(if: $withOwner){...owner}type}fragment owner on Person {idnamessurname }
变量:
{"id":1,"withOwner": false}
这将排除所有者(正确包括):
{"data": {"membership": {"membershipId": 1,"type": "FULL"}}}
分页
让我们使用get all查询,但要分页。
query Memberships($itemsPerPage:Int!,$pageNumber:Int!) {memberships(first:$itemsPerPage,skip:$pageNumber) {membershipIdowner{namessurname}type}}
变量:
{"itemsPerPage": 2,"pageNumber": 1}
这将返回前2个结果,然后您可以通过增加“ pageNumber”值来进行分页。
变异
创建
mutation CreateMember {createMembership(membership: {type:FULL,owner: {names: "James",surname:"Small"}}) {membershipId}}
这将创建新的成员资格并返回ID。
更新资料
mutation EditMember($membership: MembershipInput!) {createMembership(membership:$membership) {membershipId}}
变量:
{"membership": {"membershipId": 2,"owner": {"names": ["Charmaine","Juliet"],"surname": "Krüger"},"type": "FULL"}}
(在克鲁格大学添加了变音符号,现在应该是克鲁格)
删除
mutation DeleteMembership($id:Int!){deleteMembership(membershipId:$id){membershipId}}
变量:
{"id":1}
这将删除成员资格1。
例外。
MembershipErrorHandler转换一个ConstraintViolationException(在bean验证失败时抛出),并为GraphQL创建一个不错的错误消息。
因此,让我们尝试创建一个姓氏仅为一个字母的成员。
mutation CreateMember($membership: MembershipInput!) {createMembership(membership:$membership) {membershipId}}
变量:
{"membership": {"owner": {"names": "Christina","surname": "S"},"type": "FULL"}}
这将返回bean验证错误消息:
{"data": {"createMembership": null},"errors": [{"message": "Surname 'S' is too short, minimum 2 characters","path": null,"extensions": null}]}
如果您查看Person POJO:
@NotNull(message = "Surname can not be empty") @Size(min=2, message = "Surname '${validatedValue}' is too short, minimum {min} characters")private String surname;
内省
GraphQL的另一个好处是,它具有可查询的架构和类型系统:
{__schema {queryType {namefields {name}}mutationType{namefields{name}}subscriptionType {namefields{name}}}}
上面将描述此端点上可用的查询和变异。
您还可以描述您的模型:
{__type(name: "Membership") {namekindfields {nameargs {name}}}}
摘要
在此示例中,我们没有删除REST,只是添加了GraphQL作为使用者的替代选项。
到现在为止,应该清楚的是,客户端具有更多的选项来完全根据需要过滤和查询数据。 所有这些都无需服务器做任何额外的工作。 这样可以在客户端进行快速的产品迭代。
线上的有效负载已得到优化,我们正在节省带宽!
再次,我希望–我们很快将在Java EE(或Jakarta EE或MicroProfile)中提供一个JAX-QL(或其他东西),以使其变得更加简单!
翻译自: https://www.javacodegeeks.com/2018/05/graphql-on-wildfly-swarm.html