http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html has details on when things are saved.
Look down in the “Unsaved objects and associations” section.
The interaction between ActiveRecord and the database is very simple when working with a single record – it’s always pretty clear when the database is going to be changed. What about when you’re working with multiple records and associations? I did some experiments way back at the start of the Hobo project, but recently I wanted to check if anything had changed.
So I threw together some simple experiments, and turned on logging in the console. It’s a bit rough and certainly not exhaustive, but I formatted it in markdown out of habit and then though hey, I should post this, so here it is.
Is this stuff documented somewhere? I never found it if it is. I wonder if most Rails devs know about all this already.
This is all in Rail 2.0.2 BTW.
class Post < ActiveRecord::Base
has_many :comments
has_many :categorisations
has_many :categories, :through => :categorisations
end
class Comment < ActiveRecord::Base
belongs_to :post
end
class Category < ActiveRecord::Base
has_many :categorisations
end
class Categorisation < ActiveRecord::Base
belongs_to :post
belongs_to :category
end
has_many (not through)New comments are created along with a new post:
>> p = Post.new
=> #<Post id: nil>
>> p.comments = [Comment.new]
=> [#<Comment id: nil, post_id: nil>]
>> p.save
Post Create (0.000601) INSERT INTO posts VALUES(NULL)
Comment Create (0.000195) INSERT INTO comments ("post_id") VALUES(1)
=> true
For a post that exists, the appended comments are created immediately:
>> p
=> #<Post id: 1>
>> p.comments << Comment.new
Comment Create (0.000481) INSERT INTO comments ("post_id") VALUES(1)
=> [#<Comment id: 1, post_id: 1>, #<Comment id: 2, post_id: 1>]
Comments no longer in the array have their foreign_key set to NULL. (I’d guess this changes if you declare :dependent => :destroy, but I didn’t try it)
>> p.comments
=> [#<Comment id: 1, post_id: 1>, #<Comment id: 2, post_id: 1>]
>> p.comments = []
Comment Update (0.001335) UPDATE comments SET post_id = NULL WHERE (post_id = 1 AND id IN (1,2))
=> []
New comments in the array are created immediately:
>> p.comments = [Comment.new]
Comment Create (0.000504) INSERT INTO comments ("post_id") VALUES(1)
=> [#<Comment id: 3, post_id: 1>]
Existing comments have their foreign key set
>> p2 = Post.create
Post Create (0.000820) INSERT INTO posts VALUES(NULL)
=> #<Post id: 2>
>> c = p.comments.first
=> #<Comment id: 3, post_id: 1>
>> p2.comments = [c]
Comment Load (0.000292) SELECT * FROM comments WHERE (comments.post_id = 2)
Comment Update (0.000684) UPDATE comments SET "post_id" = 2 WHERE "id" = 3
=> [#<Comment id: 3, post_id: 2>]
belongs_toWhen assigning c.post on an existing comment, the change is saved when the comment is saved:
>> c.post == p2
=> true
>> c.post = p
=> #<Post id: 1>
>> c.save
Comment Update (0.000778) UPDATE comments SET "post_id" = 1 WHERE "id" = 3
=> true
When assigning a c.post to a new post, the post is created when the comment is saved:
>> c
=> #<Comment id: 3, post_id: 1>
>> c.post = Post.new
=> #<Post id: nil>
>> c.save
Post Create (0.000464) INSERT INTO posts VALUES(NULL)
Comment Update (0.000148) UPDATE comments SET "post_id" = 3 WHERE "id" = 3
=> true
This happens the same way when the comment is new — both are created together:
>> c = Comment.new
=> #<Comment id: nil, post_id: nil>
>> c.post = Post.new
=> #<Post id: nil>
>> c.save
Post Create (0.000499) INSERT INTO posts VALUES(NULL)
Comment Create (0.000161) INSERT INTO comments ("post_id") VALUES(4)
=> true
has_many :throughAssignment to p.categories where p is an existing post:
>> p
=> #<Post id: 1>
>> cat = Category.create
Category Create (0.000427) INSERT INTO categories VALUES(NULL)
=> #<Category id: 1>
>> p.categories = [cat]
Category Load (0.000289) SELECT categories.* FROM categories INNER JOIN categorisations ON categories.id = categorisations.category_id WHERE ((categorisations.post_id = 1))
=> [#<Category id: 1>]
>> p.save
=> true
Note there were no changes to the categories table.
Assignment to p.categories where p is a new post:
>> p = Post.new
=> #<Post id: nil>
>> p.categories = [cat]
=> [#<Category id: 1>]
>> p.save
Post Create (0.000513) INSERT INTO posts VALUES(NULL)
=> true
Again, nothing happens to the categories table
Can’t append to a has-many-through on a new record:
>> p = Post.new
=> #<Post id: nil>
>> p.categories << cat
ActiveRecord::HasManyThroughCantAssociateNewRecords: Cannot associate new records through 'Post#categorisations' on '#'. Both records must have an id in order to create the has_many :through record associating them.
Can append to a has-many-through on an existing record. The joining record is created immediately:
>> p = Post.find(:first)
Post Load (0.000365) SELECT * FROM posts LIMIT 1
=> #<Post id: 1>
>> p.categories
Category Load (0.000294) SELECT categories.* FROM categories INNER JOIN categorisations ON categories.id = categorisations.category_id WHERE ((categorisations.post_id = 1))
=> []
>> p.categories << cat
Categorisation Create (0.000479) INSERT INTO categorisations ("post_id", "category_id") VALUES(1, 1)
=> [#<Category id: 1>]
But this is not allowed if the category is new:
>> p.categories << Category.new
ActiveRecord::HasManyThroughCantAssociateNewRecords: Cannot associate new records through 'Post#categorisations' on '#'. Both records must have an id in order to create the has_many :through record associating them.
Did you learn something?
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html has details on when things are saved.
Look down in the “Unsaved objects and associations” section.
Thanks Tom.
Yeah, this is one of the things that drives me crazy about Rails and ActiveRec. I don’t like the fact that DB calls happen silently in the background (and therefore fail silently in the background). There have been a couple of times where I’ve had tables messed up with half-formed object graphs on account of it.
It gets even better with the #build series of methods, where you can associate to a collection without any save at all. My only problem with #build is that it only allows me to pass in attributes for a new objects rather than to assign an existing object and postpone the save for later.
Another great ruby skills post! I hope we can expect more of these in the future (obviously when yall have more time and ant busy with actually working on the hobo codebase)
Articles like this are what ruby/rails needs to lower the barrier of entry for newcomers.
Thanks for the nice writeup Tom :)
I remember there was only little information about when things get saved, back when I started with rails.