
前書き
言語的に中立なフォーマットで定義されたエンティティ スキーマを持つことは、ソフトウェア エンジニアリング組織の規模が大きくなるにつれ、多くの利点があります。 そのために、多くの組織ではプロトコルバッファをインターフェース記述言語 (IDL) として使用しています。 また、Google社内のStubbyをモデルにしたProtobufベースのRPCフレームワークであるgRPCは、その効率性とコード生成能力の高さから人気を集めています。
IDLであるgRPCは、データアクセスレイヤーの実装に関する具体的なガイドラインを規定していないため、その実装方法は様々です。 Entは、あらゆるGoアプリケーションのデータアクセスレイヤーを構築するための自然な候補であり、この2つの技術を統合することには大きな可能性があります。
本日は、entユーザーのためにProtobufとgRPCのサポートを追加するための実験版entproto
、Goパッケージ、およびコマンドラインツールを発表します。 entproto
を使えば、開発者は完全に動作するCRUD gRPCサーバを数分で立ち上げることができます。 今回の記事では、その方法を具体的にご紹介します。
セットアップ
このチュートリアルの最終版はGitHubで公開されていますので、そちらでフォローしたい方はクローンを作成してください。
まずは、プロジェクトの新しいGoモジュールを初期化しましょう。
mkdir ent-grpc-example
cd ent-grpc-example
go mod init ent-grpc-example
次に、go run
を使ってentコードジェネレータを起動し、スキーマを初期化します。
go run -mod=mod entgo.io/ent/cmd/ent new User
これで私たちのディレクトリは次のようになります。
.
├── ent
│ ├── generate.go
│ └── schema
│ └── user.go
├── go.mod
└── go.sum
次に、entproto
パッケージをプロジェクトに追加してみましょう。
go get -u entgo.io/contrib/entproto
次に、User
エンティティのスキーマを定義します。 ent/schema/user.go
を開き、以下のように編集します。
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema"
)
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Unique(),
field.String("email_address").
Unique(),
}
}
このステップでは、User
エンティティに2つのユニークなフィールドを追加しました。name
とemail_address
です。 ent.Schema
は単なるスキーマの定義であり、そこから使用可能なプロダクションコードを作成するには、Entのコード生成ツールを実行する必要があります。 以下を実行してください。
ここで、スキーマ定義からたくさんの新しいファイルが作成されていることに注目してください。
├── ent
│ ├── client.go
│ ├── config.go
// .... many more
│ ├── user
│ ├── user.go
│ ├── user_create.go
│ ├── user_delete.go
│ ├── user_query.go
│ └── user_update.go
├── go.mod
└── go.sum
この時点で、データベースへの接続を開き、マイグレーションを実行してusers
テーブルを作成し、データの読み書きを開始することができます。 この点についてはチュートリアルのセットアップで説明していますので、ここでは本題に入り、スキーマからProtobuf定義とgRPCサーバーを生成する方法を学びましょう。
entproto
でGo Protobufsを生成する
entとProtobufのスキーマは同一ではないので、entproto
がProtobufの定義 (protobufの用語では「メッセージ」と呼ばれます) を生成する方法を正確に把握できるように、スキーマにいくつかのアノテーションを提供する必要があります。
まず必要なのは、entproto.Message()
アノテーションを追加することです。 これはProtobufスキーマ生成のオプトインです。私たちは必ずしもスキーマエンティティのすべてからprotoメッセージやgRPCサービス定義を生成したいわけではありません。このアノテーションによりその制御が可能になります。 それをUserに追加したい場合は、ent/schema/user.go
に追加します。
func (User) Annotations() []schema.Annotation {
return []schema.Annotation{
entproto.Message(),
}
}
次に、各フィールドにアノテーションを付けて、フィールド番号を割り当てる必要があります。 protobufのメッセージタイプを定義するとき、各フィールドには一意の番号を割り当てる必要があることを思い出してください。 そのためには、各フィールドにentproto.Field
アノテーションを追加します。 ent/schema/user.go
のFields
を以下のように更新します。
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),
),
}
}
フィールドの番号を1から始めていないことに注意してください。これは、ent
が暗黙のうちにエンティティのID
フィールドを作成し、そのフィールドには自動的に1の番号が割り当てられるためです。 これで、protobufのメッセージタイプ定義を生成することができます。 そのためには、ent/generate.go
に、entproto
コマンドラインツールを呼び出すgo:generate
ディレクティブを追加します。 以下のようになります。
コードを再生成してみましょう。
すべてのprotobuf関連の生成コードを含む新しいディレクトリが作成されたことを確認してください。ent/proto
です。 以下が含まれています。
ent/proto
└── entpb
├── entpb.proto
└── generate.go
2つのファイルが作成されました。 その内容を見てみましょう。
syntax = "proto3";
package entpb;
option go_package = "ent-grpc-example/ent/proto/entpb";
message User {
int32 id = 1;
string user_name = 2;
string email_address = 3;
}
いいね! User
スキーマにマッピングするメッセージタイプ定義を含む新しい.proto
ファイルが作成されました!
新しい generate.go
ファイルが作成され、.proto
ファイルから Go コードを生成する方法を指示する protobuf コードジェネレータ protoc
への呼び出しが行われました。 このコマンドを動作させるためには、まずprotoc
と3つのprotobufプラグインをインストールする必要があります。protoc-gen-go
(Go Protobuf構造体を生成する)、protoc-gen-go-grpc
(Go gRPCサービスインターフェースとクライアントを生成する)、protoc-gen-entgrpc
(サービスインターフェースの実装を生成する) です。 これらがインストールされていない場合は、以下の指示に従ってください。
これらの依存関係をインストールした後、コード生成を再実行します。
ent/proto/entpb/entpb.pb.go
という名前の新しいファイルが作成され、エンティティ用に生成されたGo構造体が含まれていることを確認してください。
これを使って、すべてが正しく配線されていることを確認するテストを書いてみましょう。 pb_test.go
という名前の新規ファイルを作成し、書き込みます。
package main
import (
"testing"
"ent-grpc-example/ent/proto/entpb"
)
func TestUserProto(t *testing.T) {
user := entpb.User{
Name: "rotemtam",
EmailAddress: "rotemtam@example.com",
}
if user.GetName() != "rotemtam" {
t.Fatal("expected user name to be rotemtam")
}
if user.GetEmailAddress() != "rotemtam@example.com" {
t.Fatal("expected email address to be rotemtam@example.com")
}
}
こちらを実行してください。
go get -u./... # 生成されたパッケージの依存先をインストールする
go test ./...
ヨシッ! テストは合格です。 Entスキーマから正しく機能するGo Protobuf構造体の生成に成功しました。 次に、スキーマからCRUD gRPC serverを自動的に生成する方法を見てみましょう。
スキーマから完全に動作する gRPC サーバーを生成する
ent.Schema
からProtobuf構造体を生成させることは便利ですが、私たちが本当に興味を持っているのは、実際のデータベースからエンティティを作成、読み取り、更新、削除できる実際のサーバーを手に入れることです。 そのためには、たった1行のコードを更新するだけでいいのです! スキーマへentproto.Service
というアノテーションを追加すると、gRPCサービス定義の生成に興味があることをentproto
のコードジェネレータに伝え、protoc-gen-entgrpc
がその定義を読み込んでサービスの実装を生成します。 ent/schema/user.go
を編集し、スキーマのAnnotations
を修正します。
func (User) Annotations() []schema.Annotation {
return []schema.Annotation{
entproto.Message(),
+ entproto.Service(), // <-- この行を追加
}
}
ここでコード生成を再実行します。
すると、ent/proto/entpb
に興味深い変化が見られます。
ent/proto/entpb
├── entpb.pb.go
├── entpb.proto
├── entpb_grpc.pb.go
├── entpb_user_service.go
└── generate.go
まず、entproto
では、entpb.proto
にサービス定義を追加しました。
service UserService {
rpc Create ( CreateUserRequest ) returns ( User );
rpc Get ( GetUserRequest ) returns ( User );
rpc Update ( UpdateUserRequest ) returns ( User );
rpc Delete ( DeleteUserRequest ) returns ( google.protobuf.Empty );
}
また、新たに2つのファイルが作成されました。 1つ目のent_grpc.pb.go
には、gRPCクライアントスタブとインターフェース定義が含まれています。 ファイルを開くと、その中に (他の多くのものの中から) 見つけることができます。
type UserServiceClient interface {
Create(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error)
Get(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error)
Update(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error)
Delete(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
2つ目のファイル、entpub_user_service.go
には、このインターフェイスの実装が生成されています。 例えば、Get
メソッドの実装です。
func (svc *UserService) Get(ctx context.Context, req *GetUserRequest) (*User, error) {
get, err := svc.client.User.Get(ctx, int(req.GetId()))
switch {
case err == nil:
return toProtoUser(get), nil
case ent.IsNotFound(err):
return nil, status.Errorf(codes.NotFound, "not found: %s", err)
default:
return nil, status.Errorf(codes.Internal, "internal error: %s", err)
}
}
悪くない! 次に、サービスへのリクエストを提供するgRPCサーバーを作成します。
サーバーを作成する
新規ファイルcmd/server/main.go
を作成し、以下のように書き込みます。
package main
import (
"context"
"log"
"net"
_ "github.com/mattn/go-sqlite3"
"ent-grpc-example/ent"
"ent-grpc-example/ent/proto/entpb"
"google.golang.org/grpc"
)
func main() {
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
defer client.Close()
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
svc := entpb.NewUserService(client)
server := grpc.NewServer()
entpb.RegisterUserServiceServer(server, svc)
lis, err := net.Listen("tcp", ":5000")
if err != nil {
log.Fatalf("failed listening: %s", err)
}
if err := server.Serve(lis); err != nil {
log.Fatalf("server ended: %s", err)
}
}
github.com/mattn/go-sqlite3
のインポートを追加したことに注目してしてください。これをモジュールに追加する必要があります。
go get -u github.com/mattn/go-sqlite3
次に、サーバーを実行しながら、そのサーバーと通信するクライアントを書きましょう。
go run -mod=mod ./cmd/server
クライアントを作成する
サーバーにいくつかの呼び出しを行うシンプルなクライアントを作ってみましょう。 cmd/client/main.go
という名前の新規ファイルを作成し、以下を書き込みます。
package main
import (
"context"
"fmt"
"log"
"math/rand"
"time"
"ent-grpc-example/ent/proto/entpb"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
func main() {
rand.Seed(time.Now().UnixNano())
conn, err := grpc.Dial(":5000", grpc.WithInsecure())
if err != nil {
log.Fatalf("failed connecting to server: %s", err)
}
defer conn.Close()
client := entpb.NewUserServiceClient(conn)
ctx := context.Background()
user := randomUser()
created, err := client.Create(ctx, &entpb.CreateUserRequest{
User: user,
})
if err != nil {
se, _ := status.FromError(err)
log.Fatalf("failed creating user: status=%s message=%s", se.Code(), se.Message())
}
log.Printf("user created with id: %d", created.Id)
get, err := client.Get(ctx, &entpb.GetUserRequest{
Id: created.Id,
})
if err != nil {
se, _ := status.FromError(err)
log.Fatalf("failed retrieving user: status=%s message=%s", se.Code(), se.Message())
}
log.Printf("retrieved user with id=%d: %v", get.Id, get)
}
func randomUser() *entpb.User {
return &entpb.User{
Name: fmt.Sprintf("user_%d", rand.Int()),
EmailAddress: fmt.Sprintf("user_%d@example.com", rand.Int()),
}
}
クライアントは、当社のサーバーが待ち受けているポート5000への接続を確立し、新しいユーザーを作成するためにCreate
リクエストを発行し、データベースからユーザーを取得するために2回目のGet
リクエストを発行します。 それではクライアントのコードを実行しましょう。
アウトプットを観察する
2021/03/18 10:42:58 user created with id: 1
2021/03/18 10:42:58 retrieved user with id=1: id:1 name:"user_730811260095307266" email_address:"user_7338662242574055998@example.com"
素晴らしい! スキーマにいくつかのアノテーションを付けることで、コード生成の超能力を使って、あっという間に動くgRPCサーバーを作りました!
注意点と制限事項
entproto
はまだ実験段階で、いくつかの基本的な機能がありません。 例えば、多くのアプリケーションでは、サービスにList
やFind
メソッドが必要になると思いますが、これらはまだサポートされていません。 また、その他の課題についても、今後取り組んでいく予定です。
- 現在は"一意"なエッジのみがサポートされています (O2O、O2M)
- 生成された "更新系"メソッド (Create/Update) は、現在、ゼロ/null値やフィールドのnull可能性を無視して、すべてのフィールドを設定しています
- すべてのフィールドは、gRPCリクエストからentクライアントにコピーされます。また、フィールド/エッジアノテーションを追加することで、一部のフィールドをサービスで設定できないようにする機能もサポートされる予定です
次のステップ
私たちは、ent
+ gRPCが、Goでサーバーアプリケーションを構築するための素晴らしい方法になると信じています。 例えば、アプリケーションが管理するエンティティに対してきめ細かなアクセスコントロールを設定するために、開発者はすでに、gRPCインテグレーションですぐに機能するプライバシーポリシーを使用することができます。 エンティティのさまざまなライフサイクルイベントに対して任意のGoコードを実行するために、開発者はカスタムのフックを利用することができます。
ent
でgRPCサーバを構築したいですか? If you want some help setting up or want the integration to support your use case, please reach out to us via our Discussions Page on GitHub or in the #ent channel on the Gophers Slack or our Discord server.
より多くのEntのニュースと最新情報をお届けします