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!

Top comments (0)