Making self-referencing models - ruby-on-rails

All,
I'm still working my way around learning Rails and I'm not having much luck finding the relevant answers I need; that said, I suspect it's something that's been done before since I've done this by hand many times in the past myself.
I have a table called tab_accounts with account_id(int) as PK. I also have a lookup table called mtom_account_relations. This table has two int columns (account_subject, account_associate), both of which are FK to tab_accounts.account_id. The purpose of this layout is to allow for a many-to-many relationship between tab_account entries. The goal is to create an endpoint that returns an account's details along with a list of its associates, also accounts.
At this point I have the following:
models/account_relation.rb:
class AccountRelation < ApplicationRecord
self.table_name = "mtom_account_relations"
belongs_to :subject, foreign_key: "account_id", class_name: "Account"
belongs_to :associate, foreign_key: "account_id", class_name: "Account"
end
models/account.rb
class Account < ApplicationRecord
self.table_name = "tab_accounts"
self.primary_key = "account_id"
...
has_many :account_relations
has_many :associates, :through => :account_relations
has_many :subjects, :through => :account_relations
end
controllers/account_controller.rb
class AccountsController < ApplicationController
...
def associates
_account_id = params[:account_id]
#rs_account = Account
.select("tab_accounts.account_id, tab_accounts.screen_name, tab_accounts.friends, tab_accounts.followers")
.where(:tab_accounts => {account_id: _account_id})
.as_json[0]
#rs_account['associates'] = Account.select("tab_accounts.account_id, tab_accounts.screen_name")
.joins(:subjects)
.where(:tab_accounts => {account_id: _account_id})
.as_json
render json: #rs_account
end
end
config/routes.rb:
Rails.application.routes.draw do
...
get 'accounts/associates/:account_id', :to => "accounts#associates"
end
When I run the method I get the following error:
PG::UndefinedColumn: ERROR: column mtom_account_relations.account_id does not exist LINE 1: ..._accounts" INNER JOIN "mtom_account_relations" ON "mtom_acco... ^ : SELECT tab_accounts.account_id, tab_accounts.screen_name FROM "tab_accounts" INNER JOIN "mtom_account_relations" ON "mtom_account_relations"."account_id" = "tab_accounts"."account_id" INNER JOIN "tab_accounts" "subjects_tab_accounts" ON "subjects_tab_accounts"."account_id" = "mtom_account_relations"."account_id" WHERE "tab_accounts"."account_id" = $1
I suspect the call to the non-existent table "subjects_tab_accounts" is being created from my .joins(:subjects) clause in the controller.
It thinks there's a "mtom_account_relations"."account_id" column.
I'd be grateful for any actionable assistance. Thank you for your attention.
Joe

Consider this example of a family tree. Since Person can be either in the parent_id or child_id column setting up has_many :relationships relation won't work since we don't know which column to use in the join.
Instead we need to setup separate relationships depending on which foreign key on Relationship we joining and then query through this relationship
class Person
has_many :relationships_as_child,
class_name: 'Relationship'
foreign_key: 'child_id'
has_many :relationships_as_parent,
class_name: 'Relationship'
foreign_key: 'parent_id'
has_many :parents,
through: :relationships_as_child,
source: :parent
has_many :children,
through: :relationships_as_child,
source: :child
end
class Relationship
belongs_to :parent, class_name: 'Person'
belongs_to :child, class_name: 'Person'
end
class AccountRelation < ApplicationRecord
self.table_name = "mtom_account_relations"
belongs_to :subject,
foreign_key: "account_id",
class_name: "Account"
belongs_to :associate, foreign_key: "account_id",
class_name: "Account"
end
class Account < ApplicationRecord
self.table_name = "tab_accounts"
self.primary_key = "account_id"
...
has_many :account_relations_as_subject,
class_name: 'AccountRelation',
foreign_key: 'subject_id'
has_many :account_relations_as_associate,
class_name: 'AccountRelation',
foreign_key: 'associate_id'
has_many :associates,
through: :account_relations_as_subject,
source: :associate
has_many :subjects,
through: :account_relations_as_associate,
source: :subject
end
When learning Rails I would really encourage you to learn to love the conventions when it comes to naming tables, primary keys and columns in general. Don't make it harder on yourself.
When it comes to the controller I would set it up like so:
#routes.rb
resources :accounts do
resources :associates, only: [:index]
end
class AssociatesController
# GET /accounts/:account_id/associates
def index
#account = Account.joins(:associates).find(params[:account_id])
#associates = #account.associates
end
end
This models associates as a RESTful resource which is nested under an account and gives you straight forward and conventional way to add more CRUD actions.

Related

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.

self join how to reference the association from parent

I have Category class shown below
class Category < ActiveRecord::Base
has_many :subcategories, class_name: "Category", foreign_key: "parent_category_id"
belongs_to :parent_category, class_name: "Category"
belongs_to :main_category
end
and I wonder if I can define main_category association the rails way that I can
reference the #main_category on subcategories but leaving the main_category_id empty (as the reference on subcategory#main_category_id will duplicate the data which is in parent_category#main_category_id or it is just premature optimization?😅 ).
category = Category.new main_category: main_category
subcategory = Category.new parent_category: category
assert_equal category.main_category, subcategory.main_category
you can create a proxy that subcategories will delegate main_category to parent, the drawback that there're 3 queries to get main_category
class Category < ActiveRecord::Base
has_many :subcategories, class_name: "Category", foreign_key: "parent_category_id"
belongs_to :parent_category, class_name: "Category"
belongs_to :main_category, class_name: "Category"
# with subcategories, there're 3 queries:
# super return nil -> find parent_category -> find main_category of the parent
def main_category
super || parent_category&.main_category
end
end
You can use indirect assocations to setup "short-cuts" through the tree.
class Category < ActiveRecord::Base
# Going up...
has_many :subcategories,
class_name: "Category",
foreign_key: "parent_category_id"
has_many :grand_child_categories,
class_name: "Category",
through: :subcategories
has_many :great_grand_child_categories,
class_name: "Category",
through: :grand_child_categories
# Going down...
belongs_to :parent_category,
class_name: "Category"
has_one :grand_parent_category,
through: :parent_category,
class_name: "Category"
has_one :great_grand_parent_category,
through: :grand_parent_category,
class_name: "Category"
end
However if you have a heirarchy of unlimited depth ActiveRecord::Assocations can't really solve the problem of finding the node at the bottom of the tree. That requires more advanced SQL like a recursive common table expression (CTE).
While ActiveRecord does has the basic tools for creating self joining assocations most of the more andvaced stuff is out of scope and can be handled with gems like Ancestry.

Rails: Bad Associations? [has_many , through] How to test if working?

