Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Active Record scope chaining, ignore joins, if already joined

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?

like image 634
dboyd68 Avatar asked Mar 27 '15 05:03

dboyd68


1 Answers

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.

like image 128
Ilya Lavrov Avatar answered Oct 02 '22 17:10

Ilya Lavrov