ent Protocol Buffers Skima 및 gRPC 서비스의 코드 생성 기능 시도

51483 단어 Gotech
이 보도는 거의 다 ent를 재제작하는 공식 강좌입니다.

개시하다


Go를 배우기로 결심한 ORM, 이번에 손에 넣은 건 ent.
저도 Go의 ORM이 GORM[1]인 것을 알고 있지만 엔티를 먼저 만져보고 싶은 것은 엔티의 공식 블로그에서 Generate a fully-working Go gRPC server in two minutes with Ent라는 기사를 봤기 때문입니다.
보아하니 ent모드에서Protocol buffers(이하protobuf라고 부른다)모드와 원래 스스로 실행해야 하는 gRPC의 서비스까지 코드를 생성할 수 있을 것 같다.
Go와 gRPC는 모두 Google산으로 함께 사용하면 조금 무섭다는 편견이 있어 이 기능에 깊이 빠져들었다.
이 글의 취지는 ent의 probuf 모드와 gRPC 서비스 생성 기능을 시도하는 것이기 때문에 ent의 ORM 부분과 그다지 관련이 없다.

사전 정의 및 모드 정의


먼저 디렉토리를 생성하고 Go Module을 초기화하며 ent CLI를 사용하여 솔리드 모드의 초기 형태를 생성합니다.
$ mkdir ent-grpc-example
$ cd ent-grpc-example
ent-grpc-example$ go mod init ent-grpc-example
ent-grpc-example$ ent init User
목록은 이렇습니다.
다음 생성된 사용자입니다.go를 사용하여 User 솔리드의 모드를 정의합니다.
Fields 메서드는 테이블의 열을 정의하고 Edges 메서드는 솔리드 간의 관계를 정의합니다.
초기화 상태는 이런 느낌입니다.
실제로 Fields 방법과 Edges 방법 이외에도 일부 방법이 존재하지만 모두 실시할 필요는 없다.
user.go
.
├── ent
│   ├── generate.go
│   └── schema
│       └── user.go //先ほど初期化したUserエンティティのスキーマの雛形。
└── go.mod
user.go를 이용하여 User 실체 (users 표) 에서name과 이메일 필드 (열) 를 정의합니다.ent에서 모든 실체가 id 필드를 은밀하게 정의하기 때문에 User 실체는 실제로 세 개의 필드를 가지고 있다.
user.go
package schema

import "entgo.io/ent"

// User holds the schema definition for the User entity.
type User struct {
	ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
	return nil
}

// Edges of the User.
func (User) Edges() []ent.Edge {
	return nil
}
드문 기회인 만큼 User 솔리드의 모드에서 DDL을 생성하려고 합니다.데이터베이스는 docker-compose로 실행됩니다.
ent-grpc-example/main.go
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            Unique(),
        field.String("email").
            Unique(),
    }
}
출력 결과를 성형하다.매우 간단하다.
package main

import (
	"context"
	"ent-grpc-example/ent"
	"fmt"
	"log"
	"os"

	_ "github.com/lib/pq"
)

func main() {
	client, err := ent.Open("postgres", fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
		"localhost", "5432", "postgres", "postgres", "password"))
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	ctx := context.Background()
	if err := client.Schema.WriteTo(ctx, os.Stdout); err != nil {
		log.Fatal(err)
	}
}
User 솔리드의 패턴 정의가 완료되면 코드가 다시 생성됩니다.
그래서 각종 새로운 파일을 대량으로 생성했다.
BEGIN
;
CREATE TABLE IF NOT EXISTS "users"(
    "id" bigint GENERATED BY
    DEFAULT AS IDENTITY NOT NULL,
    "name" varchar UNIQUE NOT NULL,
    "email" varchar UNIQUE NOT NULL,
    PRIMARY KEY("id")
)
;
COMMIT
;
ent-grpc-example$ go generate ./...

protobuf 모드 생성


