One relationship with multiple models [interesting rails relations concept] - ruby-on-rails

I have this app where the model User can have multiple "channels". The channels part of the app has to be easily extendable each with its own model.
I started creating a Channel model with a belongs_to User relationship with the reverse of User has_many Channel.
I was thinking about having an API similar to this:
user = User.create(name: 'John')
user.channels.create(name: 'google', account_id: '1234') # should create a GoogleChannel::Google Model
user.channels.create(name: 'facebook', account_name: 'qwerty') # should create a FacebookChannel::Facebook Model
I am playing around with having a Channel model and each channel model having a dedicated model for each of the channels (google, facebook, etc.) with a has_one relationship to that model.
Update:
I'm using mongoid with rails

I'm not sure if this works. It uses STI.
First approach: Single Table Inheritance
class User << ApplicationRecord
has_many :channels
delegate :google_channels, :facebook_channels, :twitter_channels, to: :channels
end
class Channel << ApplicationRecord
belongs_to :user
self.inheritance_column = :brand
scope :google_channels, -> { where(brand: 'Google') }
scope :facebook_channels, -> { where(brand: 'Facebook') }
scope :twitter_channels, -> { where(brand: 'Twitter') }
def self.brands
%w(Google Facebook Twitter)
end
end
class GoogleChannel << Channel; end
class FacebookChannel << Channel; end
class TwitterChannel << Channel; end
I think you can:
current_user.channels << GoogleChannel.new(name: "First Google Channel")
current_user.channels << Facebook.new(name: "Facebook")
current_user.channels << Twitter.new(name: "Tweety")
current_user.channels << GoogleChannel.new(name: "Second Google Channel")
googs = current_user.google_channels
all = current_user.channels
# etc.
All channels share the same table. If you need different attributes for each different brand, this would not be the best option.
Second approach: Polymorphic models
If you need different tables for each model (brand), you can use a polymorphic approach (not tested):
class User << ApplicationRecord
has_many :channels
has_many :google_channels, through: :channels, source: :branded_channel, source_type: 'GoogleChannel'
has_many :facebook_channels, through: :channels, source: :branded_channel, source_type: 'FacebookChannel'
end
class Channel << ApplicationRecord
belongs_to :user
belongs_to :branded_channel, polymorphic: true
end
#This channel has its own table, and can have more attributes than Channel
class GoogleChannel << ApplicationRecord
has_one :channel, as: :branded_channel
end
#This channel has its own table, and can have more attributes than Channel
class FacebookChannel << ApplicationRecord
has_one :channel, as: :branded_channel
end
goog = GoogleChannel.create(all_google_channel_params)
face = GoogleChannel.create(all_facebook_channel_params)
current_user.channels << Channel.new(branded_channel: goog)
current_user.channels << Channel.new(branded_channel: face)

I assume you want to create STI where User has many channels, so in Rails 5 you can try this:
class User < ApplicationRecord
has_many :user_channels
has_many :channels, through: :user_channels
has_many :facebook_channels, class_name: 'FacebookChannel', through: :user_channels
has_many :google_channels, class_name: 'GoogleChannel', through: :user_channels
end
class UserChannel < ApplicationRecord
# user_id
# channel_id
belongs_to :user
belongs_to :channel
belongs_to :facebook_channel, class_name: 'FacebookChannel', foreign_key: :channel_id, optional: true
belongs_to :google_channel, class_name: 'GoogleChannel', foreign_key: :channel_id, optional: true
end
class Channel < ApplicationRecord
# You need to have "type" column to be created for storing different chanels
has_many :user_channels
has_many :users, through: :user_channels
end
class FacebookChannel < Channel
has_many :user_channels
has_many :users, through: :user_channels
end
class GoogleChannel < Channel
has_many :user_channels
has_many :users, through: :user_channels
end
Now you can do current_user.facebook_channels.create(name: "My Facebook Channel") and get all FacebookChannel with current_user.facebook_channels
Basically it works like regular has_many through relationship with extra feature of STI - you store sub-model name in type column of Channel model.
Update
I'm sorry, I didn't know my suggestion is not working with MongoDB. Maybe you can simply create channel_type column in your channels table, have simple channel belongs_to user relationship and then do:
current_user.channels.create(name: "My Channel Name", channel_type: "facebook")
current_user.channels.where(channel_type: "facebook")
You could do simply current_user.channels which gives you all channels for user and then if you need you can do what ever you need with each record according to channel_type value.
You can even create some scopes in Channel model:
class Channel < ApplicationRecord
scope :facebook_channels, -> { where(channel_type: "facebook") }
end
And then you can do your desired current_user.channels.facebook_channels
channel_type column could be string or integer if you do it with enums.
In addition if you create visit column (e.g. "boolean"), you can do current_user.channels.where(visit: true) or create scope :visit_true, -> { where(visit: true) } and do something like current_user.channels.visit_true
What do you say?

Related

Rails Associations Structure

How do I structure these 3 models with associations?
Issue
I'm having a lot of trouble setting up the model associations for a personal project I'm working on. Essentially I'm building a referee assigning system consisting of 3 models:
Assignor: the user who assigns referees to games
Referee: the user assigned to the game
Game: has 1-4 referees assigned
What I have so far in my models is:
class Assignor < ApplicationRecord
has_many :games
has_many :referees
has_many :assigned_referees, through:referees
end
class Game < ApplicationRecord
belongs_to :assignor
has_many :referees
end
class Referee < ApplicationRecord
has_many :assignors
has_many :games, through: :assignor
has_many :assigned_games, :through:assignor
end
What I'd like to do with these associations within my app is:
List the referees an assignor has => Assignor.referees
List the assignors a referee has => Referee.assignors
Where I'm having trouble is...
List the referees assigned to a game
List the referees NOT assigned to a game
-For Example:
If a user(Assignor) was to pull up a game and assign a referee, I want to make a drop down that populates with a list of referees NOT assigned
I would just go for a 3 way join table:
class Game < ApplicationRecord
has_many :referee_assignments
has_many :referees,
through: :referee_assignments,
inverse_of: :assigned_games
has_many :assigners,
through: :referee_assignments
def build_assignment(referee:, assigner:)
referee_assignments.new(
referee: referee.
assigner: assigner
)
end
end
class RefereeAssignment
# User who assigned the referee
belongs_to :assigner,
class_name: 'User'
belongs_to :game
belongs_to :referee
end
class Referee < ApplicationRecord
has_many :referee_assignments
has_many :assigners,
through: :referee_assignments
has_many :assigned_games,
through: :referee_assignments,
source: :game,
inverse_of: :referees
def build_assignment(game:, assigner:)
referee_assignments.new(
game: game,
assigner: assigner
)
end
end
List the referees assigned to a game
game = Game.find(1)
game.referees.each do |r|
# ...
end
List the referees not assigned to a game
referees = Referee.where.not(
id: game.referees
)
Using a subquery is the simplest possible way but you could also use NOT EXIST or a join.

How to create a group of users (roomates) within one product (property) in Rails

