Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>There are several kinds of many-to-many relationships; you have to ask yourself the following questions:</p> <ul> <li>Do I want to store additional information with the association? (Additional fields in the join table.)</li> <li>Do the associations need to be implicitly bi-directional? (If post A is connected to post B, then post B is also connected to post A.)</li> </ul> <p>That leaves four different possibilities. I'll walk over these below.</p> <p>For reference: <a href="http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html" rel="noreferrer">the Rails documentation on the subject</a>. There's a section called “Many-to-many”, and of course the documentation on the class methods themselves.</p> <h2>Simplest scenario, uni-directional, no additional fields</h2> <p>This is the most compact in code.</p> <p>I'll start out with this basic schema for your posts:</p> <pre><code>create_table "posts", :force =&gt; true do |t| t.string "name", :null =&gt; false end </code></pre> <p>For any many-to-many relationship, you need a join table. Here's the schema for that:</p> <pre><code>create_table "post_connections", :force =&gt; true, :id =&gt; false do |t| t.integer "post_a_id", :null =&gt; false t.integer "post_b_id", :null =&gt; false end </code></pre> <p>By default, Rails will call this table a combination of the names of the two tables we're joining. But that would turn out as <code>posts_posts</code> in this situation, so I decided to take <code>post_connections</code> instead.</p> <p>Very important here is <code>:id =&gt; false</code>, to omit the default <code>id</code> column. Rails wants that column everywhere <strong>except</strong> on join tables for <code>has_and_belongs_to_many</code>. It will complain loudly.</p> <p>Finally, notice that the column names are non-standard as well (not <code>post_id</code>), to prevent conflict.</p> <p>Now in your model, you simply need to tell Rails about these couple of non-standard things. It will look as follows:</p> <pre><code>class Post &lt; ActiveRecord::Base has_and_belongs_to_many(:posts, :join_table =&gt; "post_connections", :foreign_key =&gt; "post_a_id", :association_foreign_key =&gt; "post_b_id") end </code></pre> <p>And that should simply work! Here's an example irb session run through <code>script/console</code>:</p> <pre><code>&gt;&gt; a = Post.create :name =&gt; 'First post!' =&gt; #&lt;Post id: 1, name: "First post!"&gt; &gt;&gt; b = Post.create :name =&gt; 'Second post?' =&gt; #&lt;Post id: 2, name: "Second post?"&gt; &gt;&gt; c = Post.create :name =&gt; 'Definitely the third post.' =&gt; #&lt;Post id: 3, name: "Definitely the third post."&gt; &gt;&gt; a.posts = [b, c] =&gt; [#&lt;Post id: 2, name: "Second post?"&gt;, #&lt;Post id: 3, name: "Definitely the third post."&gt;] &gt;&gt; b.posts =&gt; [] &gt;&gt; b.posts = [a] =&gt; [#&lt;Post id: 1, name: "First post!"&gt;] </code></pre> <p>You'll find that assigning to the <code>posts</code> association will create records in the <code>post_connections</code> table as appropriate.</p> <p>Some things to note:</p> <ul> <li>You can see in the above irb session that the association is uni-directional, because after <code>a.posts = [b, c]</code>, the output of <code>b.posts</code> does not include the first post.</li> <li>Another thing you may have noticed is that there is no model <code>PostConnection</code>. You normally don't use models for a <code>has_and_belongs_to_many</code> association. For this reason, you won't be able to access any additional fields.</li> </ul> <h2>Uni-directional, with additional fields</h2> <p>Right, now... You've got a regular user who has today made a post on your site about how eels are delicious. This total stranger comes around to your site, signs up, and writes a scolding post on regular user's ineptitude. After all, eels are an endangered species!</p> <p>So you'd like to make clear in your database that post B is a scolding rant on post A. To do that, you want to add a <code>category</code> field to the association.</p> <p>What we need is no longer a <code>has_and_belongs_to_many</code>, but a combination of <code>has_many</code>, <code>belongs_to</code>, <code>has_many ..., :through =&gt; ...</code> and an extra model for the join table. This extra model is what gives us the power to add additional information to the association itself.</p> <p>Here's another schema, very similar to the above:</p> <pre><code>create_table "posts", :force =&gt; true do |t| t.string "name", :null =&gt; false end create_table "post_connections", :force =&gt; true do |t| t.integer "post_a_id", :null =&gt; false t.integer "post_b_id", :null =&gt; false t.string "category" end </code></pre> <p>Notice how, in this situation, <code>post_connections</code> <strong>does</strong> have an <code>id</code> column. (There's <strong>no</strong> <code>:id =&gt; false</code> parameter.) This is required, because there'll be a regular ActiveRecord model for accessing the table.</p> <p>I'll start with the <code>PostConnection</code> model, because it's dead simple:</p> <pre><code>class PostConnection &lt; ActiveRecord::Base belongs_to :post_a, :class_name =&gt; :Post belongs_to :post_b, :class_name =&gt; :Post end </code></pre> <p>The only thing going on here is <code>:class_name</code>, which is necessary, because Rails cannot infer from <code>post_a</code> or <code>post_b</code> that we're dealing with a Post here. We have to tell it explicitly.</p> <p>Now the <code>Post</code> model:</p> <pre><code>class Post &lt; ActiveRecord::Base has_many :post_connections, :foreign_key =&gt; :post_a_id has_many :posts, :through =&gt; :post_connections, :source =&gt; :post_b end </code></pre> <p>With the first <code>has_many</code> association, we tell the model to join <code>post_connections</code> on <code>posts.id = post_connections.post_a_id</code>.</p> <p>With the second association, we are telling Rails that we can reach the other posts, the ones connected to this one, through our first association <code>post_connections</code>, followed by the <code>post_b</code> association of <code>PostConnection</code>.</p> <p>There's just <em>one more thing</em> missing, and that is that we need to tell Rails that a <code>PostConnection</code> is dependent on the posts it belongs to. If one or both of <code>post_a_id</code> and <code>post_b_id</code> were <code>NULL</code>, then that connection wouldn't tell us much, would it? Here's how we do that in our <code>Post</code> model:</p> <pre><code>class Post &lt; ActiveRecord::Base has_many(:post_connections, :foreign_key =&gt; :post_a_id, :dependent =&gt; :destroy) has_many(:reverse_post_connections, :class_name =&gt; :PostConnection, :foreign_key =&gt; :post_b_id, :dependent =&gt; :destroy) has_many :posts, :through =&gt; :post_connections, :source =&gt; :post_b end </code></pre> <p>Besides the slight change in syntax, two real things are different here:</p> <ul> <li>The <code>has_many :post_connections</code> has an extra <code>:dependent</code> parameter. With the value <code>:destroy</code>, we tell Rails that, once this post disappears, it can go ahead and destroy these objects. An alternative value you can use here is <code>:delete_all</code>, which is faster, but will not call any destroy hooks if you are using those.</li> <li>We've added a <code>has_many</code> association for the <strong>reverse</strong> connections as well, the ones that have linked us through <code>post_b_id</code>. This way, Rails can neatly destroy those as well. Note that we have to specify <code>:class_name</code> here, because the model's class name can no longer be inferred from <code>:reverse_post_connections</code>.</li> </ul> <p>With this in place, I bring you another irb session through <code>script/console</code>:</p> <pre><code>&gt;&gt; a = Post.create :name =&gt; 'Eels are delicious!' =&gt; #&lt;Post id: 16, name: "Eels are delicious!"&gt; &gt;&gt; b = Post.create :name =&gt; 'You insensitive cloth!' =&gt; #&lt;Post id: 17, name: "You insensitive cloth!"&gt; &gt;&gt; b.posts = [a] =&gt; [#&lt;Post id: 16, name: "Eels are delicious!"&gt;] &gt;&gt; b.post_connections =&gt; [#&lt;PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil&gt;] &gt;&gt; connection = b.post_connections[0] =&gt; #&lt;PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil&gt; &gt;&gt; connection.category = "scolding" =&gt; "scolding" &gt;&gt; connection.save! =&gt; true </code></pre> <p>Instead of creating the association and then setting the category separately, you can also just create a PostConnection and be done with it:</p> <pre><code>&gt;&gt; b.posts = [] =&gt; [] &gt;&gt; PostConnection.create( ?&gt; :post_a =&gt; b, :post_b =&gt; a, ?&gt; :category =&gt; "scolding" &gt;&gt; ) =&gt; #&lt;PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding"&gt; &gt;&gt; b.posts(true) # 'true' means force a reload =&gt; [#&lt;Post id: 16, name: "Eels are delicious!"&gt;] </code></pre> <p>And we can also manipulate the <code>post_connections</code> and <code>reverse_post_connections</code> associations; it will neatly reflect in the <code>posts</code> association:</p> <pre><code>&gt;&gt; a.reverse_post_connections =&gt; #&lt;PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding"&gt; &gt;&gt; a.reverse_post_connections = [] =&gt; [] &gt;&gt; b.posts(true) # 'true' means force a reload =&gt; [] </code></pre> <h2>Bi-directional looped associations</h2> <p>In normal <code>has_and_belongs_to_many</code> associations, the association is defined in <strong>both</strong> models involved. And the association is bi-directional.</p> <p>But there is just one Post model in this case. And the association is only specified once. That's exactly why in this specific case, associations are uni-directional.</p> <p>The same is true for the alternative method with <code>has_many</code> and a model for the join table.</p> <p>This is best seen when simply accessing the associations from irb, and looking at the SQL that Rails generates in the log file. You'll find something like the following:</p> <pre><code>SELECT * FROM "posts" INNER JOIN "post_connections" ON "posts".id = "post_connections".post_b_id WHERE ("post_connections".post_a_id = 1 ) </code></pre> <p>To make the association bi-directional, we'd have to find a way to make Rails <code>OR</code> the above conditions with <code>post_a_id</code> and <code>post_b_id</code> reversed, so it will look in both directions.</p> <p>Unfortunately, the only way to do this that I know of is rather hacky. You'll have to manually specify your SQL using options to <code>has_and_belongs_to_many</code> such as <code>:finder_sql</code>, <code>:delete_sql</code>, etc. It's not pretty. (I'm open to suggestions here too. Anyone?)</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload