Setting up a polymorphic association - ruby-on-rails

I am trying to add a "following" like functionality to my site but I am having trouble finding the right way to use a polymorphic association. A user needs to be able to follow 3 different classes, these 3 classes do not follow the user back. I have created a user following user in the past but this is proving to be more difficult.
My Migration was
class CreateRelationships < ActiveRecord::Migration
def change
create_table :relationships do |t|
t.integer :follower_id
t.integer :relations_id
t.string :relations_type
t.timestamps
end
end
end
My Relationship model is
class Relationship < ActiveRecord::Base
attr_accessible :relations_id
belongs_to :relations, :polymorphic => true
has_many :followers, :class_name => "User"
end
In my User model
has_many :relationships, :foreign_key => "supporter_id", :dependent => :destroy
and in the other 3 models
has_many :relationships, :as => :relations
Am I missing something with setting up this association?

You basically have it right, except for a few minor errors:
attr_accessible :relations_id is redundant. Remove it from your Relationship model.
Both Relationship and User models call has_many to associate with each other. Relationship should call belongs_to because it contains the foreign key.
In your User model, set :foreign_key => "follower_id".
Here is how I would do it.
Have a Follow middle class with polymorphic association on the followable content side and has_many on the follower user side (user has many follows).
First, create a follows table:
class CreateFollows < ActiveRecord::Migration
def change
create_table :follows do |t|
t.integer :follower_id
t.references :followable, :polymorphic => true
t.timestamps
end
end
end
Replace Relationship model with a Follow model:
class Follow < ActiveRecord::Base
belongs_to :followable, :polymorphic => true
belongs_to :followers, :class_name => "User"
end
Include in User model:
has_many :follows, :foreign_key => :follower_id
Include in your three followable classes:
has_many :follows, :as => :followable
You can now do this:
TheContent.follows # => [Follow,...] # Useful for counting "N followers"
User.follows # => [Follow,...]
Follow.follower # => User
Follow.followable # => TheContent

Related

Ruby on Rails: has_and_belongs_to_many connection to inherited subclass

I am trying to connect two classes (conversation and user) by a many-to-many relationship in Ruby on Rails. I set them both up and added a connection table called conversations_custom_users to connect them and it was working. Once we needed our User model to inherit from another User model, setting conversations in a user object was failing and looking for a connection table with the parent class.
My classes and the conversation migration looks like below (I haven't modified the User migration for the many-to-many relationship):
class CustomUser < Spree::User
serialize :resources, Array
has_and_belongs_to_many :conversations, :foreign_key => :conversation_ids, class_name: 'Conversation'
end
class Conversation < ApplicationRecord
has_and_belongs_to_many :receiver, :foreign_key => :receiver_id, class_name: 'CustomUser'
end
class CreateConversations < ActiveRecord::Migration[6.1]
def change
create_table :conversations do |t|
t.timestamps
end
create_table :conversations_custom_users, id: false do |t|
t.belongs_to :conversation, foreign_key: 'conversation_id', index: true
t.belongs_to :custom_user, foreign_key: 'receiver_id', index: true
end
end
end
I think I shouldn't need to add another table called conversations_spree_users, but I also tried adding one. It didn't solve the problem since then Rails was looking for a spree_user_id field. I also tried adding the spree_user_id field to the conversations_spree_users table, but it wouldn't migrate because it was a duplicate column name!
I think I'm missing something about many-to-many relations or inheritance or both in Ruby. If someone can help with this issue I'd really appreciate it.
you could use polymorphic associations to build many-to-many association, the benefit of this approach is that you can use only one join-table for all user's hierarchy inheritance.
class CreateConversationals < ActiveRecord::Migration[6.1]
def change
create_table :conversationals do |t|
# ...
t.references :contributor, polymorphic: true, null: false
t.integer :conversation_id
t.timestamps
end
end
end
class Conversational < ApplicationRecord
belongs_to :contributor, polymorphic: true
belongs_to :conversation
end
class Conversation < ApplicationRecord
has_many :conversationals, :foreign_key => :conversation_id
has_many :custom_users, :through => :conversationals, :source => :contributor, :source_type => 'CustomUser'
has_many :other_users, :through => :conversationals, :source => :contributor, :source_type => 'OtherUser'
end
class CustomUser < Spree::User
has_many :conversationals, as: :contributor
has_many :conversations, :through => :conversationals, :as => :contributor
end
# i assume you use STI
class OtherUser < CustomUser
end
then
user1 = CustomUser.create(...)
user2 = OtherUser.create(...)
conversation = Conversation.create(...)
conversational1 = Conversational.create(..., conversation_id: conversation.id, contributor: user1)
conversation1 = Conversational.create(..., conversation_id: conversation.id, contributor: user2)
# many-to-many
user1.conversations
user2.conversations
conversation.custom_users
conversation.other_users

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

Rails - Model Doubt

Given the fact that I have models like this:
class Person
has_many :owned_groups, :class_name => "Group", :foreign_key => :owner_id
has_many :owned_group_memberships, :through => :owned_groups,
:source => :group_memberships
has_many :group_memberships, :foreign_key => "member_id"
has_many :groups, :through => :group_memberships
end
class GroupMembership
belongs_to :member, :class_name => 'Person'
belongs_to :group
end
class Group
belongs_to :owner, :class_name => "Person"
has_many :group_memberships
has_many :members, :through => :group_memberships
end
How can I access the members a group has? Always I do #group.members or things like that, gives me an error saying that the relation can't be found in the model.
Thanks in advance.
##EDIT##
The error I'm getting is: Could not find the association "group_memberships" in model Group
I do a similar thing on a site I'm working on but the associations are a little different to how you're doing it but maybe it'll help. I think you need to use the has_and_belongs_to_many association to join up your many-to-many's.
In my database I have Users, Members and UsersMembers
You don't need to create a UsersMembers model (GroupMembership in your case) but you do need a database table to link the two.
#Migration
create_table :bands_users, :id => false, :force => true do |t|
t.integer :band_id, :null => false
t.integer :user_id, :null => false
end
#Models
class Band < ActiveRecord::Base
has_and_belongs_to_many :members, :class_name => 'User'
end
class User < ActiveRecord::Base
has_and_belongs_to_many :bands
end
From this I can now call #band.members or #user.bands
You may need to specify :class_name => 'Person' in your has_many :members statement.

Polymorphic association using namespaced classes

I am using Ruby on Rails 3 and I would like to set a polymorphic association using namespaced classes.
Migrations are:
create_table :users_users do |t|
t.integer :id
t.string :full_name
t.references :userable, :polymorphic => true
end
create_table :users_profiles do |t|
t.integer :id
...
end
create_table :users_accounts do |t|
t.integer :id
...
end
Classes are:
class Users::User < ActiveRecord::Base
# Association ...
end
class Users::Profile < ActiveRecord::Base
# Association ...
end
class Users::Account < ActiveRecord::Base
# Association ...
end
How I must write code associations for above classes (using :class_name => "Users:User", ...?) in order to auto-create and auto-destroy associated model records, "mapping" those in the users_users table and viceversa?
Do you have some advice about that? What string values I will have in userable_type attributes (example: 'Users::Profile', 'Profile', ...)?
To setup the associations, you don't need to use class name...
class Users::User < ActiveRecord::Base
belongs_to :userable, :polymorphic => true
end
class Users::Profile < ActiveRecord::Base
has_one :user, :as => :userable, :dependent => :destroy
end
class Users::Account < ActiveRecord::Base
has_one :user, :as => :userable, :dependent => :destroy
end
:dependent => :destroy will deal with deleting them when the Users::User is destroyed, but in terms of creating, you've got the same options as you have with normal relationships. If you're doing it from a form, it's best to use nested attributes.
In the database, the userable_type column would include the namespace. So it'd be 'Users::Account' or 'Users::Profile'.

Resources