I have a question on a platform I'm developing in Ruby on Rails 5.2.
I have an Owner model which is the owner of properties/property. The owner will post a property so that users (in this case roomates) can share the same property/house/department, etc.
I have Owners and I have Users (both tables are created using devise):
Owner.rb:
class Owner < ApplicationRecord
has_many :properties
end
User.rb:
class User < ApplicationRecord
#Theres nothing here (yet)
end
This is where the magic happens. Property.rb:
class Property < ApplicationRecord
belongs_to :owner
has_many :amenities
has_many :services
accepts_nested_attributes_for :amenities
accepts_nested_attributes_for :services
mount_uploaders :pictures, PropertypictureUploader
validates :amenities, :services, presence: true
scope :latest, -> { order created_at: :desc }
end
How can multiple users share a property? I'm aware that it will have a many-to-many association but I'm a bit confused how to connect these relationships so when the owner posts a property it will display something like:
Property available for: 3 users
And then begin to limit users until it completes the amount of users available.
This sounds like your average many to many assocation:
class User < ApplicationRecord
has_many :tenancies, foreign_key: :tenant_id
has_many :properties, through: :tenancies
end
class Tenancy < ApplicationRecord
belongs_to :tenant, class_name: 'User'
belongs_to :property
end
class Property < ApplicationRecord
has_many :tenancies
has_many :tenants, through: :tenancies
def availablity
# or whatever attribute you have that defines the maximum number
max_tenants - tenancies.count
end
end
You can restrict the number of tenants with a custom validation.
You can use a join table, called users_properties. This table will have a property_id and user_id. You'll then have the following in your properties model:
has_many :users_properties
has_many :users, through: :users_properties
Read more about it here https://guides.rubyonrails.org/association_basics.html

has_many through roles and scopes on the third model

Lets say I have movies, people and movies_people
class Person < ActiveRecord::Base
has_many :movies_people
has_many :movies, through: :movies_people
class Movies < ActiveRecord::Base
has_many :movies_people
has_many :people, through: :movies_people
class MoviesPerson < ActiveRecord::Base
belongs_to :movie
belongs_to :person
end
The table movies_people has a role attribute, where I want to store the person's job in the movie. Right now I can do things like this in the console:
u = User.first
m = Movie.first
m.people << u
then find the right movies_people entry and set 'role'
retrieving looks like this:
m.people.where(movies_people: {role: :actor})
Whats the best way to:
Save the role (to the third table) when joining people to movies?
Return all the actors in a movie vs. all the directors vs. all the writers?
One solution is to create Roles which contains a list of existing roles and a MovieRole
class which joins Movies, People and Roles.
class Movie < ActiveRecord::Base
has_many :movie_roles, class_name: "MovieRole"
has_many :roles, through: :movie_roles
has_many :people, through: :movie_roles
end
class People < ActiveRecord::Base
has_many :movie_roles, class_name: "MovieRole"
has_many :movies, through: :movie_roles
has_many :roles, through: :movie_roles
end
class Role < ActiveRecord::Base
has_many :movie_roles, class_name: "MovieRole"
has_many :people, through: :movie_roles
has_many :movies, through: :movie_roles
end
class MovieRole < ActiveRecord::Base
belongs_to :movie
belongs_to :people
belongs_to :role
end
All the relations are stores in movie_roles which is a three way join table:
class CreateMovieRoles < ActiveRecord::Migration
def change
create_table :movie_roles do |t|
t.references :movie, index: true
t.references :people, index: true
t.references :role, index: true
t.timestamps
end
end
end
Some examples of how you could query this association:
#roles = Movie.find_by(title: 'Godzilla').roles
#directors = People.joins(:roles).where(roles: {name: 'Director'})
#directed_by_eastwood = Movie.joins(:people, :roles)
.merge(Role.where(name: 'Director'))
.merge(People.where(name: 'Clint Eastwood'))
Added:
To associate a person with a movie you would:
MovieRole.create(people: person, movie: movie, role: role)
But you will want to setup convinience methods like:
class People < ActiveRecord::Base
def add_role(role, movie)
role = Role.find_by(name: role) unless role.is_a?(Role)
MovieRole.create(people: self, movie: movie, role: role)
end
end
class Movie < ActiveRecord::Base
def add_cast(role, person)
role = Role.find_by(name: role) unless role.is_a?(Role)
MovieRole.create(people: person, movie: self, role: role)
end
end
To save the role, just:
person = Person.find(person_id)
movie = Movie.find(movie_id)
movie.movies_people.create(person: person, role: :actor)
To retrieve by role,
movie = Movie.includes(:people).where(id: movie_id).where(movies_people: {role: :actor})
Edit: I don't advice to add a roles table, unless you need it. I follow the agile principles, in this case: "Simplicity--the art of maximizing the amount of work not done--is essential."

