8000 proposal: add support for relation/edge schema · Issue #1949 · ent/ent · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

proposal: add support for relation/edge schema #1949

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
a8m opened this issue Sep 15, 2021 · 26 comments
Closed

proposal: add support for relation/edge schema #1949

a8m opened this issue Sep 15, 2021 · 26 comments
Labels

Comments

@a8m
Copy link
Member
a8m commented Sep 15, 2021

This is a feature that I have wanted to add for a long time to ent, and will be happy to get feedback, suggestion for improvements or just hear your thoughts before I create a proper PR for it.

The idea is instead of adding a completely new API for adding fields for edges (columns for join tables), configure their indexes, hooks, privacy, etc, or even support multi-column PKs, we'll add 2 new small changes for the ent/schema for supporting all those issues.

field.ID Annotation

A new field.ID option will be added to the schema/field package, and this will allow configuring multi-column PKs, but more than that, it will make it possible to configure relation/edge schema manually. For example:

// UserGroup defines the UserGroup relation schema.
type UserGroup struct {
	ent.Schema
}

func (UserGroup) Annotations() []schema.Annotation {
	return []schema.Annotation{
		// This will generate the following struct:
		//
		//	type UserGroupID struct {
		//		UserID, GroupID int
		//	}
		//
		field.ID("user_id", "group_id"),
	}
}

func (UserGroup) Fields() []ent.Field {
	return []ent.Field{
		field.Int("user_id"),
		field.Int("group_id"),
		field.Time("created_at").
			Immutable().
			Default(time.Now),
	}
}

func (UserGroup) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("user", User.Type).
			Unique().
			Field("user_id"),
		edge.To("group", Group.Type).
			Unique().
			Field("group_id"),
	}
}

Indexes, hooks, privacy, and all other options will also be available for this type of schema, but in order to add a user to a group, the flow is a bit longer:

a8m := client.User.Create().SetName(..).SaveX(ctx)
hub := client.Group.Create().SetName(..).SaveX(ctx)
// Instead of calling hub.AddGroups(a8m), the API is:
client.UserGroup.Create().SetUserID(a8m.ID).SetGroupID(hub.ID).ExecX(ctx)
client.UserGroup.GetX(ctx, ent.UserGroupID{UserID: a8m.ID, GroupID: hub.ID})

We can bypass this long flow by providing an additional config option for edges:

The edge.Through Option

The Through option is supported by other frameworks and has already been proposed here before. The idea is to allow schemas that use the relation/edge-schema to CRUD directly their relations. However, I have an open question (atm) regarding the design of the generated code.

The configuration looks as follows:

func (Group) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("users", User.Type).
			Through(UserGroup.Type),
	}
}

And the generated API is as follows:

a8m := client.User.Create().SetName(..).SaveX(ctx)
hub := client.Group.Create().SetName(..).AddUsers(a8m).SaveX(ctx)

An open question I have is on how do we set/update relation-schema fields in case they don't have default values or hooks configured on their ent/schema.

Thoughts?

@a8m a8m added the Proposal label Sep 15, 2021
@rotemtam
Copy link
Collaborator

Cc @tarrencev

@tarrencev
Copy link
Contributor

This works great for my use case! Maybe it makes most sense to start with the field.ID annotation and then work on the Through api? For now we could use the more verbose code

@marwan-at-work
Copy link
Contributor

Looks great. I like the through option because it feels intuitive and many people coming from the Rails and other domains will find it natural.

@Vilsol
Copy link
Vilsol commented Sep 18, 2021

This would be an amazing addition. Would be the last piece of the puzzle to be able to switch to ent.

Regarding the defaults in the through tables. Would it maybe be possible to detect if a table is referenced to as a "through" table, and then check if any non-default fields are missing, then panic/error out?

A potential issue/feature that should be considered: When creating the annotation field.ID("user_id", "group_id") it would be great if it also supported the case where user_id was a string, but group_id was an int (or other type combos).