I am struggling with an issue in my data model. I do have the following models:
class User < ActiveRecord::Base
...
has_many :claims #user-claims
has_many :claims, through: :rulings, as: :commissars
...
end
class Claim < ActiveRecord::Base
...
belongs_to :user
has_many :users, through: :rulings, as: :commissars
...
end
class Ruling < ActiveRecord::Base
belongs_to :user
belongs_to :claim
end
Error:
undefined method `commissars' for #<Claim:0xc5ac090>
Model Explanation:
User can write claims (A claim belongs to one user), and users could do the role of commissars to do the ruling of the claim (max numbers of commissars = 3 per claim).
Is there any way to fix this or improve the relationship?
This domain model requires som pretty complex relations so there is no shame in not getting it on the first try.
Lets start with user and claims:
class User < ActiveRecord::Base
has_many :claims, foreign_key: 'claimant_id',
inverse_of: :claimant
end
class Claim < ActiveRecord::Base
belongs_to :claimant, class_name: 'User',
inverse_of: :claims
end
This is a pretty basic one to many relation with a twist. Since User will have a bunch of relations to Claim we call the relation something other than the default user so that the nature of the relation is defined.
The class_name: 'User' option tells ActiveRecord to load the class User and use it to figure out what table to query and also what class to return the results as. Its needed whenever the class name cannot be directly derived from the name of the association. The option should be a string and not a constant due to the way Rails lazily resolves class dependencies.
Now lets add the commissar role. We will use ruling as the join table:
class Ruling < ActiveRecord::Base
belongs_to :claim
belongs_to :commissioner, class_name: 'User'
end
Notice that here we have a relation to User that we call commissioner for clarity. Now we add the relations to Claim:
class Claim < ActiveRecord::Base
belongs_to :claimant, class_name: 'User',
inverse_of: :claims
has_many :rulings
has_many :commissioners, through: :rulings
end
Then we need to setup the relations on the User side:
class User < ActiveRecord::Base
has_many :claims, foreign_key: 'claimant_id',
inverse_of: :claimant
# rulings as claimant
has_many :rulings, through: :claims
has_many :rulings_as_commissioner, class_name: 'Ruling',
foreign_key: 'commissioner_id'
has_many :claims_as_commissioner, through: :rulings_as_commissioner,
source: :claim
end
Note the source: :claim option where we tell ActiveRecord which party we want from the join table.
Of course for this to work we need to setup the columns and the foreign keys properly. These migrations are to create the tables from scratch but you can easily rewrite them to alter your existing tables:
class CreateClaims < ActiveRecord::Migration
def change
create_table :claims do |t|
t.belongs_to :claimant, index: true, foreign_key: false
t.timestamps null: false
end
# we need to setup the fkey ourself since it is not conventional
add_foreign_key :claims, :users, column: :claimant_id
end
end
class CreateRulings < ActiveRecord::Migration
def change
create_table :rulings do |t|
t.belongs_to :claim, index: true, foreign_key: true
t.belongs_to :commissioner, index: true, foreign_key: false
t.timestamps null: false
end
add_foreign_key :rulings, :users, column: :commissioner_id
add_index :rulings, [:claim_id, :commissioner_id], unique: true
end
end
max numbers of commissars = 3 per claim
This is not really part of the associations rather you would enforce this rule by adding a validation or an association callback.
class Ruling < ActiveRecord::Base
# ...
validate :only_three_rulings_per_claim
private
def only_three_rulings_per_claim
if claim.rulings.size >= 3
errors.add(:claim, "already has the max number of commissars")
end
end
end
See:
Rails Guides: Active Record Migrations
Rails Guides: the has_many though: relations
First, I would suggest you go back and read the Guide carefully as I believe you have fundamentally misunderstood a number of things. The as: option, for instance, does not indicate role but, rather, the presence of a polymorphic join. Also, you can't declare has_many :claims twice on the same model. Anyway, go give it another read.
But, to your question - a functional although somewhat inelegant approach might look like:
class User < ActiveRecord::Base
...
has_many :claims
has_many :claim_commissars, foreign_key: "commissar_id"
has_many :commissar_claims, through: :claim_commissars, class_name: "Claim"
# ^^^^^^^^^^^^^^^^^^^^^
# this bit may be wrong
...
end
class Claim < ActiveRecord::Base
...
belongs_to :user
has_one :ruling
has_many :claim_commissars
has_many :commissars, through: :claim_commissars
...
end
class ClaimCommissar < ActiveRecord::Base
...
belongs_to :claim
belongs_to :commissar, class_name: "User"
...
end
class Ruling < ActiveRecord::Base
...
belongs_to :claim
belongs_to :commissar, class_name: "User"
...
end
You would need to enforce your 'max 3 commissars` in the code.
This is not tested and you will likely need to fiddle with it to get it to go. But, hopefully, it sets you in a better direction.
Good luck!

many to many polymorphic association

