[譯] 使用 Go 編寫微服務及其 GraphQL 網關

幾個月前,一個優秀的 GraphQL Go 包 vektah/gqlgen 開始流行。本文描述了在 Spidey 項目(一個在線商店的基本微服務)中如何實現 GraphQL。前端

下面列出的一些代碼可能存在一些缺失,完整的代碼請訪問 GitHubandroid

架構

Spidey 包含了三個不一樣的服務並暴露給了 GraphQL 網關。集羣內部的通訊則經過 gRPC 來完成。ios

帳戶服務管理了全部的帳號;目錄服務管理了全部的產品;訂單服務則處理了全部的訂單建立行爲。它會與其餘兩個服務進行通訊來告知訂單是否正常完成。git

Architecture

獨立的服務包含三層:Server 層Service 層以及Repository 層。服務端做負責通訊,也就是 Spidey 中使用 gRPC。服務則包含了業務邏輯。倉庫則負責對數據庫進行讀寫操做。github

起步

運行 Spidey 須要 DockerDocker ComposeGoProtocol Buffers 編譯器及其 Go 插件以及很是有用的 vektah/gqlgen 包。golang

你還須要安裝 vgo(一個處於早期開發階段的包管理工具)。工具 dep 也是一種選擇,可是包含的 go.mod 文件會被忽略。sql

譯註:在 Go 1.11 中 vgo 做爲官方集成的 Go Modules 發佈,已集成在 go 命令中,使用 go mod 進行使用,指令與 vgo 基本一致。docker

Docker 設置

每一個服務在其自身的子文件夾中實現,並至少包含一個 app.dockerfile 文件。app.dockerfile 文件用戶構建數據庫鏡像。數據庫

account
├── account.proto
├── app.dockerfile
├── cmd
│   └── account
│       └── main.go
├── db.dockerfile
└── up.sql
複製代碼

全部服務經過外部的 docker-compose.yaml 定義。json

下面是截取的一部分關於 Account 服務的內容:

version: "3.6"

services:
 account:
 build:
 context: "."
 dockerfile: "./account/app.dockerfile"
 depends_on:
 - "account_db"
 environment:
 DATABASE_URL: "postgres://spidey:123456@account_db/spidey?sslmode=disable"
 account_db:
 build:
 context: "./account"
 dockerfile: "./db.dockerfile"
 environment:
 POSTGRES_DB: "spidey"
 POSTGRES_USER: "spidey"
 POSTGRES_PASSWORD: "123456"
 restart: "unless-stopped"
複製代碼

設置 context 的目的是保證 vendor 目錄可以被複制到 Docker 容器中。全部服務共享相同的依賴、某些服務還依賴其餘服務的定義。

帳戶服務

帳戶服務暴露了建立以及索引帳戶的方法。

服務

帳戶服務的 API 定義的接口以下:

account/service.go

type Service interface {
  PostAccount(ctx context.Context, name string) (*Account, error)
  GetAccount(ctx context.Context, id string) (*Account, error)
  GetAccounts(ctx context.Context, skip uint64, take uint64) ([]Account, error)
}

type Account struct {
  ID   string `json:"id"`
  Name string `json:"name"`
}
複製代碼

實現須要用到 Repository:

type accountService struct {
  repository Repository
}

func NewService(r Repository) Service {
  return &accountService{r}
}
複製代碼

這個服務負責了全部的業務邏輯。PostAccount 函數的實現以下:

func (s *accountService) PostAccount(ctx context.Context, name string) (*Account, error) {
  a := &Account{
    Name: name,
    ID:   ksuid.New().String(),
  }
  if err := s.repository.PutAccount(ctx, *a); err != nil {
    return nil, err
  }
  return a, nil
}
複製代碼

它將線路協議解析處理爲服務端,並將數據庫處理爲 Repository。

數據庫

一個帳戶的數據模型很是簡單:

