Ent: Will polymorphism be available?

Created on 15 Dec 2020  路  3Comments  路  Source: ent/ent

  • [x] I have searched the issues of this repository and believe that this is not a duplicate.

Summary 馃挕

I first used polymorphism in ActiveRecord, it is called Polymorphic Associations.

Here is how I expected this should work:

For example, a retail company have two types of Transaction: InboundTransaction (add stock, procurement, expense, etc) and OutboundTransaction (client order, end user order, etc). But it is have one in common, amount to track down the budget. It will be easier for the accountant to keep track on them if it is placed as single table, just like how mutation works. Inbound and outbound transaction has its own behaviour, will be troublesome if we merged them into one.


OutboundTransaction

type OutboundTransaction struct {
  ent.Schema
}

func (OutboundTransaction) Fields() []ent.Field {
  return []ent.Field{
    fie.d.String("sales_channel"),
    field.JSON("products_dealed", ProductDealed{}),
    field.String("courier"),
    field.Strign("courier_tracking_number")
  }
}


InboundTransaction

type InboundTransaction struct {
  ent.Schema
}

func (OutboundTransaction) Fields() []ent.Field {
  return []ent.Field{
    field.String("department_in_charge"),
    field.String("procurement_document_url"),
    field.Time("arival"),
  }
}


Transaction

type Transaction struct {
  ent.Schema
}

func (Transaction) Fields() []ent.Field {
  return []ent.Field{
    field.Int("amount"),
  }
}

func (Transaction) IsA() []ent.IsA {
  return []ent.IsA{
    isA.To(OutboundTransaction.Type),
    isA.To(InboundTransaction.Type),
  }
}

Motivation 馃敠

I found it pretty interesting when you have several nodes within common structure but have another specific different thing. Those different fields could be the edges or fields, make it to hard if we keep them intake but at the same time make it to hard not to take them out.

Proposal

Most helpful comment

I'm also quite interested in this. It's quite a common pattern especially in IAM tables. Things like permissions are almost always polymorphic (happy to give examples if needed).

@a8m What if instead of 1 interface type that abstracts N polymorphic relations, we expose N concrete relations that ent knows are polymorphic under the hood?

If that's okay, then I think ent has almost everything in place for this to work, the only thing it needs is a simple "WHERE" clause that needs to be defined at compile time.

For example, given the following table:

describe photos;
+-------------+-------------+
| Field       | Type        |
+-------------+-------------+
| owner_type  | int         |
| owner_id    | varchar     |
+-------------+-------------+

select * from photos;
+-------------+-------------+
| owner_type  | owner_id    |
+-------------+-------------+
| User        | 123         |
| Product     | 32          |
+-------------+-------------+

We can describe in ent as follows (don't worry about how the API/syntax look, the idea here is what I'm interested to propose):

func (User) Edges() []ent.Edge {
  return []ent.Edge{
    edge.To("photos", Photo.Type).Polymorphic("owner_type", "User").StorageKey(edge.Column("owner_id"))
  }
}

func (Product) Edges() []ent.Edge {
  return []ent.Edge{
    edge.To("photos", Photo.Type).Polymorphic("owner_type", "Product").StorageKey(edge.Column("owner_id"))
  }
}

func (Photo) Edges() []ent.Edge {
  return []ent.Edge{
    edge.From("user", User.Type).Unique()
    edge.From("product", Product.Type).Unique()
  }
}

And that's it!

All the relationships are now explicit and ent already handles everything correctly.

The only new thing that got added is the .Polymorphic method. and all it does is to store a "WHERE owner_type = ''" at compile time and so that whenever a programmer writes:

user.QueryPhotos() 

under the hood it will do everything exactly the same, but it will just know to append the WHERE photos.owner_type = "USER" clause correctly.

As for syntax there are a lot of ways that you can do:

.Polymorphic("owner") // automatically assumes owner_type, owner_id.

OR

.Polymorphic("onwer_type") // automatically assumes the value is User or Product since that's the model it was defined in, although it would be nice to add another option in case the value was different such as .Polymorphic("owner_type").PolymorphicValue("is_user")

OR

You can have it by an option of .StorageKey such as: .StorageKey(edge.Column("owner_id"), edge.Where("owner_type", "User"))

OR any other way really.

The idea here is that we're defining multiple edges explicitly so everything remains type safe. Those edges just happen to point to the same foreign-key column. Ent here already all the mechanisms in place, it just needs to add the one WHERE clause and maybe a couple of validations.

All 3 comments

Thanks for raising up this issue @satriahrh.

I found it pretty interesting when you have several nodes within common structure but have another specific different thing.

Agree. We currently provide a mixed-in schema option for users, but that's of course doesn't solve some of the problems you mentioned.

Now, regarding your example (or the one in ActiveRecords website) - edges are resolved to concrete-types atm, and the way to implement your use case today is to define your edges as follows:

func (Transaction) Edges() []ent.Edge {
  return []ent.Edge{
    edge.To("inbound", InboundTransaction.Type),
    edge.To("outbound", OutboundTransaction.Type),
  }
}

func (InboundTransaction) Edges() []ent.Edge {
  return []ent.Edge{
    edge.From("transaction", Transaction.Type).
        Ref("inbound").
        Unique(),
  }
}

If we'll add support for polymorphic associations, it means that our edge queries will return an interface and we'll loose a bit the type-safety (not saying that it's bad, just wondering how the API will look like).

Another question (didn't check it yet), how do you define a FK constraint when the "foreign_key" column points to multiple tables.

I'd love to get more feedback and thoughts regarding this issue.

Thanks for the reply @a8m.

Another question (didn't check it yet), how do you define a FK constraint when the "foreign_key" column points to multiple tables.

There won't be possible to define a FK constraint to multiple table. In ActiveRecord, the Transaction table have two column to define which row it is pointing.

+----------------------+--------------+
| Field                | Type         |
+----------------------+--------------+
| other_columns        | ~            |
| transactionable_type | varchar(255) |
| transactionable_id   | int(11)      |
+----------------------+--------------+

Then, the application should have a logical way to lookup the data to desired table given transactionable_type (destination table) and transactionable_id (destination primary key). Optionally, transactionable_id and transactionable_type can be indexed for backward reference.

Alternatively, there is away to make a FK constraint but I didn't think it as the best solution. We can create a FK constraint to the destination table. But this will possibly break the O2O relation. For example outbound_transactions have set a foreign key to a specific transactions X. There is no rule to restrict inbound_transactions also settig a foreign key to transactions X. Moreover if you want to lookup the polymorphic from transactions, you have to lookup to multiple tables.

I'm also quite interested in this. It's quite a common pattern especially in IAM tables. Things like permissions are almost always polymorphic (happy to give examples if needed).

@a8m What if instead of 1 interface type that abstracts N polymorphic relations, we expose N concrete relations that ent knows are polymorphic under the hood?

If that's okay, then I think ent has almost everything in place for this to work, the only thing it needs is a simple "WHERE" clause that needs to be defined at compile time.

For example, given the following table:

describe photos;
+-------------+-------------+
| Field       | Type        |
+-------------+-------------+
| owner_type  | int         |
| owner_id    | varchar     |
+-------------+-------------+

select * from photos;
+-------------+-------------+
| owner_type  | owner_id    |
+-------------+-------------+
| User        | 123         |
| Product     | 32          |
+-------------+-------------+

We can describe in ent as follows (don't worry about how the API/syntax look, the idea here is what I'm interested to propose):

func (User) Edges() []ent.Edge {
  return []ent.Edge{
    edge.To("photos", Photo.Type).Polymorphic("owner_type", "User").StorageKey(edge.Column("owner_id"))
  }
}

func (Product) Edges() []ent.Edge {
  return []ent.Edge{
    edge.To("photos", Photo.Type).Polymorphic("owner_type", "Product").StorageKey(edge.Column("owner_id"))
  }
}

func (Photo) Edges() []ent.Edge {
  return []ent.Edge{
    edge.From("user", User.Type).Unique()
    edge.From("product", Product.Type).Unique()
  }
}

And that's it!

All the relationships are now explicit and ent already handles everything correctly.

The only new thing that got added is the .Polymorphic method. and all it does is to store a "WHERE owner_type = ''" at compile time and so that whenever a programmer writes:

user.QueryPhotos() 

under the hood it will do everything exactly the same, but it will just know to append the WHERE photos.owner_type = "USER" clause correctly.

As for syntax there are a lot of ways that you can do:

.Polymorphic("owner") // automatically assumes owner_type, owner_id.

OR

.Polymorphic("onwer_type") // automatically assumes the value is User or Product since that's the model it was defined in, although it would be nice to add another option in case the value was different such as .Polymorphic("owner_type").PolymorphicValue("is_user")

OR

You can have it by an option of .StorageKey such as: .StorageKey(edge.Column("owner_id"), edge.Where("owner_type", "User"))

OR any other way really.

The idea here is that we're defining multiple edges explicitly so everything remains type safe. Those edges just happen to point to the same foreign-key column. Ent here already all the mechanisms in place, it just needs to add the one WHERE clause and maybe a couple of validations.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dom3k picture dom3k  路  4Comments

juanpabloaj picture juanpabloaj  路  3Comments

ernado picture ernado  路  3Comments

DeedleFake picture DeedleFake  路  5Comments

rubensayshi picture rubensayshi  路  3Comments