has_many :through, nested polymorphic relations - ruby-on-rails

Is there a way to directly reference (using rails directly, without resorting to a lot of custom SQL) a relation that is nested behind a polymorphic relation? In the below example, is there a way to define a has_many relation in User that references LayerTwo?
I'd like to do (in User)
has_many :layer_twos, :through => layer_ones
but this approach doesn't take into the account of the previously specified has_many relations through the polymorphic relation. Any suggestions? It may not be possibly through the existing rails conventions, but figured I'd defer the question to people smarter then myself.
class CreateOwners < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.timestamps
end
create_table :owners do |t|
t.timestamps
t.references :owned, :polymorphic => :true
t.references :user
end
create_table :layer_ones do |t|
end
create_table :layer_twos do |t|
t.references :layer_one
end
end
end
class Owner < ActiveRecord::Base
belongs_to :user
belongs_to :owned, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :owners
has_many :layer_ones, :through => :owners, :source => :owned, :source_type => 'LayerOne'
end
class LayerOne < ActiveRecord::Base
has_many :owners, :as => :owned
has_many :layer_twos
end
class LayerTwo < ActiveRecord::Base
belongs_to :LayerOne
end

It should be noted now that Rails 3.1 has nested has_many :through associations built in. an ASCIIcast on this

As far as I know, ActiveRecord does not support :through a :through relationship. You can work around this using some tricks and hacks, such as creating a VIEW which remaps the relationship into a more direct one, which can simplify your ActiveRecord model at the expense of database complexity.
Polymorphic associations are particularly ornery.

I'm not sure of its support for nesting through polymorphic asociations but it might be worth checking out the nested_has_many_through plugin, which from the README:
…makes it possible to define
has_many :through relationships that
go through other has_many :through
relationships, possibly through an
arbitrarily deep hierarchy. This
allows associations across any number
of tables to be constructed, without
having to resort to find_by_sql (which
isn't a suitable solution if you need
to do eager loading through :include
as well).

Try this (Rails 3):
class LayerOne < ActiveRecord::Base
class << self
def layer_twos
LayerTwo.where(:layer_one_id => all.map(&:id))
end
end
end

Related

How can I use two records from the same Rails model as foreign keys in a different Rails model?