이어서 ent 모드에서 프로필 모드를 생성합니다.
먼저 생성 모드에 필요한 소프트웨어 패키지를 프로젝트에 추가합니다.
.
├── ent
│   ├── client.go
│   ├── config.go
│   ├── context.go
│   ├── ent.go
│   ├── enttest
│   │   └── enttest.go
│   ├── generate.go
│   ├── hook
│   │   └── hook.go
│   ├── migrate
│   │   ├── migrate.go
│   │   └── schema.go
│   ├── mutation.go
│   ├── predicate
│   │   └── predicate.go
│   ├── runtime
│   │   └── runtime.go
│   ├── runtime.go
│   ├── schema
│   │   └── user.go
│   ├── tx.go
│   ├── user
│   │   ├── user.go
│   │   └── where.go
│   ├── user.go
│   ├── user_create.go
│   ├── user_delete.go
│   ├── user_query.go
│   └── user_update.go
├── go.mod
└── go.sum
여기에는 위에서 설명한 임의의 구현 방법 중 하나인 Annotaitons 방법의 사용자입니다.go에 추가하여 User 솔리드에 해당하는 Protocol Buffers 메시지를 생성할 수 있습니다.
또한 Annotaions 방법을 실현해야 할 뿐만 아니라, 필드를 편집하고 프로필 모드를 생성할 때 메시지의 각 필드 [2] 의 번호를 지정해야 한다.
위에서 말한 바와 같이 모든 실체는 id 필드(1분배)를 은밀하게 가지고 있기 때문에 분배할 수 있는 번호는 2로 구성된다.
user.go
ent-grpc-example$go get -u entgo.io/contrib/entproto
프로토 파일을 생성하는 디렉터리를 추가하고 코드를 다시 생성합니다.
ent-grpc-example/ent/generate.go
func (User) Fields() []ent.Field {
	return []ent.Field{
		field.String("name").
			Unique().
			Annotations(
				entproto.Field(2),
			),
		field.String("email_address").
			Unique().
			Annotations(
				entproto.Field(3),
			),
	}
}

func (User) Edges() []ent.Edge {
	return nil
}

func (User) Annotations() []schema.Annotation {
	return []schema.Annotation{
		entproto.Message(),
	}
}
package ent

//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
//go:generate go run -mod=mod entgo.io/contrib/entproto/cmd/entproto -path ./schema
proto 파일을 포함하는 새 디렉터리를 생성합니다.
ent-grpc-example$ go generate ./...
프로필의 내용은 이렇습니다.user.goo를 보니 직감적인 상상과는 거리가 멀다고 생각해요.
ent/proto
└── entpb
    ├── entpb.proto
    └── generate.go
여기서부터 개인이 걸어 넘어진 곳.
위의 DDL에서 알 수 있듯이 스텔스 필드의 id 필드는 소위 Salo문 키이지만 실제로는 다음과 같이 덮어쓸 수 있다.
user.go
// Code generated by entproto. DO NOT EDIT.
syntax = "proto3";

package entpb;

option go_package = "ent-grpc-example/ent/proto/entpb";

message User {
  int32 id = 1;

  string user_name = 2;

  string email = 3;
}
그러나 이렇게 생성된 프로토 파일과 User 엔티티의 패턴에 따라 생성된 DDL은 이렇습니다.
field.UID 함수를 검사하는 StorageKey 방법은 표의 열 이름을 필드 이름과 다른 내용으로 지정하기 위한 것일 뿐 포토 파일에 영향을 미치지 않는다고 합니다.
불평을 해도 id 필드를 포함하는 프로토버프의 메시지에 대응하는 필드 이름을 어떻게 지정해야 할지 모르겠습니다. 아시는 분 있으면 m() 꼭 알려주세요.m
func (User) Fields() []ent.Field {
	return []ent.Field{
		field.UUID("id", uuid.UUID{}).
			Default(uuid.New).
			StorageKey("oid").
			Annotations(
				entproto.Field(1),
			),
		field.String("name").
			Unique().
			Annotations(
				entproto.Field(2),
			),
		field.String("email").
			Unique().
			Annotations(
				entproto.Field(3),
			),
	}
}
message User {
  bytes id = 1;

  string name = 2;

  string email = 3;
}
그나저나프로토 파일과 동시에generate를 생성합니다.goo는 protobuf 코드를 사용하여 CLI 즉 protoc를 생성하는 지시를 포함합니다.
BEGIN
;
CREATE TABLE IF NOT EXISTS "users"(
  "oid" uuid NOT NULL,
  "name" varchar UNIQUE NOT NULL,
  "email" varchar UNIQUE NOT NULL,
  PRIMARY KEY("oid")
)
;
COMMIT
;
protoo 파일에서 실제 코드를 생성하기 위해서는 상기 protoc와 3개의 플러그인이 필요합니다.
protoc 설치 절차
protoc에서 Go 코드를 생성하는 플러그인 설치 방법
세 번째 protooc-gen-entgrpc를 설치합니다.
package entpb
//go:generate protoc -I=.. --go_out=.. --go-grpc_out=.. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative --entgrpc_out=.. --entgrpc_opt=paths=source_relative,schema_path=../../schema entpb/entpb.proto

