11/11/08
polymorphic association and single inheritance combined
Every once in a while I get an odd solution to a simply problem. I know these moments come as I try to DRY things up and make things more extensible. Often hardly anyone agrees with my solution, but they don't have to: it's mine ;). This solution might just be one of those.
I've had a few cases where I've needed a relational hierarchy that both acts as a polymorphic association and as a single inheritance relationship. For example, let's say I want a post on a blog to have many threaded comments: comments with infinite depth slashdot style. I do not want the threaded comments to see beyond their direct parent. Which means if a comment is say two deep I don't even want it to know it's connected to a post down the line.
Why would I want to do that you say? Well just call me crazy and say that I may want to be able to move comment threads to another post later, or maybe make the first comment a post of it's own without losing the rest of the conversation. Say whatever you want and just follow me down this trail here.
So first the final bits of code for you to glance at:
I'm not including the post schema because it's really irrelevant. All you need for this to work is a posts table with an id column. So moving on.
With this kind of setup I have the post model looking to the comments table for comments related to it. We do this polymorphically using the standard resource_id and resource_type we all learned in the early days of rails (you did right?). Next we have the comment model reflecting on it's own table to pull any child_comments polymorphically as well.
The last part was the most tricky part, getting a parent_comment. I tried various ways, :through, :as, the acts_as_polymorph plugin, etc, but just couldn't get that last detail. It kept writing sql code that was looking for a resource_id with the same id as itself. So I wrote a short def instead. I could have simply written an sql string to do this, but I wanted something simpler and easier to digest for new people.
Simply put, there is nothing that I could find tonight that directly hits this niche in the modeling realm. Please post something in the comments section if you know of anything. I raise this as a temporary solution, and I may write a plugin or patch to acts_as_polymorph for the future, but I felt that other people out there might be looking for such a crazy solution as well. So this is for you wild and crazy people. Oh, and just for kicks, here's some unit tests:
And some comments fixtures:
Cheers!
I've had a few cases where I've needed a relational hierarchy that both acts as a polymorphic association and as a single inheritance relationship. For example, let's say I want a post on a blog to have many threaded comments: comments with infinite depth slashdot style. I do not want the threaded comments to see beyond their direct parent. Which means if a comment is say two deep I don't even want it to know it's connected to a post down the line.
Why would I want to do that you say? Well just call me crazy and say that I may want to be able to move comment threads to another post later, or maybe make the first comment a post of it's own without losing the rest of the conversation. Say whatever you want and just follow me down this trail here.
So first the final bits of code for you to glance at:
#-- polymorph / sti table structure --
create_table "comments", :force => true do |t|
t.integer "user_id"
t.integer "resource_id"
t.string "resource_type"
t.text "content"
t.datetime "created_at"
t.datetime "updated_at"
end
#-- post class --
class Post < ActiveRecord::Base
#...
belongs_to :user
has_many :comments, :as => :resource
#...
end
# Lines wrapped for the sake of this blog post
#-- comment class --
class Comment < ActiveRecord::Base
#...
belongs_to :user
belongs_to :resource, :polymorphic => true
has_many :child_comments,
:as => :resource,
:class_name => "Comment"
def parent_comment
return Comment.find(
:first,
:conditions => {
:resource_id => self.resource_id,
:resource_type => 'Comment'
}
)
end
#...
end
I'm not including the post schema because it's really irrelevant. All you need for this to work is a posts table with an id column. So moving on.
With this kind of setup I have the post model looking to the comments table for comments related to it. We do this polymorphically using the standard resource_id and resource_type we all learned in the early days of rails (you did right?). Next we have the comment model reflecting on it's own table to pull any child_comments polymorphically as well.
The last part was the most tricky part, getting a parent_comment. I tried various ways, :through, :as, the acts_as_polymorph plugin, etc, but just couldn't get that last detail. It kept writing sql code that was looking for a resource_id with the same id as itself. So I wrote a short def instead. I could have simply written an sql string to do this, but I wanted something simpler and easier to digest for new people.
Simply put, there is nothing that I could find tonight that directly hits this niche in the modeling realm. Please post something in the comments section if you know of anything. I raise this as a temporary solution, and I may write a plugin or patch to acts_as_polymorph for the future, but I felt that other people out there might be looking for such a crazy solution as well. So this is for you wild and crazy people. Oh, and just for kicks, here's some unit tests:
#-- post_test --
require 'test_helper'
class PostTest < ActiveSupport::TestCase
fixtures :posts
#...
def test_has_many_comments_and_comments_have_an_author
post = Post.find(1)
assert post.comments, "We were not able to find any attached comments: #{post.comments}"
post.comments.each do |comment|
assert comment.user.valid?, "We can't find a user attached to the comment: #{comment.user}"
end
end
#...
end
#-- comment_test --
require 'test_helper'
class CommentTest < ActiveSupport::TestCase
fixtures :comments
#...
def test_should_have_parent_comments
comment = Comment.find(2)
assert comment.parent_comment.valid?,
"There is no parent_comment: #{comment.parent_comment}"
end
def test_should_have_child_comments
comment = Comment.find(1)
assert comment.child_comments.length > 0,
"There are no child_comments: #{comment.child_comments}"
end
def test_child_comment_should_have_child_and_parent_comments
comment = Comment.find(2)
assert comment.child_comments.length > 0,
"There are no child_comments: #{comment.child_comments}"
assert comment.parent_comment.valid?,
"There is no parent_comment: #{comment.parent_comment}"
end
#...
end
And some comments fixtures:
one:
id: 1
user_id: 2
resource_id: 1
resource_type: Post
content: This post rocks! This will be done in no time!
two:
id: 2
user_id: 1
resource_id: 1
resource_type: Comment
content: I didn't know you liked quilting so much. That's fabulous!
three:
id: 3
user_id: 1
resource_id: 2
resource_type: Comment
content: I didn't know you liked quilting so much. That's fabulous!
four:
id: 4
user_id: 1
resource_id: 2
resource_type: Comment
content: I didn't know you liked quilting so much. That's fabulous!
five:
id: 5
user_id: 1
resource_id: 1
resource_type: Comment
content: I didn't know you liked quilting so much. That's fabulous!
Cheers!
Labels: database, programming, ruby on rails



3 Comments:
At November 19, 2008 10:14 PM ,
All of us said...
Emily
At December 2, 2008 11:14 AM ,
Hugues Lamy said...
You should be able to make it work with this. You had to define a column called parent_id:integer to make it work. I used it at the same time as a single table inheritance.
Good luck
Hugues - hugues (dot) lamy (at) m2i3 (dot) com
At December 4, 2008 5:24 PM ,
w3bsmith said...
@Hugues_Lamy: I've known about acts_as_tree for a bit, but when I talk about a subject I like to boil it down to it's raw components if possible. I will give acts_as_tree a try for the final production code.
Links to this post:
Create a Link
<< Home