I have two models: person.rb and relationship.rb
I need my :relationships table to reference two rows from the :people table as foreign keys.
Here are the migrations for both tables:
class CreatePeople < ActiveRecord::Migration[5.1]
def change
create_table :people do |t|
t.string :first_name
t.string :second_name
t.integer :age
t.string :gender
t.timestamps
end
end
end
class CreateRelationships < ActiveRecord::Migration[5.1]
def change
create_table :relationships do |t|
t.references :person_a
t.references :person_b
t.string :status
t.timestamps
end
end
end
The idea is the :person_a and :person_b fields will both be individual records from the :people table referenced as foreign keys, while the :status field will just be a description of their relationship ("Married", "Friends", "Separated", etc.)
I'm trying to find out:
1) What is the additional code I have to write in the CreateRelationships migration above in order to set :person_a and :person_b up as foreign keys from the :people table?
2) What code do I need to write in the model files (person.rb and relationship.rb) for both tables below to define the relationship structure I'm talking about?
class Person < ApplicationRecord
end
class Relationship < ApplicationRecord
end
I've found one other question on here that deals with this issue, but the answers given were conflicting, some incomplete, and others working with older versions of Rails. None of them have worked for me.
I'm using Rails 5.1.4
You have defined you migration correctly just add the following in your model to define the relationship between the model.
class Person < ApplicationRecord::Base
has_many :relationships, dependent: :destroy
end
class Relationship < ApplicationRecord::Base
belongs_to :person_a, :class_name => 'Person'
belongs_to :person_b, :class_name => 'Person'
end
This allows you to access the Person that a Relationship belongs to like this:
#relationship.person_a #user assigned as the first person.
#relationship.person_b #user assigned as the second person.
Hope this works.
EDIT: Apologies for a rushed and wrong answer.
Initially, I thought that simple has_many/belongs_to association is possible and sufficient.
class Person < ApplicationRecord
has_many :relationships #dependent: :destroy
end
class Relationship < ApplicationRecord
belongs_to :person_a, class_name: "Person"
belongs_to :person_b, class_name: "Person"
enum status: [:married, :friends, :separated]
end
As #engineersmnky pointed out, has_many association can't work here because there is no person_id column in relationships table. Since we can declare only one custom foreign key in has_many association, it's not possible to declare it here this way. belongs_to will work, but I don't think that's enough.
One way is to skip declaring has_many and stick to custom method for querying relationships:
class Person < ApplicationRecord
def relationships
Relationship.where("person_a_id = ? OR person_b_id = ?", id, id)
end
end
It will give you an ActiveRecord::Relation to work with, containing exactly the records you need. The drawbacks of this solution are numerous - depending on your needs, you will probably need more code for inserting data, starting with a setter method to assign relationships to people...
What could be a real solution, is to have a composite primary key in Relationship model - composed of :person_a_id and :person_b_id. ActiveRecord doesn't support composite primary keys, but this gem seems to fill the gap. Apparently it allows to declare such key and use it as a foreign key in a has_many association. The catch is that your person_a/person_b pairs would have to be unique across relationships table.
I had to do the same for a chat module, this is an example to how you can do it:
class Conversations < ApplicationRecord
belongs_to :sender, foreign_key: :sender_id, class_name: 'User'
belongs_to :recipient, foreign_key: :recipient_id, class_name: 'User'
end
class User < ApplicationRecord
...
has_many :conversations
has_many :senders, through: :conversations, dependent: :destroy
has_many :recipients, through: :conversations, dependent: :destroy
...
end
More explanations at complex-has-many-through
Hope it helps,
You can do like this
In relationship model write
belongs_to : xyz, class_name: "Person", foreign_key: "person_a"
belongs_to : abc, class_name: "Person", foreign_key: "person_b"
In Person model write
has_many :relationships, dependent: :destroy
hope it will help

Model design: Users have friends which are users

