self join how to reference the association from parent - ruby-on-rails

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.

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.

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

Act as list with self-referential association

I have a self-referential association for categories table:
class Category < ApplicationRecord
has_many :subcategories,
class_name: 'Category',
foreign_key: 'parent_id',
dependent: :destroy,
inverse_of: :parent
belongs_to :parent,
class_name: 'Category',
optional: true,
inverse_of: :subcategories
end
I wanted to add act_as_list gem to manage position of elements for main categories and subcategories in scope of main category. As I read documentation it seems that there is no possibility for such scenario. Is there any workaround?

Making self-referencing models

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.

Rails scope checking for no associations

I have a Category model where a category may have some subcategories (category.categories). I want a scope that gives me all Categorys that have no subcategories.
In other words, I can write
without_subcategories = Category.select{|category| category.categories.none?}
but I would like to write this as a scope. How do I do this?
In case it's not clear, this is my model:
class Category < ActiveRecord::Base
belongs_to :parent, class_name: 'Category'
has_many :categories, foreign_key: :parent_id, class_name: 'Category'
scope :without_subcategories, lambda { WHAT GOES IN HERE? }
end
the best practice is minimize db queries by implementing a counter cache.
In rails this is super simple by adding an option :counter_cache => true to the belongs_to association. This assumes you create a 'categories_count' integer column in your categories db table. Given this, your scope is trivial.
class Category < ActiveRecord::Base
belongs_to :parent, class_name: 'Category', :counter_cache => true
has_many :categories, foreign_key: :parent_id, class_name: 'Category'
scope :without_subcategories, where(categories_count: 0)
end
hope this helped.

Resources