Nevermind the top suggestion, realized that it would no longer add an ID field, and the 2 custom ID fields would be defined by the user field types themselves.

@e48
8000 Copy link
e48 commented Sep 21, 2021
a8m := client.User.Create().SetName(..).SaveX(ctx)

// GroupUsersThrough has setters for fields except "group"
thr := client.Group.UsersThrough().
	SetUser(a8m).
	SetCreatedAt(time.Now)

hub := client.Group.Create().SetName(..).AddUsersThrough(thr).SaveX(ctx)

@masseelch
Copy link
Collaborator

Any particular reason this is added as annotation and not as part of the schema methods?

@tankbusta
Copy link
Contributor

This would solve the issue I’m running into now as well. The changes to API flow isn’t a big deal because now that logic can be customized for upserts.

@a8m a8m mentioned this issue Nov 19, 2021
27 tasks
@panakour
Copy link
panakour commented Jan 8, 2022

Any update on this?

1 similar comment
@euskadi31
Copy link

Any update on this?

@a8m
Copy link
Member Author
a8m commented Mar 2, 2022

While I started implementing, I started to think of a different approach. Defining a new schema type named edge.Schema feels better to me. Please, see the following example:

// Tweet schema.
type Tweet struct {
	ent.Schema
}

func (Tweet) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("users_liked", User.Type).
			// Through allows defining M2M (join tables) explicitly using edge.Schemas.
			// The first argument ("likes") allows interacting with the join table by its name. 
			Through("likes", TweetLike.Type),
	}
}

// TweetLike schema (the join table).
// See the edge.Schema below.
type TweetLike struct {
	edge.Schema
}

// Additional fields.
func (TweetLike) Fields() []ent.Field {
	return []ent.Field{
		field.Time("timestamp").
			Default(time.Now),
	}
}

func (TweetLike) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("tweet", Tweet.Type).
			Unique(),
		edge.To("user", User.Type).
			Unique(),
	}
}

In the above example, there's no need/way to define the PK explicitly. It's always set to the foreign keys (as it is today). Composite PKs can be added, but I feel it's not really part of this feature.

Usage API:

Keep the generated code the same as it's today and provide extended API to allow query M2M edges (with Through) in 2 flavors:

// Query without going through the join-table explicitly. 
tweer.QueryUsersLiked().All(ctx)

// Query through.
tweer.QueryLikes().Where(tweetlike.TimestampGT(...)).QueryUsers().All(ctx)

Thoughts?

@euskadi31
< 8000 details class="details-overlay details-reset position-relative d-inline-block"> Copy link

I have little experience with Entgo, but it sounds good to me!

@masseelch
Copy link
Collaborator

ent.Schema and edge.Schema are hard to distinguish. Can we call it ent.Edge? Or maybe ent.Pivot?

I like! 💯

@gmhafiz
Copy link
Contributor
gmhafiz commented Mar 3, 2022

Laravel uses the Pivot vocab, fwiw.

@thmeitz
Copy link
Contributor
thmeitz commented Mar 3, 2022

Each framework has its own specialized language, syntax and semantics. All I need as a user is a good introduction to the framework in the documentation.

We could call it ent.Join (which I think describes it best for developers working with SQL databases) or ent.Pivot.

8000
@a8m
Copy link
Member Author
a8m commented Mar 6, 2022

Thanks all for the feedback. I need to digest it a bit and come up with a final naming proposal.

ent.Edge is already taken and represents an edge in ent. Pivot/Join are too SQL-ish, and we support more database types atm, and plan to support more in the future.

edge.Schema is like an ent.Schema, you can attach policy, hooks, annotations, other edges, etc., but it represents an edge and not a node.

@thmeitz
Copy link
Contributor
thmeitz commented Mar 6, 2022

Interestingly, however, you described it exactly this way, so that we understand it: (the join table)

// TweetLike schema (the join table)

@tonyabracadabra
Copy link

While I started implementing, I started to think of a different approach. Defining a new schema type named edge.Schema feels better to me. Please, see the following example:

