Creating a many-to-many record on Rails - ruby-on-rails

I have a simple task list app that has users and lists on it, the users are managed by Devise, and can create task lists, as well as favorite lists created by other users, or by themself. The relation of ownership between users and lists were easy to establish, but I am having trouble setting up the relation of a user favoriting a list. I envision it being a many-to-many relation after all, a user can favorite many lists and a list can be favorited by many users, this relationship happening on top of another already existing one-to-many relationship of list ownership by a user gave me some pause as to whether this is good practice to do, but I proceeded with my attempt regardless.
Currently I have two models, one for the user, and one for the list, and I tried to create a migration for the favorites by running rails g migration CreateJoinTableFavorites users lists, which resulted in the following migration
class CreateJoinTableFavorites < ActiveRecord::Migration[5.2]
def change
create_join_table :users, :lists do |t|
t.index [:user_id, :list_id] <-- I uncommented this line
# t.index [:list_id, :user_id]
t.timestamps <-- I added this line
end
end
end
I thought this would create a table named "Favorites" that would automatically link users and lists, but instead it created a table called "lists_users". Now I am stuck as to what to do next. I have read that I need to create a model for this join table, but I don't know how to go about doing that. What command do I run? rails g model Favorites? rails g model ListsUsers? do I also inform the fields I want to add such as rails g model Favorites user_id:integer list_id:integer, or is there another better way to do it such as perhaps rails g model Favorites user:references list:references? What's the best practice here
Beyond that, I have added a button inside my list#show view for the user to click to add that list to their favorites, and had some trouble routing it. What I did was create a button like this:
<%= button_to 'Add to favorites', add_favorites_path({list_id: #list.id}), method: :post %>
as well as a new route:
post 'add_favorites', to: 'lists#add_favorites'
Though this I managed to have access to the list id and user id in that action, now I don't know how to proceed to create the "favorite" database entry in my lists_users table. To illustrate, I'll paste here my "add_favorite" action
def add_favorites
user_id = current_user.id
list_id = params[:list_id]
#TODO: create the relation in lists_items table
end
I'm aware that I can't get this to work without the model for the join table, but even if I had that model, I haven't had much luck searching for what to do within the controller to create that relation. Anyway, my models are as follows:
class List < ApplicationRecord
belongs_to :user
has_many :users, through: :lists_users
end
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :lists
has_many :lists, through: :lists_users
end
So to summarize, I am aware that I am missing a model for the join table, and would like a step-by-step as to how to create it, what name to give it, etc, as well as how to proceed within my action in my controller to create a new favorite entry

There are two ways to create a many-to-many relation in Rails. What you're doing seems to conflate the two, which I suspect is the source of your problem.
Briefly, the two methods are:
1) has_many :other_models, through: :relation or
2) has_and_belongs_to_many :other_models
The main difference being that the "has_many through" method expects the join table to be a separate model which can be handled independently of this relationship if need be, while the "has_and_belongs_to_many" method does not require the join table to have a corresponding model. In the latter case, you will not be able to deal with the join table independently. (This makes timestamps on the join table useless, by the way.)
Which method you should go with depends on your use case. The docs summarize the criteria nicely:
The simplest rule of thumb is that you should set up a has_many :through relationship if you need to work with the relationship model as an independent entity. If you don't need to do anything with the relationship model, it may be simpler to set up a has_and_belongs_to_many relationship (though you'll need to remember to create the joining table in the database). (emphasis added)
Now for your question: When you use create_join_table, you're treating it as though you're setting things up for a has_and_belongs_to_many relation. create_join_table will create a table named "#{table1}_#{table2}" with ids pointing to those tables. It alphabetizes them too, which is why you got "lists_users" instead of "users_lists". This is in fact the standard naming convention for rails join tables if you are planning on using has_and_belongs_to_many, and generally shouldn't be renamed.
If you really want to use has_and_belongs_to_many, keep the migration with the create_join_table and just do the following in your models:
# user.rb
class User
has_and_belongs_to_many :lists
end
# list.rb
class List
has_and_belongs_to_many :users
end
And voila. No Favorite model is needed, and rails is smart enough to handle the relationships through the table on its own. Although a bit easier, the downside is, as stated above, that you won't be able to deal with the join table as an independent model. (Again, timestamps on the join table are useless in this case, as Rails won't set them.)
Edit: Since you can't directly touch lists_users, you'd create relationships by setting the lists relation on a user, or by setting the users relation on lists, like so:
def add_favorites
list = List.find(params[:list_id])
current_user.lists << list # creates the corresponding entry in lists_users
# Don't forget to test how this works when the current_user has already favorited a list!
# If you want to prevent that from happening, try
# current_user.lists << list unless current_user.lists.include?(list)
# Alternatively you can do the assignment in reverse:
# list.users << current_user
# Again, because the join table is not an independent model, Rails won't be able to do much to other columns on lists_users out of the box.
# This includes timestamps
end
On the other hand, if you want to use "has_many through", don't use create_join_table. If you're using has_many through, the join table should be thought of almost as an entirely separate model, that just happens to have two foreign keys and tie two other models together in a many-to-many relationship. In this case, you'd do something like:
# migration
class CreateFavorites < ActiveRecord::Migration[5.2]
def change
create_table :favorites do |t|
t.references :list
t.references :user
t.timestamps
end
end
end
# user.rb
class User
has_many :favorites
has_many :lists, through: :favorites
end
# list.rb
class List
has_many :favorites
has_many :users, through: :favorites
end
# favorite.rb
class Favorite
belongs_to :list
belongs_to :user
end
# controller
def add_favorites
# You actually have a Favorite model in this case, while you don't in the other. The Favorite model can be more or less independent of the List and User, and can be given other attributes like timestamps.
# It's the rails methods like `save`, `create`, and `update` that set timestamps, so this will track those for you as any other model.
Favorite.create(list_id: params[:list_id], user: current_user)
end
You might want to reflect on which method to use. Again, this really depends on your use case, and on the criteria above. Personally, when I'm not sure, I prefer the "has_many through" method as it gives you more tools to work with and is generally more flexible.