CREATE TABLE IF NOT EXISTS accounts (
  id CHAR(27) PRIMARY KEY,
  name VARCHAR(24) NOT NULL
);
複製代碼

上面定義數據的 SQL 文件會複製到 Docker 容器中執行。

account/db.dockerfile

FROM postgres:10.3

COPY up.sql /docker-entrypoint-initdb.d/1.sql

CMD ["postgres"]
複製代碼

PostgreSQL 數據庫經過下面的 Repository 接口進行訪問:

account/repository.go

type Repository interface {
  Close()
  PutAccount(ctx context.Context, a Account) error
  GetAccountByID(ctx context.Context, id string) (*Account, error)
  ListAccounts(ctx context.Context, skip uint64, take uint64) ([]Account, error)
}
複製代碼

Repository 基於 Go 標準庫 SQL 包進行封裝:

type postgresRepository struct {
  db *sql.DB
}

func NewPostgresRepository(url string) (Repository, error) {
  db, err := sql.Open("postgres", url)
  if err != nil {
    return nil, err
  }
  err = db.Ping()
  if err != nil {
    return nil, err
  }
  return &postgresRepository{db}, nil
}
複製代碼

gRPC

帳戶服務的 gRPC 服務定義了下面的 Protocol Buffer:

account/account.proto

syntax = "proto3";
package pb;

message Account {
  string id = 1;
  string name = 2;
}

message PostAccountRequest {
  string name = 1;
}

message PostAccountResponse {
  Account account = 1;
}

message GetAccountRequest {
  string id = 1;
}

message GetAccountResponse {
  Account account = 1;
}

message GetAccountsRequest {
  uint64 skip = 1;
  uint64 take = 2;
}

message GetAccountsResponse {
  repeated Account accounts = 1;
}

service AccountService {
  rpc PostAccount (PostAccountRequest) returns (PostAccountResponse) {} rpc GetAccount (GetAccountRequest) returns (GetAccountResponse) {} rpc GetAccounts (GetAccountsRequest) returns (GetAccountsResponse) {} } 複製代碼

因爲這個包被設置爲了 pb,因而生成的代碼能夠從 pb 子包導入使用。

gRPC 的代碼可使用 Go 的 generate 指令配合 account/server.go 文件最上方的註釋進行編譯生成:

account/server.go

//go:generate protoc ./account.proto --go_out=plugins=grpc:./pb
package account
複製代碼

運行下面的命令就能夠將代碼生成到 pb 子目錄:

$ go generate account/server.go
複製代碼

服務端做爲 Service 服務接口的適配器,對應轉換了請求和返回的類型。

type grpcServer struct {
  service Service
}

func ListenGRPC(s Service, port int) error {
  lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
  if err != nil {
    return err
  }
  serv := grpc.NewServer()
  pb.RegisterAccountServiceServer(serv, &grpcServer{s})
  reflection.Register(serv)
  return serv.Serve(lis)
}
複製代碼

下面是 PostAccount 函數的實現:

func (s *grpcServer) PostAccount(ctx context.Context, r *pb.PostAccountRequest) (*pb.PostAccountResponse, error) {
  a, err := s.service.PostAccount(ctx, r.Name)
  if err != nil {
    return nil, err
  }
  return &pb.PostAccountResponse{Account: &pb.Account{
    Id:   a.ID,
    Name: a.Name,
  }}, nil
}
複製代碼

用法

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

type Config struct {
  DatabaseURL string `envconfig:"DATABASE_URL"`
}

func main() {
  var cfg Config
  err := envconfig.Process("", &cfg)
  if err != nil {
    log.Fatal(err)
  }

  var r account.Repository
  retry.ForeverSleep(2*time.Second, func(_ int) (err error) {
    r, err = account.NewPostgresRepository(cfg.DatabaseURL)
    if err != nil {
      log.Println(err)
    }
    return
  })
  defer r.Close()

  log.Println("Listening on port 8080...")
  s := account.NewService(r)
  log.Fatal(account.ListenGRPC(s, 8080))
}
複製代碼