gRPC 서비스의 생성


User 엔터티의 gRPC 서비스를 생성하려면 user가 필요합니다.goo에 한 줄만 추가!
user.go
$ go get -u entgo.io/contrib/entproto/cmd/protoc-gen-entgrpc
또한 코드 생성.
func (User) Annotations() []schema.Annotation {
    return []schema.Annotation{
        entproto.Message(),
        entproto.Service(),
    }
}
proto 파일을 포함하는 디렉터리에 약간의 변화가 있습니다.
ent-grpc-example$ go generate ./...
프로토 파일에도 서비스를 추가합니다.
ent/entpb
    ├── entpb.pb.go
    ├── entpb.proto
    ├── entpb_grpc.pb.go
    ├── entpb_user_service.go
    └── generate.go
entpb_user_service.go는 entpbgrpc.pb.고의 인터페이스를 실현하였다.이 부분은 원래 스스로 설치한 것이기 때문에 매우 가볍다!
entpb_user_service.go
service UserService {
  rpc Create ( CreateUserRequest ) returns ( User );

  rpc Get ( GetUserRequest ) returns ( User );

  rpc Update ( UpdateUserRequest ) returns ( User );

  rpc Delete ( DeleteUserRequest ) returns ( google.protobuf.Empty );
}

gRPC 서버 제작 및 CRUD 작업


서비스는 코드를 생성할 수 있지만 구체적인 서버는 스스로 실현해야 한다.
공식 홈페이지에 따르면 사용하는 중간부품(인정과 로거) 등은 어느 정도 개발진에 의존하기 때문에 이 글을 작성할 때 서버는 코드 생성 대상에 속하지 않지만 앞으로 변경될 수 있다.
gRPC 서버
ent-grpc-example/server/main.go
// Create implements UserServiceServer.Create
func (svc *UserService) Create(ctx context.Context, req *CreateUserRequest) (*User, error) {
	user := req.GetUser()
	m := svc.client.User.Create()
	userEmail := user.GetEmail()
	m.SetEmail(userEmail)
	userName := user.GetName()
	m.SetName(userName)
	res, err := m.Save(ctx)
	switch {
	case err == nil:
		proto, err := toProtoUser(res)
		if err != nil {
			return nil, status.Errorf(codes.Internal, "internal error: %s", err)
		}
		return proto, nil
	case sqlgraph.IsUniqueConstraintError(err):
		return nil, status.Errorf(codes.AlreadyExists, "already exists: %s", err)
	case ent.IsConstraintError(err):
		return nil, status.Errorf(codes.InvalidArgument, "invalid argument: %s", err)
	default:
		return nil, status.Errorf(codes.Internal, "internal error: %s", err)
	}

}
정상 가동
package main

import (
	"context"
	"fmt"
	"log"
	"net"

	"ent-grpc-example/ent"
	"ent-grpc-example/ent/proto/entpb"

	_ "github.com/lib/pq"
	"google.golang.org/grpc"
)

func main() {
	log.Print("server is starting...")
	client, err := ent.Open("postgres", fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
		"localhost", "5432", "postgres", "postgres", "password"))
	if err != nil {
		log.Fatalf("failed to connect to db: %s", err)
	}
	defer client.Close()

	if err := client.Schema.Create(context.Background()); err != nil {
		log.Fatalf("failed to create schema: %s", err)
	}

	svc := entpb.NewUserService(client)
	server := grpc.NewServer()
	entpb.RegisterUserServiceServer(server, svc)

	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %s", err)
	}

	if err := server.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %s", err)
	}
}
생성된 서비스를 사용하여 CRUD 작업을 시도합니다.
go
ent-grpc-example$ docker-compose up -d
ent-grpc-example$ go run server/main.go 
2021/11/03 09:47:58 server is starting...
gRPC 컨설팅 결과잘 움직여. 감동.
서버 쪽 Logging이 없었기 때문에, 역시 그곳은 자기가 어떻게 생각하는지 느꼈다.
package main

import (
	"context"
	"log"

	"ent-grpc-example/ent/proto/entpb"

	"google.golang.org/grpc"
)

func main() {
	conn, err := grpc.Dial(":50051", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("failed connect to server: %s", err)
	}
	defer conn.Close()

	client := entpb.NewUserServiceClient(conn)

	// Create a new User
	ctx := context.Background()
	created, err := client.Create(ctx, &entpb.CreateUserRequest{
		User: &entpb.User{
			Name:  "hoge",
			Email: "[email protected]",
		},
	})
	if err != nil {
		log.Fatalf("failed to create user: %s", err)
	}

	log.Printf("created an user:ID: %d, Name: %s, Email: %s", created.Id, created.Name, created.Email)

	// Get an User
	got, err := client.Get(ctx, &entpb.GetUserRequest{
		Id: created.Id,
	})
	if err != nil {
		log.Fatalf("failed to get an user: %s", err)
	}

	log.Printf("got an user:ID: %d, Name: %s, Email: %s", got.Id, got.Name, got.Email)

	// Update an User
	updated, err := client.Update(ctx, &entpb.UpdateUserRequest{
		User: &entpb.User{
			Id:    got.Id,
			Name:  "fuga",
			Email: "[email protected]",
		},
	})
	if err != nil {
		log.Fatalf("failed to update an user: %s", err)
	}

	log.Printf("updated an user: ID: %d Name: %s, Email: %s", updated.Id, updated.Name, updated.Email)

	/* Delete an User
	_, err = client.Delete(ctx, &entpb.DeleteUserRequest{
		Id: updated.Id,
	})
	if err != nil {
		log.Fatalf("failed to delete an user: %s", err)
	}

	log.Printf("deleted an user: ID: %d Name: %s Email: %s", updated.Id, updated.Name, updated.Email)
	*/
}
신중을 기하기 위해 PostgerSQL의 Docker 용기에 잠입했는지 확인하고 실체를 적당히 추가했다.
ent-grpc-example$ go run client/main.go 
2021/11/03 09:48:05 created an user:ID: 1, Name: hoge, Email: [email protected]
2021/11/03 09:48:05 got an user:ID: 1, Name: hoge, Email: [email protected]
2021/11/03 09:48:05 updated an user: ID: 1 Name: fuga, Email: [email protected]

끝말


이번에 우리는 ent의 공식 강좌를 참고하여 ent Skima에서protobuf Skima와 서비스 코드를 생성할 수 있는지 시험해 보았다.
현재 방정식 문서 대부분 일본어 번역(감사m()m) 관심 있는 분들은 꼭 참고해주세요.
나는 앞으로 공부할 것을 GiitHub에게 주고 싶다.
읽어주셔서 감사합니다.
본문의 창고
https://github.com/unm3/ent-grpc-example

참고물


https://entgo.io/ja/
https://future-architect.github.io/articles/20210728a/
https://zenn.dev/spiegel/books/a-study-in-postgresql
각주
Go의 학습 로드맵도 필수로 간주된다.↩︎
여기서 설명하는 필드는 Protocol Buffers의 컨텍스트입니다.↩︎

좋은 웹페이지 즐겨찾기