// Tweet schema.
type Tweet struct {
	ent.Schema
}

func (Tweet) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("users_liked", User.Type).
			// Through allows defining M2M (join tables) explicitly using edge.Schemas.
			// The first argument ("likes") allows interacting with the join table by its name. 
			Through("likes", TweetLike.Type),
	}
}

// TweetLike schema (the join table).
// See the edge.Schema below.
type TweetLike struct {
	edge.Schema
}

// Additional fields.
func (TweetLike) Fields() []ent.Field {
	return []ent.Field{
		field.Time("timestamp").
			Default(time.Now),
	}
}

func (TweetLike) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("tweet", Tweet.Type).
			Unique(),
		edge.To("user", User.Type).
			Unique(),
	}
}

In the above example, there's no need/way to define the PK explicitly. It's always set to the foreign keys (as it is today). Composite PKs can be added, but I feel it's not really part of this feature.

Usage API:

Keep the generated code the same as it's today and provide extended API to allow query M2M edges (with Through) in 2 flavors:

// Query without going through the join-table explicitly. 
tweer.QueryUsersLiked().All(ctx)

// Query through.
tweer.QueryLikes().Where(tweetlike.TimestampGT(...)).QueryUsers().All(ctx)

Thoughts?

❤️ love the example here and it would be a new milestone for ent to support this! One question, would there be any workaround (probably a less messy one) while we wait for this great addition to ent?

@nshaphan
Copy link
nshaphan commented Apr 2, 2022

Hey,
Any progress on this issue? I want to add additional fields to the pivot table. any idea?

@fgm
Copy link
Contributor
fgm commented Apr 4, 2022

The edge.Schema approach seems more intuitive indeed, and you might want to rename ent.Schema to field.Schema for consistency (and probably keep ent.Schema as a deprecated alias to field.Schema for some releases).

@CNTWDev
Copy link
CNTWDev commented Apr 9, 2022

A new field.ID option will be added to the schema/field package, and this will allow configuring multi-column PKs, but more than that, it will make it possible to configure relation/edge schema manually.

when i user Field.ID,error occurred.

how can i use the options.

@CNTWDev
Copy link
CNTWDev commented Apr 9, 2022

func (TweetLike) Edges() []ent.Edge { return []ent.Edge{ edge.To("tweet", Tweet.Type). Unique(), edge.To("user", User.Type). Unique(), } }

why define unique?
one person like one tweet?

@CNTWDev
Copy link
CNTWDev commented Apr 10, 2022

func (Group) Edges() []ent.Edge { return []ent.Edge{ edge.To("users", User.Type). Through(UserGroup.Type), } }

prefer this way.
it's smooth, and much more stable.

@a8m
Copy link
Member Author
a8m commented May 24, 2022

Hey all! The initial support was added, and it's in review atm. See #2560 and please feel free to share your thoughts/feedback before I merge it.

@JamesHovious
Copy link

Is the Edges function required on the relation schema?
i.e. would a use case of wanting to add properties to the relation necessitate creating both a Fields and Edges on that schema, or could I just have Fields serving as the property for something like created_at ?

Will this feature be available for other relation types in addition to M2M ?

Can mixins work for the Fields of the relation just like they already exist?

The privacy policy on this is a really nice feature ;)

@a8m
Copy link
Member Author
a8m commented May 25, 2022

Is the Edges function required on the relation schema?

Yes, the Edges is required as mentioned in #2560. Ent needs those fields/edges in the generated entities and their builders, and the idea here is to be explicit and consistent. If there's an edge or a field in the generated type, it must be part of the schema.

Will this feature be available for other relation types in addition to M2M?

Yes, it's mentioned in the docs.

Can mixins work for the Fields of the relation just like they already exist?

The priv 6D40 acy policy on this is a really nice feature ;)

All features of regular schemas are supported. e.g. Mixin, Hooks, and Privacy.

@a8m
Copy link
Member Author
a8m commented May 25, 2022

Closed with #2560. Thank you all for the feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

0