I'm looking to make sure my methods are correct before pursuing this association. The implementation sounds too complicated, so I think there must be something wrong with my plan. I am using structured (SQL) data storage, conforming to the rails storage conventions. What I have is a User model, it has an email address, password_digest, and name in the Schema.
class User < ActiveRecord::Base
has_many :posts
end
I'd like to implement a has_many association to a friends collection, so that Users can belong_to Users (as friends). I'm hoping to be able to have User.last.friends.last return a User object when properly built and populated.
I believe I can create a model for this association like:
Class Friend < ActiveRecord::Base
belongs_to :user
belongs_to :friendlies, class: 'User'
end
Class User < ActiveRecord::Base
has_many :posts
has_many :friends
has_many :friendly, class: 'Friend'
end
But I think that will require me to add an as to the models and query using User.last.friends.last.user So what I was thinking is this is a has_and_belongs_to_many relationship. Can I get away with the following (or something similar):
class User < ActiveRecord::Base
has_and_belongs_to_many :friends, class: 'User'
end
I found this:
class User < ActiveRecord::Base
has_many :user_friendships
has_many :friends, through: :user_friendships
class UserFriendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: 'User', foreign_key: 'friend_id'
And this(self proclaimed 'standard'):
has_many :friendships, :dependent => :destroy
has_many :friends, :through => :friendships, :dependent => :destroy
has_many :inverse_friendships, :class_name => "Friendship", :foreign_key => "friend_id", :dependent => :destroy
has_many :inverse_friends, :through => :inverse_friendships, :source => :user, :dependent => :destroy
Which I assume requires a Friendship model. I don't feel like I need a friendship model. I think that the class UserFriendship method looks right, but it requires an additional model. Now onto the questions:
Can I do a has_and_belongs_to_many relationship with a table that relates user to friends which are users, without incurring an additional model to do so?
Is it prudent to have an additional model 'in case' additional requirements pop up later?
What you're looking for is something called a self referential join, which means that you can reference the same model with a different association:
#app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :friendships,
class_name: "User",
join_table: :friendships,
foreign_key: :user_id,
association_foreign_key: :friend_user_id
end
You'd have to create a habtm join table to have a reference:
$ rails g migration CreateFriendshipsTable
#db/migrate/create_friendships_table____.rb
class CreateFriendShipsTable < ActiveRecord::Migration
def change
create_table :friendships, id: false do |t|
t.integer :user_id
t.integer :friend_user_id
end
add_index(:friendships, [:user_id, :friend_user_id], :unique => true)
add_index(:friendships, [:friend_user_id, :user_id], :unique => true)
end
end
$ rake db:migrate
There is a great reference here: Rails: self join scheme with has_and_belongs_to_many?
Is it prudent to have an additional model 'in case' additional requirements pop up later?
Only if you know you're going to have extra attributes. If not, you can get away with the habtm for as long as you want.
You can try this
--friendship.rb
class Friendship < ApplicationRecord
belongs_to :user
belongs_to :friend, :class_name => 'User'
end
--user.rb
class User < ApplicationRecord
has_many :friendships
has_many :friends, through: :friendships
end
--db migrate xxxx_create_friendship.rb
class CreateFriendships < ActiveRecord::Migration[5.0]
def change
create_table :friendships do |t|
t.belongs_to :user
t.belongs_to :friend, class: 'User'
t.timestamps
end
end
end
Rich's answer was an excellent resource. I managed to subvert a few steps by following this convention:
# app/model/user.rb
has_and_belongs_to_many :friends,
class_name: "User",
join_table: :friends_users,
foreign_key: :user_id,
association_foreign_key: :friend_id
This migration was sufficient (without modifying the resulting code):
rails g migration CreateFriendlyJoinTable users friends
The JoinTable syntax was found using rails generate migration -h

HABTM duplicate records

I have a 2 models Game & Theme and they have a has_and_belongs_to_many association. I have tried many solutions to prevent duplicate records in the games_themes table, but no solutions work. The problem is, games_themes is a table, but it is not a model, so I can't figure out a way to run validations on it effectively.
Heres a solution I tried
class Theme < ActiveRecord::Base
has_and_belongs_to_many :games, :uniq => true
end
class Game < ActiveRecord::Base
has_and_belongs_to_many :themes, :uniq => true
end
You should use database-level validation:
#new_migration
add_index :games_themes, [:game_id, :theme_id], :unique => true
HABTM
This will prevent you saving any duplicate data in the database. Takes the burden off Rails & ensures you only have game or theme. The problem is because HABTM doesn't have a model, there's no validation you can perform in Rails, meaning you need to make it db-level
As mentioned in the comments, this means you'll have to handle the exceptions raised from the db like this:
#app/controllers/games_controller.rb
def create
#creation stuff here
if #game.save
#successful save
else
#capture errors
end
end
Use:
validates_uniqueness_of :theme_id, :scope => :game_id
As follows:
class Theme < ActiveRecord::Base
has_many :games, through: :games_themes
end
class Game < ActiveRecord::Base
has_many :themes, through: :games_themes
end
class GamesThemes < ActiveRecord::Base
belongs_to :game
belongs_to :theme
validates_uniqueness_of :theme_id, :scope => :game_id
end
To run validations on join table you should use has_many :through association instead.
http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association
Creating new Model GameTheme for validation purpose is not a good idea. We can validate itself in migration.
Theme Model:
class Theme < ActiveRecord::Base
has_and_belongs_to_many :games,
:association_foreign_key => 'theme_id',
:class_name => 'Theme',
:join_table => 'games_themes'
end
Game Model:
class Theme < ActiveRecord::Base
has_and_belongs_to_many :games,
:association_foreign_key => 'game_id',
:class_name => 'Game',
:join_table => 'games_themes'
end
games_themes migration:
You can add uniqueness to join table, Have a look here for more detail.
class GamesThemesTable < ActiveRecord::Migration
def self.up
create_table :games_themes, :id => false do |t|
t.references :game
t.references :theme
end
add_index :games_themes, [:theme_id, :game_id], :unique => true
end
def self.down
drop_table :games_themes
end
end

