The acts_as_taggable
plugin, although built in to Rails, is basically deprecated. And the gem version is old and requires a separate join table for every taggable model, which is silly.
update
Please see the updated has_many_polymorphs documentation, which has instructions for using the new, built-in tagging generator.
install
Uninstall whatever acts_as_taggable
version you have, to prevent fighting among the plugins. Then, install has_many_polymorphs
:
script/plugin install -x svn://rubyforge.org/var/svn/fauna/has_many_polymorphs/trunk
models
We will assume you are using the default acts_as_taggable
models. You should have a Tag model, with a name
field, as well as a Taggings join model. However, these models are part of the acts_as_taggable
plugin source, whereas their migrations have to be part of your app (which is confusing). Instead, with has_many_polymorphs
, we have to make the models part of the app, too:
class Tag < ActiveRecord::Base
has_many_polymorphs :taggables,
:from => [:books, :magazines],
:through => :taggings,
:dependent => :destroy
end
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :taggable, :polymorphic => true
def before_destroy
# disallow orphaned tags
tag.destroy_without_callbacks if tag.taggings.count < 2
end
end
See the line [:books, :magazines]
? This line replaces all the acts_as_taggable
macro calls strewn through your models. Simply list in the array the models that you want to be able to tag. (You can even tag Tags, see below.)
migrations
We need to make sure your existing tag schema fits this. It should be something like:
class AddTagSupport < ActiveRecord::Migration
def self.up
create_table :tags do |t|
t.column :name, :string, :null => false
end
add_index :tags, :name, :unique => true
create_table :taggings do |t|
t.column :tag_id, :integer, :null => false
t.column :taggable_id, :integer, :null => false
t.column :taggable_type, :string, :null => false
end
add_index :taggings, [:tag_id, :taggable_id, :taggable_type], :unique => true
end
def self.down
drop_table :tags
drop_table :taggings
end
end
If your schema isn’t like this already, you have two choices. You can add more options to the association macros in your models, to show them how to relate to your schema, or you can write a migration to convert your schema into canonical form. The first option is possibly quicker to implement, but the second one will be easier to maintain.
api
Hey, you’re done! Well, not really. The API is different, which is the biggest sticking point. We’ll write some convenience methods to mimic the old way. You should put the methods in RAILS_ROOT/lib/tag_extensions.rb
or similar.
class ActiveRecord::Base
def tag_with tags
tags.split(" ").each do |tag|
Tag.find_or_create_by_name(tag).taggables << self
end
end
def tag_list
tags.map(&:name).join(' ')
end
end
Pretty straightforward, which makes it easy to modify. For example, to make creating a model from params
more transparent, we could add:
alias :tags= :tag_with
Or to enforce lowercase tags, just do:
def tag_with tags
tags.downcase.split(" ").each do |tag|
Tag.find_or_create_by_name(tag).taggables << self
end
end
If you want to allow tags with spaces in them, we can accept an array instead of a string:
def tag_with *tags
tags.flatten.each do |tag|
Tag.find_or_create_by_name(tag).taggables << self
end
end
To delete tags (don’t forget to downcase
the tag_string
if you need to):
def tag_delete tag_string
split = tag_string.split(" ")
tags.delete tags.select{|t| split.include? t.name}
end
To get all models for a tag (and all in a single SQL query, thanks to the plugin):
Tag.find_by_name("artichoke").taggables
To get only a specific model for a tag:
Tag.find_by_name("artichoke").books
Easy and powerful. And if you need to find which tags are most popular, see here.
Make sure that you require
the file containing your API methods in environment.rb
, since Rails won’t load it automatically:
require 'tag_extensions'
performance note
It is more efficient if we add :skip_duplicates => false
to the has_many_polymorphs :taggables
call. Then the taggables
for each Tag won’t get loaded at all during the <<
, because there is no reason to check them.
If we do this, though, and use database constraints to enforce uniqueness, we need to manually rescue
assignment errors in our tag_with
method:
def tag_with tags
tags.split(" ").each do |tag|
begin
Tag.find_or_create_by_name(tag).taggables << self
rescue ActiveRecord::StatementInvalid => e
raise unless e.to_s[/Duplicate entry/]
end
end
end
self-referential tagging
What if you want to be able to tag tags? This is more useful than might first appear. It lets you create an ad-hoc non-exclusive hierarchy of categories without extra models. This is much more maintainable than one based on hard-coded models for each level. But you need some way to distinguish “the tags tagged by a tag” and “the tags a tag is tagged by”—a directed graph. So we will rename the parent relationship:
class Tag < ActiveRecord::Base
has_many_polymorphs :taggables,
:from => [:books, :magazines, :tags],
:through => :taggings,
:dependent => :destroy,
:as => :tagger
end
class Tagging < ActiveRecord::Base
belongs_to :tagger,
:class_name => "Tag",
:foreign_key => "tagger_id"
belongs_to :taggable,
:polymorphic => true
end
Modify the migration accordingly. Now, you can use some_tag.taggables
to get the targets of some_tag
, and some_tag.taggers
to get the tags for which some_tag
is itself a target.
Note that your tag_delete
method might have to be more longwinded due to this long-outstanding Rails bug. (Rails 1.2.3 might be ok now; it’s unclear.)
wrapping up
You should find, once you complete the move, that tag lookup is faster, that the API is more intuitive and in line with regular ActiveRecord, that database compatibility is improved, and that the subsystem is more extensible. Quite a benefit!
Since has_many_polymorphs
is basically a huge superset of the acts_as_taggable
features, there is not much that isn’t supported. Many people use has_many_polymorphs
in production for tagging already. Join them!
acts_as_polymorphic_taggable
?Hey, this is just what I needed.
Chris, I thought about that, but I think most people need to eventually customize the way
_taggable
works. So I would rather not lock them down—even if only in appearances.And I dislike publishing arbitrary APIs, like
_taggable
does. That’s not learning; that’s memorization.First off, this looks like a great plugin—no more need to enumerate every reverse
has_many
/belongs_to
relationships between tags and their various taggable models (e.g.tag.posts
,tag.comments
, etc…).In the self-referential tagging paragraph, however, shouldn’t it be:
:from => [:books, :magazines, :tags]
instead of::from => [:books, :magazines]
Otherwise, how does the some_tag in
some_tag.taggers
know what to look up? From what I understand, adding:tags
to the:from
array makes the Tag class a polymorph so that you can tagsome_tag
withanother_tag
whereby the taggings table stores:1.
some_tag
as ataggable
(e.g.taggable_id = some_tag.id
&taggable_type = some_tag.class
) and, 2.another_tag
as atagger
(e.g.tagger_id = another_tag.id
)Is this completely wrong? If it is, could you clarify and expand that part of the post for this noobie? Thank you.
You’re exactly right; my oversight.
While we’re on the topic, when you have the self-reference defined as above, you get the following methods on Tag:
Tag#taggers
parent Tags of an instance
Tag#taggables
all polymorphic children of an instance (Books, Magazines, and Tags)
Tag#tags
only child Tags of an instance
Without the
:as => :tagger
key, there is no way to distinguish child Tags from parent Tags, and_polymorphs
will throw an error if you include:tags
in the:from => []
array.Tag#tags
is not specially named (child_tags
, etc.) because it needs to parallelTag#books
andTag#magazines
.People always find this to be the most confusing part, but I’m not sure there’s much I can do about it. Self-reference is kinda confusing by nature.
Your plugin (and its use for tagging) have solved two problems for me in one day!
Thanks!
Thank you for the quick reply. I thought I was missing something.
If you were to add a
user_id
to thetaggings
table above to enable sorting by user, e.g.:how would you go about filtering a tag’s taggables (or a subset of taggables such as books or magazines) using a specific
user_id
? Can we add something akin to:conditions =>
to thehas_many_polymorphs :taggables
declaration in the Tag model to make this work?If there is a workable method to achieve the preceding, can we also use this method to filter one association with one criterion, say
Tag#books
withtaggings.user_id
, and another association with a different criterion, sayTag#magazines
, withtaggings.created_on
? Scratching head?!?You can do that on the individual collections:
>> tag.books.find(:all, :conditions => 'taggings.user_id = 2')
You can’t yet use a custom finder against the polymorphic collection because of the way the proxy object works. I plan to fix that in a couple of weeks.
For now you can find against the join model and map:
>> tag.taggings.find(:all, :conditions => 'user_id = 2').map(&:taggable)
but it’s not efficient for large collections because the map makes it O(n) instead of O(1).
Thank you. I’m still learning the ins and outs of this very handy plugin and I was hoping there might be a magic incantation that I’d yet to discover. ;-)
Evan, rather than adding to ActiveRecord::Base, could you not create a mixin and reference it in every class you wish to tag?
Do you mean that you would reference the mixin in every child class of the relationship, or only in the parent class?
I was planning to do it in every child class, but that isn’t very DRY is it?
This forum thread discusses what an approach like that might entail. It’s kind of a toss-up, but to me it makes a more maintainable app if the details of the relationship are specified in a centralized place.
Thanks for the link Evan – it explained what I was trying to do. My app is in a state that I probably won’t need to use the convenience methods as you’ve described but code my own in the Tag model. Thanks again.
A few things..
1. The has_many_polymorphs page says version 22 was released Jan 2006 instead of 2007
2. The code in the self-referential tagging section should say
:as => :tagger
, not:as => tagger
3. With the self-referential setup, Rails doesn’t know to eager-load the Tag model:
4. Where would you recommend putting the API code. I currently have it in
environment.rb
, but this feels wrong.5. How would you go about removing tags. Is there a better way than what I came up with.
1 and 2: Fixed, thanks.
3. Missing methods is a common problem; it’s an inversion of control issue. Polymorphs dynamically adds methods to the child classes at runtime. But it can’t kick in until the Tag model makes the
has_many_polymorphs
macro call, and Rails doesn’t like to load a model until the last possible minute, which in this case is too late. One fix is to reference theTag
constant, all on its own, inapplication.rb
. This ensures that the polymorphic methods will get injected on every request.Read this for more details and an alternate solution, especially when working in the console.
4. I recommend making a file in
/lib
calledactiverecord_base_tag_extensions.rb
and requiring that inenvironment.rb
. If you don’t mind being a little dirty you could just stick the code after the Tag class declaration inapp/models/tag.rb
. (If adding the methods ontoBase
seems too invasive, you could put the methods in a module and manuallyinclude
that module in each of your taggable classes.)5. How about:
And then in the Tagging model:
Note that ActiveRecord already has an internal method called
remove_tags()
which can cause trouble.Hi
I have the same issue as no. 5 in the last comment. In this I mean that I have still have the entry in the
taggings
table withtaggable_id == 0
.Then I tried the above fix but I can’t seam to get the
after_save
to react on thetags.delete
.Any suggestions?
Ok. We need to add
:dependent => :destroy
to thehas_many_polymorphs
call and usebefore_destroy
.The situation is a bit confused.
.delete
on ahas_many :through
skips join record save callbacks. Its default behavior is to nullify the foreign key (in this case in the join record), even though this doesn’t make sense with ahas_many :through
(because a join record is useless without both keys). With MySQL, if you nullify a:null => false
integer field, it assigns zero instead. But if we add:dependent => :destroy
to thehas_many_polymorphs
call (which will only affect the join record, and not the association target), then thebefore_destroy
callback will get hit, and subsequently the join record will get destroyed.I’m gonna update the examples above.
Changing the
tags.delete
totags.destroy
also seems to work. Thedelete
method issues a direct SQL query, whereas thedestroy
method ensures that the before/after_save methods are invoked.While debugging, I discovered this interesting warning your code triggers:
Unfortunately there is no information on the provided link yet.
Finally, how would I prevent caching as such:
Upon further inspection,
destroy
does not produce the desired results. Usingdelete
with:dependent => :destroy
works.I guess in Rails 1.2 we’re supposed to use
taggings.count
instead.Regarding the caching, you need to be able to reach the
.reload
method on the.tags
collection somehow. This will be difficult if you’ve overridden.tags
with your own method. But otherwise, just adding atags.reload
in thetag_with
oradd_tags
method will clear it up. Same goes fordelete_tags
. I didn’t include them above because in most cases it doesn’t matter if the instance goes slightly stale within the action, so there is no reason to force another database hit.You could implement your own mini-cache within the class to keep
tag_list
synchronized if this is critical to you.Using the code now posted, I’m getting an infinite recursion:
If I’m not wrong, I think the
before_destroy
in the tagging is calling thetag.destroy
, which is then calling thetagging.destroy
(and so on), or am I just not seeing something?I thought about that possibility, but it worked in my console so I thought ActiveRecord was smart enough to avoid it. Does
.destroy_without_callbacks
work for you instead?Yep, that works fine over here.
Given a list of tags, I wrote two functions to return all associated Places:
These are elegant, but far from optimal. I’m not sure where to start looking in terms of speeding these functions up. Should I run straight SQL queries instead, or is there a cleaner way?
Also, to get counts of all tags, I have:
Aman, I was just talking with David Bluestein about these same issues.
For getting counts of tags, make sure you see this.
You are right that the combinatorics are bad for finding subsets. Caching can mitigate a lot of this. I had basically the same solution as you, but shorter:
Once some upcoming changes to the plugin are in place, I should be able to give you O(1) methods instead of O(n).
Note that both
&
and|
are set operations, so you don’t needuniq
.Thanks Evan, looking forward to the has_many_polymorphs update.
With the self-referential tagging setup, the tagging model doesn’t work as expected.
Everything so far works wonderfully. Now, if I try to use the Tagging model to find tagged tags:
You need
:class_name => "Tag"
and:foreign_key => "tagger_id"
keys in thebelongs_to :tagger
macro intagging.rb
. My fault.Hey Evan,
I like what you’ve got going here, but I’m just too thick to ‘get it’.
First off, I’m trying to get
tag_with
andtag_list
to work but I can’t seem to do it. All I get is “undefined method `tag_list'
” I put the code after the class Tag declaration as well as in a separate file in/lib/
but I still get the error.Second, I’m trying to figure out the self-referential Tagging bit but I don’t know how to “Modify the migration accordingly”.
Lil help? Thanks!
Thanks for the help in IRC Evan.
For others’ benefit:
I got the Self Referential migration figured out (replace the
tag_id
column withtagger_id
)Once that was in place,
tag_list
needs to be changed to:And don’t forget to reference ‘Tag’ when playing in the console!
Evan—I’m new to this, sorry. Where in my app do I put the
tag_with
andtag_list
methods?In the tags model?
Thomas, see my response to Aman, above.
Thank you, evan.
Ran an svn up today and the updates seem to have broken something:
I can’t seem to reproduce your issue. What version of Rails are you on? Anything else unusual about your environment? If you incrementally roll back, can you find a prior revision number that works?
Found the problem:
Another plugin with files of the same name.
Is this an ActiveSupport::Dependencies bug?
Hey, good spelunking. I wouldn’t really say it’s Rails a bug necessarily.
require
tracks loaded dependencies based on the string by which they were requested, soclass_methods.rb
only gets loaded once if it is not identified by a more explicit path. Sinceacts_as_ferret
starts witha
, its folders came first when ActiveSupport::Dependencies built up the load paths (of which there are a lot).See if the latest svn works for you.
Latest svn works without any problems. Thanks.
Should I be able to do O(1) searches now?
Still in progress. I’ll make a big blog post when it’s ready.
I was wondering what the “before_destroy” method for the Tagging class was doing exactly.
And also why it was ”< 2” not ”< 1”
Brian, the
before_destroy
prevents you from having Tag records that aren’t associated with anytaggable
.Each time a Tagging relationship is about to be deleted, the callback is run, and checks if it is the only tagging for that Tag. (Since the tagging we are deleting still exists at this point, we have to check that there are not two, rather than not one.) If it finds it is the only relationship left, it deletes the Tag before allowing itself to get deleted.
The
_without_callbacks
prevents the infinite loop Giao mentions above.Thanks Evan, for the walkthrough, and a great plug-in!
With the following code:
I get this error:
I’ve defined
tag_names=
andtag_names
methods instead oftag_with
andtag_list
. If I dop.save
before I calltag_names=
, then it works. So is that just the way it works, you have to have a saved taggable before you can tag it? That’s kind of a limitation if you are making a form that allows a user to create a taggable object and tag it at the same time, but I suppose I could just save the object and then update it with the tags if I have to.This is a limitation inherited from
has_many :through
:I recommend just saving the record first before associating. It is technically possible to add
build()
support to the plugin, which is what is at issue here, but it’s a lot of work for little benefit.Is this functionality critical to you?
Don’t know if I correctly understood this polymorphic thing.
It will not be possible to use a foreign key for
taggable_id
, since it will be receiving ids from all my taggable models, is this correct?If so, are there some other thing that can be done to guarantee or maximize referential integrity?
When I use the following to save an article and its tags:
What I do to validate tag input? I tested validate methods inside tag model, but they seem not work.
James, you can use Rail’s validations and callbacks, as well as
:dependent => :destroy
, to guarantee that you can’t get invalid records in the Tagging column. How exactly it needs to work will be dependent on your particular app, though. You might be able to use a stored procedure to implement a fancy conditional constraint in your database itself, but I doubt it’s worth the effort.Fmc, what exactly doesn’t work about it?
I keep getting:
What version of Rails is this plugin/gem compatible with?
I’m also using ferret so not sure if that is causing a conflict as it did with Aman Gupta.
Our app is running Rails 1.1.6 and I have tried both the gem and the plugin of has_many_polymorphs (version 27).
Cheers
Version 24 was the last version that supported Rails 1.1.6 (there is a changelog in the plugin’s
README.txt
which mentions this).What’s keeping you from upgrading? Just curious.
Cheers Evan…I should have checked the changelog…
Basically we have developed a monster of an application at work and the test suite is enormous…Rake on our CI server takes around 45 minutes…
We have a lot of assert_tag statements in our functional tests and deadlines looming…Basically the overhead involved in correcting our tests once switching to 1.2.x is enormous…
Once we hit these deadlines and start decoupling our app into manageable chunks we’ll definitely look to upgrade…
I’ll give version 24 a go…
As of version 27.1 of the plugin, you don’t have to worry about referencing or requiring the parent class (Tag) before you use the injected methods. The plugin should auto-discover and preload it.
I’m always getting the
undefined local variable or method `tags'
error when I usetag_list
without calling Tag someway.For example:
And in the view:
The exception is raised. But if I just put Tag in the controller, it works:
Is this common behavior?
Fernando, there was a bug related to turning on the
has_many_polymorphs_cache_classes
option inconfig/environments/development.rb
. This is now deprecated in 27.2; please use Rails’ regularconfig.cache_classes
(not in theafter_initialize
block) if you don’t want model reloading.Hopefully this resolves the issues. Rails’ dependency code is byzantine.
If the behavior persists for you, please post on the forum with details about your Rails environment, etc., and we’ll work it out.
Just an update…
We’ve now almost completed the upgrade to Rails 1.2.3.
Thanks to
cruisecontrol.rb
andhas_many_polymorphs
amongst a few other factors we’ve convinced the powers that be that the upgrade was worth the effort.Cheers.
I couldn’t get the migration to run, with this error:
Mysql::Error: Table 'writr_development.taggings' doesn't exist:
But it turned out the problem had something to do with Hobo—I removed the
tag
andtagging
model files temporarily and the migration ran.Hi Evan—
I installed and got the nice message:
However, I tried it and got this error:
and switching ‘generator’ to ‘generate’ got:
YAChris: it should be
./script/generate tagging ...
That is just a typo.
James: thanks, true enough, but that doesn’t work either, alas.
Hey all… I’m gonna try to work on the tagging system tonight, and finish the generator. Watch for a blog post shortly.
In other news, it’s probably better to post problems on the forum.
I get the following error when I use the technique described above for tagging:
You’re not using Rails 1.2.3, which is required.
My bad—I’ve fixed it.
Thanks Evan!
Like Noobie, I want to have tagging to belong to an user. I modified the models accordingly but now I have to modify tag_with to be called with one more argument to something like:
But I don’t know how to do it. Thanks for any suggestions.
So I have an input field on my model for tags. If I put in “outdoors water” and click save, the tags save just fine.
If I then edit it and add “outdoors water summer”, the ‘summer’ tag is added. But if I edit again and remove a tag: “outdoors summer”… the “water” tag still remains.
Do you have a “best practice” to deal with this issue?
Is there any reason why taking the taggable code from the generator and placing it into
vendor/plugins
should not work? I’m requiringtagging_extensions
ininit.rb
and the models are found okay, but I’m still getting the messageSomeModel is not a taggable model
.Thanks!
Nice work. Seems like a lot of extra functions I need to write just to get tagging working with basic features that either the
acts_as_taggable
gem or plugin already have, though. Like related tags, for example.LLG, I found a post on RubyForge which solves your user problem:
It seems that I am not able to create tag that are numbers. Any hints?
Notice that the last select statement is using tags.`id` instead of tags.`name`:
Lei, if you’re using the generator, it’s because of this line in
tag_cast_to_string()
:Just remove it, and you should be fine.
It’s better to post questions on the forum these days. Someone is more likely to help you sooner.
I’m trying to add tagging to my little camping app, but I think I’m having trouble with the gem verson of
has_many_polymorphs
.The end of the trace looks like
Link is a model name which I want to add tagging to. Maybe it has something to do with the different naming conventions in Camping? Please let me know if there’s anything I can do to make this work.
Assign all your models to toplevel constants, and it should be able to figure it out: