Am I reinventing polymorphic associations?

Am I reinventing polymorphic associations?

I have a fairly standard blog with comments model, with the following
additions:

(1) Multiple models can accept comments (blog post, bug report, etc).

(2) Each group of comments has a list of subscribers that will be
emailed when a new comment is posted.

Requirement (1) leads me to a polymorphic association. But I can’t see
how to fit requirement (2) into that. Each group of comments needs
some place to store the list of subscribers.

So I added a CommentGroup table.

class BlogPost < ActiveRecord::Base
belongs_to :comment_group
end

class BugReport < ActiveRecord::Base
belongs_to :comment_group
end

class CommentGroup < ActiveRecord::Base
has_one :blog_post
has_one :bug_report # note: one or the other will be nil

has_and_belongs_to_many :subscribers
end

class Comment < ActiveRecord::Base
belongs_to :comment_group
end

But now table :comment_group only has one field: id. And that just
seems wrong to me.

Is that bad?

I think you are. I’d make Comment polymorphic, a nested set, and
has_many :subscribers, which should be based on the top parent of the
comment thread.

-eric

A nested set seems a little overkill.

I realized I could make two polymorphic associations: one in Comments,
and one in the join table for commentable_subscribers.

I’m not sure if that’s a better plan or not, though. I like that I can
enforce foreign key constraints when I use the extra CommentGroup
table.

Scott J. wrote:

A nested set seems a little overkill.

It’s not overkill at all if you’re threading your comments.

I realized I could make two polymorphic associations: one in Comments,
and one in the join table for commentable_subscribers.

I’m not sure if that’s a better plan or not, though. I like that I can
enforce foreign key constraints when I use the extra CommentGroup
table.

Why not keep CommentGroup and have a polymorphic association that will
bind it to either BlogPost or BugReport?

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

I ended up refactoring this to remove CommentGroup. The controller
code for CommentGroup was too ugly.

Here’s how my associations ended up:

class User < ActiveRecord::Base
has_many :subscriptions
end
class Comment < ActiveRecord::Base
belongs_to :author, :class_name => “User”
belongs_to :commentable, :polymorphic => true
end
class Subscription < ActiveRecord::Base
belongs_to :subscribable, :polymorphic => true
belongs_to :user
end
class BlogPost < ActiveRecord::Base
has_many :subscriptions, :as => :subscribable
has_many :subscribers, :through => :subscriptions, :source => :user
has_many :comments, :as => :commentable
end

Subscribable and Commentable are basically the same thing.

Then I added routes that appear as nested routes for any of the
commentable/subscribable types:

map.comments(":commentable_type/:commentable_id/comments",
:controller => :comments,
:action => :create,
:conditions => {:method => :post}
)

map.subscribers(":subscribable_type/:subscribable_id/subscribers",
:controller => :subscribers,
:action => :update,
:conditions => {:method => :post}
)
map.subscribers_table(":subscribable_type/:subscribable_id/
subscribers/table",
:controller => :subscribers,
:action => :table,
:conditions => {:method => :get}
)

Then I have controllers and views for Comments and Subscribers that
operate on commentable or subscribable and don’t care what type they
are.

To enable an additional model class as commentable (and subscribable)
I just add the three has_many lines (like BlogPost), and add the
comments partial to the appropriate view.

Next: how can I DRY up those three has_many lines? I want to put
something like ‘acts_as_commentable’ in the model and have it do those
three has_many lines.

On Oct 16, 8:43 am, Marnen Laibow-Koser <rails-mailing-l…@andreas-
s.net> wrote:

Scott J. wrote:

A nested set seems a little overkill.

It’s not overkill at all if you’re threading your comments.

Good point, and I hadn’t thought of that, but I don’t need nesting.

I realized I could make two polymorphic associations: one in Comments,
and one in the join table for commentable_subscribers.

I’m not sure if that’s a better plan or not, though. I like that I can
enforce foreign key constraints when I use the extra CommentGroup
table.

Why not keep CommentGroup and have a polymorphic association that will
bind it to either BlogPost or BugReport?

That sounds like the best plan. Thanks for the help.

Scott J. wrote:
[…]

Next: how can I DRY up those three has_many lines? I want to put
something like ‘acts_as_commentable’ in the model and have it do those
three has_many lines.

Define it as an ActiveRecord::Base class method.

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

On Oct 23, 11:11 am, Marnen Laibow-Koser <rails-mailing-l…@andreas-
s.net> wrote:

Define it as an ActiveRecord::Base class method.

Sure, but where? In lib/* somewhere? I’ll need it to get loaded before
any of these models get loaded. (Or I would have to add a ‘require’ to
each commentable model.)

Many thanks… #2 is what I was thinking, but I don’t understand how
lib/commentable.rb would get loaded in that case?

On Oct 23, 1:12 pm, Marnen Laibow-Koser <rails-mailing-l…@andreas-

Scott J. wrote:

On Oct 23, 11:11 am, Marnen Laibow-Koser <rails-mailing-l…@andreas-
s.net> wrote:

Define it as an ActiveRecord::Base class method.

Sure, but where? In lib/* somewhere? I’ll need it to get loaded before
any of these models get loaded. (Or I would have to add a ‘require’ to
each commentable model.)

You have a number of choices:

/lib/commentable.rb
module Commentable
def self.included(base)
base.has_many :subscriptions, :as => :subscribable
# and so on
end
end

/app/models/blog_post.rb
class BlogPost < AR::B
include Commentable

rest of class definition

end

/lib/commentable.rb

as in 1, but after the end of the module (or in environment.rb):

class ActiveRecord::Base
def self.acts_as_commentable
include Commentable
end
end

/app/models/blog_post.rb
class BlogPost < AR::B
acts_as_commentable
end

3 (probably not recommended).
/config/environment.rb
class ActiveRecord::Base
def self.acts_as_commentable
has_many :subscriptions, ;as => :subscribable
# and the other has_manys
end
end

  1. Write an acts_as plugin (but call it something else – I think
    acts_as_commentable is already taken as a plugin name).

Does that help?

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

Scott J. wrote:

Many thanks… #2 is what I was thinking, but I don’t understand how
lib/commentable.rb would get loaded in that case?

On Oct 23, 1:12 pm, Marnen Laibow-Koser <rails-mailing-l…@andreas-

Ah, good point – I’ve never done it that way, so I didn’t think of
that. Rails’ autoloading might take care of it, but in development mode
maybe not. I’ll have to check.

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

In most projects I’m using an initializer to load all lib/*.rb files,
like:

config/initializers/requires.rb:

auto-load all ruby files from RAILS_ROOT/lib/

Dir[File.join(RAILS_ROOT, ‘lib’, ‘*.rb’)].each do |file|
require file

Note, if you have any files you do not want auto-loaded, you certainly
don’t want to use that initializer :). Alternate option would be to
create a very simple plugin:

vender/plugins/acts_as_commentable/init.rb:

class ActiveRecord::Base
def self.acts_as_commentable
include Commentable
end
end

This should work as well.

  • lukas