How do I share two different associations with the same object?

I have a belongs to and has_many relationship.
A child belongs_to a parent
A parent has_many children.
However, I also have another way a Parent can have a child, and that's through a join_table I use to create a grouping of a type of parent.
Here's my awful guess at how to do this :
# child.rb
belongs_to :parent
belongs_to :parent_group, :dependent => :destroy
delegate :parent, :to => :parent_group
# parent.rb
has_many :children
has_many :children, through: :parent_groups
Note, I don't actually use these naming conventions. These were just changed to keep my work anonymous.
Then my migrations look like this :
class CreateParentGroup < ActiveRecord::Migration
def self.up
create_table :parent_groups do |t|
t.integer :parent_id
t.timestamps
end
add_column :child, :parent_group_id, :integer
end
So my goal is to make it that if I type Parent.find(n).children, it will return Child objects that are either through a parent_group AND any children directly related to it.
Vice versa, if I were to select Child.find(n).parent, it would select its parent whether it was through a parent group or not.
And then finally, I would be able to select parent_groups and select collections of parents.
Any ideas?
First of all, if you're talking about a join table then I think your schema setup for this table isn't quite correct. It should be like this:
class CreateParentGroup < ActiveRecord::Migration
def self.up
create_table :parent_groups do |t|
t.integer :parent_id
t.integer :child_id
t.timestamps
end
end
end
So, now your table parent_groups is able to hold many children for many parents.
As for setting up your associations in your models: You can't/shouldn't name two different association with the same name. Ergo you can't do has_many :children and has_many :children, :through => :parent_groups at the same time in one model. Cause if you access the children by Parent.find(n).children Rails doesn't know which association to use.
I'd do something like this:
class Parent < AR
has_many :regular_children, :class_name => 'Child'
has_many :group_children, :class_name => 'Child', :through => :parent_groups
# implement a method that combines them both
def children
regular_children + group_children
end
end
class Child < AR
belongs_to :parent
belongs_to :parent_group, :dependent => :destroy
# forget the delegate, otherwise your invoke of Child.find(n).parent always
# gets delegated to parent_group which is/can be wrong due to data
end
I think that's more the way to go...

Is there a name to this Ruby on Rails common model pattern? Polylink?

There seems to be no name for this common model pattern.
It's used in many plugins like acts_as_taggable[_whatever] and it basically allows
linking a certain model, like Tag, with any other model without having to put
ever-more belongs_to statements in the Tag model.
It works by having your model (Tag) linked to a polymorphic join model (Tagging)
representing the join table. That creates a self-contained model in which any
other model can relate.
(They relate via a has_many :as & a has_many :through)
I often want to refer to this type of model relationship as one thing.
Maybe call it a "polylink model" or "polylinked model"?
Such as, "Make it a polylink model and relate it to any other models as you code."
Any other suggestions?
Here's the internal workings for acts_as_taggable models:
class Tag < ActiveRecord::Base
has_many :taggings
end
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :taggable, :polymorphic => true
end
class Whatever < ActiveRecord::Base
has_many :taggings, :as => :taggable, :dependent => :destroy
has_many :tags, :through => :taggings
end
class CreateTaggings < ActiveRecord::Migration
def self.up
create_table :taggings do |t|
t.references :tag
t.references :taggable, :polymorphic => true
t.timestamps
end
end
end
In Rails jargon I've seen this usually referred to as plain "has_many :through". With the polymorphism, "polymorphic has_many :through". Taking the Rails jargon out of it, I guess the general pattern could be called a "polymorphic many-to-many relationship".

Resources