-2

I have four models. Event, Invitation, Profile and User.

They have the following relations:

  • Events have many Invitations
  • Profiles have many Invitations
  • Profiles belong to Users
  • ...and in turn, Invitations obviously belong to both Events and Profiles

My issue is, when I'm logged in with a user, I have access on the frontend to its id, and based on that, I'd like to query a list of Events so that I know if a user (their profile) has already been invited or they can still request an invitation.

My pseudo-description of what I'd like is the following: Take all the events, if an event has invitations check if one of them belongs to the profile that belongs to my user. If it does, add an applied = true field to that particular event. Return all events, whether applied is true or false

I guess its a nice chain of where and join statements, but I guess I hit a wall with my Ruby knowledge here.

Thank you for your guidance!

zcserei
  • 577
  • 7
  • 30
  • Are you trying to display a list of all events scoped by the user and then display true or false under the applied column? – Moiz Mansur Jul 04 '20 at 16:01

2 Answers2

4

I'll write out the code so the setup is clear.

class Invitation < ApplicationRecord
  belongs_to :event
  belongs_to :profile
end

class Event < ApplicationRecord
  has_many :invitations
end

class Profile < ApplicationRecord
  has_many :invitations
  belongs_to :user
end

class User < ApplicationRecord
  has_many :profiles
end

If you want to know an Event's users, use has_many :through to access an association through another one. First we set up Profile and Events to have each other through their Invitations.

class Event < ApplicationRecord
  has_many :invitations
  has_many :profiles, through: :invitations
end

class Profile < ApplicationRecord
  belongs_to :user
  has_many :invitations
  has_many :events, through: :invitations
end

And now we can set up User to have Invitations through Profiles and Events through Invitations.

class User < ApplicationRecord
  has_many :profiles
  has_many :invitations, through: :profiles
  has_many :events, through: :invitations
end

And we do the same for Event to find its Users through Profiles.

class Event < ApplicationRecord
  has_many :invitations
  has_many :profiles, through: :invitations
  has_many :users, through: :profiles
end

Now events can find their users and users can find their events.

users = event.users
events = users.events

Rails now knows how to join from events to users making your task, and many others, much easier.

What you want to avoid is having to load every Event into memory. You also want to avoid an N+1 query where you query the list of Events, and then query if each Event matches your user. That will get very slow. We want to get all events and check if they've been applied in a single query.

First, we'll add a fake column attribute to the Event called applied. This lets us store some extra information from a query.

class Event < ApplicationRecord
  has_many :invitations
  has_many :profiles, through: :invitations
  has_many :users, through: :profiles

  attribute :applied, :boolean
end

And then write a query which populates it. We can take advantage of the Event -> User relationship we just established to join Event directly with User.

# We can't use bind parameters in select, quoting will have to do.
user_id = ActiveRecord::Base.connection.quote(user_id)

@events = Event
  # A left outer join ensures we get all events, even ones without users.
  .left_outer_joins(:users)
  .select(
    # Grab all of event's columns plus...
    "events.*",

    # Populate applied.
    "(users.id = #{user.id}) as applied"
  )

And now you can efficiently iterate through the events in a single query and without having to load them all into memory.

@events.find_each { |event|
  do_something if event.applied
}

The finishing touch is to package this up as a scope for ease of use.

class Event < ApplicationRecord
  has_many :invitations
  has_many :profiles, through: :invitations
  has_many :users, through: :profiles

  attribute :applied, :boolean

  scope :applied_to_user, ->(user_id) {
    # We can't use bind parameters in select, quoting will have to do.
    user_id = ActiveRecord::Base.connection.quote(user_id)

    left_outer_joins(:users)
      .select(
        # Grab all of event's columns plus...
        "events.*",
    
        # Populate applied.
        "(users.id = #{user.id}) as applied"
      )
  }
end

Event.applied_to_user(user_id).find_each { |event|
  ...
}
Schwern
  • 153,029
  • 25
  • 195
  • 336
  • This is a good solution if OP only wanted to show the events the user is invited to. OP wants to show all events and check if the user is invited to it or not in which case this would just be a subset of those events – Moiz Mansur Jul 04 '20 at 16:28
  • @MoizMansur has_many :through makes that much easier and more efficient. I'll add it. – Schwern Jul 04 '20 at 16:34
1

You want to know if an event has invitations where any of them belongs to the profile of the given user, so define a method on Event that tells you exactly that:

# app/models/event.rb
class Event < ApplicationRecord
  def applied(user_id)
    invitations.any? do |invitation|
      invitation.profile.user_id == user_id
    end
  end
end

# app/controllers/events_controller.rb
class EventsController < ApplicationController
  def index
    @events = Event.all
    @current_user = current_user
  end
end

Then I suppose you want to show the list in a view something like:

# app/views/events/index.erb
<%= render @events %>

# app/views/events/_event.erb
<tr>
  <td> <%= event.name %> </td>
  <td> <%= event.applied(@current_user.id) %>
</tr>

Now there are ways to be more efficient, like eager loading, but this is the basic suggested approach. Does this meet your needs?

Les Nightingill
  • 5,662
  • 1
  • 29
  • 32