- Оригинальный адрес:Using GraphQL with Microservices in Go
- Оригинальный автор:Tin Rabzelj
- Перевод с:Программа перевода самородков
- Постоянная ссылка на эту статью:GitHub.com/rare earth/gold-no…
- Переводчик:Changkun Ou
- Корректор:razertory
Несколько месяцев назад отличный пакет GraphQL Govektah/gqlgenстал популярным. В этой статье описывается, как реализовать GraphQL в проекте Spidey, базовом микросервисе для интернет-магазина.
Некоторые из кодов, перечисленных ниже, могут отсутствовать, полный код, пожалуйста, посетитеGitHub.
Архитектура
Spidey содержит три разных сервиса, доступных для шлюза GraphQL. Коммуникация внутри кластера проходит черезgRPCЧто нужно сделать.
Служба учетных записей управляет всеми учетными записями, служба каталогов управляет всеми продуктами, служба заказов занимается созданием всех заказов. Он связывается с двумя другими службами, чтобы сообщить, был ли заказ выполнен нормально.
Автономный сервис состоит из трех уровней:Уровень сервера,Сервисный уровеньа такжеСлой репозитория. За связь отвечает сервер, то есть в Spidey используется gRPC. Сервисы содержат бизнес-логику. Хранилище отвечает за операции чтения и записи в базу данных.
Начало
Для запуска Паучка требуетсяDocker,Docker Compose,Go,Protocol BuffersКомпилятор и его плагины Go и очень полезныеvektah/gqlgenСумка.
вам также необходимо установитьvgo(инструмент управления пакетами в ранней разработке). инструментdepтоже вариант, но в комплектеgo.mod
файл будет проигнорирован.
Аннотация: В Go 1.11 vgo был выпущен как официальный интегрированный модуль Go, который был интегрирован в команду go и использовался с модом go Инструкции в основном такие же, как и у vgo.
Настройки докера
Каждый сервис реализован в своей подпапке и содержит как минимум одинapp.dockerfile
документ.app.dockerfile
Пользователи файлов создают зеркальное отображение базы данных.
account
├── account.proto
├── app.dockerfile
├── cmd
│ └── account
│ └── main.go
├── db.dockerfile
└── up.sql
Все услуги через внешниеdocker-compose.yamlопределение.
Следующее является частью перехвата службы 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 службы учетной записи, выглядит следующим образом:
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"`
}
Реализация должна использовать репозиторий:
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
}
Он обрабатывает синтаксический анализ проводного протокола как сервер и базу данных как репозиторий.
база данных
Модель данных для учетной записи очень проста:
CREATE TABLE IF NOT EXISTS accounts (
id CHAR(27) PRIMARY KEY,
name VARCHAR(24) NOT NULL
);
Файл SQL с определенными выше данными будет скопирован в контейнер Docker для выполнения.
FROM postgres:10.3
COPY up.sql /docker-entrypoint-initdb.d/1.sql
CMD ["postgres"]
Доступ к базе данных PostgreSQL осуществляется через следующий интерфейс репозитория:
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)
}
Репозиторий упакован на основе пакета SQL стандартной библиотеки Go:
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 службы учетных записей определяет следующие буферы протокола:
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 может использовать Gogenerate
координация команд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 для сохранения элементов.
Служить
Службы каталогов следуют следующему интерфейсу:
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"`
}
база данных
Репозиторий на основе Elasticsearcholivere/elasticпакет для реализации.
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 хранит документы и идентификаторы отдельно, вспомогательная структура элемента реализована без идентификаторов:
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реализован в. В отличие от службы учетных записей, она не определяет все конечные точки в интерфейсе службы.
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
Сообщения содержат дополнительные поля, но ищутся и индексируются по идентификатору.
Код ниже показываетGetProducts
Реализация функции:
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.
за/products?[ids=...]&[query=...]&skip=0&take=100
В форме запросов относительно легко создать конечную точку для завершения вызова API.
Заказать услугу
Заказ Служба заказа сложнее. Ему нужно позвонить в службу учетной записи и каталога, чтобы подтвердить запрос, потому что заказ может быть создан только для определенной учетной записи и существующего элемента.
Service
Service
Интерфейс определяет интерфейс для создания и индексации всех заказов через аккаунт.
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
В таблице идентификатор описан какproduct_id
заказанных товаров и количество таких товаров. иproduct_id
Поля должны быть извлекаемыми из службы каталогов.
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
Интерфейс прост:
type Repository interface {
Close()
PutOrder(ctx context.Context, o Order) error
GetOrdersForAccount(ctx context.Context, accountID string) ([]Order, error)
}
Но реализовать его непросто.
Заказ должен быть вставлен в два шага с использованием механизма транзакций, а затем запрошен оператором соединения.
Чтение заказа из базы данных требует синтаксического анализа табличной структуры и чтения ее в объектную структуру. Код ниже считывает элементы в заказ на основе идентификатора заказа:
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
Серверу gRPC службы заказов необходимо установить соединение со службами учетных записей и каталогов при его реализации.
Буферы протокола определяются следующим образом:
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-адреса другой службы:
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 определена в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
Структурировано:
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 ссылается на все остальные службы.
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
Структура должна реализовать все сгенерированные преобразователи. Мутация может быть обнаружена в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 }
}
Обратите внимание на возвращенное значение идентификатора:
{
"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. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.
Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллекти другие поля, если вы хотите видеть больше качественных переводов, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.