Skip to content

Commit 72f69f4

Browse files
authored
Merge pull request #2501 from mroderick/fix/member-attending-cache
feat: cache member attending event IDs to eliminate N+1 queries
2 parents 3db193f + aa8b9d0 commit 72f69f4

File tree

8 files changed

+109
-5
lines changed

8 files changed

+109
-5
lines changed

app/models/invitation.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class Invitation < ApplicationRecord
1313
scope :coaches, -> { where(role: 'Coach') }
1414
scope :verified, -> { where(verified: true).order(:updated_at) }
1515

16+
after_save :clear_member_cache, if: :saved_change_to_attending?
17+
1618
def student_spaces?
1719
for_student? && event.student_spaces?
1820
end
@@ -24,4 +26,10 @@ def coach_spaces?
2426
def to_param
2527
token
2628
end
29+
30+
private
31+
32+
def clear_member_cache
33+
member.clear_attending_event_ids_cache!
34+
end
2735
end

app/models/meeting_invitation.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,13 @@ class MeetingInvitation < ApplicationRecord
1010
scope :accepted, -> { where(attending: true) }
1111
scope :attended, -> { where(attended: true) }
1212

13+
after_save :clear_member_cache, if: :saved_change_to_attending?
14+
1315
alias event meeting
16+
17+
private
18+
19+
def clear_member_cache
20+
member.clear_attending_event_ids_cache!
21+
end
1422
end

app/models/member.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ def past_rsvps
121121
@past_rsvps ||= rsvps(period: :past).reverse
122122
end
123123

124+
def attending_event_ids
125+
@attending_event_ids ||= begin
126+
event_ids = invitations.accepted.pluck(:event_id)
127+
workshop_ids = workshop_invitations.accepted.pluck(:workshop_id)
128+
meeting_ids = meeting_invitations.accepted.pluck(:meeting_id)
129+
(event_ids + workshop_ids + meeting_ids).to_set
130+
end
131+
end
132+
133+
def clear_attending_event_ids_cache!
134+
@attending_event_ids = nil
135+
end
136+
124137
def flag_to_organisers?
125138
multiple_no_shows? && attendance_warnings.last_six_months.length >= 2
126139
end

app/models/workshop_invitation.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class WorkshopInvitation < ApplicationRecord
2525
scope :on_waiting_list, -> { joins(:waiting_list) }
2626
scope :with_notes_and_their_authors, -> { includes(member: { member_notes: :author }) }
2727

28+
after_save :clear_member_cache, if: :saved_change_to_attending?
29+
2830
def waiting_list_position
2931
@waiting_list_position ||= WaitingList.by_workshop(workshop)
3032
.where_role(role)
@@ -47,4 +49,10 @@ def student_attending?
4749
def not_attending?
4850
attending == false
4951
end
52+
53+
private
54+
55+
def clear_member_cache
56+
member.clear_attending_event_ids_cache!
57+
end
5058
end

app/presenters/member_presenter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def newbie?
1212
end
1313

1414
def attending?(event)
15-
event.invitations.accepted.where(member: model).exists?
15+
model.attending_event_ids.include?(event.id)
1616
end
1717

1818
def subscribed_to_newsletter?

spec/models/invitation_spec.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,30 @@
1212
it { is_expected.to validate_inclusion_of(:role).in_array(%w[Student Coach]) }
1313
end
1414

15-
context '#student_spaces?' do
15+
describe '#student_spaces?' do
1616
it 'checks if there are any available spaces for students at the event' do
1717
student_invitation = Fabricate(:invitation)
1818

1919
expect(student_invitation.student_spaces?).to eq(true)
2020
end
2121
end
2222

23-
context '#coach_spaces?' do
23+
describe '#coach_spaces?' do
2424
it 'checks if there are any available spaces for coaches at the event' do
2525
coach_invitation = Fabricate(:coach_invitation)
2626

