How to cascade delete a belongs_to relationship?

I have two schemas: User and Address. User has an :address_id key. Address has nothing pointing to the parent User.

How can I delete an Address when the User is deleted?

delete_all on the schema does what I want, but is discouraged in the docs

schema "users" do
  belongs_to(:address, Address, on_delete: :delete_all)

delete_all in the migration does the opposite of what I want: it deletes User when Address is deleted:

alter table(:users) do
  add(:address_id, references(:addresses, on_delete: :delete_all))

I think what I want in SQL is this, but I’m not sure how to do it in Ecto:

FOREIGN KEY (address_id)
REFERENCES Address(address_id)

Apologies for such a basic question. I’m sure there are already answers about this but I’m struggling to find them.

This is kind of bending my mind because the answer in the linked question seems backward to me but since it’s Matt saying it I feel I must be reading it wrong. I use on_delete: :delete_all often enough and it works like you were thinking it should in the other thread.

I tested for my own sanity:

  def change do
    create table(:users) do
      add :name, :text

    create table(:addresses) do
      add :address, :text
      add :user_id, references(:users, on_delete: :delete_all)

Then in psql:

psql=# insert into users (name) values ('One'), ('Two');
psql=# insert into addresses (address, user_id) values ('Addr One', 1), ('Addr Two', 2);
psql=# select * from users;
 id |   name
  1 | One
  2 | Two

psql=# select * from addresses;
 id | address  | user_id
  1 | Addr One |       1
  2 | Addr Two |       2

psql=# delete from addresses where id = 2;
psql=# select * from users;
 id |   name
  1 | One
  2 | Two

psql=# delete from users where id = 1;
psq=# select * from addresses
 id | address | user_id
(0 rows)

It’s totally possible I’m missing something or read something wrong? I dunno, going a bit batty!

Once it does work in SQL, you don’t have to do anything to make it work in Ecto, the rows will just get deleted. Just to add my 2 cents (which you are of course welcome to leave on the table), I would recommend against relying on this behaviour, though. You absolutely should have it for data integrity, but even if it feels a bit weird, I would run an Ecto.Multi that explicitly deletes all the relationships. This way the behaviour is explicitly spelled out in the business logic and not hidden away in the database. In the case of user, people probably expect that a bunch of other stuff is being deleted as well, but they won’t know what without doing some digging.

1 Like

Yeah, that’s what really has me tripped up. I’m wondering if it’s because most people would add the reference the other way?

EG a has_one on the Address, so address.user_id references the record to be deleted with? In my case I want Address to be used with a variety of parents, so I don’t want the ID setup that way.

I think the multi is a good idea. It would certainly make the behavior a lot more explicit too than having to dig through past migrations.

Oh ya ok, yeesh, I spent more time looking at your linked thread and less at your example above! My brain clearly switched around users and address_id up there (as happens to me often). Oh boy, sorry about that.

If address can belong to different parents then I’m actually not sure postgres has something like that out of the box. If an address can only have one parent then my brain is clearly not working super well today so I don’t know the answer right now :sweat_smile: If addresses are denormalized then this is one of those cases where I just go for an embedded schema due to simplicity. There was some interesting discussion on that recently which might make me reconsider but probably not :slight_smile:

Anyway, sorry I couldn’t be of more help!

Oops, I meant to say that an Address can have different types of parents, so I don’t want multiple nullable refs back to the various kinds of parents (eg address.user_id, address.store_id, address.home_id… But an Address will always only have one parent, so if that parent with an :address_id is deleted, the Address should be too.

You’ve been a big help, thanks! I’ll go with the multi and hope there’s an additional SQL/migration setting I’m missing as a fallback like you suggested.

1 Like

Ya, it seems I’m not crazy and cascading from child to parent is not a thing (at least not according to, for example, this).

I realize that if you use a Multi, you’re good to go so long as you have an fkey constraint set up. While it won’t cascade, the database will make it impossible to delete a user without first deleting the address, so you’d be forced to deal with it manually anyway.

I think from that link you shared it’s highlighting how most people would do this: have the child reference back to it’s parent. In Ecto this association is even called belongs_to.

when the parent (referenced) record is deleted, any child (referencing) records are also deleted

In my case my parent is referencing the child, so the association is reversed.

Yes, it’s in answer to a question asking if it’s possible to do it the other way!

1 Like