Skip to content

Commit 6b2d293

Browse files
authored
PERF: N+1 when assignments status are enabled (#614)
This properly preload assignment statuses on topic lists when they're enabled to prevent N+1. Internal ref - t/142804
1 parent 032b34c commit 6b2d293

File tree

1 file changed

+106
-94
lines changed

1 file changed

+106
-94
lines changed

plugin.rb

Lines changed: 106 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ module ::DiscourseAssign
157157
Site.preloaded_category_custom_fields << "enable_unassigned_filter"
158158
end
159159

160-
BookmarkQuery.on_preload do |bookmarks, bookmark_query|
160+
BookmarkQuery.on_preload do |bookmarks, _bookmark_query|
161161
if SiteSetting.assign_enabled?
162162
topics =
163163
Bookmark
@@ -173,8 +173,10 @@ module ::DiscourseAssign
173173
.index_by(&:topic_id)
174174

175175
topics.each do |topic|
176-
assigned_to = assignments[topic.id]&.assigned_to
177-
topic.preload_assigned_to(assigned_to)
176+
assignment = assignments[topic.id]
177+
# NOTE: preloading to `nil` is necessary to avoid N+1 queries
178+
topic.preload_assigned_to(assignment&.assigned_to)
179+
topic.preload_assignment_status(assignment&.status)
178180
end
179181
end
180182
end
@@ -184,101 +186,102 @@ module ::DiscourseAssign
184186
end
185187

186188
TopicList.on_preload do |topics, topic_list|
187-
if SiteSetting.assign_enabled?
188-
can_assign = topic_list.current_user && topic_list.current_user.can_assign?
189-
allowed_access = SiteSetting.assigns_public || can_assign
190-
191-
if allowed_access && topics.length > 0
192-
assignments =
193-
Assignment.strict_loading.active.where(topic: topics).includes(:target, :assigned_to)
194-
assignments_map = assignments.group_by(&:topic_id)
195-
196-
user_ids =
197-
assignments.filter { |assignment| assignment.assigned_to_user? }.map(&:assigned_to_id)
198-
users_map = User.where(id: user_ids).select(UserLookup.lookup_columns).index_by(&:id)
199-
200-
group_ids =
201-
assignments.filter { |assignment| assignment.assigned_to_group? }.map(&:assigned_to_id)
202-
groups_map = Group.where(id: group_ids).index_by(&:id)
203-
204-
topics.each do |topic|
205-
assignments = assignments_map[topic.id]
206-
direct_assignment =
207-
assignments&.find do |assignment|
208-
assignment.target_type == "Topic" && assignment.target_id == topic.id
209-
end
210-
indirectly_assigned_to = {}
211-
assignments
212-
&.each do |assignment|
213-
next if assignment.target_type == "Topic"
214-
next if !assignment.target
215-
next(
216-
indirectly_assigned_to[assignment.target_id] = {
217-
assigned_to: users_map[assignment.assigned_to_id],
218-
post_number: assignment.target.post_number,
219-
}
220-
) if assignment&.assigned_to_user?
221-
next(
222-
indirectly_assigned_to[assignment.target_id] = {
223-
assigned_to: groups_map[assignment.assigned_to_id],
224-
post_number: assignment.target.post_number,
225-
}
226-
) if assignment&.assigned_to_group?
227-
end
228-
&.compact
229-
&.uniq
230-
231-
assigned_to =
232-
if direct_assignment&.assigned_to_user?
233-
users_map[direct_assignment.assigned_to_id]
234-
elsif direct_assignment&.assigned_to_group?
235-
groups_map[direct_assignment.assigned_to_id]
236-
end
237-
topic.preload_assigned_to(assigned_to)
238-
topic.preload_indirectly_assigned_to(indirectly_assigned_to)
189+
next unless SiteSetting.assign_enabled?
190+
191+
can_assign = topic_list.current_user&.can_assign?
192+
allowed_access = SiteSetting.assigns_public || can_assign
193+
194+
next if !allowed_access || topics.empty?
195+
196+
assignments =
197+
Assignment.strict_loading.active.where(topic: topics).includes(:target, :assigned_to)
198+
assignments_map = assignments.group_by(&:topic_id)
199+
200+
user_ids = assignments.filter(&:assigned_to_user?).map(&:assigned_to_id)
201+
users_map = User.where(id: user_ids).select(UserLookup.lookup_columns).index_by(&:id)
202+
203+
group_ids = assignments.filter(&:assigned_to_group?).map(&:assigned_to_id)
204+
groups_map = Group.where(id: group_ids).index_by(&:id)
205+
206+
topics.each do |topic|
207+
if assignments = assignments_map[topic.id]
208+
topic_assignments, post_assignments = assignments.partition { _1.target_type == "Topic" }
209+
210+
direct_assignment = topic_assignments.find { _1.target_id == topic.id }
211+
212+
indirectly_assigned_to = {}
213+
214+
post_assignments.each do |assignment|
215+
next unless assignment.target
216+
217+
if assignment.assigned_to_user?
218+
indirectly_assigned_to[assignment.target_id] = {
219+
assigned_to: users_map[assignment.assigned_to_id],
220+
post_number: assignment.target.post_number,
221+
}
222+
elsif assignment.assigned_to_group?
223+
indirectly_assigned_to[assignment.target_id] = {
224+
assigned_to: groups_map[assignment.assigned_to_id],
225+
post_number: assignment.target.post_number,
226+
}
227+
end
239228
end
229+
230+
assigned_to =
231+
if direct_assignment&.assigned_to_user?
232+
users_map[direct_assignment.assigned_to_id]
233+
elsif direct_assignment&.assigned_to_group?
234+
groups_map[direct_assignment.assigned_to_id]
235+
end
240236
end
237+
238+
# NOTE: preloading to `nil` is necessary to avoid N+1 queries
239+
topic.preload_assigned_to(assigned_to)
240+
topic.preload_assignment_status(direct_assignment&.status)
241+
topic.preload_indirectly_assigned_to(indirectly_assigned_to)
241242
end
242243
end
243244

244245
Search.on_preload do |results, search|
245-
if SiteSetting.assign_enabled?
246-
can_assign = search.guardian&.can_assign?
247-
allowed_access = SiteSetting.assigns_public || can_assign
248-
249-
if allowed_access && results.posts.length > 0
250-
topics = results.posts.map(&:topic)
251-
assignments =
252-
Assignment
253-
.strict_loading
254-
.where(topic: topics, active: true)
255-
.includes(:assigned_to, :target)
256-
.group_by(&:topic_id)
257-
258-
results.posts.each do |post|
259-
topic_assignments = assignments[post.topic.id]
260-
direct_assignment =
261-
topic_assignments&.find { |assignment| assignment.target_type == "Topic" }
262-
indirect_assignments =
263-
topic_assignments&.select { |assignment| assignment.target_type == "Post" }
264-
265-
post.topic.preload_assigned_to(direct_assignment&.assigned_to)
266-
post.topic.preload_indirectly_assigned_to(nil)
267-
if indirect_assignments.present?
268-
indirect_assignment_map =
269-
indirect_assignments.reduce({}) do |acc, assignment|
270-
if assignment.target
271-
acc[assignment.target_id] = {
272-
assigned_to: assignment.assigned_to,
273-
post_number: assignment.target.post_number,
274-
}
275-
end
276-
acc
277-
end
278-
post.topic.preload_indirectly_assigned_to(indirect_assignment_map)
279-
end
246+
next unless SiteSetting.assign_enabled?
247+
248+
can_assign = search.guardian&.can_assign?
249+
allowed_access = SiteSetting.assigns_public || can_assign
250+
251+
next if !allowed_access || results.posts.empty?
252+
253+
topics = results.posts.map(&:topic)
254+
255+
assignments =
256+
Assignment
257+
.strict_loading
258+
.active
259+
.where(topic: topics)
260+
.includes(:assigned_to, :target)
261+
.group_by(&:topic_id)
262+
263+
results.posts.each do |post|
264+
if topic_assignments = assignments[post.topic_id]
265+
direct_assignment = topic_assignments.find { _1.target_type == "Topic" }
266+
indirect_assignments = topic_assignments.select { _1.target_type == "Post" }
267+
end
268+
269+
if indirect_assignments.present?
270+
indirect_assignment_map = {}
271+
272+
indirect_assignments.each do |assignment|
273+
next unless assignment.target
274+
indirect_assignment_map[assignment.target_id] = {
275+
assigned_to: assignment.assigned_to,
276+
post_number: assignment.target.post_number,
277+
}
280278
end
281279
end
280+
281+
# NOTE: preloading to `nil` is necessary to avoid N+1 queries
282+
post.topic.preload_assigned_to(direct_assignment&.assigned_to)
283+
post.topic.preload_assignment_status(direct_assignment&.status)
284+
post.topic.preload_indirectly_assigned_to(indirect_assignment_map)
282285
end
283286
end
284287

@@ -442,6 +445,11 @@ module ::DiscourseAssign
442445
@assigned_to = assignment.assigned_to if assignment&.active
443446
end
444447

448+
add_to_class(:topic, :assignment_status) do
449+
return @assignment_status if defined?(@assignment_status)
450+
@assignment_status = assignment.status if SiteSetting.enable_assign_status && assignment&.active
451+
end
452+
445453
add_to_class(:topic, :indirectly_assigned_to) do
446454
return @indirectly_assigned_to if defined?(@indirectly_assigned_to)
447455
@indirectly_assigned_to =
@@ -465,6 +473,10 @@ module ::DiscourseAssign
465473

466474
add_to_class(:topic, :preload_assigned_to) { |assigned_to| @assigned_to = assigned_to }
467475

476+
add_to_class(:topic, :preload_assignment_status) do |assignment_status|
477+
@assignment_status = assignment_status
478+
end
479+
468480
add_to_class(:topic, :preload_indirectly_assigned_to) do |indirectly_assigned_to|
469481
@indirectly_assigned_to = indirectly_assigned_to
470482
end
@@ -531,9 +543,9 @@ module ::DiscourseAssign
531543
:assignment_status,
532544
include_condition: -> do
533545
SiteSetting.enable_assign_status && (SiteSetting.assigns_public || scope.can_assign?) &&
534-
object.topic.assignment.present?
546+
object.topic.assignment_status.present?
535547
end,
536-
) { object.topic.assignment.status }
548+
) { object.topic.assignment_status }
537549

538550
# SuggestedTopic serializer
539551
add_to_serializer(
@@ -590,9 +602,9 @@ module ::DiscourseAssign
590602
:assignment_status,
591603
include_condition: -> do
592604
SiteSetting.enable_assign_status && (SiteSetting.assigns_public || scope.can_assign?) &&
593-
object.assignment.present?
605+
object.assignment_status.present?
594606
end,
595-
) { object.assignment.status }
607+
) { object.assignment_status }
596608

597609
# SearchTopicListItem serializer
598610
add_to_serializer(

0 commit comments

Comments
 (0)