客戶端結構體的實現位於 account/client.go 文件中。這樣帳戶服務就能夠在無需瞭解 RPC 內部實現的狀況下進行實現,咱們以後再來詳細討論。

account, err := accountClient.GetAccount(ctx, accountId)
if err != nil {
  log.Fatal(err)
}
複製代碼

目錄服務

目錄服務負責處理 Spidey 商店的商品。它實現了相似於帳戶服務的功能,可是使用了 Elasticsearch 對商品進行持久化。

服務

目錄服務遵循下面的接口:

catalog/service.go

type Service interface {
  PostProduct(ctx context.Context, name, description string, price float64) (*Product, error)
  GetProduct(ctx context.Context, id string) (*Product, error)
  GetProducts(ctx context.Context, skip uint64, take uint64) ([]Product, error)
  GetProductsByIDs(ctx context.Context, ids []string) ([]Product, error)
  SearchProducts(ctx context.Context, query string, skip uint64, take uint64) ([]Product, error)
}

type Product struct {
  ID          string  `json:"id"`
  Name        string  `json:"name"`
  Description string  `json:"description"`
  Price       float64 `json:"price"`
}
複製代碼

數據庫

Repository 基於 Elasticsearch olivere/elastic 包進行實現。

catalog/repository.go

type Repository interface {
  Close()
  PutProduct(ctx context.Context, p Product) error
  GetProductByID(ctx context.Context, id string) (*Product, error)
  ListProducts(ctx context.Context, skip uint64, take uint64) ([]Product, error)
  ListProductsWithIDs(ctx context.Context, ids []string) ([]Product, error)
  SearchProducts(ctx context.Context, query string, skip uint64, take uint64) ([]Product, error)
}
複製代碼

因爲 Elasticsearch 將文檔和 ID 分開存儲,所以實現的一個商品的輔助結構沒有包含 ID:

type productDocument struct {
  Name        string  `json:"name"`
  Description string  `json:"description"`
  Price       float64 `json:"price"`
}
複製代碼

將商品插入到數據庫中:

func (r *elasticRepository) PutProduct(ctx context.Context, p Product) error {
  _, err := r.client.Index().
    Index("catalog").
    Type("product").
    Id(p.ID).
    BodyJson(productDocument{
      Name:        p.Name,
      Description: p.Description,
      Price:       p.Price,
    }).
    Do(ctx)
  return err
}
複製代碼

gRPC

目錄服務的 gRPC 服務定義在 catalog/catalog.proto 文件中,並在 catalog/server.go 中進行實現。與帳戶服務不一樣的是,它沒有在服務接口中定義全部的 endpoint。

catalog/catalog.proto

syntax = "proto3";
package pb;

message Product {
  string id = 1;
  string name = 2;
  string description = 3;
  double price = 4;
}

message PostProductRequest {
  string name = 1;
  string description = 2;
  double price = 3;
}

message PostProductResponse {
  Product product = 1;
}

message GetProductRequest {
  string id = 1;
}

message GetProductResponse {
  Product product = 1;
}

message GetProductsRequest {
  uint64 skip = 1;
  uint64 take = 2;
  repeated string ids = 3;
  string query = 4;
}

message GetProductsResponse {
  repeated Product products = 1;
}

service CatalogService {
  rpc PostProduct (PostProductRequest) returns (PostProductResponse) {} rpc GetProduct (GetProductRequest) returns (GetProductResponse) {} rpc GetProducts (GetProductsRequest) returns (GetProductsResponse) {} } 複製代碼

儘管 GetProductRequest 消息包含了額外的字段,但經過 ID 的搜索與索引實現。

下面的代碼展現了 GetProducts 函數的實現:

catalog/server.go

func (s *grpcServer) GetProducts(ctx context.Context, r *pb.GetProductsRequest) (*pb.GetProductsResponse, error) {
  var res []Product
  var err error
  if r.Query != "" {
    res, err = s.service.SearchProducts(ctx, r.Query, r.Skip, r.Take)
  } else if len(r.Ids) != 0 {
    res, err = s.service.GetProductsByIDs(ctx, r.Ids)
  } else {
    res, err = s.service.GetProducts(ctx, r.Skip, r.Take)
  }
  if err != nil {
    log.Println(err)
    return nil, err
  }

  products := []*pb.Product{}
  for _, p := range res {
    products = append(
      products,
      &pb.Product{
        Id:          p.ID,
        Name:        p.Name,
        Description: p.Description,
        Price:       p.Price,
      },
    )
  }
  return &pb.GetProductsResponse{Products: products}, nil
}
複製代碼

它決定了當給定何種參數來調用何種服務函數。其目標是模擬 REST HTTP 的 endpoint。

對於 /products?[ids=...]&[query=...]&skip=0&take=100 形式的請求,只有設計一個 endpoint 來完成 API 調用會相對容易一些。

Order 服務

Order 訂單服務就比較棘手了。他須要調用帳戶和目錄服務來驗證請求,由於一個訂單隻能給一個特定的帳號和一個存在的商品進行建立。

Service

Service 接口定義了經過帳戶建立和索引所有訂單的接口。

order/service.go

type Service interface {
  PostOrder(ctx context.Context, accountID string, products []OrderedProduct) (*Order, error)
  GetOrdersForAccount(ctx context.Context, accountID string) ([]Order, error)
}

type Order struct {
  ID         string
  CreatedAt  time.Time
  TotalPrice float64
  AccountID  string
  Products   []OrderedProduct
}

type OrderedProduct struct {
  ID          string
  Name        string
  Description string
  Price       float64
  Quantity    uint32
}
複製代碼

數據庫

一個訂單能夠包含多個商品,所以數據模型必須支持這種形式。下面的 order_products 表描述了 ID 爲 product_id 的訂購產品以及此類產品的數量。而 product_id 字段必須能夠從目錄服務進行檢索。

order/up.sql

CREATE TABLE IF NOT EXISTS orders (
  id CHAR(27) PRIMARY KEY,
  created_at TIMESTAMP WITH TIME ZONE NOT NULL,
  account_id CHAR(27) NOT NULL,
  total_price MONEY NOT NULL
);

CREATE TABLE IF NOT EXISTS order_products (
  order_id CHAR(27) REFERENCES orders (id) ON DELETE CASCADE,
  product_id CHAR(27),
  quantity INT NOT NULL,
  PRIMARY KEY (product_id, order_id)
);
複製代碼

Repository 接口很簡單:

order/repository.go

type Repository interface {
  Close()
  PutOrder(ctx context.Context, o Order) error
  GetOrdersForAccount(ctx context.Context, accountID string) ([]Order, error)
}
複製代碼

但實現它卻並不簡單。

一個訂單必須使用事務機制分兩步插入,而後經過 join 語句進行查詢。

從數據庫中讀取訂單須要解析一個表狀結構數據讀取到對象結構中。下面的代碼基於訂單 ID 將商品讀取到訂單中:

orders := []Order{}
order := &Order{}
lastOrder := &Order{}
orderedProduct := &OrderedProduct{}
products := []OrderedProduct{}

