Managing Relationships - Cascading deletes for has_one relationships.

I'm trying to work through setting up relationships the Ash-way and here's roughly the structure I have: * A Campaign Has One Ship Has One Stash ** Has Many Crew Members I have set up my campaign resource like this:
def MyApp.Campaigns.Campaign do
#...
postgres do
#...
references do
reference :stash, on_delete: :delete
reference :ship, on_delete: :delete
end
end

actions do
create :create do
#...
change manage_relationship(:stash, type: :direct_control)
change manage_relationship(:ship, type: :direct_control)
end
# ... other actions
destroy :destroy do
accept [:id]

change cascade_destroy(:stash, action: :destroy)
change cascade_destroy(:ship, action: :destroy)
end
end

relationships do
# ... other relationships
has_one :stash, FiveApps.Campaigns.Stash
has_one :ship, FiveApps.Campaigns.Ship
end
end
def MyApp.Campaigns.Campaign do
#...
postgres do
#...
references do
reference :stash, on_delete: :delete
reference :ship, on_delete: :delete
end
end

actions do
create :create do
#...
change manage_relationship(:stash, type: :direct_control)
change manage_relationship(:ship, type: :direct_control)
end
# ... other actions
destroy :destroy do
accept [:id]

change cascade_destroy(:stash, action: :destroy)
change cascade_destroy(:ship, action: :destroy)
end
end

relationships do
# ... other relationships
has_one :stash, FiveApps.Campaigns.Stash
has_one :ship, FiveApps.Campaigns.Ship
end
end
3 Replies
AngryBadger
AngryBadgerOP2mo ago
I created a test to make sure I knew what I was doing (obvs I don't):
# Wrote a test confirming that I can create a campaign with a stash (and a previous one that
# an empty ship is created if not provided... This passes.
test "creates a campaign with a stash" do
actor = generate(user())

{:ok, campaign} =
Campaigns.create_campaign(
%{
name: "Test Campaign",
stash: %{notes: "test notes", credits: 1, patrons: 1, rivals: 1}
},
actor: actor,
load: [:stash]
)

assert campaign.stash.id != nil
assert campaign.stash.notes == "test notes"
assert campaign.stash.credits == 1
assert campaign.stash.patrons == 1
assert campaign.stash.rivals == 1
end

# Then I wrote the delete test. Which fails.
test "deletes the campaign" do
campaign = generate(campaign())

assert {:ok, _} = Campaigns.delete_campaign(campaign.id)
assert Campaigns.get_campaign(campaign.id) == nil
end
# Wrote a test confirming that I can create a campaign with a stash (and a previous one that
# an empty ship is created if not provided... This passes.
test "creates a campaign with a stash" do
actor = generate(user())

{:ok, campaign} =
Campaigns.create_campaign(
%{
name: "Test Campaign",
stash: %{notes: "test notes", credits: 1, patrons: 1, rivals: 1}
},
actor: actor,
load: [:stash]
)

assert campaign.stash.id != nil
assert campaign.stash.notes == "test notes"
assert campaign.stash.credits == 1
assert campaign.stash.patrons == 1
assert campaign.stash.rivals == 1
end

# Then I wrote the delete test. Which fails.
test "deletes the campaign" do
campaign = generate(campaign())

assert {:ok, _} = Campaigns.delete_campaign(campaign.id)
assert Campaigns.get_campaign(campaign.id) == nil
end
The error appears to be that when I delete the campaign it's not cascading the deletes to the has_one relationships. I followed the instructions on the documentation about deletion order and then on Ash.Resource.Change.CascadeDestroy but I am still getting this test failure:
code: assert {:ok, _} = Campaigns.delete_campaign(campaign.id)
left: {:ok, _}
right: {
:error,
%Ash.Error.Invalid{errors: [%Ash.Error.Changes.InvalidAttribute{field: :id, message: "would leave records behind", private_vars: [constraint: "stashes_campaign_id_fkey", constraint_type: :foreign_key, detail: "Key (id)=(3b1fddf2-e317-49d1-9985-2f56c16f480b) is still referenced from table \"stashes\"."], value: nil, has_value?: false, splode: Ash.Error, bread_crumbs: [], vars: [], path: [], stacktrace: #Splode.Stacktrace<>, class: :invalid}]}
}
code: assert {:ok, _} = Campaigns.delete_campaign(campaign.id)
left: {:ok, _}
right: {
:error,
%Ash.Error.Invalid{errors: [%Ash.Error.Changes.InvalidAttribute{field: :id, message: "would leave records behind", private_vars: [constraint: "stashes_campaign_id_fkey", constraint_type: :foreign_key, detail: "Key (id)=(3b1fddf2-e317-49d1-9985-2f56c16f480b) is still referenced from table \"stashes\"."], value: nil, has_value?: false, splode: Ash.Error, bread_crumbs: [], vars: [], path: [], stacktrace: #Splode.Stacktrace<>, class: :invalid}]}
}
I feel like I missed something super obvious in the documentation.
ZachDaniel
ZachDaniel2mo ago
IIRC there are options to cascade_destroy that may help you there you may need the before_action option for example
AngryBadger
AngryBadgerOP2mo ago
Thanks @ZachDaniel , both suggestions resulted in the solution: add the option after_action?: false.

Did you find this page helpful?