mirror of
https://github.com/ent/ent.git
synced 2026-05-02 23:50:54 +03:00
doc/md: a detailed tutorial on the gRPC integration (#1663)
This commit is contained in:
282
doc/md/tutorial-grpc-edges.md
Normal file
282
doc/md/tutorial-grpc-edges.md
Normal file
@@ -0,0 +1,282 @@
|
||||
---
|
||||
id: grpc-edges
|
||||
title: Working with Edges
|
||||
sidebar_label: Working with Edges
|
||||
---
|
||||
Edges enable us to express the relationship between different entities in our ent application. Let's see how they work
|
||||
together with generated gRPC services.
|
||||
|
||||
Let's start by adding a new entity, `Category` and create edges relating our `User` type to it:
|
||||
|
||||
```go
|
||||
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)),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Creating the inverse relation on the `User`:
|
||||
|
||||
```go {4-6}
|
||||
// Edges of the User.
|
||||
func (User) Edges() []ent.Edge {
|
||||
return []ent.Edge{
|
||||
edge.From("administered", Category.Type).
|
||||
Ref("admin").
|
||||
Annotations(entproto.Field(5)),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice a few things:
|
||||
|
||||
* Our edges also receive an `entproto.Field` annotation. We will see why in a minute.
|
||||
* We created a one-to-many relation ship where a `Group` has a single `admin`, and a `User` can administer multiple
|
||||
groups.
|
||||
|
||||
Re-generating the project with `go generate ./...`, notice the changes to the `.proto` file:
|
||||
|
||||
```protobuf {1-7,18}
|
||||
message Category {
|
||||
int32 id = 1;
|
||||
|
||||
string name = 2;
|
||||
|
||||
User admin = 3;
|
||||
}
|
||||
|
||||
message User {
|
||||
int32 id = 1;
|
||||
|
||||
string name = 2;
|
||||
|
||||
string email_address = 3;
|
||||
|
||||
google.protobuf.StringValue alias = 4;
|
||||
|
||||
repeated Category administered = 5;
|
||||
}
|
||||
```
|
||||
|
||||
Observe the following changes:
|
||||
|
||||
* A new message, `Category` was created. This message has a field named `admin` corresponding to the `admin` edge on
|
||||
the `Category` schema. It is a non-repeated field because we set the edge to be `.Unique()`. It's field number is `3`,
|
||||
corresponding to the `entproto.Field` annotation on the edge definition.
|
||||
* A new field `administered` was added to the `User` message definition. It is a `repeated` field, correspending to the
|
||||
fact that we did not mark the edge as `Unique` in this direction. It's field number is `5`, corresponding to the
|
||||
`entproto.Field` annotation on the edge.
|
||||
|
||||
### Creating Entities with their Edges
|
||||
|
||||
Let's demonstrate how to create an entity with it's edges by writing a test:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/rotemtam/ent-grpc-example/ent/category"
|
||||
"github.com/rotemtam/ent-grpc-example/ent/enttest"
|
||||
"github.com/rotemtam/ent-grpc-example/ent/proto/entpb"
|
||||
"github.com/rotemtam/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. Notice we won't be opening an actual port and
|
||||
// creating a gRPC server and instead we are just calling the library code directly.
|
||||
svc := entpb.NewUserService(client)
|
||||
|
||||
// next, we create a category directly using the ent client.
|
||||
// Notice we are initializing it with no relation to a User.
|
||||
cat := client.Category.Create().SetName("cat_1").SaveX(ctx)
|
||||
|
||||
// next, we invoke the User service's `Create` method. Notice we are
|
||||
// passing a list of entpb.Category instances with only the ID set.
|
||||
create, err := svc.Create(ctx, &entpb.CreateUserRequest{
|
||||
User: &entpb.User{
|
||||
Name: "user",
|
||||
EmailAddress: "user@service.code",
|
||||
Administered: []*entpb.Category{
|
||||
{Id: int32(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")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
To create the edge from the created `User` to the existing `Category` we do not need to populate the entire `Category`
|
||||
object. Instead we only populate the `Id` field. This is picked up by the generated service code:
|
||||
|
||||
```go {5-7}
|
||||
// Create implements UserServiceServer.Create
|
||||
func (svc *UserService) Create(ctx context.Context, req *CreateUserRequest) (*User, error) {
|
||||
user := req.GetUser()
|
||||
// truncated ...
|
||||
for _, item := range user.GetAdministered() {
|
||||
m.AddAdministeredIDs(int(item.GetId()))
|
||||
}
|
||||
res, err := m.Save(ctx)
|
||||
// truncated ...
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieving Edge IDs for Entities
|
||||
|
||||
We have seen how to create relations between entities, but how do we retrieve that data from the generated gRPC
|
||||
service?
|
||||
|
||||
Consider this example test:
|
||||
|
||||
```go
|
||||
func TestGet(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. Notice we won't be opening an actual port and
|
||||
// creating a gRPC server and instead we are just calling the library code directly.
|
||||
svc := entpb.NewUserService(client)
|
||||
|
||||
// next, create a user, a category and set that user to be the admin of the category
|
||||
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: int32(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: int32(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")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As you can see in the test, by default, edge information is not returned by the `Get` method of the service. This is
|
||||
done deliberately because the amount of entities related to an entity is unbound. To allow the caller of to specify
|
||||
whether or not to return the edge information or not, the generated service adheres to [AIP-157](https://google.aip.dev/157)
|
||||
(Partial Responses). In short, the `GetUserRequest` message includes an enum named `View`:
|
||||
|
||||
```protobuf
|
||||
message GetUserRequest {
|
||||
int32 id = 1;
|
||||
|
||||
View view = 2;
|
||||
|
||||
enum View {
|
||||
VIEW_UNSPECIFIED = 0;
|
||||
|
||||
BASIC = 1;
|
||||
|
||||
WITH_EDGE_IDS = 2;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Consider the generated code for the `Get` method:
|
||||
|
||||
```go
|
||||
// Get implements UserServiceServer.Get
|
||||
func (svc *UserService) Get(ctx context.Context, req *GetUserRequest) (*User, error) {
|
||||
// .. truncated ..
|
||||
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")
|
||||
}
|
||||
// .. truncated ..
|
||||
}
|
||||
```
|
||||
By default, `client.User.Get` is invoked, which does not return any edge ID information, but if `WITH_EDGE_IDS` is passed,
|
||||
the endpoint will retrieve the `ID` field for any `Category` related to the user via the `administered` edge.
|
||||
Reference in New Issue
Block a user