Choosing between has_many :through and has_and_belongs_to_many association - ruby-on-rails

I’m new to programming and have difficulty determining the model association I need. I already have two tables: Organizations and Users with a 1:many relationship, since an organization has multiple users/members. Not all users are a member of an organization though but that’s not a problem.
# In the organization model:
has_many :users
# In the user model:
belongs_to :organization, inverse_of: :users
# In the users migration file:
t.references :organization, index: true, foreign_key: true
I need to extend this with a moderator function: A user can be a moderator for multiple organizations and an organization can have multiple users as their moderator. So there will then be two separate reasons for a relationship between the user and organization model (user as a member of an organization and user as a moderator of an organization; no need to a member before you can be a moderator).
I read here about the two different possible ways to create the association, namely has_many :through or has_and_belongs_to_many.
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).
You should use has_many :through if you need validations, callbacks,
or extra attributes on the join model.
I'm not sure what is meant by "if you need to work with the relationship model as an independent entity". In my use case is a moderator will have additional rights. The association will, I belief, be used in two ways: 1) displaying the moderators for an organization, and displaying the organizations that a user is the moderator of; and 2) look up if a user has moderator rights to allow him these rights. So I don't belief I will need validations (organization will itself be able to select users as moderator) or extra attributes. I'm not sure about callbacks (don't know exactly what they are).
So in short, which one of the two forms of association is the best association in my use case?

"if you need to work with the relationship model as an independent entity" means you will be able to operate on the Relationship model as any other ActiveRecord model, i.e you can create, save, destroy etc. records or assign various types of attributes to the relationship.
Suppose, for example, that a user could have different types of relationship with the organization (e.g. moderator, employee, stockholder et). Using the :through association, you would have:
class Relationship < ActiveRecord::Base
belongs_to :user
belongs_to :organization
end
class User < ActiveRecord::Base
has_many :organizations, through: relationships
end
class Organization < ActiveRecord::Base
has_many :users, through: relationships
end
# schema.rb
ActiveRecord::Schema.define(:version => 20150511223747) do
# . . . .
create_table "relationships", :force => true do |t|
t.string "relationship_type"
t.integer "user_id"
t.integer "organization_id"
t.datetime "created_at"
t.datetime "updated_at"
end
end
# things you can do:
org = Organization.first
moderators_or_employees = org.users.where("relationship_type = 'moderator' OR relationship_type = 'employee'")
new_moderators = org.users.where("relationship_type = 'moderator' AND DATE(relationships.created_at) > '2015-01-01'")
These are things you could not do with the has_and_belongs_to_many association since you have no way of querying on relationship type or created_at. Because of these limitations, I have often found that the :through relationship, although a bit more complicated, gives you a lot more flexibility.

My nutshell explanation when people ask:
habtm – only a join table, e.g., two ids
hm:t – a join table with meta data about the join relationship, e.g., two ids and other data like times, counts, etc.

Related

What is the best way to implement belongs to many logic in Rails?

For example I have model Match and model Player. What I want is for Player to be able to participate in many matches.
So it should look smth like this:
#match_first = Match.first
#match_last = Match.last
#match_first.players
#player1, player3, player4
#match_last.players
#player1, player4, player5
Both matches can have same players simultanously.
In your Match model:
has_many: :players, through: :appearances
In your Player model:
has_many: :matches, through: :appearances
class Appearance < ActiveRecord::Base
belongs_to :match
belongs_to :player
You could have extra attributes on appearance like 'goals_scored'. The through model doesn't have to be called Appearance... it could be Attendance or MatchPlayer or PlayerMatch, but you can see that the last two names are constraining, because out of this association you can also see all the matches a player appeared in.
Rails provides two ways to accomplish the many-to-many relationship. The first, which #zoot's answer describes, is has_many :through. This is a great option if the relationship is a Model in its own right (i.e., needs additional attributes or validations).
If the relationship between the two entities is direct such that you don't really need a third model, you can use the has_and_belongs_to_many association.
In your Match model:
has_and_belongs_to_many :players
In your Player model:
has_and_belongs_to_many :matches
When you use has_and_belongs_to_many you do need to create a join table that holds the relationship. The migration for that (assuming a relatively recent version of Rails) might look like:
class CreateJoinTableMatchesPlayers < ActiveRecord::Migration
def change
create_join_table :matches_players, :users do |t|
t.index [:match_id, :player_id], unique: true
end
end
end
There's a section in the Active Record Associations guide that talks about how to choose between the two approaches.

Creating a many-to-many record 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

Can a Rails Model have an array of associations?