I'm not sure how to create this, I'd like to create a many-to-many polymorphic association.
I have a question model, which belongs to a company.
Now the question can has_many users, groups, or company. Depending on how you assign it.
I'd like to be able to assign the question to one / several users, or one / several groups, or the company it belongs to.
How do I go about setting this up?
In this case I would add a Assignment model which acts as an intersection between questions and the entities which are assigned to it.
Create the table
Lets run a generator to create the needed files:
rails g model assignment question:belongs_to assignee_id:integer assignee_type:string
Then let's open up the created migration file (db/migrations/...__create_assignments.rb):
class CreateAssignments < ActiveRecord::Migration
def change
create_table :assignments do |t|
t.integer :assignee_id
t.string :assignee_type
t.belongs_to :question, index: true, foreign_key: true
t.index [:assignee_id, :assignee_type]
t.timestamps null: false
end
end
end
If you're paying attention here you can see that we add a foreign key for question_id but not assignee_id. That's because the database does not know which table assignee_id points to and cannot enforce referential integrity*. We also add a compound index for [:assignee_id, :assignee_type] as they always will be queried together.
Setting up the relationship
class Assignment < ActiveRecord::Base
belongs_to :question
belongs_to :assignee, polymorphic: true
end
The polymorpic: true option tells ActiveRecord to look at the assignee_type column to decide which table to load assignee from.
class User < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
class Group < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
class Company < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
end
Unfortunately one of the caveats of polymorphic relationships is that you cannot eager load the polymorphic assignee relationship. Or declare a has_many :assignees, though: :assignments.
One workaround is:
class Group < ActiveRecord::Base
has_many :assignments, as: :assignee
has_many :questions, through: :assignments
def assignees
assignments.map(&:assignee)
end
end
But this can result in very inefficient SQL queries since each assignee will be loaded in a query!
Instead you can do something like this:
class Question < ActiveRecord::Base
has_many :assignments
# creates a relationship for each assignee type
['Company', 'Group', 'User'].each do |type|
has_many "#{type.downcase}_assignees".to_sym,
through: :assignments,
source: :assignee,
source_type: type
end
def assignees
(company_assignees + group_assignees + user_assignees)
end
end
Which will only cause one query per assignee type which is a big improvement.

In RoR, how do I create TWO one to one relationship between two tables?

In RoR3,
I have Users and Skills and each skill is created by a user. I wanted to record that, so I created a one to many relationship.
class User < ActiveRecord::Base
has_many :skills
end
class Skill < ActiveRecord::Base
belongs_to :user
end
However, each user also has many skills in the sense that, user "Bob" created skill "Kung Fu", user "Charlie" created skill "Karate" and user "Bob" both created and is able to do both "Kung Fu" and "Karate"
How should I represent this with ActiveRecord? Should I just create a new table "user_skills" which has_many :skills? and belong_to :user?
There are two different associations here. The first is a one-to-many association. An user can be the creator of any number of skills. The second one is a many-to-many association, an user can have many skills and a skill can have many users.
The first one is a simple belongs_to <-> has_many declaration. For the second one, you either need a has_and_belongs_to_many declaration in both models, and a related join table, or a dedicated join model, and a has_many :through declaration. Let's try the first one:
Method 1: HABTM
class User < ActiveRecord::Base
has_many :created_skills, :class_name => 'Skill', :inverse_of => :creator
has_and_belongs_to_many :skills
end
class Skill < ActiveRecord::Base
belongs_to :creator, :class_name => 'User', :inverse_of => :created_skills
has_and_belongs_to_many :users
end
This requires a join table called "skills_users" that has columns named user_id and skill_id
Method 2: Has many through (Join model)
The second one is similar, but adds a model that acts as the middleman. This has an added benefit that you can include additional columns in the join model, like for example a skill level.
class User < ActiveRecord::Base
has_many :created_skills, :class_name => 'Skill', :inverse_of => :creator
has_many :user_skills
has_many :skills, :through => :user_skills
end
class Skill < ActiveRecord::Base
belongs_to :creator, :class_name => 'User', :inverse_of => :created_skills
has_many :user_skills
has_many :users, :through => :user_skills
end
class UserSkill < ActiveRecord::Base
belongs_to :user
belongs_to :skill
end
Having those two models
class User < ActiveRecord::Base
has_and_belongs_to_many :skills
end
class Skill < ActiveRecord::Base
has_and_belongs_to_many :users
end
You would have to create an extra migration (without the model)
rails generate migration CreateSkillsUsersJoin
which will give you
class CreateSkillsUsersJoin < ActiveRecord::Migration
def self.up
create_table :skills_users, id => false do |t|
t.references "user"
t.references "skill"
end
add_index :skills_users,["user_id","skill_id"]
end
def self.down
drop_table :skills_users
end
end
The methods self.up and self.down you will have yo add them
You'd be well served using a gem like acts_as_taggable_on which you'd be able to simply setup and use in your User model, something like:
acts_as_taggable_on :skills
Honestly, they've figured all this stuff out, as it's not as simple as what you're trying to do, OR I should rephrase that and say, what you are trying to do is overtly 'complex' and this gem allows you to just keep on, keeping on after it's set up.
Read the Readme.

Resources