使用 Go 编写微服务及其 GraphQL 网关

几个月前,一个优秀的 GraphQL Go 包 vektah/gqlgen 开始流行。本文描述了在 Spidey 项目(一个在线商店的基本微服务)中如何实现 GraphQL。

下面列出的一些代码可能存在一些缺失,完整的代码请访问 GitHub

架构

Spidey 包含了三个不同的服务并暴露给了 GraphQL 网关。集群间的通信则通过 gRPC 来完成。

账户服务管理了所有的账号;目录管理了所有的产品;订单服务则处理了所有的订单创建行为。它会与其他两个服务进行通信来告知订单是否正常完成。

Architecture

独立的服务包含三层:服务端服务以及仓库。服务端作负责通信,也就是 Spidey 中使用 gRPC。服务则包含了业务逻辑。仓库则负责对数据库进行读写操作。

起步

运行 Spidey 需要 DockerDocker ComposeGoProtocol Buffers 编译器及其 Go 插件以及非常有用的 vektah/gqlgen 包。

你还需要安装 vgo(一个处于早期开发阶段的包管理工具)。工具 dep 也是一种选择,但是包含的 go.mod 文件会被忽略。

译注:在 Go 1.11 中 vgo 作为官方集成的 Go Modules 发布,已集成在 go 命令中,使用 go mod 进行使用,指令与 vgo 基本一致。

Docker 设置

每个服务在其自身的子文件夹中实现,并至少包含一个 app.dockerfile 文件。app.dockerfile 文件用户构建数据库镜像。

  1. account
  2. ├── account.proto
  3. ├── app.dockerfile
  4. ├── cmd
  5. └── account
  6. └── main.go
  7. ├── db.dockerfile
  8. └── up.sql

所有服务通过外部的 docker-compose.yaml 定义。

下面是截取的一部分关于 Account 服务的内容:

  1. version: "3.6"
  2. services:
  3. account:
  4. build:
  5. context: "."
  6. dockerfile: "./account/app.dockerfile"
  7. depends_on:
  8. - "account_db"
  9. environment:
  10. DATABASE_URL: "postgres://spidey:123456@account_db/spidey?sslmode=disable"
  11. account_db:
  12. build:
  13. context: "./account"
  14. dockerfile: "./db.dockerfile"
  15. environment:
  16. POSTGRES_DB: "spidey"
  17. POSTGRES_USER: "spidey"
  18. POSTGRES_PASSWORD: "123456"
  19. restart: "unless-stopped"

设置 context 的目的是保证 vendor 目录能够被复制到 Docker 容器中。所有服务共享相同的依赖、某些服务还依赖其他服务的定义。

账户服务

账户服务暴露了创建以及索引账户的方法。

服务

账户服务的 API 定义的接口如下:

account/service.go

  1. type Service interface {
  2. PostAccount(ctx context.Context, name string) (*Account, error)
  3. GetAccount(ctx context.Context, id string) (*Account, error)
  4. GetAccounts(ctx context.Context, skip uint64, take uint64) ([]Account, error)
  5. }
  6. type Account struct {
  7. ID string `json:"id"`
  8. Name string `json:"name"`
  9. }

实现需要用到 Repository:

  1. type accountService struct {
  2. repository Repository
  3. }
  4. func NewService(r Repository) Service {
  5. return &accountService{r}
  6. }

这个服务负责了所有的业务逻辑。PostAccount 函数的实现如下:

  1. func (s *accountService) PostAccount(ctx context.Context, name string) (*Account, error) {
  2. a := &Account{
  3. Name: name,
  4. ID: ksuid.New().String(),
  5. }
  6. if err := s.repository.PutAccount(ctx, *a); err != nil {
  7. return nil, err
  8. }
  9. return a, nil
  10. }

它将线路协议解析处理为服务端,并将数据库处理为 Repository。

数据库

一个账户的数据模型非常简单:

  1. CREATE TABLE IF NOT EXISTS accounts (
  2. id CHAR(27) PRIMARY KEY,
  3. name VARCHAR(24) NOT NULL
  4. );

上面定义数据的 SQL 文件会复制到 Docker 容器中执行。

account/db.dockerfile

  1. FROM postgres:10.3
  2. COPY up.sql /docker-entrypoint-initdb.d/1.sql
  3. CMD ["postgres"]

PostgreSQL 数据库通过下面的 Repository 接口进行访问:

account/repository.go

  1. type Repository interface {
  2. Close()
  3. PutAccount(ctx context.Context, a Account) error
  4. GetAccountByID(ctx context.Context, id string) (*Account, error)
  5. ListAccounts(ctx context.Context, skip uint64, take uint64) ([]Account, error)
  6. }

Repository 基于 Go 标准库 SQL 包进行封装:

  1. type postgresRepository struct {
  2. db *sql.DB
  3. }
  4. func NewPostgresRepository(url string) (Repository, error) {
  5. db, err := sql.Open("postgres", url)
  6. if err != nil {
  7. return nil, err
  8. }
  9. err = db.Ping()
  10. if err != nil {
  11. return nil, err
  12. }
  13. return &postgresRepository{db}, nil
  14. }

gRPC

账户服务的 gRPC 服务定义了下面的 Protocol Buffer:

account/account.proto

  1. syntax = "proto3";
  2. package pb;
  3. message Account {
  4. string id = 1;
  5. string name = 2;
  6. }
  7. message PostAccountRequest {
  8. string name = 1;
  9. }
  10. message PostAccountResponse {
  11. Account account = 1;
  12. }
  13. message GetAccountRequest {
  14. string id = 1;
  15. }
  16. message GetAccountResponse {
  17. Account account = 1;
  18. }
  19. message GetAccountsRequest {
  20. uint64 skip = 1;
  21. uint64 take = 2;
  22. }
  23. message GetAccountsResponse {
  24. repeated Account accounts = 1;
  25. }
  26. service AccountService {
  27. rpc PostAccount (PostAccountRequest) returns (PostAccountResponse) {}
  28. rpc GetAccount (GetAccountRequest) returns (GetAccountResponse) {}
  29. rpc GetAccounts (GetAccountsRequest) returns (GetAccountsResponse) {}
  30. }

由于这个包被设置为了 pb,于是生成的代码可以从 pb 子包导入使用。

gRPC 的代码可以使用 Go 的 generate 指令配合 account/server.go 文件最上方的注释进行编译生成:

account/server.go

  1. //go:generate protoc ./account.proto --go_out=plugins=grpc:./pb
  2. package account

运行下面的命令就可以将代码生成到 pb 子目录:

  1. $ go generate account/server.go

服务端作为 Service 服务接口的适配器,对应转换了请求和返回的类型。

  1. type grpcServer struct {
  2. service Service
  3. }
  4. func ListenGRPC(s Service, port int) error {
  5. lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
  6. if err != nil {
  7. return err
  8. }
  9. serv := grpc.NewServer()
  10. pb.RegisterAccountServiceServer(serv, &grpcServer{s})
  11. reflection.Register(serv)
  12. return serv.Serve(lis)
  13. }

下面是 PostAccount 函数的实现:

  1. func (s *grpcServer) PostAccount(ctx context.Context, r *pb.PostAccountRequest) (*pb.PostAccountResponse, error) {
  2. a, err := s.service.PostAccount(ctx, r.Name)
  3. if err != nil {
  4. return nil, err
  5. }
  6. return &pb.PostAccountResponse{Account: &pb.Account{
  7. Id: a.ID,
  8. Name: a.Name,
  9. }}, nil
  10. }

用法

gRPC 服务端在 account/cmd/account/main.go 文件中进行初始化:

  1. type Config struct {
  2. DatabaseURL string `envconfig:"DATABASE_URL"`
  3. }
  4. func main() {
  5. var cfg Config
  6. err := envconfig.Process("", &cfg)
  7. if err != nil {
  8. log.Fatal(err)
  9. }
  10. var r account.Repository
  11. retry.ForeverSleep(2*time.Second, func(_ int) (err error) {
  12. r, err = account.NewPostgresRepository(cfg.DatabaseURL)
  13. if err != nil {
  14. log.Println(err)
  15. }
  16. return
  17. })
  18. defer r.Close()
  19. log.Println("Listening on port 8080...")
  20. s := account.NewService(r)
  21. log.Fatal(account.ListenGRPC(s, 8080))
  22. }

客户端结构体的实现位于 account/client.go 文件中。这样账户服务就可以在无需了解 RPC 内部实现的情况下进行实现,我们之后再来详细讨论。

  1. account, err := accountClient.GetAccount(ctx, accountId)
  2. if err != nil {
  3. log.Fatal(err)
  4. }

目录服务