You may try following :
class User
has_and_belongs_to_many :lists
end
class List
has_and_belongs_to_many :users
end
class CreateUsersAndLists
def change
create_table :users do |t|
# Code
end
create_table :lists do |t|
# Code
end
create_table :users_lists id: false do |t|
t.belongs_to :user, index: true
t.belongs_to :list, index: true
t.boolean :is_favourite
end
end
end

Related

How did my old create joined tables work with no indexes?

I previously created a joined table with the migration:
class CreateJoinTableCategoryListing < ActiveRecord::Migration[5.2]
def change
create_join_table :categories, :listings do |t|
# t.index [:category_id, :listing_id]
# t.index [:listing_id, :category_id]
end
end
end
I was looking back on this as i am going to be creating a new join tables. But while looking at it i noticed i migrated with the t.index's still commented out and the joined tables still operate correctly.
I read into this and i haven't found any posts about either someone doing the same or not needing them.
How is it operating with those index's never migrated and how needed are they?
I am creating a new migration:
class CreateJoinTable < ActiveRecord::Migration[5.2]
def change
create_join_table :users, :affiliates do |t|
# t.index [:user_id, :affiliate_id]
# t.index [:affiliate_id, :user_id]
end
end
end
Which index should i be choosing here?
How it should work is that an affiliate is able to manually submit a "commission" to the table (which does need to be added to the migration), but if the commission is updated, it should take the place of the column and not create a new row.
A user will have really nothing to do with this and will be mostly updated by the affiliate to update the commission rates they have on the user.
Update:
Is it even possible to add another field to the join table?
I wanted to add a :commission to the table but i can't find any docs to do anything for that. Should i just be defining the commission rate within the users table and do away with the join table?
UPDATE 2:
Ended up scratching this idea and keeping my current method of doing it with the users and affiliates association only. I did away with the UsersAffiliates idea as it's not needed for this case.
How is it operating with those index's never migrated and how needed
are they?
All types of assocations in Rails will work without indices. The only thing that is required is that the correct tables and columns exist.
Indices are however critical for performance as the size of your database grows. They also provide constraints such as uniqueness that ensure that duplicate data cannot be inserted due to race conditions.
Which index should i be choosing here?
The whole reason that Rails generates two different indices is that you should choose the index that cooresponds to how you will most often be searching the table. If you are most often using User.joins(:affilitates) you would choose t.index [:user_id, :affiliate_id] for example.
How it should work is that an affiliate is able to manually submit a
"commission" to the table (which does need to be added to the
migration).
The create_join_table macro creates a join table named for has_and_belongs_to_many assocations.
The main problem with has_and_belongs_to_many assocations is that they are headless. There is no model and therefore no way to query the table directly or add additional metadata columns.
What you instead want is a has_many :through association.
class User < ApplicationRecord
has_many :user_affiliates
has_many :affiliates, through: :user_affiliates
end
class Affiliate < ApplicationRecord
has_many :user_affiliates
has_many :affiliates, through: :user_affiliates
end
# call this whatever you want
class UserAffiliate < ApplicationRecord
belongs_to :user
belongs_to :affilitate
end
While has_and_belongs_to_many uses the table naming scheme users_affilities (plural_plural) you want to use user_affilities for a has_many through: association.
You can fix this by:
Just generating the table/model through the normal generator rail g model user_affiliate.
If the table exists write a migration to rename the table.
but if the commission is updated, it should take the place
of the column and not create a new row.
You can solve this by:
Add a unique compound index on the two columns t.index [:user_id, :affiliate_id], unique: true.
Add a uniqueness validation in the join model. validates_uniqueness_of :user_id, scope: :affiliate_id.
Use .find_or_initialize_by in your controller to update an existing row if it exists instead of creating a new row if one already exists.

