Commit 866f37d8 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'sh-fix-epics-api-nplus-one' into 'master'

Eliminate N+1 queries in Epics API

Closes #10243

See merge request gitlab-org/gitlab-ee!9897
parents 489b1697 e71b7ff2
...@@ -77,6 +77,8 @@ module EE ...@@ -77,6 +77,8 @@ module EE
reorder('relative_position ASC', 'id DESC') reorder('relative_position ASC', 'id DESC')
end end
scope :with_api_entity_associations, -> { preload(:author, :labels, :group) }
def etag_caching_enabled? def etag_caching_enabled?
true true
end end
......
---
title: Eliminate N+1 queries in Epics API
merge_request: 9897
author:
type: performance
...@@ -33,9 +33,10 @@ module API ...@@ -33,9 +33,10 @@ module API
use :pagination use :pagination
end end
get ':id/(-/)epics' do get ':id/(-/)epics' do
epics = paginate(find_epics(finder_params: { group_id: user_group.id })) epics = paginate(find_epics(finder_params: { group_id: user_group.id })).with_api_entity_associations
present epics, with: EE::API::Entities::Epic, user: current_user, epics_metadata: issuable_meta_data(epics, 'Epic') # issuable_metadata is the standard used by the Todo API
present epics, with: EE::API::Entities::Epic, user: current_user, issuable_metadata: issuable_meta_data(epics, 'Epic')
end end
desc 'Get details of an epic' do desc 'Get details of an epic' do
......
...@@ -205,17 +205,17 @@ module EE ...@@ -205,17 +205,17 @@ module EE
epic.labels.map(&:title).sort epic.labels.map(&:title).sort
end end
expose :upvotes do |epic, options| expose :upvotes do |epic, options|
if options[:epics_metadata] if options[:issuable_metadata]
# Avoids an N+1 query when metadata is included # Avoids an N+1 query when metadata is included
options[:epics_metadata][epic.id].upvotes options[:issuable_metadata][epic.id].upvotes
else else
epic.upvotes epic.upvotes
end end
end end
expose :downvotes do |epic, options| expose :downvotes do |epic, options|
if options[:epics_metadata] if options[:issuable_metadata]
# Avoids an N+1 query when metadata is included # Avoids an N+1 query when metadata is included
options[:epics_metadata][epic.id].downvotes options[:issuable_metadata][epic.id].downvotes
else else
epic.downvotes epic.downvotes
end end
......
...@@ -4,7 +4,8 @@ describe API::Epics do ...@@ -4,7 +4,8 @@ describe API::Epics do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) } let(:project) { create(:project, :public, group: group) }
let(:epic) { create(:epic, group: group) } let(:label) { create(:label) }
let(:epic) { create(:labeled_epic, group: group, labels: [label]) }
let(:params) { nil } let(:params) { nil }
shared_examples 'error requests' do shared_examples 'error requests' do
...@@ -85,6 +86,22 @@ describe API::Epics do ...@@ -85,6 +86,22 @@ describe API::Epics do
it 'matches the response schema' do it 'matches the response schema' do
expect(response).to match_response_schema('public_api/v4/epics', dir: 'ee') expect(response).to match_response_schema('public_api/v4/epics', dir: 'ee')
end end
it 'avoids N+1 queries', :request_store do
epic
# Avoid polluting queries with inserts for personal access token
pat = create(:personal_access_token, user: user)
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api(url, personal_access_token: pat)
end
label_2 = create(:label)
create_list(:labeled_epic, 2, group: group, labels: [label_2])
expect { get api(url, personal_access_token: pat) }.not_to exceed_all_query_limit(control)
expect(response).to have_gitlab_http_status(200)
end
end end
context 'with multiple epics' do context 'with multiple epics' do
......
...@@ -5,9 +5,44 @@ describe API::Todos do ...@@ -5,9 +5,44 @@ describe API::Todos do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:epic) { create(:epic, group: group) } let(:epic) { create(:epic, group: group) }
subject { post api("/groups/#{group.id}/epics/#{epic.iid}/todo", user) } describe 'GET /todos' do
let(:author_1) { create(:user) }
let!(:pat) { create(:personal_access_token, user: user) }
before do
group.add_developer(user)
group.add_developer(author_1)
create_todo_for_new_epic
end
def create_todo_for_new_epic
new_group = create(:group)
label = create(:label)
new_epic = create(:labeled_epic, group: new_group, labels: [label])
new_group.add_developer(author_1)
new_group.add_developer(user)
create(:todo, project: nil, group: new_group, author: author_1, user: user, target: new_epic)
end
it 'avoids N+1 queries', :request_store do
create_todo_for_new_epic
get api('/todos', personal_access_token: pat)
control = ActiveRecord::QueryRecorder.new { get api('/todos', personal_access_token: pat) }
create_todo_for_new_epic
expect { get api('/todos', personal_access_token: pat) }.not_to exceed_query_limit(control)
expect(response.status).to eq(200)
end
end
describe 'POST :id/epics/:epic_iid/todo' do describe 'POST :id/epics/:epic_iid/todo' do
subject { post api("/groups/#{group.id}/epics/#{epic.iid}/todo", user) }
context 'when epics feature is disabled' do context 'when epics feature is disabled' do
it 'returns 403 forbidden error' do it 'returns 403 forbidden error' do
subject subject
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment