In database design, many-to-many relationships occur when each record in one table can be related to multiple records in another, and vice versa. Rails provides two main approaches to model such relationships: has_and_belongs_to_many and has_many :through.
has_and_belongs_to_many(name, scope = nil, **options, &extension)
Specifies a many-to-many relationship with another class. This associates two classes via an intermediate join table. Unless the join table is explicitly specified as an option, it is guessed using the lexical order of the class names. So a join between Developer and Project will give the default join table name of “developers_projects” because “D” precedes “P” alphabetically. Note that this precedence is calculated using the < operator for String. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables “paper_boxes” and “papers” to generate a join table name of “papers_paper_boxes” because of the length of the name “paper_boxes”, but it in fact generates a join table name of “paper_boxes_papers”. Be aware of this caveat, and use the custom :join_table option if you need to. If your tables share a common prefix, it will only appear once at the beginning. For example, the tables “catalog_categories” and “catalog_products” generate a join table name of “catalog_categories_products”.
The join table should not have a primary key or a model associated with it. You must manually generate the join table with a migration such as this:
class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[7.1] def change create_join_table :developers, :projects end end
It’s also a good idea to add indexes to each of those columns to speed up the joins process. However, in MySQL it is advised to add a compound index for both of the columns as MySQL only uses one index per table during the lookup.
Adds the following methods for retrieval and query:
collection is a placeholder for the symbol passed as the name argument, so has_and_belongs_to_many :categories would add among others categories.empty?.
collection
Returns a Relation of all the associated objects. An empty Relation is returned if none are found.
collection<<(object, …)
Adds one or more objects to the collection by creating associations in the join table (collection.push and collection.concat are aliases to this method). Note that this operation instantly fires update SQL without waiting for the save or update call on the parent object, unless the parent object is a new record.
collection.delete(object, …)
Removes one or more objects from the collection by removing their associations from the join table. This does not destroy the objects.
collection.destroy(object, …)
Removes one or more objects from the collection by running destroy on each association in the join table, overriding any dependent option. This does not destroy the objects.
collection=objects
Replaces the collection’s content by deleting and adding objects as appropriate.
collection_singular_ids
Returns an array of the associated objects’ ids.
collection_singular_ids=ids
Replace the collection by the objects identified by the primary keys in ids.
collection.clear
Removes every object from the collection. This does not destroy the objects.
collection.empty?
Returns true if there are no associated objects.
collection.size
Returns the number of associated objects.
collection.find(id)
Finds an associated object responding to the id and that meets the condition that it has to be associated with this object. Uses the same rules as ActiveRecord::FinderMethods#find.
collection.exists?(…)
Checks whether an associated object with the given conditions exists. Uses the same rules as ActiveRecord::FinderMethods#exists?.
collection.build(attributes = {})
Returns a new object of the collection type that has been instantiated with attributes and linked to this object through the join table, but has not yet been saved.
collection.create(attributes = {})
Returns a new object of the collection type that has been instantiated wit attributes, linked to this object through the join table, and that has already been saved (if it passed the validation).
collection.reload
Returns a Relation of all of the associated objects, forcing a database read. An empty Relation is returned if none are found.
class Developer < ActiveRecord::Base has_and_belongs_to_many :projects end Declaring <b>has_and_belongs_to_many :projects</b> adds the following methods (and more): developer = Developer.find(11) project = Project.find(9) developer.projects developer.projects << project developer.projects.delete(project) developer.projects.destroy(project) developer.projects = [project] developer.project_ids developer.project_ids = [9] developer.projects.clear developer.projects.empty? developer.projects.size developer.projects.find(9) developer.projects.exists?(9) developer.projects.build # similar to Project.new(developer_id: 11) developer.projects.create # similar to Project.create(developer_id: 11) developer.projects.reload
You can pass a second argument scope as a callable (i.e. proc or lambda) to retrieve a specific set of records or customize the generated query when you access the associated collection.
Scope examples:
has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } has_and_belongs_to_many :categories, ->(post) { where("default_category = ?", post.default_category) }
The extension argument allows you to pass a block into a has_and_belongs_to_many association. This is useful for adding new finders, creators, and other factory-type methods to be used as part of the association.
# app/models/developer.rb class Developer < ApplicationRecord has_and_belongs_to_many :projects, extension: DeveloperExtension end # app/models/project.rb class Project < ApplicationRecord has_and_belongs_to_many :developers, extension: DeveloperExtension end # lib/developer_extension.rb module DeveloperExtension def total_experience projects.sum(:experience_years) end end
In this example:
developer = Developer.first puts "Total experience of #{developer.name}: #{developer.total_experience} years"
This example demonstrates how you can use the :extension option to encapsulate and reuse functionality related to an association. The total_experience method is now neatly organized in a separate module, enhancing modularity and maintainability.
:class_name option:
Specifies the class name of the association. This is useful when the class name cannot be inferred from the association name.
class Enrollment < ApplicationRecord belongs_to :student belongs_to :course end class Student < ApplicationRecord has_and_belongs_to_many :courses, class_name: 'Enrollment' end class Course < ApplicationRecord has_and_belongs_to_many :students, class_name: 'Enrollment' end
:join_table option:
Specifies the name of the join table that represents the many-to-many relationship. If not specified, Rails will automatically generate the table name.
class Student < ApplicationRecord has_and_belongs_to_many :courses, join_table: 'enrollments' end class Course < ApplicationRecord has_and_belongs_to_many :students, join_table: 'enrollments' end
:foreign_key option:
Specifies the foreign key used in the join table. By default, Rails uses the singular form of the association name appended with _id.
class Student < ApplicationRecord has_and_belongs_to_many :courses, foreign_key: 'student_id' end class Course < ApplicationRecord has_and_belongs_to_many :students, foreign_key: 'student_id' end
:association_foreign_key option:
Specifies the foreign key used in the join table for the associated model. By default, Rails uses the singular form of the associated model's name appended with _id.
class Student < ApplicationRecord has_and_belongs_to_many :courses, association_foreign_key: 'course_id' end class Course < ApplicationRecord has_and_belongs_to_many :students, association_foreign_key: 'course_id' end
:validate option:
If set to false, it skips the validation of the associated records. Default is true.
class Student < ApplicationRecord has_and_belongs_to_many :courses, validate: false end class Course < ApplicationRecord has_and_belongs_to_many :students, validate: false end
:autosave option:
The :autosave option determines whether associated objects should be saved automatically when the parent object is saved. This is useful when you want to save both the parent and the associated objects in a single call.
class Student < ApplicationRecord has_and_belongs_to_many :courses, autosave: true end class Course < ApplicationRecord has_and_belongs_to_many :students, autosave: true end
In this example, when you save a Student instance that has associated Course instances, both the student and the associated courses will be saved in a single call to save. The same applies when saving a Course instance with associated Student instances.
:strict_loading option:
Introduced in Rails 6.1, the :strict_loading option helps enforce strict loading for associations, preventing the accidental loading of associations when querying the database.
class Student < ApplicationRecord has_and_belongs_to_many :courses, strict_loading: true end class Course < ApplicationRecord has_and_belongs_to_many :students, strict_loading: true end
With strict_loading: true, Rails will raise an error if an association is accessed without being explicitly loaded. This is useful in ensuring that developers are conscious about when and how associations are loaded, helping to prevent performance issues related to the N+1 query problem.
Please note that :strict_loading is available starting from Rails 6.1, so make sure your Rails version supports this option if you intend to use it.
A has_one :through association sets up a one-to-one connection with another model. This association indicates that the declaring model can be matched with one instance of another model by proceeding through a third model. For example, if each supplier has one account, and each account is associated with one account history, then the supplier model could look like this:
class Supplier < ApplicationRecord has_one :account has_one :account_history, through: :account end class Account < ApplicationRecord belongs_to :supplier has_one :account_history end class AccountHistory < ApplicationRecord belongs_to :account End The corresponding migration might look like this: class CreateAccountHistories < ActiveRecord::Migration[7.1] def change create_table :suppliers do |t| t.string :name t.timestamps end create_table :accounts do |t| t.belongs_to :supplier t.string :account_number t.timestamps end create_table :account_histories do |t| t.belongs_to :account t.integer :credit_rating t.timestamps end end end
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_manyrelationship (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.
While has_and_belongs_to_many suggests creating a join table with no primary key via id: false, consider using a composite primary key for the join table in the relationship.