Home > Back-end >  How to handle multiple belongs_to relations to the same model?
How to handle multiple belongs_to relations to the same model?

Time:06-06

I am trying to build an invitation system for my app and I feel like there's a nice way to do what I need, yet I'm not sure about it.

I have a User model and I created a UserInvite model.

When an existing user sends out a new invite, a UserInvite is created. I am storing when the invite was sent, generate an invite token to make signing up easier and send an email to the invited user.

When the invite is accepted, I would also like to store the user id of the new user on the UserInvite object.

That is, a UserInvite belongs to both inviter and the invitee, and the latter is only being added to the object later on, not at creation, since I don't know the id of the to-be-created User.

What is the right way to model this relationship?

I'm using Rails 7.

CodePudding user response:

You can have something like inviter_id and invitee_id fields in your UserInvite model and then define belongs_to relationships in UserInvite

belongs_to :inviter, foreign_key: inviter_id, class_name: "User"
belongs_to :invitee, foreign_key: invitee_id, class_name: "User", optional: true

You set invitee_id later on when it's available that's why you set invitee to optional.

CodePudding user response:

If you look at Devise::Invitable as a reference then the answer is You Ain't Gonna Need It. It just adds columns to the user model to track invitations:

create_table :users do
  ...
    ## Invitable
    t.string   :invitation_token
    t.datetime :invitation_created_at
    t.datetime :invitation_sent_at
    t.datetime :invitation_accepted_at
    t.integer  :invitation_limit
    t.integer  :invited_by_id
    t.string   :invited_by_type
  ...
end
add_index :users, :invitation_token, unique: true

These columns are used to bypass several validations so that you can create a user record without a password - which is then "claimed" by updating the record with a password.

If you want to do a separate table for invitations then there isn't really a clear need to store the invited users id either unless you actually plan on displaying or using the invitations in some way.

The invitation will simply be used as a token when signing the user up and can then be considered spent and deletable. If you want to record who invited the user I would just add a inviter_id to the users table:

class AddInviterToUsers < ActiveRecord::Migration[7.0]                                                                    
  def change                                                                                                                
    add_reference :users, :inviter, null: true, foreign_key: { to_table: :users }
  end
end    

class User < ApplicationRecord
  belongs_to :inviter, class_name: 'User',
                       optional: true
  has_many :invited_users, 
    class_name: 'User',
    foreign_key: :inviter_id
end

If you really want to go down the path of two columns pointing to the users table then you can do it with something like:

class CreateUserInvitations < ActiveRecord::Migration[7.0]                                                                
  def change                                                                                                                
    create_table :user_invitations do |t|  
      t.string :token                                                                                   
      t.belongs_to :inviter, null: false, foreign_key: { to_table: :users }                                                                  
      t.belongs_to :invitee, null: true, foreign_key: { to_table: :users }                                                                                                                                                                                          
      t.timestamps                                                                                                          
    end                                                                                                                   
  end                                                                                                                   end    

class UserInvitation < ApplicationRecord
  belongs_to :invitee, class_name: 'User',
                       optional: true
  belongs_to :inviter, class_name: 'User'
end

class User < ApplicationRecord
  has_many :user_invitations_as_inviter,
    class_name: 'UserInvitation',
    foreign_key: :inviter_id
  has_many :invited_users,
    through: :user_invitations_as_inviter,
    source: :invitee
  has_many :user_invitations_as_invitee,
    class_name: 'UserInvitation',
    foreign_key: :invitee_id
end

But again - YAGNI. This will add additional complexity and a UPDATE query.

  • Related