I've looked everywhere, and can't find anything (maybe cause it's not possible)
I have a Meeting model and a Language model (which has a string column called language). Each Meeting has 2 Languages.
Is there a way to make an association, such as:
rails g migration AddLanguageToMeetings language:references
And then store an array of 2 language_ids in the reference?
For example, Meeting.language_id = [1,2]
And then be able to call the Language, like:
meeting.language_id[0].language
How can I se up this association? Do I need to have 2 different columns with each associated id?
Thanks!!
What you want is a N-to-N relation. Create another model called MeetingLanguage with two columns:
create_table :meeting_languages do |t|
t.references :meetings
t.references :languages
end
and associations:
class MeetingLanugage < ActiveRecord::Base
belongs_to :language
belongs_to :meeting
end
And then in Meeting module:
has_many :meeting_languages
has_many :languages, through: :meeting_languages, source: :language
Now you can have as many languages as you want for a single meeting.
What you're describing is a has_many, where the foreign key exists in the languages table, or in a joining table between the languages and meetings tables.
If you want each Meeting to point two exactly two Languages, then you can use two foreign keys in the meetings table, give each language a real name, and then have two belongs_to associations in your Meeting.

Rails 4 return tasks associated with more than one user

Question: How can I return Assignments associates with one of many users in the users array?
I researched the Rails guides and some only posts but I can't figure this out yet.
https://codereview.stackexchange.com/questions/46319/is-there-a-better-approach-to-searching-has-and-belongs-to-many-relations
http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
http://guides.rubyonrails.org/active_record_querying.html#retrieving-multiple-objects-in-batches
I am associating users to assignments two different ways.
1- user "user_id" is the one who creates the assignment
2- The assignment is given to multiple users. Users are associated to assignments using has_and_belongs_to_many :users
Basically each assignment is associated to the user who owns the task.
The assignment is also given to multiple users who will work on it.
I can successfully return all tasks associated with user but not with users.
I'm trying to display only the tasks associated with the current_user (devise)
This works for user:
assignment.rb
class Assignment < ActiveRecord::Base
belongs_to :deliverable
belongs_to :user
has_and_belongs_to_many :users
end
user.rb
has_and_belongs_to_many :assignments
The association are working fine. In the console I can get all the users associated via HABTM to the assignment.
I have a designer dashboard where i only want to display assignments given to the current user.
designer_dashboard controller:
#if I do this I'll get all the assignments:
#assignments = Assignment.all
# but I want to be able to do something like this to get only the assingments associated with the current user via HABTM
#assignments = Assignment.includes(:users).where(["user_ids =?", current_user])
The data isn't modelled to reflect the 2 different types of relationship that exist between users and tasks - task owners and task designers. You've only set up one of them.
You will need to remodel the data and give the relationships more meaningful names.
One way would be to use has_many_through for the association that a Task has with who its assigned to. A Task has_many Designers through AssignedTasks. The association between Task and User can be named as TaskOwner. If you set up both these associations you will be able to get current_user.owned tasks and current_user.assigned tasks which is more clear than referring to user and users.
class Task
belongs_to :task_owner, class_name: "User"
has_many :assigned_tasks
has_many :designers, through: assigned_tasks, class_name: "User"
end
class User
has_many :owned_tasks, class_name: "Task"
has_many :assigned_tasks, foreign_key: designer_id
has_many :tasks, through: :assigned_tasks,
end
class AssignedTask
belongs_to :designer
belongs_to :task
end
You will need to generate some migrations to add the requisite ids.
Also, I seem to remember reading somewhere that Task is a reserved word. You may want to rename task to something else.
Margo answer is correct.
This works in controller:
#assignments = current_user.assignments

ActiveRecords [something] belongs_to [User] and has_many [Users]

I'm modelling a scenario with Users and Tools, where a Tool is owned by one User but can be used by many Users including to one owning it.
I was thinking about adding an owner_id column to Tools and say it has_many Users or by adding a new relationsship table.
I'm really new to Rails and I have problems setting up the right associations in the models though, maybe you can point me in the right direction?
Thank you very much.
Your should add owner_id to the Tools table.
Associations will be like that.
class User < ActiveRecord::Base
has_and_belongs_to_many :tools
end
class Tool < ActiveRecord::Base
has_and_belongs_to_many :users
belongs_to :owner, :class_name => 'User'
end
You'll need a tools_users table in order to use habtm-association. Generate a migration and create a table with option id: false and two columns user_id and tool_id:
class CreateToolsUsersTable < ActiveRecordMigration
def change
create_table :tools_users, id: false do |t|
t.integer :tool_id
t.integer :user_id
end
end
end
After that you can call something like #user.tools or #user.owner
Read more there
User has many tools
Tool belongs to user in owner
Tool has many users
is what I would do.
I'm not sure about the wording because I don't use Active Record but this is how it works in other orms

Resources