// 將每行讀取到 Order 結構體
for rows.Next() {
  if err = rows.Scan(
    &order.ID,
    &order.CreatedAt,
    &order.AccountID,
    &order.TotalPrice,
    &orderedProduct.ID,
    &orderedProduct.Quantity,
  ); err != nil {
    return nil, err
  }
  // 讀取訂單
  if lastOrder.ID != "" && lastOrder.ID != order.ID {
    newOrder := Order{
      ID:         lastOrder.ID,
      AccountID:  lastOrder.AccountID,
      CreatedAt:  lastOrder.CreatedAt,
      TotalPrice: lastOrder.TotalPrice,
      Products:   products,
    }
    orders = append(orders, newOrder)
    products = []OrderedProduct{}
  }
  // 讀取商品
  products = append(products, OrderedProduct{
    ID:       orderedProduct.ID,
    Quantity: orderedProduct.Quantity,
  })

  *lastOrder = *order
}

// 添加最後一個訂單 (或者第一個 :D)
if lastOrder != nil {
  newOrder := Order{
    ID:         lastOrder.ID,
    AccountID:  lastOrder.AccountID,
    CreatedAt:  lastOrder.CreatedAt,
    TotalPrice: lastOrder.TotalPrice,
    Products:   products,
  }
  orders = append(orders, newOrder)
}
複製代碼

gRPC

Order 服務的 gRPC 服務端須要在實現時與帳戶和目錄服務創建聯繫。

Protocol Buffers 定義以下:

order/order.proto

syntax = "proto3";
package pb;

message Order {
  message OrderProduct {
    string id = 1;
    string name = 2;
    string description = 3;
    double price = 4;
    uint32 quantity = 5;
  }

  string id = 1;
  bytes createdAt = 2;
  string accountId = 3;
  double totalPrice = 4;
  repeated OrderProduct products = 5;
}

message PostOrderRequest {
  message OrderProduct {
    string productId = 2;
    uint32 quantity = 3;
  }

  string accountId = 2;
  repeated OrderProduct products = 4;
}

message PostOrderResponse {
  Order order = 1;
}

message GetOrderRequest {
  string id = 1;
}

message GetOrderResponse {
  Order order = 1;
}

message GetOrdersForAccountRequest {
  string accountId = 1;
}

message GetOrdersForAccountResponse {
  repeated Order orders = 1;
}

service OrderService {
  rpc PostOrder (PostOrderRequest) returns (PostOrderResponse) {} rpc GetOrdersForAccount (GetOrdersForAccountRequest) returns (GetOrdersForAccountResponse) {} } 複製代碼

運行訂單服務須要傳遞其餘服務的 URL:

order/server.go

type grpcServer struct {
  service       Service
  accountClient *account.Client
  catalogClient *catalog.Client
}

func ListenGRPC(s Service, accountURL, catalogURL string, port int) error {
  accountClient, err := account.NewClient(accountURL)
  if err != nil {
    return err
  }

  catalogClient, err := catalog.NewClient(catalogURL)
  if err != nil {
    accountClient.Close()
    return err
  }

  lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
  if err != nil {
    accountClient.Close()
    catalogClient.Close()
    return err
  }

  serv := grpc.NewServer()
  pb.RegisterOrderServiceServer(serv, &grpcServer{
    s,
    accountClient,
    catalogClient,
  })
  reflection.Register(serv)

  return serv.Serve(lis)
}
複製代碼

建立訂單涉及調用賬戶服務、檢查賬戶是否存在、而後對產品執行相同操做。計算總價時還須要讀取產品價格。你不會但願用戶能傳入本身的商品的總價。

