I have modelled, User has Attendances in an Event.
class User
has_many :attendances
has_many :events, through: :attendances
class Event
has_many :attendances
scope :is_attending, -> { joins(:attendances).where(attendances:{attend_status: Attendance.attend_statuses[:attending] })}
class Attendance
belongs_to :event
belongs_to :user
enum attend_status: { attending: 0, not_attending: 1}
My Question is about scoped queries and best practice.
I have put most of my scope queries on Event.
I want to get all the events for a specific user where attend_status = 0
user = User.find(...)
user.events.is_attending
Logically I would think, this reads the best and makes most sense
However that would give me a double INNER JOIN
SELECT "events".* FROM "events"
INNER JOIN "attendances" "attendances_events" ON "attendances_events"."event_id" = "events"."id"
INNER JOIN "attendances" ON "events"."id" = "attendances"."event_id"
WHERE "attendances"."user_id" = $1 AND "attendances"."attend_status" = 0
Obviously this creates duplicates which isn't what I wanted.
So options I know I can do
1) USE MERGE
Event
scope :for_user, -> (user){ joins(:attendances).where(attendances: {user: user})}
then call
Event.for_user(user).merge(Event.is_attending)
which gives me the sql
SELECT "events".* FROM "events" INNER JOIN "attendances" ON "attendances"."event_id" = "events"."id" WHERE "attendances"."user_id" = 59 AND "attendances"."attend_status" = 0
This is what I want. But this seems terrible syntax and is confusing.
2) USE INCLUDES
If I use includes instead of join, I don't get duplicate join. As it loads events separately and is smart enough to not duplicate.
Event
scope :is_attending, -> { includes(:attendances).where(attendances: {attend_status: Attendance.attend_statuses[:attending] })}
However I don't want to eager load.
3) ASSUME Table is already joined outside of scope
Finally I can assume that the table is already joined outside of calling the scope,
Event
scope :is_attending, -> { where(attendances: {attend_status: Attendance.attend_statuses[:attending] })}
But this seems kinda silly design to me, and makes this named scope less re-usable.
So my questions
1) What is the best approach to this?
The most logical
user.events.is_attending
is the one I ideally want to use.
2) Is there a way to tell Active Record to ignore joins if they have already happened?
You may add an association with is_attending
condition to User
model:
class Attendance < ActiveRecord::Base
belongs_to :event
belongs_to :user
enum attend_status: { attending: 0, not_attending: 1}
scope :is_attending, -> { where(attend_status: attend_statuses[:attending]) }
end
class User < ActiveRecord::Base
has_many :attendances
has_many :events, through: :attendances
has_many :attending_events, -> { Attendance.is_attending }, through: :attendances, source: :event
end
Now you may receive attending events without duplication of join:
u.attending_events
SELECT "events".* FROM "events"
INNER JOIN "attendances" ON "events"."id" = "attendances"."event_id"
WHERE "attendances"."user_id" = 1 AND "attendances"."attend_status" = 0 [["user_id", 1], ["attend_status", 0]]
This approach has a drawback: you can’t combine conditions. But it makes sense if you are working with status columns. Because this approach reflects logic of relations.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With