目录服务负责处理 Spidey 商店的商品。它实现了类似于账户服务的功能,但是使用了 Elasticsearch 对商品进行持久化。

服务

目录服务遵循下面的接口:

catalog/service.go

  1. type Service interface {
  2. PostProduct(ctx context.Context, name, description string, price float64) (*Product, error)
  3. GetProduct(ctx context.Context, id string) (*Product, error)
  4. GetProducts(ctx context.Context, skip uint64, take uint64) ([]Product, error)
  5. GetProductsByIDs(ctx context.Context, ids []string) ([]Product, error)
  6. SearchProducts(ctx context.Context, query string, skip uint64, take uint64) ([]Product, error)
  7. }
  8. type Product struct {
  9. ID string `json:"id"`
  10. Name string `json:"name"`
  11. Description string `json:"description"`
  12. Price float64 `json:"price"`
  13. }

数据库

Repository 基于 Elasticsearch olivere/elastic 包进行实现。

catalog/repository.go

  1. type Repository interface {
  2. Close()
  3. PutProduct(ctx context.Context, p Product) error
  4. GetProductByID(ctx context.Context, id string) (*Product, error)
  5. ListProducts(ctx context.Context, skip uint64, take uint64) ([]Product, error)
  6. ListProductsWithIDs(ctx context.Context, ids []string) ([]Product, error)
  7. SearchProducts(ctx context.Context, query string, skip uint64, take uint64) ([]Product, error)
  8. }

由于 Elasticsearch 将文档和 ID 分开存储,因此实现的一个商品的辅助结构没有包含 ID:

  1. type productDocument struct {
  2. Name string `json:"name"`
  3. Description string `json:"description"`
  4. Price float64 `json:"price"`
  5. }

将商品插入到数据库中:

  1. func (r *elasticRepository) PutProduct(ctx context.Context, p Product) error {
  2. _, err := r.client.Index().
  3. Index("catalog").
  4. Type("product").
  5. Id(p.ID).
  6. BodyJson(productDocument{
  7. Name: p.Name,
  8. Description: p.Description,
  9. Price: p.Price,
  10. }).
  11. Do(ctx)
  12. return err
  13. }

gRPC

目录服务的 gRPC 服务定义在 catalog/catalog.proto 文件中,并在 catalog/server.go 中进行实现。与账户服务不同的是,它没有在服务接口中定义所有的 endpoint。

catalog/catalog.proto

  1. syntax = "proto3";
  2. package pb;
  3. message Product {
  4. string id = 1;
  5. string name = 2;
  6. string description = 3;
  7. double price = 4;
  8. }
  9. message PostProductRequest {
  10. string name = 1;
  11. string description = 2;
  12. double price = 3;
  13. }
  14. message PostProductResponse {
  15. Product product = 1;
  16. }
  17. message GetProductRequest {
  18. string id = 1;
  19. }
  20. message GetProductResponse {
  21. Product product = 1;
  22. }
  23. message GetProductsRequest {
  24. uint64 skip = 1;
  25. uint64 take = 2;
  26. repeated string ids = 3;
  27. string query = 4;
  28. }
  29. message GetProductsResponse {
  30. repeated Product products = 1;
  31. }
  32. service CatalogService {
  33. rpc PostProduct (PostProductRequest) returns (PostProductResponse) {}
  34. rpc GetProduct (GetProductRequest) returns (GetProductResponse) {}
  35. rpc GetProducts (GetProductsRequest) returns (GetProductsResponse) {}
  36. }

尽管 GetProductRequest 消息包含了额外的字段,但通过 ID 的搜索与索引实现。

下面的代码展示了 GetProducts 函数的实现:

catalog/server.go

  1. func (s *grpcServer) GetProducts(ctx context.Context, r *pb.GetProductsRequest) (*pb.GetProductsResponse, error) {
  2. var res []Product
  3. var err error
  4. if r.Query != "" {
  5. res, err = s.service.SearchProducts(ctx, r.Query, r.Skip, r.Take)
  6. } else if len(r.Ids) != 0 {
  7. res, err = s.service.GetProductsByIDs(ctx, r.Ids)
  8. } else {
  9. res, err = s.service.GetProducts(ctx, r.Skip, r.Take)
  10. }
  11. if err != nil {
  12. log.Println(err)
  13. return nil, err
  14. }
  15. products := []*pb.Product{}
  16. for _, p := range res {
  17. products = append(
  18. products,
  19. &pb.Product{
  20. Id: p.ID,
  21. Name: p.Name,
  22. Description: p.Description,
  23. Price: p.Price,
  24. },
  25. )
  26. }
  27. return &pb.GetProductsResponse{Products: products}, nil
  28. }

它决定了当给定何种参数来调用何种服务函数。其目标是模拟 REST HTTP 的 endpoint。

对于 /products?[ids=...]&[query=...]&skip=0&take=100 形式的请求,只有设计一个 endpoint 来完成 API 调用会相对容易一些。

Order 服务

Order 订单服务就比较棘手了。他需要调用账户和目录服务来验证请求,因为一个订单只能给一个特定的账号和一个存在的商品进行创建。

Service

Service 接口定义了通过账户创建和索引全部订单的接口。

order/service.go

  1. type Service interface {
  2. PostOrder(ctx context.Context, accountID string, products []OrderedProduct) (*Order, error)
  3. GetOrdersForAccount(ctx context.Context, accountID string) ([]Order, error)
  4. }
  5. type Order struct {
  6. ID string
  7. CreatedAt time.Time
  8. TotalPrice float64
  9. AccountID string
  10. Products []OrderedProduct
  11. }
  12. type OrderedProduct struct {
  13. ID string
  14. Name string
  15. Description string
  16. Price float64
  17. Quantity uint32
  18. }

数据库

一个订单可以包含多个商品,因此数据模型必须支持这种形式。下面的 order_products 表描述了 ID 为 product_id 的订购产品以及此类产品的数量。而 product_id 字段必须可以从目录服务进行检索。

order/up.sql

  1. CREATE TABLE IF NOT EXISTS orders (
  2. id CHAR(27) PRIMARY KEY,
  3. created_at TIMESTAMP WITH TIME ZONE NOT NULL,
  4. account_id CHAR(27) NOT NULL,
  5. total_price MONEY NOT NULL
  6. );
  7. CREATE TABLE IF NOT EXISTS order_products (
  8. order_id CHAR(27) REFERENCES orders (id) ON DELETE CASCADE,
  9. product_id CHAR(27),
  10. quantity INT NOT NULL,
  11. PRIMARY KEY (product_id, order_id)
  12. );

Repository 接口很简单:

order/repository.go

  1. type Repository interface {
  2. Close()
  3. PutOrder(ctx context.Context, o Order) error
  4. GetOrdersForAccount(ctx context.Context, accountID string) ([]Order, error)
  5. }

但实现它却并不简单。

一个订单必须使用事务机制分两步插入,然后通过 join 语句进行查询。

从数据库中读取订单需要解析一个表状结构数据读取到对象结构中。下面的代码基于订单 ID 将商品读取到订单中:

  1. orders := []Order{}
  2. order := &Order{}
  3. lastOrder := &Order{}
  4. orderedProduct := &OrderedProduct{}
  5. products := []OrderedProduct{}
  6. // 将每行读取到 Order 结构体
  7. for rows.Next() {
  8. if err = rows.Scan(
  9. &order.ID,
  10. &order.CreatedAt,
  11. &order.AccountID,
  12. &order.TotalPrice,
  13. &orderedProduct.ID,
  14. &orderedProduct.Quantity,
  15. ); err != nil {
  16. return nil, err
  17. }
  18. // 读取订单
  19. if lastOrder.ID != "" && lastOrder.ID != order.ID {
  20. newOrder := Order{
  21. ID: lastOrder.ID,
  22. AccountID: lastOrder.AccountID,
  23. CreatedAt: lastOrder.CreatedAt,
  24. TotalPrice: lastOrder.TotalPrice,
  25. Products: products,
  26. }
  27. orders = append(orders, newOrder)
  28. products = []OrderedProduct{}
  29. }
  30. // 读取商品
  31. products = append(products, OrderedProduct{
  32. ID: orderedProduct.ID,
  33. Quantity: orderedProduct.Quantity,
  34. })
  35. *lastOrder = *order
  36. }
  37. // 添加最后一个订单 (或者第一个 :D)
  38. if lastOrder != nil {
  39. newOrder := Order{
  40. ID: lastOrder.ID,
  41. AccountID: lastOrder.AccountID,
  42. CreatedAt: lastOrder.CreatedAt,
  43. TotalPrice: lastOrder.TotalPrice,
  44. Products: products,
  45. }
  46. orders = append(orders, newOrder)
  47. }

