How to make a has_one through has_many association? - ruby-on-rails

Context:
I have 2 models ClinicalSystem and TemplateMessage
My goal: is to have ClinicalSystem with many TemplateMessage but also have multiple has_one realtion with TemplateMessage with different namings. e.g:
clinical_system.tempalte_messages, clinical_system.reschedule_template_message, clinical_system.cancellation_template_message.
How do I structure my associations to achieve that ? I have tried has_many: through but it failed.

You can define the main has_many association and the has_one associations separately like below:
class ClinicalSystem
has_many :tempalte_messages
has_one :reschedule_template_message, -> { CONDITION_FOR_RESCHEDULE }, class_name: TemplateMessage.name
has_one :cancellation_template_message, -> { CONDITION_FOR_CANCELLATION }, class_name: TemplateMessage.name
end

You can achieve that using foreign_key and class_name
class ClinicalSystem
has_many :tempalte_messages
belongs_to :reschedule_template_message, foreign_key: 'reschedule_template_message_id', class_name: 'TemplateMessage'
belongs_to :cancellation_template_message, foreign_key: 'cancellation_template_message_id', class_name: 'TemplateMessage'
end
You will also need to add migration to add foreign_keys reschedule_template_message_id and cancellation_template_message_id in the clinical_systems table

Related

Rails referral model with self referential association

I'm trying to create a Referral program on a Rails app and I struggle with the relationships.
My Referral model is pretty simple : godfather_id, godson_id, state
Both godfather and godson ids references an User, which can have many godsons but only one godfather.
class Referral < ApplicationRecord
belongs_to :user
belongs_to :godson, class_name: 'User'
end
The issue comes in my User model. I wan't to be able to do user.godsons to get an array of godsons Users and user.godfather to get the godfather User.
I tried a few things and I think those two where the closest to what I need to do (User model simplified for the example).
class User < ApplicationRecord
has_many :referrals
has_many :godson, -> { where(godfather_id: id) }, through: :referrals
has_one :godfather, -> { where(godson_id: id) }, through: :referrals
end
class User < ApplicationRecord
has_many :godson_relations, class_name: 'Referral', foreign_key: 'godson_id'
has_many :godsons, through: :godson_relations
has_one :godfather_relation, class_name: 'Referral', foreign_key: 'godfather_id'
has_one :godfather, through: :godfather_relations
end
I'm really unsure about how to materialize this relationship, any help will be appreciated 🙏
To make an actual self-referential assocation you would just add a column on the users table that points back to the same table:
class AddGodfatherToUsers < ActiveRecord::Migration[6.1]
def change
add_reference :users, :godfather, null: true,
foreign_key: { to_table: :users }
end end
class User
belongs_to :god_father,
class_name: 'User',
optional: true,
inverse_of: :god_children
has_many :god_children,
class_name: 'User',
foreign_key: :god_father_id
inverse_of: :god_father
end
If you must store Referalls as a separate table you were kind of on the right track but you got the foreign keys backwards:
class Referral < ApplicationRecord
# you better be explicit here or its going to get extremely confusing
belongs_to :godfather, class_name: 'User'
belongs_to :godson, class_name: 'User'
end
class User < ApplicationRecord
has_many :referrals_as_godfather,
class_name: 'Referral', foreign_key: 'godfather_id'
has_one :referral_as_godson,
class_name: 'Referral',
foreign_key: 'godfather_id'
has_many :godsons, through: :referrals_as_godfather
has_one :godfather, through: :referral_as_godson
end
It should be noted that has_one in no way guarentees that a user can have only one referral (and thus one godfather). It just adds a LIMIT 1 to the query. You would have to enforce that with a uniqueness constraint and validations.

Rails has_many through a HABTM relationship