2727
expect(coach_invitation.coach_spaces?).to eq(true)
2828
end
2929
end
30+
31+
describe 'cache invalidation' do
32+
let(:member) { Fabricate(:member) }
33+
let(:event) { Fabricate(:event) }
34+
35+
it 'clears member cache when attending changes' do
36+
invitation = Fabricate(:invitation, member: member, event: event, attending: false)
37+
expect(member).to receive(:clear_attending_event_ids_cache!)
38+
invitation.update!(attending: true)
39+
end
40+
end
3041
end

spec/models/member_spec.rb

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
outside_deadline = latest_workshops.last.workshop.date_and_time
4545
within_deadline = latest_workshops.fifth.workshop.date_and_time
4646

47-
old_note = Fabricate.create(:member_note, member: member, created_at: outside_deadline)
47+
Fabricate.create(:member_note, member: member, created_at: outside_deadline)
4848
new_note = Fabricate.create(:member_note, member: member, created_at: within_deadline)
4949

5050
expect(member.recent_notes.to_a).to eq([new_note])
@@ -210,4 +210,47 @@
210210
expect(managers.size).to eq(managers.distinct.size)
211211
end
212212
end
213+
214+
describe '#attending_event_ids' do
215+
let(:member) { Fabricate(:member) }
216+
217+
it 'returns event IDs where member has accepted invitation' do
218+
event = Fabricate(:event)
219+
Fabricate(:invitation, member: member, event: event, attending: true)
220+
expect(member.attending_event_ids).to include(event.id)
221+
end
222+
223+
it 'does not include events where invitation is not accepted' do
224+
event = Fabricate(:event)
225+
Fabricate(:invitation, member: member, event: event, attending: false)
226+
expect(member.attending_event_ids).not_to include(event.id)
227+
end
228+
229+
it 'includes workshop IDs' do
230+
workshop = Fabricate(:workshop)
231+
Fabricate(:workshop_invitation, member: member, workshop: workshop, attending: true)
232+
expect(member.attending_event_ids).to include(workshop.id)
233+
end
234+
235+
it 'includes meeting IDs' do
236+
meeting = Fabricate(:meeting)
237+
Fabricate(:meeting_invitation, member: member, meeting: meeting, attending: true)
238+
expect(member.attending_event_ids).to include(meeting.id)
239+
end
240+
241+
it 'caches result in instance variable' do
242+
event = Fabricate(:event)
243+
Fabricate(:invitation, member: member, event: event, attending: true)
244+
first_call = member.attending_event_ids
245+
expect(member.attending_event_ids).to equal(first_call)
246+
end
247+
248+
it 'can be cleared and re-queries on next call' do
249+
event = Fabricate(:event)
250+
Fabricate(:invitation, member: member, event: event, attending: true)
251+
member.attending_event_ids
252+
member.clear_attending_event_ids_cache!
253+
expect(member.attending_event_ids).to include(event.id)
254+
end
255+
end
213256
end

spec/presenters/member_presenter_spec.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
member_presenter.subscribed_to_newsletter?
1515
end
1616

17-
context '#pairing_details_array' do
17+
describe '#pairing_details_array' do
1818
it 'returns student pairing information' do
1919
expect(member_presenter.pairing_details_array('Student', 'Tutorial', 'Note'))
2020
.to eq([member_presenter.newbie?, member.full_name, 'Student', 'Tutorial', 'Note', 'N/A'])
@@ -25,4 +25,17 @@
2525
.to eq([member_presenter.newbie?, member.full_name, 'Coach', 'N/A', 'A note', 'java, ruby'])
2626
end
2727
end
28+
29+
describe '#attending?' do
30+
let(:event) { Fabricate(:event) }
31+
32+
it 'returns true when member is attending event' do
33+
Fabricate(:invitation, member: member, event: event, attending: true)
34+
expect(member_presenter.attending?(event)).to be true
35+
end
36+
37+
it 'returns false when member is not attending' do
38+
expect(member_presenter.attending?(event)).to be false
39+
end
40+
end
2841
end

0 commit comments

Comments
 (0)