gRPC

Order 服务的 gRPC 服务端需要在实现时与账户和目录服务建立联系。

Protocol Buffers 定义如下:

order/order.proto

  1. syntax = "proto3";
  2. package pb;
  3. message Order {
  4. message OrderProduct {
  5. string id = 1;
  6. string name = 2;
  7. string description = 3;
  8. double price = 4;
  9. uint32 quantity = 5;
  10. }
  11. string id = 1;
  12. bytes createdAt = 2;
  13. string accountId = 3;
  14. double totalPrice = 4;
  15. repeated OrderProduct products = 5;
  16. }
  17. message PostOrderRequest {
  18. message OrderProduct {
  19. string productId = 2;
  20. uint32 quantity = 3;
  21. }
  22. string accountId = 2;
  23. repeated OrderProduct products = 4;
  24. }
  25. message PostOrderResponse {
  26. Order order = 1;
  27. }
  28. message GetOrderRequest {
  29. string id = 1;
  30. }
  31. message GetOrderResponse {
  32. Order order = 1;
  33. }
  34. message GetOrdersForAccountRequest {
  35. string accountId = 1;
  36. }
  37. message GetOrdersForAccountResponse {
  38. repeated Order orders = 1;
  39. }
  40. service OrderService {
  41. rpc PostOrder (PostOrderRequest) returns (PostOrderResponse) {}
  42. rpc GetOrdersForAccount (GetOrdersForAccountRequest) returns (GetOrdersForAccountResponse) {}
  43. }

运行服务需要传递其他服务的 URL:

order/server.go

  1. type grpcServer struct {
  2. service Service
  3. accountClient *account.Client
  4. catalogClient *catalog.Client
  5. }
  6. func ListenGRPC(s Service, accountURL, catalogURL string, port int) error {
  7. accountClient, err := account.NewClient(accountURL)
  8. if err != nil {
  9. return err
  10. }
  11. catalogClient, err := catalog.NewClient(catalogURL)
  12. if err != nil {
  13. accountClient.Close()
  14. return err
  15. }
  16. lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
  17. if err != nil {
  18. accountClient.Close()
  19. catalogClient.Close()
  20. return err
  21. }
  22. serv := grpc.NewServer()
  23. pb.RegisterOrderServiceServer(serv, &grpcServer{
  24. s,
  25. accountClient,
  26. catalogClient,
  27. })
  28. reflection.Register(serv)
  29. return serv.Serve(lis)
  30. }

创建订单涉及调用帐户服务、检查帐户是否存在、然后对产品执行相同操作。计算总价时还需要读取产品价格。你不会希望用户能传入自己的商品的总价。

  1. func (s *grpcServer) PostOrder(
  2. ctx context.Context,
  3. r *pb.PostOrderRequest,
  4. ) (*pb.PostOrderResponse, error) {
  5. // 检查账户是否存在
  6. _, err := s.accountClient.GetAccount(ctx, r.AccountId)
  7. if err != nil {
  8. log.Println(err)
  9. return nil, err
  10. }
  11. // 获取订单商品
  12. productIDs := []string{}
  13. for _, p := range r.Products {
  14. productIDs = append(productIDs, p.ProductId)
  15. }
  16. orderedProducts, err := s.catalogClient.GetProducts(ctx, 0, 0, productIDs, "")
  17. if err != nil {
  18. log.Println(err)
  19. return nil, err
  20. }
  21. // 构造商品
  22. products := []OrderedProduct{}
  23. for _, p := range orderedProducts {
  24. product := OrderedProduct{
  25. ID: p.ID,
  26. Quantity: 0,
  27. Price: p.Price,
  28. Name: p.Name,
  29. Description: p.Description,
  30. }
  31. for _, rp := range r.Products {
  32. if rp.ProductId == p.ID {
  33. product.Quantity = rp.Quantity
  34. break
  35. }
  36. }
  37. if product.Quantity != 0 {
  38. products = append(products, product)
  39. }
  40. }
  41. // 调用服务实现
  42. order, err := s.service.PostOrder(ctx, r.AccountId, products)
  43. if err != nil {
  44. log.Println(err)
  45. return nil, err
  46. }
  47. // 创建订单响应
  48. orderProto := &pb.Order{
  49. Id: order.ID,
  50. AccountId: order.AccountID,
  51. TotalPrice: order.TotalPrice,
  52. Products: []*pb.Order_OrderProduct{},
  53. }
  54. orderProto.CreatedAt, _ = order.CreatedAt.MarshalBinary()
  55. for _, p := range order.Products {
  56. orderProto.Products = append(orderProto.Products, &pb.Order_OrderProduct{
  57. Id: p.ID,
  58. Name: p.Name,
  59. Description: p.Description,
  60. Price: p.Price,
  61. Quantity: p.Quantity,
  62. })
  63. }
  64. return &pb.PostOrderResponse{
  65. Order: orderProto,
  66. }, nil
  67. }

