Saltar al contenido principal

Trabajar con relaciones

[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Las relaciones (edges) nos permiten expresar las conexiones entre diferentes entidades en nuestra aplicación Ent. Veamos cómo funcionan junto con los servicios gRPC generados.

Comencemos añadiendo una nueva entidad Category y creando relaciones que la vinculen con nuestro tipo User:

ent/schema/category.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)),
}
}

Creando la relación inversa en el User:

ent/schema/user.go
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("administered", Category.Type).
Ref("admin").
Annotations(entproto.Field(5)),
}
}

Observa algunos detalles importantes:

  • Nuestras relaciones también reciben una anotación entproto.Field. Veremos por qué en un momento.

  • Hemos creado una relación uno-a-muchos donde una Category tiene un único admin, y un User puede administrar múltiples categorías.

Al regenerar el proyecto con go generate ./..., observa los cambios en el archivo .proto:

ent/proto/entpb/entpb.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;
}

Fíjate en las siguientes modificaciones:

  • Se creó un nuevo mensaje Category. Este mensaje tiene un campo llamado admin que corresponde a la relación admin en el esquema Category. Es un campo no repetido porque marcamos la relación como .Unique(). Su número de campo es 3, correspondiente a la anotación entproto.Field en la definición de la relación.

  • Se añadió un nuevo campo administered en la definición del mensaje User. Es un campo repeated, correspondiente al hecho de que no marcamos la relación como Unique en esta dirección. Su número de campo es 5, correspondiente a la anotación entproto.Field en la relación.

Crear entidades con sus relaciones

Demostremos cómo crear una entidad con sus relaciones escribiendo una prueba:

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. 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: 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")
}
}

Para crear la relación desde el User creado hacia la Category existente, no necesitamos completar todo el objeto Category. En su lugar, solo completamos el campo Id. Esto es interpretado por el código del servicio generado:

ent/proto/entpb/entpb_user_service.go
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
}

Recuperar IDs de relaciones para entidades

Hemos visto cómo crear relaciones entre entidades, pero ¿cómo recuperamos esos datos desde el servicio gRPC generado?

Considera esta prueba de ejemplo:

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: 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")
}
}

Como puedes ver en la prueba, por defecto la información de relaciones no es devuelta por el método Get del servicio. Esto es deliberado porque la cantidad de entidades relacionadas con una entidad es ilimitada. Para permitir que el llamante especifique si desea obtener la información de relaciones, el servicio generado sigue AIP-157 (Respuestas parciales). En resumen, el mensaje GetUserRequest incluye una enumeración llamada View:

ent/proto/entpb/entpb.proto
message GetUserRequest {
int64 id = 1;

View view = 2;

enum View {
VIEW_UNSPECIFIED = 0;

BASIC = 1;

WITH_EDGE_IDS = 2;
}
}

Considera el código generado para el método Get:

ent/proto/entpb/entpb_user_service.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 ..
}

Por defecto, se invoca client.User.Get, que no devuelve información de IDs de relaciones. Pero si se pasa WITH_EDGE_IDS, el endpoint recuperará el campo ID de cualquier Category relacionada con el usuario a través de la relación administered.