func (s *grpcServer) PostOrder( ctx context.Context, r *pb.PostOrderRequest, ) (*pb.PostOrderResponse, error) {
  // 檢查帳戶是否存在
  _, err := s.accountClient.GetAccount(ctx, r.AccountId)
  if err != nil {
    log.Println(err)
    return nil, err
  }

  // 獲取訂單商品
  productIDs := []string{}
  for _, p := range r.Products {
    productIDs = append(productIDs, p.ProductId)
  }
  orderedProducts, err := s.catalogClient.GetProducts(ctx, 0, 0, productIDs, "")
  if err != nil {
    log.Println(err)
    return nil, err
  }

  // 構造商品
  products := []OrderedProduct{}
  for _, p := range orderedProducts {
    product := OrderedProduct{
      ID:          p.ID,
      Quantity:    0,
      Price:       p.Price,
      Name:        p.Name,
      Description: p.Description,
    }
    for _, rp := range r.Products {
      if rp.ProductId == p.ID {
        product.Quantity = rp.Quantity
        break
      }
    }

    if product.Quantity != 0 {
      products = append(products, product)
    }
  }

  // 調用服務實現
  order, err := s.service.PostOrder(ctx, r.AccountId, products)
  if err != nil {
    log.Println(err)
    return nil, err
  }

  // 建立訂單響應
  orderProto := &pb.Order{
    Id:         order.ID,
    AccountId:  order.AccountID,
    TotalPrice: order.TotalPrice,
    Products:   []*pb.Order_OrderProduct{},
  }
  orderProto.CreatedAt, _ = order.CreatedAt.MarshalBinary()
  for _, p := range order.Products {
    orderProto.Products = append(orderProto.Products, &pb.Order_OrderProduct{
      Id:          p.ID,
      Name:        p.Name,
      Description: p.Description,
      Price:       p.Price,
      Quantity:    p.Quantity,
    })
  }
  return &pb.PostOrderResponse{
    Order: orderProto,
  }, nil
}
複製代碼

當請求特定帳戶的訂單時,因爲須要產品的詳情,所以調用目錄服務是有必要的。

GraphQL 服務

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

scalar Time

type Account {
  id: String!
  name: String!
  orders: [Order!]!
}

type Product {
  id: String!
  name: String!
  description: String!
  price: Float!
}

type Order {
  id: String!
  createdAt: Time!
  totalPrice: Float!
  products: [OrderedProduct!]!
}

type OrderedProduct {
  id: String!
  name: String!
  description: String!
  price: Float!
  quantity: Int!
}

input PaginationInput {
  skip: Int
  take: Int
}

input AccountInput {
  name: String!
}

input ProductInput {
  name: String!
  description: String!
  price: Float!
}

input OrderProductInput {
  id: String!
  quantity: Int!
}

input OrderInput {
  accountId: String!
  products: [OrderProductInput!]!
}

type Mutation {
  createAccount(account: AccountInput!): Account
  createProduct(product: ProductInput!): Product
  createOrder(order: OrderInput!): Order
}

type Query {
  accounts(pagination: PaginationInput, id: String): [Account!]!
  products(pagination: PaginationInput, query: String, id: String): [Product!]!
}
複製代碼

gqlgen 工具會生成一堆類型,可是還須要對 Order 模型進行一些控制,在 graphql/types.json 文件中進行制定,從而不會自動生成模型:

{
  "Order": "github.com/tinrab/spidey/graphql/graph.Order"
}
複製代碼

如今能夠手動實現 Order 結構了:

graphql/graph/models.go

package graph

import time "time"

type Order struct {
  ID         string           `json:"id"`
  CreatedAt  time.Time        `json:"createdAt"`
  TotalPrice float64          `json:"totalPrice"`
  Products   []OrderedProduct `json:"products"`
}
複製代碼

生成類型的指令在 graphql/graph/graph.go 頂部:

//go:generate gqlgen -schema ../schema.graphql -typemap ../types.json
package graph
複製代碼

經過下面的命令運行:

$ go generate ./graphql/graph/graph.go
複製代碼

GraphQL 服務端引用了全部其餘服務。

graphql/graph/graph.go

type GraphQLServer struct {
  accountClient *account.Client
  catalogClient *catalog.Client
  orderClient   *order.Client
}

