Active Record’s << or push or concat method – Introduction
You have used it over and over again but yet there are some gotchas you have to keep in mind when you are using ActiveRecord’s << method of a has_may association.
I was always wondering how ActiveRecord handles the situations where you add a new object in a collection (via of course the << method) and the owner is also new, does it saves the object before adding it to the collection or waits for the owner to get saved and then saves the unsaved objects to the DB? This is one of the questions that are gonna get answered in the following post.
Active Record’s << or push or concat method – Illustration
So to illustrate what is the approach ActiveRecord takes I will use an example with a User
model that has many reviews
and requires a username (note also that a Review
requires a title
attribute to be present):
-
Unsaved User
-
Inserts both the valid and invalid reviews to the collection with no problem (it even adds the same review object again) and returns the collection:
u = User.new bad_review = Review.new good_review = Review.new(:title => "An awesome review") u.reviews << [good_review, bad_review, bad_review] # => [#<Review id: nil, user_id: nil, title: "An awesome review", created_at: nil, updated_at: nil>, #<Review id: nil, user_id: nil, title: nil, created_at: nil, updated_at: nil>, #<Review id: nil, user_id: nil, title: nil, created_at: nil, updated_at: nil>]
-
When it comes to save the user neither the user is saved or the reviews (until the user is valid)
u.save # (0.1ms) BEGIN # (0.3ms) SELECT 1 FROM `users` WHERE `users`.`username` = BINARY '' LIMIT 1 # (0.1ms) ROLLBACK
-
When we make the user valid then the saving of both the user and the reviews occur, but nothing happens because we have added a
bad_review
in the collection.u.username = "gerry" u.save # (0.1ms) BEGIN # (0.3ms) SELECT 1 FROM `users` WHERE `users`.`username` = BINARY 'gerry' LIMIT 1 # (0.1ms) ROLLBACK
-
If we now make the
bad_review
good (by adding a title) everything works as expected. The user is saved and both thegood_review
and thebad_review
are savedbad_review.title = "A super awesome title" u.save # (0.2ms) BEGIN # (0.6ms) SELECT 1 FROM `users` WHERE `users`.`username` = BINARY 'gerry' LIMIT 1 # sql insert for user # (0.4ms) INSERT INTO `reviews` (`created_at`, `title`, `updated_at`, `user_id`) VALUES ('2011-08-04 09:28:36', 'An awesome review', '2011-08-04 09:28:36', 8) # (0.4ms) INSERT INTO `reviews` (`created_at`, `title`, `updated_at`, `user_id`) VALUES ('2011-08-04 09:28:36', 'A super awesome title', '2011-08-04 09:28:36', 8) # (61.2ms) COMMIT
-
Note that it didn’t create two different records for the
bad_review
even if we have added it twice in the collection. If we call nowu.reviews
we will see three reviews in the collection (even if there are really two in the collection)u.reviews # => [#<Review id: 8, user_id: 8, title: "An awesome review", created_at: "2011-08-04 09:28:36", updated_at: "2011-08-04 09:28:36">, #<Review id: 9, user_id: 8, title: "A super awesome title", created_at: "2011-08-04 09:28:36", updated_at: "2011-08-04 09:28:36">, #<Review id: 9, user_id: 8, title: "A super awesome title", created_at: "2011-08-04 09:28:36", updated_at: "2011-08-04 09:28:36">]
-
If we restart the session and call
u.reviews
again we will see that there are two reviews in the collection of courseu = User.first u.reviews # => Review Load (0.3ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`user_id` = 8 # => [#<Review id: 8, user_id: 8, title: "An awesome review", created_at: "2011-08-04 09:28:36", updated_at: "2011-08-04 09:28:36">, #<Review id: 9, user_id: 8, title: "A super awesome title", created_at: "2011-08-04 09:28:36", updated_at: "2011-08-04 09:28:36">]
-
Inserts both the valid and invalid reviews to the collection with no problem (it even adds the same review object again) and returns the collection:
-
Saved User
-
If we create the
bad_review
and thegood_review
as before then when we try to add in the collection of a saved user the saving of the review occurs immediately:u = User.find_by_username("claire") u.reviews << good_review # (0.1ms) BEGIN # (0.2ms) INSERT INTO `reviews` (`created_at`, `title`, `updated_at`, `user_id`) VALUES ('2011-08-04 09:49:46', 'An awesome review', '2011-08-04 09:49:46', 3) # (158.3ms) COMMIT # => returns the collection u.reviews << bad_review # (0.1ms) BEGIN # (0.0ms) COMMIT # => false <--- returns false
-
Note that in the case of the
bad_review
it returnedfalse
and not the collection. So in the case of a validation error no chaining can happen (i.e. no successive << methods can apply, which is expected).
-
If we create the
Very userful. Thanks
That’s really weird, why would AR automatically save models appended to the association ? One would obviously expect that the save method must called, even if the parent already exists. We can prevent this by calling @user.reviews.build(good_params), but this would be a problem in a context where the association have an hierarchy, for example: if a Hunter has_many :animals, and an Animal can be Dog or Cat, we can’t do @hunter.dogs.build or @hunter.cats.build, intead we are stuck with @hunter.animals << Cat.new and if the Cat class has no validations, the object will automatically saved. How can I prevent this behaviour ?