I am attempting to work-around a performance issue with a 3rd party library (acts_as_taggable_on). For the one model that uses this extension, it has a N+1 Query problem where it will make one or more extra queries per record to go grab the tags for that record as part of the record initialization process.
I'm doing some large custom queries - and it would both over-complicate and make less efficient the queries if I were to include the tags as part of the query. What I would like to do is to manually "eager load" nil as the tags for each record in this case.
Now, when I perform a custom query on the table for which the model uses the acts_as_taggable_on extension, and I LEFT JOIN the tags table, I can detect in the after_initialize callback that the "tags" attribute exists and manually muck with the association to make it think that the data has already been loaded accordingly to avoid any additional queries.
class MyTaggableModel < ActiveRecord::Base
has_and_belongs_to_many :some_other_model, source: some_other, source_type: 'SomeOtherModel'
acts_as_taggable_on :tags
accepts_nested_attributes_for :tags
...
after_initialize do |model|
attrs = model.attributes.to_h
if attrs.key?('tags')
data = attrs['tags']
records = data&.any? ? data.map { |t| ActsAsTaggableOn::Tag.new(t) } : []
association = model.association(:tags)
association.loaded!
association.target.concat(records)
records.each { |r| association.set_inverse_instance(model) }
model.instance_variable_set(:@tag_list,
ActsAsTaggableOn::TagList.new(records.map(&:name)))
end
end
...
end
The above works like a charm! However, if I perform a custom query on another table that LEFT JOINs the above table, I run into trouble. Namely: I can't seem to pass the attribute 'tags': nil to the model when I new it...
class SomeOtherModel < ActiveRecord::Base
has_and_belongs_to_many :my_taggable_models
...
after_initialize do |model|
attrs = model.attributes.to_h
if attrs.key?('my_taggable_models')
records = attrs['my_taggable_models'].map { |m|
m['tags'] = nil unless m.key?('tags')
MyTaggableModel.new(m)
}
association = model.association(:my_taggable_models)
association.loaded!
association.target.concat(records)
records.each { |r| association.set_inverse_instance(model) }
end
end
...
end
The problem is that even though I'm defaulting the tags attribute to nil above when instantiating new MyTaggableModel instances, rails is behind the scenes overwritting me and stripping out the tags from the attributes - preventing the after_initialization callback within the MyTaggableModel class from doing its thing and marking the tags association as already loaded + assigning the tagList attribute to a new, empty ActsAsTaggableOn::TagList instance.
Apparently in previous versions of rails you could do something like model.assign_attributes({ ... }, :without_protection => true) and you could bypass the mass-assignment security. I have been looking around, but I have not been able to find how to do this in Rails 6. Is there another method for fully de serializing an ActiveRecord object graph or for manually pre/eager loading data? Please advise! Thank you :)
EDIT: I have found a temporary work-around, if I instantiate each MyTaggableModel instance using a block like the one below, then I can eliminate any subsequent tags/taggings queries. However, this is not an ideal solution as then I need to duplicate the logic already present in the MyTaggableModel class in any other model that needs to join to it while repressing the N+1 Queries.
MyTaggableModel.new do |mtm|
mtm.attributes = mtm_attrs
taggings = mtm.association(:taggings)
taggings.loaded!
tag_records = []
tags = mtm.association(:tags)
tags.loaded!
tags.target.concat(tag_records)
mtm.instance_variable_set(:@tag_list,
ActsAsTaggableOn::TagList.new(tag_records.map(&:name)))
end
There is one somewhat plausible solution that comes to mind. there is a
gem 'protected_attributes_continued'which adds the class methodsattr_accessibleandattr_protectedto declare white or black lists of attributes. This might help you to allow mass assignment for some special fields in your db and basically works likewithout_protection, but for limited fields.Hope it helps :)
EDIT
after reading your edit note: you dont need to write this in every model that joins your table, you can write a Concern with that code and then just include it into any model that need that table joined.
And even more - you can write an abstract functionality that can work with any model provided to it, cause it looks like you only need to work with one and the same field named
tag.