Skip to content
This repository was archived by the owner on Jan 27, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/activerecord/delay_touching.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def self.touch_records(attr, klass, records)
end
end

klass.unscoped.where(klass.primary_key => records).update_all(changes)
klass.unscoped.where(klass.primary_key => records.select(&:persisted?)).update_all(changes)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good start. However, since we're skipping the actual update for non-persisted records, it seems we should also skip calling run_callbacks on those records on line 101, right? In the case where these records didn't persist, and thus the ID falls back to null, I think we're also in the same boat on identifying those records in the Set, right? To that end, there's little value in calling state.updated for those records, as they cannot be identified from the Set for removal (same core problem as #14).

Presuming the above is correct, does it make sense to pre-filter records to only persisted records, and reference that updated collection in this method?

# only persisted records need to be updated
records.select!(&:persisted?)

#... remainder of references left as-is, since `records` was pre-filtered
klass.unscoped.where(klass.primary_key => records).update_all(changes)
# ... and so on

The unfortunate thing here is that delay_touching class now has awareness of this AR persistence problem, where we managed to isolate that concern to the state class in the infinite loop problem. I'm not sure there's a way to have State handle this scenario, though.

Copy link
Author

@oehlschl oehlschl Aug 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtuckergd I know you're not thrilled by the performance, but I think one way of doing the filtering upstream is the old implementation: https://github.com/godaddy/activerecord-delay_touching/pull/17/files#diff-c3d5a91eaad03c43d84f142a4473a06dR56

I think that would prevent unpersisted records from ever getting into this touch_records method and keep the persistence logic well encapsulated within the State. FWIW, with that you might even be able to remove the if persisted? (previously unless destroyed?) check here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also thanks for your continued support on this.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is a performance-focused library, I think compromising clean encapsulation to favor performance is probably the right direction, if we can't find a way to preserve both.

The old implementation introduces a 3N iteration overhead (all guaranteed full iterations) of the collection of records, where the final merged implementation adds at most a 1N iteration (possibly less, since there's a shortcut on first persisted record). Even introducing the iteration in the select!, added to the merged solution, leaves us at worst with 2N overhead.

Also notable: the old implementation still introduced state awareness to delay_touching because it had to trigger the state object to remove unpersisted records, so I don't think it saves us much in the leaky abstraction problem.

What do you guys think about the proposal above, adding the records.select!(&:persisted?) call? Open to other suggestions, as well.
@oehlschl ☝️

end
state.updated attr, records
records.each { |record| record.run_callbacks(:touch) }
Expand Down
18 changes: 18 additions & 0 deletions spec/activerecord/delay_touching_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,24 @@

end

it 'does not attempt to update is NULL primary key' do
bad_update = /UPDATE "tags" SET "updated_at" = .* WHERE "tags"\."id" IS NULL/
expect(ActiveRecord::Base.connection).to receive(:update).and_wrap_original do |m, *args|
arel_stmt = args.first
expect(arel_stmt.to_sql).to_not match(bad_update)
m.call(*args)
end

ActiveRecord::Base.delay_touching do
ActiveRecord::Base.transaction do
# touch any record through a custom relationship table
# NOTE: does not happen with a has_and_belongs_to_many relationship
post = Post.create!
post.tags.create!
raise ActiveRecord::Rollback
end
end
end
end

def expect_updates(tables)
Expand Down
13 changes: 13 additions & 0 deletions spec/support/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ class Pet < ActiveRecord::Base

class Post < ActiveRecord::Base
has_many :comments, dependent: :destroy

has_many :tag_relationships, dependent: :destroy
has_many :tags, through: :tag_relationships
end

class User < ActiveRecord::Base
Expand All @@ -18,3 +21,13 @@ class Comment < ActiveRecord::Base
belongs_to :post, touch: true
belongs_to :user, touch: true
end

class Tag < ActiveRecord::Base
has_many :tag_relationships, dependent: :destroy
has_many :posts, through: :tag_relationships
end

class TagRelationship < ActiveRecord::Base
belongs_to :tag, touch: true
belongs_to :post
end
8 changes: 8 additions & 0 deletions spec/support/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@
t.timestamps null: false
end

create_table :tags, force: true do |t|
t.timestamps null: false
end

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