How to organise methods in a self join model

In designing a data model, you will sometimes find a model that should have a relation to itself. Ruby On Rails Guide provides a neat example. I am using it as a template for my example
For example, you may want to store all users in a single database model, but be able to trace relationships such as between affiliate and users. This situation can be modeled with self-joining associations:
class User < ApplicationRecord
has_many :users, :through => :referrals
has_one :affiliate, :through => :referral
end
This allows me to keep both users and affiliate in a same database table which is correct because fundamentally they are all individual users.
Problem arises when the model grows. Affiliate has its own set of methods - earnings, expected_earnings etc. These methods are very specific to Affiliate and I have my qualm keeping them with other user methods.
Loading object in correctly named variable helps:
affiliate = User.find 1
affiliate.earnings # used in context of affiliate
user = User.find 1
user.subscriptions # mostly in context to user
But when I read the User model, Affiliate related methods feels out-of-place.
Is there a way to namespace these methods correctly? What is the standard way of organizing self join model methods?
One way to solve this is with Single Table Inheritance. Before accepting this approach, I would recommend searching the web for "single table inheritance rails" and reading up on the pros and cons of it. A lot of digital ink has been spent on this subject.
With the caveat out of the way, Single Table Inheritance (STI) allows you to let multiple Rails models share one database table. You do this by adding a string field called type to your database table. Rails will interpret this as the subclass of your model. You would then create several models that inherit from User.
In your specific case, the type field would either contain user or affiliate. You would also create an Affliliate class which inherits from User. All of your Affiliate specific methods would be put in the Affiliate class. Rails is smart enough to use the type field in the database to identify records from the appropriate class.
Here is the migration you would run:
class AddTypeToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :type, :string
add_index :users, :type
end
end
Next you would add an Affiliate class:
# app/models/affliliate.rb
class Affiliate < User
# Affiliate specific methods here.
end
You may also want to create a class for non-affiliate users. Call it customers:
# app/models/customer.rb
class Customer < User
# Customer specific methods here.
end
Use the appropriate class name when creating new records and rails will automatically populate the type field in the database.
You would then moving your associations to the appropriate model:
# app/models/affiliate.rb
class Affiliate < User
has_many :customers, through: :referrals, foreign_key: :user_id
end
# app/models/customer.rb
class Customer < User
has_one :affiliate, through: :referral, foreign_key: :user_id
end
I have not tested this, but it should work.

What is the process of creating a joining table which resolves a many-to-many relationship using migrations?

I would like to understand the process which can be followed to create models and migrations which in turn create a joining table within the database to resolve a many-to-many relationship. For instance. If i have a course table, a students table and i want to create a joining table called studies with the id from course and students along with extra data such as the grade and date started. Exactly What is the process for doing this?
If i am to use the generate model command for each of the above 3 table names, this will create a separate migration for all of them. Even if i go into the models and add the relevant associations this will not affect how the database is created? Please could you give me some guidance on what steps i must take in order to create the required foreign key relationships here with some examples?
Use a has_many :through association. Set it up manually.
Step 1: Generate your models separately. The joining model Study will contain foreign keys, so remember to include the columns.
rails g model Course title:string
rails g model Student name:string
rails g model Study course_id:integer student_id:integer start_date:date grade:string
Step 2: Set up associations:
# models/course.rb
class Course
has_many :studies
has_many :students, through: :studies
end
# models/student.rb
class Student
has_many :studies
has_many :courses, through: :studies
end
# models/study.rb
class Study
belongs_to :course
belongs_to :student
end
Step 3: Run migrations, restart server (if necessary), and that's it. Rails will handle the rest.
Accessing attributes in the joining table may require careful timing to ensure the correct object is being accessed, since the joining object is not returned via Rails' built-in methods, ie #course.students. Check out this answer for some ideas.
Read the guide for more information.
for many-to-many relationships use:
class Car < ActiveRecord::Base
has_and_belongs_to_many :tires
end
class Tire < ActiveRecord::Base
has_and_belongs_to_many :cars
end
Then you create a migration like this:
class CreateCarsAndTires < ActiveRecord::Migration
def change
create_table :cars do |t|
t.string :name
end
create_table :tires do |t|
t.string :something
end
create_table :cars_tires do |t|
t.belongs_to :car
t.belongs_to :tire
t.string :additional_dataA //optional
t.int :something_else //optional
end
end
end
It is very important that you name your join table in the migration in alphabetical order (c in cars comes before t for tires) as ActiveRecord will look in has_many_and_belongs_to relations for a table which is named this way pluralized-classA_pluralized_classB like apples_bananas vs bananas_apples which would not work and you would have to add the table name to your classes and it goes against the convention over configuration paradigm.
Hope it helps.

