DEV Community

marelons1337
marelons1337

Posted on • Edited on • Originally published at blog.rafaljaroszewicz.com

Creating model for "related" posts in Rails

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

rails g model post title content

First we want to have our model, nothing special about it, just an example Post class.

Next, we need a class that will connect our related posts.

rails g model post_connection from_post:references to_post:references

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 -> Post to avoid duplicate records.

class CreatePostConnections < 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

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 -> p2 and p2 -> p1. This p1 -> p1 connection is redundant.

class PostConnection < 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

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.

class Post < 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

Now let's see this in action.

p1 = Post.create(title: "First Post")
=>
#<Post:0x00000001105b6d08                                                                    
 id: 1,                                                                                      
 title: "First Post",                                                                        
 ... >   
p2 = Post.create(title: "Second Post")
=>
#<Post:0x0000000110245d18                        
 id: 2,                                          
 title: "Second Post",                                                            
 ... >

Adding our Posts we can now create a connection between them.

pc1 = PostConnection.create!(from_post: p1, to_post: p2)
pc1
=> 
#<PostConnection:0x00000001377a7988              
 id: 1,                                          
 from_post_id: 1,                                
 to_post_id: 2,                                  
 ... >

Great, we have our connection! Now let's see what happens if we try to add the exact same connection again.

pc1 = PostConnection.create!(from_post: p1, to_post: p2)

Validation failed: From post this connection already exists (ActiveRecord::RecordInvalid)

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.

pc1 = PostConnection.create!(from_post: p1, to_post: p1)

Validation failed: Post connection must be between two different posts (ActiveRecord::RecordInvalid)  

Nice, custom validation works as expected!

Let's create a connection in other direction and find all connections for a Post.

pc2 = PostConnection.create!(from_post: p2, to_post: p1)
pc2
=> 
#<PostConnection:0x0000000116fc2800                                                      
 id: 2,                                                                                  
 from_post_id: 2,                                                                        
 to_post_id: 1,                                                                          
 ... >

p1.post_connections
=>                                                             
[#<PostConnection:0x00000001132d4790                           
  id: 1,                                                       
  from_post_id: 1,                                             
  to_post_id: 2,                                               
  ... >, 
 #<PostConnection:0x00000001132d4650                           
  id: 2,                                                       
  from_post_id: 2,                                             
  to_post_id: 1,                                               
  ... >] 

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.

p1.from_posts
=>
[#<PostConnection:0x0000000113064e38                           
  id: 1,                                                       
  from_post_id: 1,                                             
  to_post_id: 2,                                               
  ... >] 

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!

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay