Active Record’s << or push or concat method

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):

  1. 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 the good_review and the bad_review are saved

      bad_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 now u.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 course

      u = 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">]
      
  2. Saved User

    • If we create the bad_review and the good_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 returned false 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).

2 Comments Active Record’s << or push or concat method

  1. Gustavo

    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 ?

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>