rails: When a parent model has `before/after_commit/rollback` hooks, changing child attributes with `accepts_nested_attributes_for` causes parent to not be touched

Steps to reproduce

Comment out line 39 after_commit { true } to get all tests to pass:

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", github: "rails/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 :users do |t|
    t.timestamps
  end

  create_table :posts do |t|
    t.references :user
    t.string :title
    t.timestamps
  end
end

class User < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts

  # Comment out to get tests to pass:
  after_commit { true }
  # Any of the following cause the same issue:
  # after_rollback { true }
  # before_commit { true }
  # before_rollback { true }
end

class Post < ActiveRecord::Base
  belongs_to :user, :touch => true
end

class BugTest < Minitest::Test

  def setup
    User.destroy_all
    Post.destroy_all

    # create a user that was updated an hour ago
    user = User.create
    user.update_columns :created_at => 1.hour.ago, :updated_at => 1.hour.ago
  end

  def test_creating_post_via_has_many_helper_touches_user
    original_updated_at = User.first.updated_at
    User.first.posts.create! :title => 'Post title'

    refute_equal original_updated_at.to_i, User.first.updated_at.to_i
  end

  def test_creating_post_with_nested_attributes_touches_user
    original_updated_at = User.first.updated_at
    User.first.update! :posts_attributes => { '0' => { :title => 'Post title' } }

    refute_equal original_updated_at.to_i, User.first.updated_at.to_i
  end

end

Expected behavior

Creating/updating/destroying a child that is set to touch the parent should do so via nested attributes whether or not the parent has a before_commit, after_commit, before_rollback or after_rollback hook set.

Actual behavior

Creating a before_commit, after_commit, before_rollback or after_rollback hook causes nested attributes to NOT touch the parent, even though :touch => true is set.

System configuration

Rails version: 5.0.0.1

Ruby version: 2.3.1

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 6
  • Comments: 21 (12 by maintainers)

Commits related to this issue

Most upvoted comments

When working on circumventing the bug by adding after_save callbacks as proposed by @cannikin, the test I was writing turned green when I added the inverse_of option on the has_many association. Manual tests confirmed that adding the option leads to the desired behaviour.

In the example provided, this would translate to

has_many :posts, inverse_of: 'user'

The option seems to ensure the correct instance for the touch.

Just housekeeping. If this is an issue for you, please go ahead and submit a PR. This isn’t an issue for our applications, so of course we are not going to prioritize it.

@eileencodes what do you mean they don’t have a modern reproduction? I assume Minitest still exists? I will be happy to make it more modern for you if you explain what you need. This issue was tagged and assigned 6 years ago but was never fixed or attempted to be fixed. The author of the offending code was tagged but has not responded. I’m quite busy too, believe it or not.

@ulferts nice find! thanks! It’s a much better work around, because after_save callbacks are called multiple times, e.g. in the example above when you update multiple posts through accepts_nested_attributes_for on the same user, the belonging user gets touched multiple times. With inverse_of all posts updates are run, then the belonging user gets touched once.