has_one association with chaining include

We have a Company, CompanyUser, User and Rating model defined like this:
Company model
class Company < ActiveRecord::Base
has_many :company_users
has_many :users, through: :company_users
has_one :company_owner, where(is_owner: true), class_name: 'CompanyUser', foreign_key: :user_id
has_one :owner, through: :company_owner
end
There is an is_owner flag in the company_users table to identify the owner of the company.
CompanyUser model
class CompanyUser < ActiveRecord::Base
belongs_to :company
belongs_to :user
belongs_to :owner, class_name: 'User', foreign_key: :user_id
end
User model
class User < ActiveRecord::Base
has_many :company_users
has_many :companies, through: :company_users
has_many :ratings
end
Rating model
class Rating
belongs_to :user
belongs_to :job
end
I am able to find the owner of a company, by the following code:
#owner = #company.owner
I need to get the ratings and the jobs of the owner along with the owner. I can do this
#owner = #company.owner
#ratings = #owner.ratings.includes(:job)
But we have already used #owner.ratings at many places in the view, and it is difficult to change all the references in the views as it is a pretty big view spanning in several partials. I tried the following to get the ratings along with the owner
#owner = #company.owner.includes(:ratings => :job)
But this gives me error as #company.owner seems to give a User object and it does not seem to support chaining.
Is there a way I can get the included associations (ratings and job) inside the #owner object?
You should be able to do this with:
#owner = Company.where(id: #company.id).includes(owner: {ratings: :job}).owner
However this is not very clean. Much better would be to actually change #company variable:
#company = Company.includes(owner: {ratings: :job}).find(params[:company_id]) # or params id or any other call you're currently using to get the company.
Company built that way will already have everything included, so:
#owner = #company.owner
will pass a model with preloaded associations.

Rails: Linking two models that inherit the same model

This is a little bit tricky, so if you need more information, don't hesitate!
I have two models, Store and Consumer that are linked by two ways:
1/ Store and Consumer inherite from the same model Profile, because they share many attributes (name, location, email, web page,...). Here is the Rails AR code:
class Profile << ActiveRecord::Base
# Attributes and validation rules go here.
end
class Store << Profile
end
class Consumer << Profile
end
This is the well known Single Table Inheritance (STI).
2/ In addition to STI, Store and Consumer are linked by a many to many relation:
Store has many Clients (many consumers)
A consumer is client to many stores
Because I need more attributes for this link (Store - Consumer), I have to create an extra model that will link them: Client.
Here are my final AR models:
class Profile << ActiveRecord::Base
# Attributes and validation rules go here.
end
class Store << Profile
has_many :clients
end
class Consumer << Profile
has_many :clients
end
class Client << ActiveRecord::Base
belongs_to :store
belongs_to :consumer
end
Problem
Using STI doesn't create store_id and consumer_id... we have only profile_id (because one real table Profile). So, how can I target the correct Client row having both store_id and client_id ?
Any idea how to do that? Thanks in advance.
I think you want to do is something like this. Also, I agree with Daniel Wright's comment.
class Profile << ActiveRecord::Base
belongs_to :store
belongs_to :consumer
end
class Store << ActiveRecord::Base
has_one :profile
has_many :clients
has_many :consumers, :through => :clients
end
class Consumer << ActiveRecord::Base
has_one :profile
has_many :clients
has_many :stores, :through => :clients
end
class Client << ActiveRecord::Base
belongs_to :store
belongs_to :consumer
end
But if you'd like to make it work with what you have you could do something like:
class Profile << ActiveRecord::Base
end
class Store << Profile
has_many :clients, :foreign_key => 'store_id'
has_many :consumers, :through => :clients
end
class Consumer << Profile
has_many :clients, :foreign_key => 'consumer_id'
has_many :stores, :through => :clients
end
class Client << ActiveRecord::Base
belongs_to :store
belongs_to :consumer
end

Resources