Create if record does not exist

I have 3 models in my rails app
class Contact < ActiveRecord::Base
belongs_to :survey, counter_cache: :contact_count
belongs_to :voter
has_many :contact_attempts
end
class Survey < ActiveRecord::Base
has_many :questions
has_many :contacts
end
class Voter < ActiveRecord::Base
has_many :contacts
end
the Contact consists of the voter_id and a survey_id. The Logic of my app is that a there can only be one contact for a voter in any given survey.
right now I am using the following code to enforce this logic. I query the contacts table for records matching the given voter_id and survey_id. if does not exist then it is created. otherwise it does nothing.
if !Contact.exists?(:survey_id => survey, :voter_id => voter)
c = Contact.new
c.survey_id = survey
c.voter_id = voter
c.save
end
Obviously this requires a select and a insert query to create 1 potential contact. When I am adding potentially thousands of contacts at once.
Right now I'm using Resque to allow this run in the background and away from the ui thread. What can I do to speed this up, and make it more efficient?
You can do the following:
Contact.where(survey_id: survey,voter_id: voter).first_or_create
You should add first a database index to force this condition at the lowest level as possible:
add_index :contacts, [:voter_id, :survey_id], unique: true
Then you should add an uniqueness validation at an ActiveRecord level:
validates_uniqueness_of :voter_id, scope: [:survey_id]
Then contact.save will return false if a contact exists for a specified voter and survey.
UPDATE: If you create the index, then the uniqueness validation will run pretty fast.
See if those links can help you.
Those links are for rails 4.0.2, but you can change in the api docks
From the apidock: first_or_create, find_or_create_by
From the Rails Guide: find-or-create-by
It would be better if you let MySQL to handle it.
Create a migration and add a composite unique key to survey_id, voter_id
add_index :contact, [:survey_id, :voter_id], :unique=> true
Now
Contact.create(:survey_id=>survey, :voter_id=>voter_id)
Will create new record only if there is no duplicates.

Do I need to manually create a migration for a HABTM join table?

I'm struggling now to get HATBM working correctly. I have a beaten scanario: articles and tags. I presume, HABTM should be used here, since it is a many-to-many relationship.
I don't know however if I should manually create a join table (articles_tags in this case).
My code currently as follows:
class Article < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :articles
end
When I run the migrations, no 3rd table is created.
Also, I would like to add that my third table doesn't bear any domain logic, just blind assignment.
I'm using Rails 2.2.2
You should do this in a migration of one of the tables, or in a separate migration if those migrations have been ran:
create_table :articles_tags, :id => false do |t|
t.references :article, :tag
end
add_index :articles_tags, [:article_id, :tag_id]
This will create the table for you and the :id => false tells Rails not to add an id field to this table. There's an index also, which will speed up lookups for this join table.
You could also generate a model (ArticlesTag) for this and do:
# article.rb
has_many :articles_tags
has_many :tags, :through => :articles_tags
# tag.rb
has_many :articles_tags
has_many :articles, :through => :articles_tags
# article_tag.rb
belongs_to :tag
belongs_to :article
And then create the table in the migration generated from the script/generate model articles_tag call.
Note that this is covered in the API.
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_and_belongs_to_many
You probably also want to add an index to the migration:
add_index "articles_tags", "article_id"
add_index "articles_tags", "tag_id"
However, if you want tagging functionality I'd recommend the acts_as_taggable_on rails plugin:
http://www.intridea.com/tag/acts_as_taggable_on
http://github.com/mbleigh/acts-as-taggable-on/
I've used it on a project and it was very easy to implement.
One of the issues with a join table for tagging is that it can easily get ugly creating a join table for each content type you wish to make taggable (ie. comments_tags, posts_tags, images_tags, etc). This plugin uses a taggings table which includes a discriminator to determine the content type without the need of a specific join table for each type.
In combination with this Qeuestion(1st answear) How to set up a typical users HABTM roles relationship and 1st answear from here, it has to be understood even by a monkey. I am new in RoR and it's got working like a charm

Resources