When calling destroy on a model in ActiveRecord, I've read it is capable of destroying all associated records, and that functionality appears to be setup by using the dependent option when setting up the association.
What I would like to know is - what happens if you don't set the option?
For example, in the below code, am I correct in saying:
The subscribers would NOT be affected
The user would NOT be affected
The comments WOULD be destroyed? (and in turn any associations they have marked with dependent: destroy would also then follow the same process)
class StackOverflowQuestion < ActiveRecord::Base
belongs_to :user
has_many :subscribers
has_many :comments, dependent: :destroy
end
My end goal is to be able to have a model which will destroy some associated records, but not necessarily all of them, as destroying all the associations would mean that data that is reference by other records would start to get wiped out (such as in this example, I'd not want a user to be deleted if their question was deleted).
The subscribers would NOT be affected
This depends on how you define your schema with the foreign key. There are 2 cases here:
Case 1: You define you schema like this:
create_table :subscribers do |t|
t.integer :stack_overflow_question_id
# other fields
end
add_index :subscribers, :stack_overflow_question_id
add_foreign_key :subscribers, :stack_overflow_question, column: :stack_overflow_question_id
This means you set the foreign key constraint for stack_overflow_question_id, so when you delete a StackOverflowQuestion, if there is any Subscriber which has the foreign key referring to that StackOverflowQuestion, rails will give you an error, this makes sense because you are referring a record to a deleted record!
Case 2: Define like Case 1 but without foreign key constraint
Rails won't give you any error, but you will smell wrong with the data, there are some records referring to the deleted records, this should be avoided
The user would NOT be affected
This makes sense because this is belongs_to relation, user wouldn't be affected.
The comments WOULD be destroyed? (and in turn any associations they
have marked with dependent: destroy would also then follow the same
process)
Yes, this is how rails works
Summary
You may redefine like this:
class StackOverflowQuestion < ActiveRecord::Base
belongs_to :user
has_many :subscribers, dependent: :nullify
has_many :comments, dependent: :destroy
end
Hence, your subscribers 's foreign key will be set to NIL when you destroy StackOverflowQuestion, and there isn't any foreign key which is not nil and invalid!
Your description is correct. But you should be aware that the subscribers records will be orphaned. If they are set up with a had_many relation, as you show, then each subscriber record contains a foreign key that is the id of the StackOverflowQuestion record, which will no longer exist after you destroy it. So it will point to an invalid record.
Related
I have the following models
class Widget
has_one :widget_sprocket
has_one :sprocket, through: :widget_sprockets
end
class Sprocket
has_many :widget_sprockets
has_many :widgets, though: :widget_sprockets
end
class WidgetSprocket
belongs_to :widget
belongs_to :sprocket
end
This works fine in the console, but I'm struggling with view updates for Widget. has_many :through gives Sprocket widget_ids, which I believe can be treated like a local attribute for most purposes, but the Rails docs evidently expect a different table configuration for has_one :through and therefore doesn't define a sprocket_id on Widget. As a result code like this throws an unknown attribute error
<%= f.collection_select(:sprocket_id, Sprocket.all, :id, :sprocket_type) %>
Of course I could use has_many :through for both models, but I consider it a last resort.
I think you're falling for a classic trap and overcomplicating this. If you want a one to many assocation between Sprocket and Widget you should just be using belongs_to and adding a sprocket_id foreign key column to the widgets table:
class AddSprocketToWidgets < ActiveRecord::Migration[6.1]
def change
add_reference :widgets, :sprocket, null: false, foreign_key: true
end
end
class Widget
belongs_to :sprocket
end
This guarentees on the database level that a Widget can only have one Sprocket because the column can only hold one single value. Your join table gives no such guarentee. You're really just selecting the first matching row off the join table and its actually a many to many relation. Unless thats acceptable or you prevent it with unique indexes thats an invitation for some nasty bugs.
While there are scenarios where you actually need an intermediadary table that describes a one to many relation - YAGNI.
has_many :through gives Sprocket widget_ids, which I believe can be treated like a local attribute for most purposes
Its not an attribute in any way or form. Its a method which will actually do a SELECT id FROM other_tablequery unless the assocation is preloaded.
but the Rails docs evidently expect a different table configuration for has_one :through and therefore doesn't define a sprocket_id on Widget.
Classic noob trap caused by the confusing semantics of the method names. has_one means there is a foreign key column on the other models table. Its like has_many but with a LIMIT 1 tacked onto the end of the query. To get the id you would actually call other.id.
In the case of belongs_to its not the relations macro that creates the attribute. Its having an actual sprocket_id column on the widgets table.
If you actually wanted to go though creating an intermediary table you can't just assign an id. You would have to use nested attributes and fields_for to create or update a WidgetSprocket instance. Again YAGNI.
From my understanding, destroy_all destroy all records, their associations, and does callbacks. However, it instantiates all of the records, which in my case is taking hours. It's going through about 70k records in one table, along with about 450k associated records in another table. It also just chews up through all the 16GB of memory trying to do this as well.
I'm trying to figure out the best way to handle this so that it's scalable across large number of rows.
The downfall that I see of delete_all is that it doesn't go through dependent: :destroy and destroy dependent associations. In this case, would it be best for me to just simply hard code the dependent deletes? So if Book has_many :pages, dependent: :destroy, would it be better for me to go through Pages.where(book_id: xyz).delete_all and then call Book.delete, or is there another best way for me to use delete_all to also catch its associated records?
Alter the database and set on delete cascade on the relevant foreign keys. Then when a record is deleted, all its associations will also be deleted. Then you can use the much more efficient SomeClass.delete_all.
Rails doesn't let you alter an existing foreign key. You need to remove it and add it back. For example, let's say you have...
class Building
has_many :rooms, dependent: :destroy
end
class Room
belongs_to :building
end
Then you'd write a migration like...
change_table :rooms do |t|
t.remove_foreign_key :buildings
t.foreign_key :buildings, on_delete: :cascade
end
This doesn't drop the column, it just drops and re-adds the foreign key constraint.
Then you can Building.where(...).delete_all. This will issue a single delete from buildings where ... statement. The database will efficiently delete all associated rooms as well.
I'm working with Rails and PostgreSQL and have a basic one-to-many relationship going on, one Auction has many Bids. However when I try and delete an auction (that has bids present) I get the following error:
ERROR: update or delete on table "auctions" violates foreign key
constraint "fk_rails_43e9021cbf" on table "bids". DETAIL: Key(id)=(1)
is still referenced from table "bids".
Deleting auctions with no bids gives no error.
The part that confuses me is that inside my Auction model, I have:
has_many :bids, dependent: :destroy
Since I have a dependent destroy clause, why am I still getting this error?
EDIT: I've tried dropping the whole DB, then recreating/re-migrating everything - still get the same error.
From Rails v4.2 you can do this:
Create a migration to update the foreign keys
20160321165946_update_foreign_key.rb
class UpdateForeignKey < ActiveRecord::Migration
def change
# remove the old foreign_key
remove_foreign_key :posts, :users
# add the new foreign_key
add_foreign_key :posts, :users, on_delete: :cascade
end
end
Are you using delete or destroy to remove the objects? I think you are using delete and you want to use destroy
See http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Delete+or+destroy-3F
My issue was that i am using #auction.delete (visible in the screenshot I posted) when trying to remove a record.
Delete will ignore any callbacks I have in place. So even though I have a dependent destroy clause, it is not being called - hence Rails is throwing an error. If/When I changed the code to read #auction.destroy, the call-back got invoked and it solved the problem.
Reference:
Difference between Destroy and Delete
Are you by chance using the paranoia gem or something like it?
If you are bids are paranoid and auctions are not, you may run into this error.
This would happen because when rails executes the dependent: destroy, it would soft-deletes the bids, but they still actually exist in the DB (they just have the deleted_at column set). Therefore, the foreign key constraint would fail.
Your error is from the database not rails. You need to delete the bids first in your app or change the foreign key constraint in the db to cascade the delete
Other answers are good, but don't mention that sometimes you want to leave the dependent record, but nullify the foreign key.
class Post < ActiveRecord::Base
has_many :comments, dependent: :nullify
end
Note that this will require ensuring the foreign key column in the database table has null: true
I'm not positive, but you may also need to add optional: true to the belongs to association defined in the dependent model.
Marc Busqué has a very good article about this problem that might can help.
"When ActiveRecord encounters a foreign key violation, it raises an ActiveRecord::InvalidForeignKey exception. Even if in its documentation it just says that it is raised when a record cannot be inserted or updated because it references a non-existent record, the fact is that it is also used in the case we are interested."
With that and a rescue_from we can just add to ApplicationController or to a controller concern:
rescue_from 'ActiveRecord::InvalidForeignKey' do
# Flash and render, render API json error... whatever
end
Here explain how to fix it.
To solve this, you probably want to modify your has_many :comments association in your User model to have a dependent option. Some possibilities:
has_many :comments, dependent: :delete_all - just automatically delete them when the user is deleted
has_many :comments, dependent: :destroy - like above, but call #destroy on each comment instead of just deleting directly in the db
has_many :comments, dependent: :nullify - don't delete comments when the user is deleted, just null out their user_id column
See http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many for more information
A really simple explanation: the associated table contains at least 1 record joined to the record in the table you're trying to destroy.
To fix this, add dependent: :destroy (assuming a User has_many Posts)
has_many :post, dependent: :destroy
I've a Rails 4 app that uses Postgresql database. I'm using UUIDs as id for my models.
Everything works as expected, I'm trying to set a dependant destroy has many relation, and the "dependant destroy" is not working.
Is there any incompativility between postgress UUIDs and dependent destroy? I need to set foreign keys?
I expalin a bit of my code:
Navigation through models is working correclty
To define the has_many I'm using
has_many :some_models, dependent: :destroy
My migrations are something like:
def change
create_table :my_model, id: :uuid do |t|
To test, I'm using console. I create a relation, delete the "some_models" and the main model is not deleted.
Thanks
You are thinking of the association backwards. dependent: destroy means: When I destroy a parent record, destroy the children that are associated with that record. Here's a contrived example:
class User
has_many :photos, dependent: :destroy
end
When the user is deleted, you want their photos to also be deleted.
If you really want to delete a parent record when a child is deleted, you can do so from the before_destroy callback like so:
class Photo
before_destroy :delete_parent_user
def delete_parent_user
user.destroy if self.user
end
end
Note that other children may still be pointing to that parent record if this is a has_many relationship so this may not be advisable.
dependent: :destroy only destroys child records. When you destroy my_model record, all some_model records belonging to it will be destroyed.
I have three activerecord classes: Klass, Reservation and Certificate
A Klass can have many reservations, and each reservation may have one Certificate
The definitions are as follows...
class Klass < ActiveRecord::Base
has_many :reservations, dependent: :destroy, :autosave => true
has_many :certificates, through: :reservations
attr_accessible :name
def kill_certs
begin
p "In Kill_certs"
self.certificates.destroy_all
p "After Destroy"
rescue Exception => e
p "In RESCUE!"
p e.message
end
end
end
class Reservation < ActiveRecord::Base
belongs_to :klass
has_one :certificate, dependent: :destroy, autosave: true
attr_accessible :klass_id, :name
end
class Certificate < ActiveRecord::Base
belongs_to :reservation
attr_accessible :name
end
I would like to be able to delete/destroy all the certificates for a particular klass within the klass controller with a call to Klass#kill_certs (above)
However, I get an exception with the message:
"In RESCUE!"
"Cannot modify association 'Klass#certificates' because the source
reflection class 'Certificate' is associated to 'Reservation' via :has_one."
I('ve also tried changing the reservation class to "has_many :certificates", and then the error is...
"In RESCUE!"
"Cannot modify association 'Klass#certificates' because the source reflection
class 'Certificate' is associated to 'Reservation' via :has_many."
It's strange that I can do Klass.first.certificates from the console and the certs from the first class are retrieved, but I can't do Klass.first.certificates.delete_all with out creating an error. Am I missing something?
Is the only way to do this..
Klass.first.reservations.each do |res|
res.certificate.destroy
end
Thanks for any help.
RoR docs have clear explanation for this (read bold only for TLDR):
Deleting from associations
What gets deleted?
There is a potential pitfall here: has_and_belongs_to_many and
has_many :through associations have records in join tables, as well as
the associated records. So when we call one of these deletion methods,
what exactly should be deleted?
The answer is that it is assumed that deletion on an association is
about removing the link between the owner and the associated
object(s), rather than necessarily the associated objects themselves.
So with has_and_belongs_to_many and has_many :through, the join
records will be deleted, but the associated records won’t.
This makes sense if you think about it: if you were to call
post.tags.delete(Tag.find_by(name: 'food')) you would want the ‘food’
tag to be unlinked from the post, rather than for the tag itself to be
removed from the database.
However, there are examples where this strategy doesn’t make sense.
For example, suppose a person has many projects, and each project has
many tasks. If we deleted one of a person’s tasks, we would probably
not want the project to be deleted. In this scenario, the delete
method won’t actually work: it can only be used if the association on
the join model is a belongs_to. In other situations you are expected
to perform operations directly on either the associated records or the
:through association.
With a regular has_many there is no distinction between the
“associated records” and the “link”, so there is only one choice for
what gets deleted.
With has_and_belongs_to_many and has_many :through, if you want to
delete the associated records themselves, you can always do something
along the lines of person.tasks.each(&:destroy).
So you can do this:
self.certificates.each(&:destroy)