当请求特定账户的订单时,由于需要产品的详情,因此调用目录服务是有必要的。

GraphQL 服务

GraphQL schema 的定义在 graphql/schema.graphql 文件中:

  1. scalar Time
  2. type Account {
  3. id: String!
  4. name: String!
  5. orders: [Order!]!
  6. }
  7. type Product {
  8. id: String!
  9. name: String!
  10. description: String!
  11. price: Float!
  12. }
  13. type Order {
  14. id: String!
  15. createdAt: Time!
  16. totalPrice: Float!
  17. products: [OrderedProduct!]!
  18. }
  19. type OrderedProduct {
  20. id: String!
  21. name: String!
  22. description: String!
  23. price: Float!
  24. quantity: Int!
  25. }
  26. input PaginationInput {
  27. skip: Int
  28. take: Int
  29. }
  30. input AccountInput {
  31. name: String!
  32. }
  33. input ProductInput {
  34. name: String!
  35. description: String!
  36. price: Float!
  37. }
  38. input OrderProductInput {
  39. id: String!
  40. quantity: Int!
  41. }
  42. input OrderInput {
  43. accountId: String!
  44. products: [OrderProductInput!]!
  45. }
  46. type Mutation {
  47. createAccount(account: AccountInput!): Account
  48. createProduct(product: ProductInput!): Product
  49. createOrder(order: OrderInput!): Order
  50. }
  51. type Query {
  52. accounts(pagination: PaginationInput, id: String): [Account!]!
  53. products(pagination: PaginationInput, query: String, id: String): [Product!]!
  54. }

gqlgen 工具会生成一堆类型,但是还需要对 Order 模型进行一些控制,在 graphql/types.json 文件中进行制定,从而不会自动生成模型:

  1. {
  2. "Order": "github.com/tinrab/spidey/graphql/graph.Order"
  3. }

现在可以手动实现 Order 结构了:

graphql/graph/models.go

  1. package graph
  2. import time "time"
  3. type Order struct {
  4. ID string `json:"id"`
  5. CreatedAt time.Time `json:"createdAt"`
  6. TotalPrice float64 `json:"totalPrice"`
  7. Products []OrderedProduct `json:"products"`
  8. }

生成类型的指令在 graphql/graph/graph.go 顶部:

  1. //go:generate gqlgen -schema ../schema.graphql -typemap ../types.json
  2. package graph

通过下面的命令运行:

  1. $ go generate ./graphql/graph/graph.go

GraphQL 服务端引用了所有其他服务。

graphql/graph/graph.go

  1. type GraphQLServer struct {
  2. accountClient *account.Client
  3. catalogClient *catalog.Client
  4. orderClient *order.Client
  5. }
  6. func NewGraphQLServer(accountUrl, catalogURL, orderURL string) (*GraphQLServer, error) {
  7. // 连接账户服务
  8. accountClient, err := account.NewClient(accountUrl)
  9. if err != nil {
  10. return nil, err
  11. }
  12. // 连接目录服务
  13. catalogClient, err := catalog.NewClient(catalogURL)
  14. if err != nil {
  15. accountClient.Close()
  16. return nil, err
  17. }
  18. // 连接订单服务
  19. orderClient, err := order.NewClient(orderURL)
  20. if err != nil {
  21. accountClient.Close()
  22. catalogClient.Close()
  23. return nil, err
  24. }
  25. return &GraphQLServer{
  26. accountClient,
  27. catalogClient,
  28. orderClient,
  29. }, nil
  30. }

GraphQLServer 结构体需要实现所有生成的 resolver。修改(Mutation)可以在 graphql/graph/mutations.go 中找到,查询(Query)则可以在 graphql/graph/queries.go 中找到。

