エッジの使い方
エッジにより、アプリケーション内の異なるエンティティ間の関係を表現することができます。 それらが生成されたgRPCサービスとどのように連携するかを見てみましょう。
新しいエンティティCategory
を追加して、User
タイプをそれに関連付けるエッジを作成することから始めましょう。
package schema
import (
"entgo.io/contrib/entproto"
"entgo.io/ent"
"entgo.io/ent/schema"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
type Category struct {
ent.Schema
}
func (Category) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Annotations(entproto.Field(2)),
}
}
func (Category) Annotations() []schema.Annotation {
return []schema.Annotation{
entproto.Message(),
}
}
func (Category) Edges() []ent.Edge {
return []ent.Edge{
edge.To("admin", User.Type).
Unique().
Annotations(entproto.Field(3)),
}
}
User
に逆リレーションを作成:
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("administered", Category.Type).
Ref("admin").
Annotations(entproto.Field(5)),
}
}
以下の点に注意してください:
- エッジにも
entproto.Field
アノテーションが付与されています。 その理由は後ほど説明します。 - We created a one-to-many relationship where a
Category
has a singleadmin
, and aUser
can administer multiple categories.
go generate ./...
でプロジェクトを再生成すると、.proto
ファイルに変更があることに気づきます:
message Category {
int64 id = 1;
string name = 2;
User admin = 3;
}
message User {
int64 id = 1;
string name = 2;
string email_address = 3;
google.protobuf.StringValue alias = 4;
repeated Category administered = 5;
}
以下の変更が確認できます:
- 新しいメッセージ
Category
が作成されました。 このメッセージには、Category
スキーマのadmin
エッジに対応するadmin
というフィールドがあります。 エッジを.Unique()
に設定しているため、これはrepeatedフィールドではありません。 これのフィールド番号は3
で、エッジ定義のentproto.Field
アノテーションに対応しています。 User
メッセージ定義に新しいフィールドadministred
が追加されました。 It is arepeated
field, corresponding to the fact that we did not mark the edge asUnique
in this direction. フィールド番号は5
で、エッジのentproto.Field
アノテーションアノテーションに対応しています。
エッジでエンティティを作成する
Let's demonstrate how to create an entity with its edges by writing a test:
package main
import (
"context"
"testing"
_ "github.com/mattn/go-sqlite3"
"ent-grpc-example/ent/category"
"ent-grpc-example/ent/enttest"
"ent-grpc-example/ent/proto/entpb"
"ent-grpc-example/ent/user"
)
func TestServiceWithEdges(t *testing.T) {
// start by initializing an ent client connected to an in memory sqlite instance
ctx := context.Background()
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
// next, initialize the UserService. ここでは、実際にポートを開いてgRPCサーバーを作成するのではなく
// ライブラリのコードを直接呼び出していることに注目してください。
svc := entpb.NewUserService(client)
// 次に、entクライアントを使って直接Categoryを作成します。
// Userとは無関係に初期化していることに注意してください。
cat := client.Category.Create().SetName("cat_1").SaveX(ctx)
// 次に、User サービスの `Create` メソッドを呼び出します。
// IDのみが設定されたentpb.Categoryインスタンスのリストを渡していることに注意してください。
create, err := svc.Create(ctx, &entpb.CreateUserRequest{
User: &entpb.User{
Name: "user",
EmailAddress: "user@service.code",
Administered: []*entpb.Category{
{Id: int64(cat.ID)},
},
},
})
if err != nil {
t.Fatal("failed creating user using UserService", err)
}
// to verify everything worked correctly, we query the category table to check
// we have exactly one category which is administered by the created user.
count, err := client.Category.
Query().
Where(
category.HasAdminWith(
user.ID(int(create.Id)),
),
).
Count(ctx)
if err != nil {
t.Fatal("failed counting categories admin by created user", err)
}
if count != 1 {
t.Fatal("expected exactly one group to managed by the created user")
}
}
作成された User
から既存の Category
へのエッジを作成するために、Category
オブジェクト全体を入力する必要はありません。 代わりにId
フィールドのみを入力します。 生成されたサービスコードがこれを処理します。
func (svc *UserService) createBuilder(user *User) (*ent.UserCreate, error) {
// truncated ...
for _, item := range user.GetAdministered() {
administered := int(item.GetId())
m.AddAdministeredIDs(administered)
}
return m, nil
}
エンティティのエッジ ID を取得する
エンティティ間のリレーションを作成する方法はわかりましたが、生成されたgRPCサービスからそのデータを取得するにはどうすればよいのでしょうか。
次のテスト例を考えてみましょう:
func TestGet(t *testing.T) {
// インメモリのsqliteインスタンスに接続されたentクライアントの初期化から始めます
ctx := context.Background()
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
// 次に、Userサービスを初期化します。 ここでは、実際にポートを開いてgRPCサーバーを作成するのではなく
// ライブラリのコードを直接呼び出していることに注目してください。
svc := entpb.NewUserService(client)
// 次に、ユーザーとカテゴリーを作成し、そのユーザーをカテゴリーの管理者に設定します。
user := client.User.Create().
SetName("rotemtam").
SetEmailAddress("r@entgo.io").
SaveX(ctx)
client.Category.Create().
SetName("category").
SetAdmin(user).
SaveX(ctx)
// next, retrieve the user without edge information
get, err := svc.Get(ctx, &entpb.GetUserRequest{
Id: int64(user.ID),
})
if err != nil {
t.Fatal("failed retrieving the created user", err)
}
if len(get.Administered) != 0 {
t.Fatal("by default edge information is not supposed to be retrieved")
}
// next, retrieve the user *WITH* edge information
get, err = svc.Get(ctx, &entpb.GetUserRequest{
Id: int64(user.ID),
View: entpb.GetUserRequest_WITH_EDGE_IDS,
})
if err != nil {
t.Fatal("failed retrieving the created user", err)
}
if len(get.Administered) != 1 {
t.Fatal("using WITH_EDGE_IDS edges should be returned")
}
}
テストを見ていただくとわかるのですが、デフォルトでは、サービスのGet
メソッドでエッジの情報は返されません。 これは、エンティティに関連するエンティティの量が制限されていないため、意図的に行われています。 エッジの情報を返すかどうかを呼び出し元が指定できるように、生成されるサービスはAIP-157(Partial Responses)に準拠しています。 つまり、GetUserRequest
メッセージには、View
という名前のenumが含まれています:
message GetUserRequest {
int64 id = 1;
View view = 2;
enum View {
VIEW_UNSPECIFIED = 0;
BASIC = 1;
WITH_EDGE_IDS = 2;
}
}
Get
メソッドの生成コードを見てみましょう:
// UserServiceServer.Getの実装
func (svc *UserService) Get(ctx context.Context, req *GetUserRequest) (*User, error) {
// .. 省略 ..
switch req.GetView() {
case GetUserRequest_VIEW_UNSPECIFIED, GetUserRequest_BASIC:
get, err = svc.client.User.Get(ctx, int(req.GetId()))
case GetUserRequest_WITH_EDGE_IDS:
get, err = svc.client.User.Query().
Where(user.ID(int(req.GetId()))).
WithAdministered(func(query *ent.CategoryQuery) {
query.Select(category.FieldID)
}).
Only(ctx)
default:
return nil, status.Errorf(codes.InvalidArgument, "invalid argument: unknown view")
}
// .. 省略 ..
}
デフォルトでは、client.User.Get
が呼び出され、エッジのID情報は返されませんが、WITH_EDGE_IDS
が渡されると、エンドポイントは、administered
エッジを介してユーザに関連するすべてのCategory
のID
フィールドを取得します。