最近,GraphQL越来越受欢迎。 请求,类型和订阅的优美语法。
似乎: “在这里-我们已经找到了进行数据交换的理想语言!” ...
我使用这种语言的开发已经有一年多了,我会告诉你:一切都不那么顺利。 GraphQL在语言设计本身中既有令人不适的时刻,又有真正的根本问题。
另一方面,大多数“设计动作”是出于某种原因-这是出于各种考虑。 实际上,GraphQL并非适合所有人,并且可能根本不是您所需要的工具。 但是首先是第一件事。
我认为有必要对我在哪里使用这种语言做一番评论。 这是一个相当复杂的SPA管理面板,其中大多数操作都是相当重要的CRUD(复杂实体)。 该材料中的很大一部分论点与应用程序的性质和已处理数据的性质有关。 在不同类型(或数据性质不同)的应用程序中,原则上不会出现此类问题。
1. NON_NULL
这不是一个严重的问题。 而是,这与GraphQL中可空工作的组织方式有关,带来了一系列不便。
有功能的(不仅是)编程语言,这种范例是monads。 因此,可能存在Maybe
(Haskel)单价或Option
(Scala)单价货币,最重要的是该单价货币中包含的值可能存在或可能不存在(即为null)。 好吧,或者可以像Rust一样通过枚举来实现。
一种或另一种方法,在大多数语言中,此值“包装”原始值,使null成为主要方法的附加选项。 从句法上讲-它始终是主要类型的补充。 这并不总是仅仅是一个单独的类型类-在某些语言中,它仅仅是后缀或前缀形式的加法?
。
在GraqhQL中,情况恰恰相反。 默认情况下,所有类型都是可为空的-相反,这不仅仅是将类型标记为可为空,而是Maybe
monad。
如果我们考虑这种方案的name
字段的自省部分:
我们发现:

封装在NON_NULL
String
类型
1.1。 输出值
为什么这样 简而言之,它与默认的“容忍”语言设计有关(除其他外,它对微服务体系结构很友好)。
要了解这种“公差”的本质,请考虑一个稍微复杂的示例,其中所有返回值都严格包装在NON_NULL中:
type User { name: String!
假设我们有一个返回用户列表的服务,还有一个单独的微服务“友谊”,它为我们返回了用户朋友的匹配项。 然后,如果“友谊”服务失败,我们将根本无法列出用户。 需要解决的情况:
type User { name: String!
这是对内部错误的容忍度。 一个例子,当然是牵强的。 但我希望您掌握了本质。
此外,在其他情况下,您可以使生活更轻松。 假设有远程用户,并且朋友的标识符可以存储在一些外部无关的结构中。 我们可以清除杂草,只返回所拥有的,但是那样我们将无法理解确切清除了什么杂草。
type Query {
一切都很好。 有什么问题吗?
一般来说,不是一个很大的问题-味道。 但是,如果您具有带有关系数据库的整体应用程序,那么最有可能的错误实际上就是错误,并且api应该尽可能严格。 您好,感叹号! 只要有可能。
我希望能够“反转”此行为,并放置问号而不是感叹号。以某种方式会更熟悉。
但是进入时,可为空是一个完全不同的故事。 这是HTML中复选框级别的标记(我认为每个人都记得当未选中复选框的字段根本不发送到背面时,这种非显而易见性)。
考虑一个例子:
type Post { id: ID! title: String!
到目前为止,一切都很好。 添加更新:
type Mutation { createPost(post: PostInput!): Post! updatePost(id: ID!, post: PostInput!): Post! }
现在的问题是:更新帖子时,我们可以从描述字段中得到什么? 该字段可以为空,也可以完全不存在。
如果缺少该字段,那么需要做什么? 不更新吗? 或将其设置为null? 最重要的是,允许空值和允许不存在字段是两件事。 但是,GraphQL是同一回事。
2.输入和输出分离
这只是痛苦。 在CRUD工作模型中,您从后面“扭曲”了对象,然后将其发送回去。 粗略地说,这是一个相同的对象。 但是您只需要描述两次-输入和输出。 除了编写用于此业务的代码生成器之外,此操作无济于事。 我宁愿将对象本身而不是对象本身分成“输入和输出”。 例如,修饰符:
type Post { input output text: String! output updatedAt(format: DateFormat = W3C): Date! }
或使用指令:
type Post { text: String! @input @output updatedAt(format: DateFormat = W3C): Date! @output }
3.多态性
将类型分为输入和输出的问题不限于双重描述。 同时,对于泛型类型,您可以定义泛型接口:
interface Commentable { comments: [Comment!]! } type Post implements Commentable { text: String! comments: [Comment!]! } type Photo implements Commentable { src: URL! comments: [Comment!]! }
或工会
type Person { firstName: String, lastName: String, } type Organiation { title: String } union Subject = Organiation | Person type Account { login: String subject: Subject }
您不能对输入类型执行相同的操作。 这样做有许多先决条件,但这部分是由于使用json作为传输的数据格式这一事实。 但是,在输出中, __typename
字段用于指定类型。 为什么在进入时不可能做同样的事情-尚不清楚。 在我看来,通过在传输过程中放弃json并输入其自己的格式,可以更优雅地解决此问题。 精神上的东西:
union Subject = OrganiationInput | PersonInput input AccountInput { login: String! password: String! subject: Subject! }
但这将有必要为此业务编写其他解析器。
4.泛型
具有泛型的GraphQL有什么问题? 一切都很简单-事实并非如此。 让我们来看一个带有分页或光标的典型CRUD索引查询,这并不重要。 我将以分页为例。
input Pagination { page: UInt, perPage: UInt, } type Query { users(pagination: Pagination): PageOfUsers! } type PageOfUsers { total: UInt items: [User!]! }
现在是组织
type Query { organizations(pagination: Pagination): PageOfOrganizations! } type PageOfOrganizations { total: UInt items: [Organization!]! }
等等...我想如何为此使用泛型
type PageOf<T> { total: UInt items: [T!]! }
那我就写
type Query { users(page: UInt, perPage: UInt): PageOf<User>! }
是大量的应用程序! 我应该告诉您有关仿制药的信息吗?
5.命名空间
他们也不在那里。 当系统中类型的数量超过一百零五种时,名称冲突的可能性将趋于百分之一百。
出现各种Service_GuideNDriving_Standard_Model_Input
。 我不是在像SOAP中那样在不同端点上使用成熟的名称空间(是的,是的,这很糟糕,但是名称空间在此完美地制作了)。 并且在一个端点上至少具有几种方案,能够“混淆”方案之间的类型。
合计
GraphQL是一个很好的工具。 它完全适合于容忍的微服务架构,该架构首先面向信息输出和简单的确定性输入。
如果要输入多态实体,则可能会有问题。
输入和输出类型的分离,以及缺少泛型-从头开始产生了很多涂鸦。
Graphql不是真的(有时一点也不 )关于CRUD。
但这并不意味着您不能吃它:)
在下一篇文章中,我想谈谈我如何(有时成功)与上述一些问题作斗争。