修改操作通过调用相关服务客户端传入参数进行实现:

  1. func (s *GraphQLServer) Mutation_createAccount(ctx context.Context, in AccountInput) (*Account, error) {
  2. ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
  3. defer cancel()
  4. a, err := s.accountClient.PostAccount(ctx, in.Name)
  5. if err != nil {
  6. log.Println(err)
  7. return nil, err
  8. }
  9. return &Account{
  10. ID: a.ID,
  11. Name: a.Name,
  12. }, nil
  13. }

查询能够互相嵌套。在 Spidey 中,查询账户还可以查询其订单,见 Account_orders 函数。

  1. func (s *GraphQLServer) Query_accounts(ctx context.Context, pagination *PaginationInput, id *string) ([]Account, error) {
  2. // 会被首先调用
  3. // ...
  4. }
  5. func (s *GraphQLServer) Account_orders(ctx context.Context, obj *Account) ([]Order, error) {
  6. // 然后执行这个函数,返回 "obj" 账户的订单
  7. // ...
  8. }

总结

执行下面的命令就可以运行 Spidey:

  1. $ vgo vendor
  2. $ docker-compose up -d --build

然后你就可以在浏览器中访问 http://localhost:8000/playground 来使用 GraphQL 工具创建一个账户了:

  1. mutation {
  2. createAccount(account: {name: "John"}) {
  3. id
  4. name
  5. }
  6. }

返回结果为:

  1. {
  2. "data": {
  3. "createAccount": {
  4. "id": "15t4u0du7t6vm9SRa4m3PrtREHb",
  5. "name": "John"
  6. }
  7. }
  8. }

然后可以创建一些产品:

  1. mutation {
  2. a: createProduct(product: {name: "Kindle Oasis", description: "Kindle Oasis is the first waterproof Kindle with our largest 7-inch 300 ppi display, now with Audible when paired with Bluetooth.", price: 300}) { id },
  3. b: createProduct(product: {name: "Samsung Galaxy S9", description: "Discover Galaxy S9 and S9+ and the revolutionary camera that adapts like the human eye.", price: 720}) { id },
  4. c: createProduct(product: {name: "Sony PlayStation 4", description: "The PlayStation 4 is an eighth-generation home video game console developed by Sony Interactive Entertainment", price: 300}) { id },
  5. d: createProduct(product: {name: "ASUS ZenBook Pro UX550VE", description: "Designed to entice. Crafted to perform.", price: 300}) { id },
  6. e: createProduct(product: {name: "Mpow PC Headset 3.5mm", description: "Computer Headset with Microphone Noise Cancelling, Lightweight PC Headset Wired Headphones, Business Headset for Skype, Webinar, Phone, Call Center", price: 43}) { id }
  7. }

注意返回的 ID 值:

  1. {
  2. "data": {
  3. "a": {
  4. "id": "15t7jjANR47uODEPUIy1od5APnC"
  5. },
  6. "b": {
  7. "id": "15t7jsTyrvs1m4EYu7TCes1EN5z"
  8. },
  9. "c": {
  10. "id": "15t7jrfDhZKgxOdIcEtTUsriAsY"
  11. },
  12. "d": {
  13. "id": "15t7jpKt4VkJ5iHbwt4rB5xR77w"
  14. },
  15. "e": {
  16. "id": "15t7jsYs0YzK3B7drQuf1mX5Dyg"
  17. }
  18. }
  19. }

然后发起一些订单:

  1. mutation {
  2. createOrder(order: { accountId: "15t4u0du7t6vm9SRa4m3PrtREHb", products: [
  3. { id: "15t7jjANR47uODEPUIy1od5APnC", quantity: 2 },
  4. { id: "15t7jpKt4VkJ5iHbwt4rB5xR77w", quantity: 1 },
  5. { id: "15t7jrfDhZKgxOdIcEtTUsriAsY", quantity: 5 }
  6. ]}) {
  7. id
  8. createdAt
  9. totalPrice
  10. }
  11. }

根据返回结果检查返回的费用:

  1. {
  2. "data": {
  3. "createOrder": {
  4. "id": "15t8B6lkg80ZINTASts92nBzyE8",
  5. "createdAt": "2018-06-11T21:18:18Z",
  6. "totalPrice": 2400
  7. }
  8. }
  9. }

完整代码请查看 GitHub

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

ft_authoradmin  ft_create_time2018-09-03 10:54
 ft_update_time2018-09-03 10:55