CQRS
命令查询职责分离模式(Command Query Responsibility Segregation,CQRS),是从业务上分离修改和查询行为的一种模型。分离后修改相关的业务和查询相关的业务可以分别使用一套独立的模型(业务模型、存储模型等)。从而使得逻辑更加清晰,便于对不同部分进行针对性的设计。
如图是一个简化的 CQRS 模型: 来自客户端的命令请求进入服务端的写操作入口,对模型状态进行修改后存入写数据库; 然后通过某种同步机制将写模型转换为查询模型存入读数据库。 * 最后客户端的查询请求将进入服务端的读操作入口,并直接从读数据库读取数据。
其中,将对写模型的变更同步到读模型的步骤有很多具体的实现方式,例如:
- 通过事件机制订阅写模型的变更事件同步(通常结合 Event Sourcing 模式)。
- 通过离线定时任务进行同步。
通过这样的分离后写模型只需要关心与状态修改相关的业务以及一致性这一类的问题。并且这部分业务的需求通常较少而且需求较稳定,可以避免为了兼容读取业务而造成增加模型的复杂度。针对读取模型因为只需要处理数据获取相关的业务则完全不需要过于复杂的建模,只需要考虑数据查询的效率和数据授权的问题即可。
除了模型复杂度的收益 CQRS 另一个显著的好处是对性能的优化,我们可以将命令模型和查询模型分别实现为两个不同的运行时分别部署,从而针对读写性能分别进行优化以提高系统的整体可用性。
GgraphQL
GraphQL 是一个基于数据类型的查询语言或者叫行为描述语言。
服务端通过定义一套数据类型用于发布可以支持的查询和操作,例如:以下 GraphQL 类型语言,描述了服务端可以提供一个注册和查询已有账户列表的操作。其中注册可以返回一个账户类型而查询账户列表则可以返回一个账户类型的数组。
schema {
query: Query
mutation: Commands
}
type Commands {
register(username: String!, password: String): Account!
}
type Query {
accounts(limit:Int = 10): [Account!]!
myAccount: Account!
}
type Account {
id: ID!
username: String!
created: DateTime!
updated: DateTime!
}
客户端按照服务端定义的类型可以根据前端的需求灵活的从服务端获取需要的数据。例如,注册的时候用户名、注册时间等信息都是客户端已知的并不需要由服务端返回,只需要服务端返回新注册账户的唯一标识,那么客户端可以通过以下查询实现
mutation register(username: 'myUsername', password: 'myPassword') {
id
}
再比如:客户端有一个页面需要展示当前登录账户信息以及一个账户列表,这本来是两个查询但在 graphQL 中可以合并为如下一个请求执行。
query getMyAccountAndAccountList {
myAccount {
id
username
created
}
accounts(limit: 20) {
id
username
created
}
}
以上只是 GraphQL 强大能力的冰山一角,基于 GraphQL 灵活的行为描述能力还可以在获取到一个类型时获取该类型实例关联的其它类型,例如查询一个用户时同时获取这个用户关注的用户列表。更多关于 GraphQL 的使用方法可以参考官方文档或中文网站。
GraphQL + CQRS
在 CQRS 中我们根据业务是否具备状态修改划分成了命令和查询两个部分,并且可能分别实现为两个运行时独立部署。但对于客户端看来它并不关心服务端使用的是什么架构模式,只关心每个页面上有哪些操作需要展示哪些数据。如果服务端直接将较为元子的操作和查询作为 API 提供给客户端的话,那么必然会对客户端开发造成很多的困扰。为此,服务端在命令总线和查询总线之上一般都会再提供一个接入层,把某些命令操作和查询操作进行一些整合包装为一个 API。然而,由于客户端需求的多样性这种对命令和查询的包装就可能会需要很多个,对于服务端接口层的开发带来不小的负担。
但是,如果使用 GraphQL 作为 CQRS 的前端这样的问题就比较好解决了。在 GraphQL 顶层类型上我们可以提供两个入口用以分别处理命令和查询操作,例如前面示例中的 query 和 mutation。
schema {
query: Query
mutation: Commands
}
在服务端可以把 Query 类型上的 Field 映射为查询总线上的查询指令;将 Commands 类型上的 Field 映射为命令总线上的指令(甚至不要命令总线和查询总线,直接映射到命令和查询处理程序)。这样,客户端就可以在一次请求中根据页面的需求灵活整合需要执行的操作。例如,在前面示例中一次请求获取当前账户信息和一个账户列表,如果用 Restful 接口客户端可能需要发起两次请求分别获取当前账户的信息和账户列表的信息,但在 GraphQL 中则只需要一次请求就可以获得所有需要的信息。有了这样的能力,服务端在做 CQRS 设计时就无需关心模型建模的粒度是否会给接口层的开发造成过多的负担。
另外,由于 GraphQL 基于类型定义的特性也可以减轻服务端命令和查询处理程序对于输入数据的验证工作。因为,例如服务端定义了注册需要提供用户名和密码两个字符串类型输入,那么请求在经过 GraphQL 的验证到达注册命令处理程序时请求参数中就肯定会有这两个字符串型输入,处理程序只需要进一步验证它们是否符合业务定义的内容结构就可以了。同理,对于查询请求也不用但心未被定义为输出类型的敏感数据会被错误输出,例如前面例子中的 Account 类型客户端永远无法通过 GraphQL 获取到一个账户的密码字段内容。