{"id":9200,"date":"2024-01-08T08:34:51","date_gmt":"2024-01-08T08:34:51","guid":{"rendered":"https:\/\/www.bacancytechnology.com\/qanda\/?p=9200"},"modified":"2024-01-08T08:34:51","modified_gmt":"2024-01-08T08:34:51","slug":"many-to-many-relationship-in-rails","status":"publish","type":"post","link":"https:\/\/www.bacancytechnology.com\/qanda\/ruby-on-rails\/many-to-many-relationship-in-rails","title":{"rendered":"Understanding Many-to-Many Associations in Rails"},"content":{"rendered":"<h3>Introduction<\/h3>\n<p>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: <strong>has_and_belongs_to_many<\/strong> and <strong>has_many :through<\/strong>.<\/p>\n<p><strong>has_and_belongs_to_many(name, scope = nil, **options, &amp;extension)<\/strong><\/p>\n<p>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 \u201cdevelopers_projects\u201d because \u201cD\u201d precedes \u201cP\u201d alphabetically. Note that this precedence is calculated using the &lt; operator for <strong>String<\/strong>. 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 \u201cpaper_boxes\u201d and \u201cpapers\u201d to generate a join table name of \u201cpapers_paper_boxes\u201d because of the length of the name \u201cpaper_boxes\u201d, but it in fact generates a join table name of \u201cpaper_boxes_papers\u201d. 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 \u201ccatalog_categories\u201d and \u201ccatalog_products\u201d generate a join table name of \u201ccatalog_categories_products\u201d.<br \/>\nThe 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:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">class CreateDevelopersProjectsJoinTable &lt; ActiveRecord::Migration[7.1]\r\n  def change\r\n    create_join_table :developers, :projects\r\n  end\r\nend\r\n<\/pre>\n<p>It\u2019s 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.<br \/>\nAdds the following methods for retrieval and query:<br \/>\ncollection is a placeholder for the symbol passed as the name argument, so <strong>has_and_belongs_to_many :categories<\/strong> would add among others <strong>categories.empty<\/strong>?.<\/p>\n<p><strong>collection<\/strong><br \/>\nReturns a <strong>Relation<\/strong> of all the associated objects. An empty <strong>Relation<\/strong> is returned if none are found.<\/p>\n<p><strong>collection&lt;&lt;(object, &#8230;)<\/strong><br \/>\nAdds 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.<\/p>\n<p><strong>collection.delete(object, &#8230;)<\/strong><br \/>\nRemoves one or more objects from the collection by removing their associations from the join table. This does not destroy the objects.<\/p>\n<p><strong>collection.destroy(object, &#8230;)<\/strong><br \/>\nRemoves 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.<\/p>\n<p><strong>collection=objects<\/strong><br \/>\nReplaces the collection\u2019s content by deleting and adding objects as appropriate.<\/p>\n<p><strong>collection_singular_ids<\/strong><br \/>\nReturns an array of the associated objects\u2019 ids.<\/p>\n<p><strong>collection_singular_ids=ids<\/strong><br \/>\nReplace the collection by the objects identified by the primary keys in ids.<\/p>\n<p><strong>collection.clear<\/strong><br \/>\nRemoves every object from the collection. This does not destroy the objects.<\/p>\n<p><strong>collection.empty?<\/strong><br \/>\nReturns true if there are no associated objects.<\/p>\n<p><strong>collection.size<\/strong><br \/>\nReturns the number of associated objects.<\/p>\n<p><strong>collection.find(id)<\/strong><br \/>\nFinds 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 <strong>ActiveRecord::FinderMethods#find<\/strong>.<\/p>\n<p><strong>collection.exists?(&#8230;)<\/strong><br \/>\nChecks whether an associated object with the given conditions exists. Uses the same rules as <strong>ActiveRecord::FinderMethods#exists?<\/strong>.<\/p>\n<p><strong>collection.build(attributes = {})<\/strong><br \/>\nReturns 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.<\/p>\n<p><strong>collection.create(attributes = {})<\/strong><br \/>\nReturns 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).<\/p>\n<p><strong>collection.reload<\/strong><br \/>\nReturns a Relation of all of the associated objects, forcing a database read. An empty Relation is returned if none are found.<\/p>\n<h3>Example<\/h3>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">class Developer &lt; ActiveRecord::Base\r\n  has_and_belongs_to_many :projects\r\nend\r\nDeclaring &lt;b&gt;has_and_belongs_to_many :projects&lt;\/b&gt; adds the following methods (and more):\r\ndeveloper = Developer.find(11)\r\nproject   = Project.find(9)\r\n\r\ndeveloper.projects\r\ndeveloper.projects &lt;&lt; project\r\ndeveloper.projects.delete(project)\r\ndeveloper.projects.destroy(project)\r\ndeveloper.projects = [project]\r\ndeveloper.project_ids\r\ndeveloper.project_ids = [9]\r\ndeveloper.projects.clear\r\ndeveloper.projects.empty?\r\ndeveloper.projects.size\r\ndeveloper.projects.find(9)\r\ndeveloper.projects.exists?(9)\r\ndeveloper.projects.build  # similar to Project.new(developer_id: 11)\r\ndeveloper.projects.create # similar to Project.create(developer_id: 11)\r\ndeveloper.projects.reload\r\n<\/pre>\n<h3>Scopes<\/h3>\n<p>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.<\/p>\n<p>Scope examples:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">\r\nhas_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }\r\nhas_and_belongs_to_many :categories, ->(post) {\r\n  where(\"default_category = ?\", post.default_category)\r\n}\r\n<\/pre>\n<h3>Extensions<\/h3>\n<p>The <strong>extension<\/strong> argument allows you to pass a block into a <strong>has_and_belongs_to_many<\/strong> association. This is useful for adding new finders, creators, and other factory-type methods to be used as part of the association.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">\r\n# app\/models\/developer.rb\r\nclass Developer < ApplicationRecord\r\n  has_and_belongs_to_many :projects, extension: DeveloperExtension\r\nend\r\n\r\n# app\/models\/project.rb\r\nclass Project < ApplicationRecord\r\n  has_and_belongs_to_many :developers, extension: DeveloperExtension\r\nend\r\n\r\n# lib\/developer_extension.rb\r\nmodule DeveloperExtension\r\n  def total_experience\r\n    projects.sum(:experience_years)\r\n  end\r\nend\r\n<\/pre>\n<p>In this example:<\/p>\n<ul>\n<li>The <strong>Developer<\/strong> and <strong>Project<\/strong> models are associated through <strong>has_and_belongs_to_many<\/strong>, and the DeveloperExtension module is included using the extension option.<\/li>\n<li>The <strong>DeveloperExtension<\/strong> module defines a method called <strong>total_experience<\/strong>, which calculates the total experience of a developer based on the <strong>experience_years<\/strong> attribute of associated projects.<\/li>\n<li>Now, you can use the <strong>total_experience<\/strong> method on instances of <strong>Developer:<\/strong><\/li>\n<\/ul>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">\r\ndeveloper = Developer.first\r\nputs \"Total experience of #{developer.name}: #{developer.total_experience} years\"\r\n<\/pre>\n<p>This example demonstrates how you can use the <strong>:extension<\/strong> option to encapsulate and reuse functionality related to an association. The <strong>total_experience<\/strong> method is now neatly organized in a separate module, enhancing modularity and maintainability.<\/p>\n<h3>Options<\/h3>\n<p><strong>:class_name option:<\/strong><br \/>\nSpecifies the class name of the association. This is useful when the class name cannot be inferred from the association name.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">\r\nclass Enrollment < ApplicationRecord\r\nbelongs_to :student\r\nbelongs_to :course\r\nend\r\nclass Student < ApplicationRecord\r\nhas_and_belongs_to_many :courses, class_name: 'Enrollment'\r\nend\r\nclass Course < ApplicationRecord\r\nhas_and_belongs_to_many :students, class_name: 'Enrollment'\r\nend\r\n<\/pre>\n<p><strong>:join_table option:<\/strong><br \/>\nSpecifies the name of the join table that represents the many-to-many relationship. If not specified, Rails will automatically generate the table name.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">\r\nclass Student < ApplicationRecord\r\nhas_and_belongs_to_many :courses, join_table: 'enrollments'\r\nend\r\n\r\nclass Course < ApplicationRecord\r\nhas_and_belongs_to_many :students, join_table: 'enrollments'\r\nend\r\n<\/pre>\n<p><strong>:foreign_key option:<\/strong><br \/>\nSpecifies the foreign key used in the join table. By default, Rails uses the singular form of the association name appended with _id.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">\r\nclass Student < ApplicationRecord\r\nhas_and_belongs_to_many :courses, foreign_key: 'student_id'\r\nend\r\n\r\nclass Course < ApplicationRecord\r\nhas_and_belongs_to_many :students, foreign_key: 'student_id'\r\nend\r\n<\/pre>\n<p><strong>:association_foreign_key option:<\/strong><br \/>\nSpecifies 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.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">\r\nclass Student < ApplicationRecord\r\nhas_and_belongs_to_many :courses, association_foreign_key: 'course_id'\r\nend\r\n\r\nclass Course < ApplicationRecord\r\nhas_and_belongs_to_many :students, association_foreign_key: 'course_id'\r\nend\r\n<\/pre>\n<p><strong>:validate option:<\/strong><br \/>\nIf set to false, it skips the validation of the associated records. Default is true.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">\r\nclass Student < ApplicationRecord\r\nhas_and_belongs_to_many :courses, validate: false\r\nend\r\n\r\nclass Course < ApplicationRecord\r\nhas_and_belongs_to_many :students, validate: false\r\nend\r\n<\/pre>\n<p><strong>:autosave option:<\/strong><br \/>\nThe :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.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">\r\nclass Student < ApplicationRecord\r\nhas_and_belongs_to_many :courses, autosave: true\r\nend\r\n\r\nclass Course < ApplicationRecord\r\n  \thas_and_belongs_to_many :students, autosave: true\r\nend\r\n<\/pre>\n<p>In this example, when you save a <strong>Student<\/strong> instance that has associated <strong>Course<\/strong> instances, both the student and the associated courses will be <strong>saved<\/strong> in a single call to save. The same applies when saving a <strong>Course<\/strong> instance with associated Student instances.<\/p>\n<p><strong>:strict_loading option:<\/strong><br \/>\nIntroduced in Rails 6.1, the :strict_loading option helps enforce strict loading for associations, preventing the accidental loading of associations when querying the database.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">\r\nclass Student < ApplicationRecord\r\n  \thas_and_belongs_to_many :courses, strict_loading: true\r\nend\r\nclass Course < ApplicationRecord\r\n  \thas_and_belongs_to_many :students, strict_loading: true\r\nend\r\n\r\n<\/pre>\n<p>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.<br \/>\nPlease 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.<\/p>\n<h3>The has_one :through Association<\/h3>\n<p>A <strong>has_one :through <\/strong>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:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"ruby\">\r\nclass Supplier < ApplicationRecord\r\n  has_one :account\r\n  has_one :account_history, through: :account\r\nend\r\nclass Account < ApplicationRecord\r\n  belongs_to :supplier\r\n  has_one :account_history\r\nend\r\nclass AccountHistory < ApplicationRecord\r\n  belongs_to :account\r\nEnd\r\n\r\nThe corresponding migration might look like this:\r\nclass CreateAccountHistories < ActiveRecord::Migration[7.1]\r\n  def change\r\n    create_table :suppliers do |t|\r\n      t.string :name\r\n      t.timestamps\r\n    end\r\n\r\n    create_table :accounts do |t|\r\n      t.belongs_to :supplier\r\n      t.string :account_number\r\n      t.timestamps\r\n    end\r\n\r\n    create_table :account_histories do |t|\r\n      t.belongs_to :account\r\n      t.integer :credit_rating\r\n      t.timestamps\r\n    end\r\n  end\r\nend\r\n<\/pre>\n<h3>Choosing Between has_many :through and as_and_belongs_to_many<\/h3>\n<p>The simplest rule of thumb is that you should set up a <strong>has_many :through<\/strong> 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 <strong>has_and_belongs_to_manyrelationship<\/strong> (though you'll need to remember to create the joining table in the database).<\/p>\n<p>You should use <strong>has_many :through<\/strong> if you need validations, callbacks, or extra attributes on the join model.<\/p>\n<p>While <strong>has_and_belongs_to_many<\/strong> 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.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction 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, &amp;extension) Specifies a many-to-many relationship with another class. This associates two classes via [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":9204,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[11],"tags":[],"class_list":["post-9200","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ruby-on-rails"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts\/9200"}],"collection":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/comments?post=9200"}],"version-history":[{"count":4,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts\/9200\/revisions"}],"predecessor-version":[{"id":9205,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts\/9200\/revisions\/9205"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/media\/9204"}],"wp:attachment":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/media?parent=9200"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/categories?post=9200"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/tags?post=9200"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}