func NewGraphQLServer(accountUrl, catalogURL, orderURL string) (*GraphQLServer, error) {
  // 鏈接帳戶服務
  accountClient, err := account.NewClient(accountUrl)
  if err != nil {
    return nil, err
  }

  // 鏈接目錄服務
  catalogClient, err := catalog.NewClient(catalogURL)
  if err != nil {
    accountClient.Close()
    return nil, err
  }

  // 鏈接訂單服務
  orderClient, err := order.NewClient(orderURL)
  if err != nil {
    accountClient.Close()
    catalogClient.Close()
    return nil, err
  }

  return &GraphQLServer{
    accountClient,
    catalogClient,
    orderClient,
  }, nil
}
複製代碼

GraphQLServer 結構體須要實現全部生成的 resolver。修改(Mutation)能夠在 graphql/graph/mutations.go 中找到,查詢(Query)則能夠在 graphql/graph/queries.go 中找到。

修改操做經過調用相關服務客戶端傳入參數進行實現:

func (s *GraphQLServer) Mutation_createAccount(ctx context.Context, in AccountInput) (*Account, error) {
  ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
  defer cancel()

  a, err := s.accountClient.PostAccount(ctx, in.Name)
  if err != nil {
    log.Println(err)
    return nil, err
  }

  return &Account{
    ID:   a.ID,
    Name: a.Name,
  }, nil
}
複製代碼

查詢可以互相嵌套。在 Spidey 中,查詢帳戶還能夠查詢其訂單,見 Account_orders 函數。

func (s *GraphQLServer) Query_accounts(ctx context.Context, pagination *PaginationInput, id *string) ([]Account, error) {
  // 會被首先調用
  // ...
}

func (s *GraphQLServer) Account_orders(ctx context.Context, obj *Account) ([]Order, error) {
  // 而後執行這個函數,返回 "obj" 帳戶的訂單
  // ...
}
複製代碼

總結

執行下面的命令就能夠運行 Spidey:

$ vgo vendor
$ docker-compose up -d --build
複製代碼

而後你就能夠在瀏覽器中訪問 http://localhost:8000/playground 來使用 GraphQL 工具建立一個帳戶了:

mutation {
  createAccount(account: {name: "John"}) {
    id
    name
  }
}
複製代碼

返回結果爲:

{
  "data": {
    "createAccount": {
      "id": "15t4u0du7t6vm9SRa4m3PrtREHb",
      "name": "John"
    }
  }
}
複製代碼

而後能夠建立一些產品:

mutation {
  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 },
  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 },
  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 },
  d: createProduct(product: {name: "ASUS ZenBook Pro UX550VE", description: "Designed to entice. Crafted to perform.", price: 300}) { id },
  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 }
}
複製代碼

注意返回的 ID 值:

{
  "data": {
    "a": {
      "id": "15t7jjANR47uODEPUIy1od5APnC"
    },
    "b": {
      "id": "15t7jsTyrvs1m4EYu7TCes1EN5z"
    },
    "c": {
      "id": "15t7jrfDhZKgxOdIcEtTUsriAsY"
    },
    "d": {
      "id": "15t7jpKt4VkJ5iHbwt4rB5xR77w"
    },
    "e": {
      "id": "15t7jsYs0YzK3B7drQuf1mX5Dyg"
    }
  }
}
複製代碼

而後發起一些訂單:

mutation {
  createOrder(order: { accountId: "15t4u0du7t6vm9SRa4m3PrtREHb", products: [
    { id: "15t7jjANR47uODEPUIy1od5APnC", quantity: 2 },
    { id: "15t7jpKt4VkJ5iHbwt4rB5xR77w", quantity: 1 },
    { id: "15t7jrfDhZKgxOdIcEtTUsriAsY", quantity: 5 }
  ]}) {
    id
    createdAt
    totalPrice
  }
}
複製代碼

根據返回結果檢查返回的費用:

{
  "data": {
    "createOrder": {
      "id": "15t8B6lkg80ZINTASts92nBzyE8",
      "createdAt": "2018-06-11T21:18:18Z",
      "totalPrice": 2400
    }
  }
}
複製代碼

完整代碼請查看 GitHub

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索