Ent: proposal: cascading deletion support

Created on 26 Mar 2020  路  5Comments  路  Source: ent/ent

We want to add support for cascading deletion in ent. We are in early discussions and your comments and feedback can help us.


GENERAL IDEA

The cascading deletion will be implemented in the framework and won't be configured
in the database (e.g. ON DELETE CASCADE in MySQL), in order to support privacy checks,
logging, tracing, etc.

What about adding support for database-level deletion? See below. I was thinking about adding support for schema DDLs, and this way, users will be able to configure ON DELETE CASCADE actions on their tables. But this is part of a different discussion.

Update: database-level cascading deletion was added in https://github.com/ent/ent/pull/1339

--

The first option is cascade-delete, which indicates that if a user is deleted, the cards
that are associated with it will be deleted as well. The schema declaration is as follows:

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("cards", Card.Type).
            Cascade(edge.Delete()),
    }
}

The second option is delete-orphan, which provides the same functionality as cascade-delete,
but in addition to that, it indicates that nodes without a reference should be deleted.
For example, if a user removes an edge from the card, the card should be deleted - because it's an orphan.

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("cards", Card.Type).
            Cascade(edge.Delete(), edge.DeleteOrphan()),
    }
}

However, sometimes we want to move a reference to a "child node" from one parent to the other.
For example, move the card ownership from Ariel to Alex - in this case, we don't want the card
to be deleted (between the ownership transition). Therefore, I'm thinking about 2 options:

  1. Add an option to disable cascade-deletion on delete operation. For example:

    client.User.
        UpdateOne(a8m).
        RemoveCards(card).
        WithCascadeDelete(false).
        Save(ctx)
    
  2. Add support for sessions - need more design, but the idea goes as follows:

    s := client.Session()
    s.User.UpdateOne(a8m).RemoveCards(card).Save(ctx)
    s.User.UpdateOne(alex).AddCards(card).Save(ctx)
    s.Commit() // Maybe, s.Flush() to not confuse with ent.Tx.
    

Since adding support for a session is a broader change, maybe we can start with option 1, and add support for sessions after V1.

2 additional notes:

  1. A node can only cascade deletion to edges of type:

    • For O2M edge: an edge from a user to its pets - A pet can't delete its owner.

    • For O2O edge: only the owner of the edge can delete it - An address can't delete its resident.

    • For bidirectional O2O: cascade-deletion is not allowed - A user can't delete its spouse.

  1. I was thinking about adding this functionality outside of the schema as well. For example, add this to the Delete/Update builders:
    go client.User.Delete(). WithCards(predicates...). Exec(ctx)

Please feel free to give feedback and comment. Nothing is set in stone yet!

Most helpful comment

Hey @aca and @erikh, and thanks for your comments.

Could you elaborate more on "sessions"?

Really like all the rest of the design. Many thanks for the hard work and explanation for the design.

I don't have any design for sessions yet (this was just a random thought of mine), but the idea was to make it possible to perform multiple operations in the application side (like moving edge ownerships between entities), and only then flush to the the database.

Since I wrote this, we came up with another suggestion (by @alexsn) to add a Move<E> operation that "moves" an edge/relation ownership from one entity to the other - with one query to the database and without loading the edge. For example:

client.User.MoveCard(from, to).Exec(ctx)

This looks great. I do have some performance concerns about doing this in go and at the connection level, but I'm not sure I'm ready to say anything without looking at code first.

Users that prefer to use ON DELETE CASCADE clause will be able to use it, but we prefer to avoid using it for a few reasons, for example:

  • We want all database operations to be audited/logged and traced by the application
  • The privacy layer (in alpha stage at the moment) might blocks users without permission for deleting some types
  • When we'll add the support for "soft-delete" this can work with it as well.

Maybe there are more pros/cons for both sides, but like you said, we need to come up with some POC to be able to compare the 2.

It would be nice if Cascade could accept a function of some kind, that
could be used to further process the node (I'm thinking about compliance
solutions specifically where data might need to be shoveled off) before
taking any final action. Maybe something like GRPC interceptors for
databases.

Did you have a chance to see the hooks API? It provides a middleware-like API for ent operations.

All 5 comments

Could you elaborate more on "sessions"?

Really like all the rest of the design. Many thanks for the hard work and explanation for the design.

This looks great. I do have some performance concerns about doing this in
go and at the connection level, but I'm not sure I'm ready to say anything
without looking at code first.

It would be nice if Cascade could accept a function of some kind, that
could be used to further process the node (I'm thinking about compliance
solutions specifically where data might need to be shoveled off) before
taking any final action. Maybe something like GRPC interceptors for
databases.

Hope this was useful commentary,

-Erik

Hey @aca and @erikh, and thanks for your comments.

Could you elaborate more on "sessions"?

Really like all the rest of the design. Many thanks for the hard work and explanation for the design.

I don't have any design for sessions yet (this was just a random thought of mine), but the idea was to make it possible to perform multiple operations in the application side (like moving edge ownerships between entities), and only then flush to the the database.

Since I wrote this, we came up with another suggestion (by @alexsn) to add a Move<E> operation that "moves" an edge/relation ownership from one entity to the other - with one query to the database and without loading the edge. For example:

client.User.MoveCard(from, to).Exec(ctx)

This looks great. I do have some performance concerns about doing this in go and at the connection level, but I'm not sure I'm ready to say anything without looking at code first.

Users that prefer to use ON DELETE CASCADE clause will be able to use it, but we prefer to avoid using it for a few reasons, for example:

  • We want all database operations to be audited/logged and traced by the application
  • The privacy layer (in alpha stage at the moment) might blocks users without permission for deleting some types
  • When we'll add the support for "soft-delete" this can work with it as well.

Maybe there are more pros/cons for both sides, but like you said, we need to come up with some POC to be able to compare the 2.

It would be nice if Cascade could accept a function of some kind, that
could be used to further process the node (I'm thinking about compliance
solutions specifically where data might need to be shoveled off) before
taking any final action. Maybe something like GRPC interceptors for
databases.

Did you have a chance to see the hooks API? It provides a middleware-like API for ent operations.

For the sake of generality, you might consider changing from .Cascade to .OnDelete (and .OnUpdate). This would allow for the usual SQL ON DELETE options (RESTRICT, CASCADE, SET NULL, NO ACTION, SET DEFAULT) and naming would align.

For example:

edge.To("cards", Card.Type).
    OnDelete(edge.Cascade()),

or:

edge.To("cards", Card.Type).
    OnDelete(edge.SetNull()),

Of course the above would work with .Cascade(edge.SetNull()) but I think that would be confusing given the SQL names.

As for the edge.DeleteOrphan option, that could perhaps be passed as an argument to edge.Cascade, but not sure if that's the best way to do it.

Just a thought.

I came across this proposal not for the titled deletion aspects but more so on the move proposal:

add a Move operation that "moves" an edge/relation ownership...

I think the api is fine, even though semantically it feels like an update. I can assume keeping it distinct from the .Update() methods would imply a lower implementation complexity.

Some quick common use cases I can think of that would benefit from the Move<E> proposal.

  • Merging an anonymous user after they login via social. e.g. move all existing relationship over to other user
  • Changing primary access rights e.g. "make this user the owner upon some condition".
  • etc
Was this page helpful?
0 / 5 - 0 ratings

Related issues

rotemtam picture rotemtam  路  4Comments

ernado picture ernado  路  4Comments

dilipkk-foyernet picture dilipkk-foyernet  路  4Comments

DeedleFake picture DeedleFake  路  5Comments

HarikiRito picture HarikiRito  路  3Comments