So I have three models, User, Team, and Game. Currently constructed as such.
class Team < ApplicationRecord
has_and_belongs_to_many :users
has_many :home_games, class_name: 'Game', foreign_key: 'home_team_id'
has_many :away_games, class_name: 'Game', foreign_key: 'away_team_id'
has_many :wins, class_name: 'Game', foreign_key: 'winner_id'
belongs_to :owner, class_name: 'User'
end
class User < ApplicationRecord
has_and_belongs_to_many :teams
has_many :teams_owned, class_name: 'Team', foreign_key: 'owner_id'
has_many :games, through: :teams
end
class Game < ApplicationRecord
belongs_to :home_team, class_name: "Team"
belongs_to :away_team, class_name: "Team"
belongs_to :winner, class_name: "Team", optional: true
end
I want to add an association between users and games. So I can call User.games and Game.users.
I tried adding this:
#in user model
has_many :games, through: :teams
#in team model
has_many :games, ->(team) { where('home_team_id = ? or away_team_id = ?', team.id, team.id) }, class_name: 'Game'
As the api docs said to do. But, when I try to call this association, I get an error that "game.team_id does not exist". Since each game has a home_team_id and away_team_id, but no team_id.
Did I just implement this extremely poorly? Or am I missing something? Any help appreciated.
I would say this isn't a really good solution.
In ActiveRecord you can't actually define associations where the foreign key can potentially be in two different columns like this:
has_many :games, ->(team) { where('home_team_id = ? or away_team_id = ?', team.id, team.id) }, class_name: 'Game'
It definitely won't work as Rails will still join the assocation as JOIN games ON games.team_id = teams.id. Just adding a WHERE clause to the query won't fix that. Since ActiveRecord actually creates a variety of different queries there is no option to simply provide a different join.
A kludge to make this work would be to add an instance method:
class Game < ApplicationRecord
def users
User.joins(:teams)
.where(teams: { id: home_team.id })
.or(Team.where(id: away_team.id))
end
end
As its not an actual association you cant join through it or use an sort of eager loading to avoid n+1 queries.
If you actually want to create a single association that you can join through you would need to add a join table between games and teams.
class Team < ApplicationRecord
# ...
has_many :game_teams
has_many :games, through: :game_teams
end
# rails g model game_team team:belongs_to game:belongs_to score:integer
class GameTeam < ApplicationRecord
belongs_to :team
belongs_to :game
end
class Game < ApplicationRecord
has_many :game_teams
has_many :teams, through: :game_teams
has_many :users, through: :teams
end
This is a better idea since it gives you a logical place to record the score per team.
As an aside if the composition of teams can change and accurate record keeping is important you might actually need additional join tables as the lineup when a game is played may not actually match the current lineup.

has_many :through with class_name and foreign_key not working

i need to create parental relation i.e child and parent relation within a customer model. For storing information about parent and child, i have created a join table i.e ParentalRelation.
My customer model is:
class Customer < ApplicationRecord
has_many :parental_relations
has_many :children, class_name: 'Customer', foreign_key: 'child_id', through: :parental_relations
has_one :parent, foreign_key: 'parent_id', class_name: 'Customer', through: :parental_relations, source: :parent
end
My parental_relation model is:
class ParentalRelation < ApplicationRecord
belongs_to :parent, class_name: 'Customer'
belongs_to :child, class_name: 'Customer'
end
I am trying to get data by:
Customer.first.children
But i am not getting data. getting like this even when there is data:
Customer::ActiveRecord_Associations_CollectionProxy:0x3fe49a819750
It would be really great help if anybody could help me out. Thank you in advance
if parent_relation has column parent_id and child_id
I believe it should be
class Customer < ApplicationRecord
has_many :children_relations, class_name: 'ParentalRelation', foreign_key: 'parent_id'
has_many :children, class_name: 'Customer', foreign_key: 'parent_id', through: :children_relations, source: :child
has_one :parent_relation, class_name: 'ParentalRelation', foreign_key: 'child_id'
has_one :parent, foreign_key: 'parent_id', class_name: 'Customer', through: :parent_relation, source: :parent
end
according to your relation, Rails will excute sql SELECT "customers".* FROM "customers" INNER JOIN "parental_relations" ON "customers"."id" = "parental_relations"."child_id" WHERE "parental_relations"."customer_id" = $1 LIMIT $2
But I don't know your table struct. So you can read the sql in rails console and find out how Rails find records. It should help you to solve this problem.
Given you have customer has_one :parent in your model, it looks like you are trying to create a one-to-many relationship. If this is correct, you don't need a join table. You only need a join table if you are creating a many-to-many relationship.
To do this as a one-to-many, remove the ParentalRelation model and table and update your customer class to something like this:
class Customer < ApplicationRecord
belongs_to parent, class_name: "Customer"
has_many children, class_name: "Customer", foreign_key: :parent_id
end
Check out the guides here for creating a self joining table:
https://guides.rubyonrails.org/association_basics.html#self-joins
Once you have that, you should be able to do this:
Customer.first.children

Rails has many through with same model

I have one user model and one viewed_contractor model. I am treating user model as customer and contractor. customer can view many contractors by visiting their respective profile.Contractor can be viewed by many customers. I have customer_id and contractor_id in my viewed_contractor. I want to handle this relation as has_many through. Is it possible thorough has_many through?
It is possible. First, you'd need to specify the class_name option for the belongs_to associations in your ViewedContractor model so that they both refer to your User class. Then you could specify the has_many through: relations in your User model.
Something like this should work:
# viewed_contractor.rb
class ViewedContractor < ActiveRecord::Base
belongs_to :contractor, class_name: 'User', foreign_key: :contractor_id
belongs_to :customer, class_name: 'User', foreign_key: :customer_id
end
# user.rb
class User < ActiveRecord::Base
has_many :viewed_contractors_as_contractor, class_name: 'ViewedContractor', foreign_key: :contractor_id
has_many :viewed_contractors_as_customer, class_name: 'ViewedContractor', foreign_key: :customer_id
has_many :visited_contractors, through: :viewed_contractors_as_customer, source: :contractor
has_many :visited_customers, through: :viewed_contractors_as_contractor, source: :customer
end

Foreign Key class_name

So, this is my first time using foreign keys, and though I think they are working properly, I don't understand the class_name portion in the syntax. Is that the class_name that the table being referred to is in?
My code:
Game Model:
belongs_to :user, foreign_key: 'white_player_id', class_name: 'User'
belongs_to :user, foreign_key: 'black_player_id', class_name: 'User'
User Model:
has_many :games, foreign_key: 'white_player_id', class_name: 'Game'
has_many :games, foreign_key: 'black_player_id', class_name: 'Game'
I was looking at: http://ricostacruz.com/cheatsheets/rails-models.html and noitced that in their example they have the class name of both belongs_to and has_many pointing to the Folder class..
belongs_to :parent, :foreign_key => 'parent_id' class_name: 'Folder'
has_many :folders, :foreign_key => 'parent_id', class_name: 'Folder'
So, that leads me to believe that the class name is supposed to point to the class that contains the foreign_key?? A little insight would be much appreciated.
If you define multiple assocations with the same name your just overwriting the same assocation.
belongs_to :white_player, foreign_key: 'white_player_id', class_name: 'User'
belongs_to :black_player, foreign_key: 'black_player_id', class_name: 'User'
class_name is the class of the related object.
foreign_key refers to the table of the model class where you are defining the relationship when defining a belongs_to relationsship.
class Game < ActiveRecord::Base
belongs_to :white_player, foreign_key: 'white_player_id', class_name: 'User'
# foreign_key is game.white_player_id
end
So when we do game.white_player Active Record looks for:
User.find(game.white_player_id)
added:
In your second example foreign_key in has_many refers to the related table.
belongs_to :parent, :foreign_key => 'parent_id' class_name: 'Folder'
has_many :folders, :foreign_key => 'parent_id', class_name: 'Folder'
And you would not need to specify the foreign key and class name explicitly:
class Folder < ActiveRecord::Base
# ActiveRecord will infer that the class name is Folder
has_many :folders, foreign_key: 'parent_id'
# Rails will infer that the foreign_key is parent_id
belongs_to :parent, class_name: 'Folder'
end
As you can see ActiveRecord is one smart cookie and can infer class names and foreign keys.
Here is an easier to explain example of has_many and foreign_keys:
class User < ActiveRecord::Base
has_many :unread_messages, -> { where read: false },
foreign_key: 'recipient_id', # refers to messages.recipient_id
class_name: 'Message'
end
user.unread_messages will query the table message:
SELECT "messages".* FROM "messages" WHERE "messages"."recipient_id" # ...
class_name is for the class which is used in method you use, if ActiveRecord can not decide which class it is. In your example you dont need class_name because your method is user and class that will connect to is User, ActiveRecord can figure that out on its own.
You have another problem. You have two relations with same name user and user, that is not posible.
You could made it like this though:
Game Model:
belongs_to :white_player, foreign_key: 'white_player_id', class_name: 'User'
belongs_to :black_player, foreign_key: 'black_player_id', class_name: 'User'
UserModel:
has_many :white_games, class_name: 'Game'
has_many :black_games, class_name: 'Game'
You might want to create it with a new model, the simpliest way to do it :
Migration - 1/2 : console
rails g model Game white_player:references black_player:references
Migration - 2/2 : db/migrate/create_games.rb
In the migration file, delete the "foreign_key: true" entry, that would look like :
t.references :white_player, foreign_key: true
t.references :black_player, foreign_key: true
Run : rails db:migrate
Model files :
Game Model :
belongs_to :white_player, class_name: 'User'
belongs_to :black_player, class_name: 'User'
User Model :
has_many :white_player_games, class_name: 'Game', foreign_key: 'white_player_id'
has_many :black_player_games, class_name: 'Game', foreign_key: 'white_player_id'
Hope that helps.

Resources