mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 07:20:45 +08:00
FEATURE: allow efficient preloading of custom fields in topic list
This commit is contained in:
@ -45,6 +45,8 @@ module HasCustomFields
|
|||||||
has_many :_custom_fields, dependent: :destroy, :class_name => "#{name}CustomField"
|
has_many :_custom_fields, dependent: :destroy, :class_name => "#{name}CustomField"
|
||||||
after_save :save_custom_fields
|
after_save :save_custom_fields
|
||||||
|
|
||||||
|
attr_accessor :preloaded_custom_fields
|
||||||
|
|
||||||
# To avoid n+1 queries, use this function to retrieve lots of custom fields in one go
|
# To avoid n+1 queries, use this function to retrieve lots of custom fields in one go
|
||||||
# and create a "sideloaded" version for easy querying by id.
|
# and create a "sideloaded" version for easy querying by id.
|
||||||
def self.custom_fields_for_ids(ids, whitelisted_fields)
|
def self.custom_fields_for_ids(ids, whitelisted_fields)
|
||||||
@ -73,6 +75,37 @@ module HasCustomFields
|
|||||||
@custom_field_types[name] = type
|
@custom_field_types[name] = type
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.preload_custom_fields(objects, fields)
|
||||||
|
if objects.present?
|
||||||
|
map = {}
|
||||||
|
|
||||||
|
empty = {}
|
||||||
|
fields.each do |field|
|
||||||
|
empty[field] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
objects.each do |obj|
|
||||||
|
map[obj.id] = obj
|
||||||
|
obj.preloaded_custom_fields = empty.dup
|
||||||
|
end
|
||||||
|
|
||||||
|
fk = (name.underscore << "_id")
|
||||||
|
|
||||||
|
"#{name}CustomField".constantize.where("#{fk} in (?)", map.keys)
|
||||||
|
.pluck(fk, :name, :value).each do |id, name, value|
|
||||||
|
|
||||||
|
preloaded = map[id].preloaded_custom_fields
|
||||||
|
|
||||||
|
if preloaded[name].nil?
|
||||||
|
preloaded.delete(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
HasCustomFields::Helpers.append_field(preloaded, name, value, @custom_field_types)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def reload(options = nil)
|
def reload(options = nil)
|
||||||
@ -80,12 +113,36 @@ module HasCustomFields
|
|||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def custom_field_preloaded?(name)
|
||||||
|
@preloaded_custom_fields && @preloaded_custom_fields.key?(name)
|
||||||
|
end
|
||||||
|
|
||||||
def clear_custom_fields
|
def clear_custom_fields
|
||||||
@custom_fields = nil
|
@custom_fields = nil
|
||||||
@custom_fields_orig = nil
|
@custom_fields_orig = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class PreloadedProxy
|
||||||
|
def initialize(preloaded)
|
||||||
|
@preloaded = preloaded
|
||||||
|
end
|
||||||
|
|
||||||
|
def [](key)
|
||||||
|
if @preloaded.key?(key)
|
||||||
|
@preloaded[key]
|
||||||
|
else
|
||||||
|
# for now you can not mix preload an non preload, it better just to fail
|
||||||
|
raise StandardError, "Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def custom_fields
|
def custom_fields
|
||||||
|
|
||||||
|
if @preloaded_custom_fields
|
||||||
|
return @preloaded_proxy ||= PreloadedProxy.new(@preloaded_custom_fields)
|
||||||
|
end
|
||||||
|
|
||||||
@custom_fields ||= refresh_custom_fields_from_db.dup
|
@custom_fields ||= refresh_custom_fields_from_db.dup
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -3,6 +3,9 @@ require_dependency 'avatar_lookup'
|
|||||||
class TopicList
|
class TopicList
|
||||||
include ActiveModel::Serialization
|
include ActiveModel::Serialization
|
||||||
|
|
||||||
|
cattr_accessor :preloaded_custom_fields
|
||||||
|
self.preloaded_custom_fields = []
|
||||||
|
|
||||||
attr_accessor :more_topics_url,
|
attr_accessor :more_topics_url,
|
||||||
:prev_topics_url,
|
:prev_topics_url,
|
||||||
:draft,
|
:draft,
|
||||||
@ -78,6 +81,10 @@ class TopicList
|
|||||||
ft.topic_list = self
|
ft.topic_list = self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if TopicList.preloaded_custom_fields.present?
|
||||||
|
Topic.preload_custom_fields(@topics, TopicList.preloaded_custom_fields)
|
||||||
|
end
|
||||||
|
|
||||||
@topics
|
@topics
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -320,21 +320,51 @@ describe TopicQuery do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'preload api' do
|
||||||
|
let(:topics) { }
|
||||||
|
|
||||||
|
it "preloads data correctly" do
|
||||||
|
TopicList.preloaded_custom_fields << "tag"
|
||||||
|
TopicList.preloaded_custom_fields << "age"
|
||||||
|
TopicList.preloaded_custom_fields << "foo"
|
||||||
|
|
||||||
|
topic = Fabricate.build(:topic, user: creator, bumped_at: 10.minutes.ago)
|
||||||
|
topic.custom_fields["tag"] = ["a","b","c"]
|
||||||
|
topic.custom_fields["age"] = 22
|
||||||
|
topic.save
|
||||||
|
|
||||||
|
new_topic = topic_query.list_new.topics.first
|
||||||
|
|
||||||
|
expect(new_topic.custom_fields["tag"].sort).to eq(["a","b","c"])
|
||||||
|
expect(new_topic.custom_fields["age"]).to eq("22")
|
||||||
|
|
||||||
|
expect(new_topic.custom_field_preloaded?("tag")).to eq(true)
|
||||||
|
expect(new_topic.custom_field_preloaded?("age")).to eq(true)
|
||||||
|
expect(new_topic.custom_field_preloaded?("foo")).to eq(true)
|
||||||
|
expect(new_topic.custom_field_preloaded?("bar")).to eq(false)
|
||||||
|
|
||||||
|
TopicList.preloaded_custom_fields.clear
|
||||||
|
|
||||||
|
# if we attempt to access non preloaded fields explode
|
||||||
|
expect{new_topic.custom_fields["boom"]}.to raise_error
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with a new topic' do
|
context 'with a new topic' do
|
||||||
let!(:new_topic) { Fabricate(:topic, user: creator, bumped_at: 10.minutes.ago) }
|
let!(:new_topic) { Fabricate(:topic, user: creator, bumped_at: 10.minutes.ago) }
|
||||||
let(:topics) { topic_query.list_new.topics }
|
let(:topics) { topic_query.list_new.topics }
|
||||||
|
|
||||||
|
|
||||||
it "contains the new topic" do
|
|
||||||
expect(topics).to eq([new_topic])
|
|
||||||
end
|
|
||||||
|
|
||||||
it "contains no new topics for a user that has missed the window" do
|
it "contains no new topics for a user that has missed the window" do
|
||||||
|
|
||||||
|
expect(topic_query.list_new.topics).to eq([new_topic])
|
||||||
|
|
||||||
user.new_topic_duration_minutes = 5
|
user.new_topic_duration_minutes = 5
|
||||||
user.save
|
user.save
|
||||||
new_topic.created_at = 10.minutes.ago
|
new_topic.created_at = 10.minutes.ago
|
||||||
new_topic.save
|
new_topic.save
|
||||||
expect(topics).to eq([])
|
expect(topic_query.list_new.topics).to eq([])
|
||||||
end
|
end
|
||||||
|
|
||||||
context "muted topics" do
|
context "muted topics" do
|
||||||
|
Reference in New Issue
Block a user