Creating model for "related" posts in Rails
Tags: rails, ruby, tutorial, webdev
Published: 2022-04-23 21:03:11

<p>I wanted to create a section on my page that leads to related posts. One proposition was to create additional table and store <code>from</code> and <code>to</code> ID's of related posts.</p>
<p></p>

<!-- wp:code -->
<pre class="wp-block-code"><code lang="bash" class="language-bash">rails g model post title content</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>First we want to have our model, nothing special about it, just an example Post class.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Next, we need a class that will connect our related posts.</p>
<!-- /wp:paragraph -->

<!-- wp:code {"lineNumbers":false} -->
<pre class="wp-block-code"><code lang="ruby" class="language-ruby">rails g model post_connection from_post:references to_post:references</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Here's my migration file, I'm also adding index on both ids to make sure that there can only be one PostConnection per each combination of Post -&gt; Post to avoid duplicate records.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="ruby" class="language-ruby">class CreatePostConnections &lt; ActiveRecord::Migration[7.0]
  def change
    create_table :post_connections do |t|
      t.references :from_post
      t.references :to_post

      t.index [:from_post_id, :to_post_id], unique: true

      t.timestamps
    end
  end
end</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now, I also want to make sure that users will not be able to relate the post to itself. So the only connections available will be p1 -&gt; p2 and p2 -&gt; p1. This p1 -&gt; p1 connection is redundant. </p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="ruby" class="language-ruby">class PostConnection &lt; ApplicationRecord
  belongs_to :from_post, class_name: 'Post'
  belongs_to :to_post, class_name: 'Post'

  validate :connection_uniqueness
  validates :from_post_id, uniqueness: { scope: [:to_post_id, :from_post_id], message: 'this connection already exists' }

  def connection_uniqueness
    if self.from_post_id == self.to_post_id
      errors.add(:post_connection, 'must be between two different posts')
    end
  end
end</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>On the Post side, we need to make sure that we can access specific connections. Using dependent: :destroy will make sure that whenever a Post is destroyed, so does connection.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="ruby" class="language-ruby">class Post &lt; ApplicationRecord
  has_many :from_posts, foreign_key: :from_post_id, class_name: 'PostConnection', dependent: :destroy
  has_many :to_posts, foreign_key: :to_post_id, class_name: 'PostConnection', dependent: :destroy

  def post_connections
    from_posts.or(to_posts)
  end
end
</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Now let's see this in action.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="ruby" class="language-ruby">p1 = Post.create(title: "First Post")
=&gt;
#&lt;Post:0x00000001105b6d08                                                                    
 id: 1,                                                                                      
 title: "First Post",                                                                        
 ... &gt;   
p2 = Post.create(title: "Second Post")
=&gt;
#&lt;Post:0x0000000110245d18                        
 id: 2,                                          
 title: "Second Post",                                                            
 ... &gt;</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Adding our Posts we can now create a connection between them.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="ruby" class="language-ruby">pc1 = PostConnection.create!(from_post: p1, to_post: p2)
pc1
=&gt; 
#&lt;PostConnection:0x00000001377a7988              
 id: 1,                                          
 from_post_id: 1,                                
 to_post_id: 2,                                  
 ... &gt;</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Great, we have our connection! Now let's see what happens if we try to add the exact same connection again.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="ruby" class="language-ruby">pc1 = PostConnection.create!(from_post: p1, to_post: p2)

Validation failed: From post this connection already exists (ActiveRecord::RecordInvalid)</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Awesome, our validation works, and just to double check custom validation that was added to PostConnection we can try assigning a connection from and to the same Post.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="ruby" class="language-ruby">pc1 = PostConnection.create!(from_post: p1, to_post: p1)

Validation failed: Post connection must be between two different posts (ActiveRecord::RecordInvalid)  </code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Nice, custom validation works as expected!</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Let's create a connection in other direction and find all connections for a Post.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="ruby" class="language-ruby">pc2 = PostConnection.create!(from_post: p2, to_post: p1)
pc2
=&gt; 
#&lt;PostConnection:0x0000000116fc2800                                                      
 id: 2,                                                                                  
 from_post_id: 2,                                                                        
 to_post_id: 1,                                                                          
 ... &gt;

p1.post_connections
=&gt;                                                             
[#&lt;PostConnection:0x00000001132d4790                           
  id: 1,                                                       
  from_post_id: 1,                                             
  to_post_id: 2,                                               
  ... &gt;, 
 #&lt;PostConnection:0x00000001132d4650                           
  id: 2,                                                       
  from_post_id: 2,                                             
  to_post_id: 1,                                               
  ... &gt;] </code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Yes, we can access all our connections now! I can also use a specific association to access only one part of PostConnections for specific Post. For example I can select only from_posts to see which connections were created directly from this post.</p>
<!-- /wp:paragraph -->

<!-- wp:code -->
<pre class="wp-block-code"><code lang="ruby" class="language-ruby">p1.from_posts
=&gt;
[#&lt;PostConnection:0x0000000113064e38                           
  id: 1,                                                       
  from_post_id: 1,                                             
  to_post_id: 2,                                               
  ... &gt;] </code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Got it! One thing that could probably be done is query optimization when calling post_connections as it uses OR statement which is not very efficient for filtering. Let me know if you have any suggestions to improve this, I hope that it helps you!</p>
<!-- /wp:paragraph -->