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)