Inconsistent collection setter behaviour (has_many / has_many through)

Collection has a different setter logic for persistent and new record.

New record writes changes to the database only after save is triggered, persisted record on the contrary writes changes to the database once ids or array of records assigned via setter.

This inconsistency leads to a different issues, even not related to the inconsistency between new and persisted record.

I just list some of them I already faced with in a production:

  • Validation raises an error. Lets asume we have has_many records, which should create a has_many through associations while passing ids to the parent record via collection_ids. In case when has_many through will have a validation error - this will raise an error, which only could be rescued, even if record is saved with soft *save*, not *save!*.
  • Collection assignment writes changes at the moment of passing data via setter, not while save is invoked. Lets asume I have a model with a lot of different associations. This model feeded via large form with different ids and other fields. When I set a parameters through a *assign_attributes* method (without any transaction block), and save returns false because of the validation failed, collection ids will be changed anyway. One of the solutions would be use transactions and raise an exception with ActiveRecord::Rollback. Other solution would be use update_attributes or update in the latest version of rails. Other one is to use undocumented with_transaction_returning_status**.**
  • Difference between new and persisted record. I believe there are not many developers who work with rails framework know about the difference between new and persisted record with has_many and has_many through records.
  • has_one works differently. Means you can update _id of a has_many or belongs_to via setter and it will not modify anything in the database.

Main case to reproduce an issue:

begin
require “bundler/inline”
rescue LoadError => e
$stderr.puts “Bundler version 1.10 or later is required. Please update your Bundler”
raise e
end

gemfile(true) do
source “https://rubygems.org
gem “rails”, path: “…/projects/rails/”
gem “sqlite3”
end

require “active_record”
require “minitest/autorun”
require “logger”

This connection will do for database-independent bug reports.

ActiveRecord::Base.establish_connection(adapter: “sqlite3”, database: “:memory:”)
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
create_table :posts, force: true do |t|
end

create_table :comments, force: true do |t|
t.integer :post_id
end
end

class Post < ActiveRecord::Base
has_many :comments
end

class Comment < ActiveRecord::Base
belongs_to :post
end

class BugTest < Minitest::Test
def test_association_stuff
comment = Comment.create!
attributes = {comment_ids: [comment.id]}

#new_post = Post.new(attributes)
#assert_equal 1, new_post.comments.size

post = Post.create!
post.assign_attributes(attributes)

assert_equal 1, post.comments.size
assert_equal 0, post.comments.count

post.save!

assert_equal 1, post.comments.size
assert_equal 1, post.comments.count

end
end

``

Please let me know if you would like to see the other cases I’ve listed above.

Solution would be to change behaviour for the autosave has_many records, so the associations of the autosave records will never modify database on data passed to a setter.