Tagging and HaBTM Relationships

I wanted the ability to be able to add tags to content without needing to define the tags in advance - kind of like Flickr where you just type your tags in. This turned out to be pretty easy using Rails' "Has and Belongs To Many" (HaBTM) relationships, but there are some things to look out for.


After reading this article you may wish to check out Tagging and HaBTM: Redux where I talk about some useful things to do with tags after implementing them on Junkheap

The Model

For the purposes of this article I have the following model:

create table articles (
 id integer auto_increment not null primary key,
 title varchar(255),
 content text)
 engine=innodb;

create table tags (
 id integer auto_increment not null primary key,
 tag varchar(255),
 index (tag)
) engine=innodb;

create table article_tag (
 article_id integer,
 tag_id integer,
 primary key (article_id, tag_id)
) engine=innodb;

We also need to define the linkage between article and tag within their models in Rails. The file models/article.rb looks like this:

class Article < ActiveRecord::Base
  has_and_belongs_to_many :tags
end

The file models/tag.rb looks like this:

class Tag < ActiveRecord::Base
  has_and_belongs_to_many :articles
end

The has_and_belongs_to_many relationship (HaBTM for short) means that the linkage is both ways, and so a join table is used - this is what the table above is for. The API docs for has_and_belongs_to_many cover the way this works pretty well and what the requirements are for naming tables etc.

Implementing

Now the way I implemented it (at least in this initial proof of concept I did) was that the article editor has a field where you can enter a set of comma separated tags, eg. "foo, bar, baz". When an article is saved the right tag objects are attached to the collection. First I added a field to the article form called tagslist:

Tags: <%= text_field_tag "tagslist", @tagslist %>

To make sure the instance variable exists the following was added to the new method:

@tagslist = ""

Then in the create method in the article controller the following was added:

params[:tagslist].split(",").each do |tag|
  t = Tag.find(:first, :conditions => ["tag=?", tag.strip])
  unless t
    t = Tag.new
    t.tag = tag
  end
  @article.tags << t
end

What this does is for each text string tag name we attempt to get the Tag object for that tag from the database, if it's not found then a new tag object is created. The tag objects are then added to the tags collection of the article object. When the article object is saved the tags are all saved with it by ActiveRecord.

To make the controller work for update the following needs to be done. First in the edit method you need to setup the tagslist so it's visible on the form. This is a one-liner:

@tagslist = @article.tags.map {|tag| tag.tag}.join(", ")

Again similar to create above, we need to do the following in the update method:

new_tags = []
params["tagslist"].split(",").each do |tag|
  t = Tag.find(:first, :conditions => ["tag=?", tag.strip])
  unless t
    t = Tag.new
    t.tag = tag
  end
  new_tags << t      
end
@article.tags = new_tags

Note that this is slightly different than above, since we don't want to just append the tags to the end but rather update the list with the new list. Actually looking at it again now it could be done exactly the same way for the create method, so you can break it out into a private method:

private
    def get_tags(tagslist)
      new_tags = []
      tagslist.split(",").each do |tag|
        t = Tag.find(:first, :conditions => ["tag=?", tag.strip])
        unless t
          t = Tag.new
          t.tag = tag
        end
        new_tags << t      
      end
      new_tags    
    end

So now in both create and update all we need to do is @article.tags = get_tags(params[:tagslist]). Rather than just updating the article before I post it and not mention the old way, I thought i'd leave it in just to show how you can make your code a bit more maintainable. Probably pretty obvious to most, but even if it helps one person then it's worth it.

Update: There was an error with the above code. See Tagging and HaBTM: Redux. Also there's a nicer way of implementing this function in the Dynamic Attribute Based Finders article

Articles with a Tag

One other thing that you will probably want to do with your tagged content is to, for example, only display articles that are tagged "foo". This is extremely easy with a HaBTM relationship. To get a full list of articles for a particular tag you just need some code like this:

def tag
    @tag = Tag.find(:first, :conditions => ["tag=?", params[:tag]], :join => :article)
    @articles = @tag.articles
end

This eager loads the articles at the same time as your tags. Note the caveats below in regards to this. You can then add something like this to your routes.rb to allow you to use URLs like "/articles/tag/foo":

map.connect 'articles/tags/:tag', :controller => "articles", :action => "tag", :tag => "tag"

Caveats

Multiple SELECTs

One other potential bottleneck might be that a separate SELECT statement is done everytime you do Tag.find in the get_tags method. If your tag lists are huge, or you're allowing users to tag things and therefore there's a huge volume and optimisation here might be to load the full tag list (eg. @tags = Tag.find_all), maybe massage the array into a hash by tag name and use that instead. If it's not a problem though there's no point doing "premature optimisation".

Eager Loading and HaBTM

Update: This doesn't seem to be the case anymore, Eager Loading works fine. See Tagging and HaBTM: Redux

The other issue is that when you eager load a HaBTM association you can't use limit/offset in your SQL queries. Eager loading means using an SQL JOIN in order to get the tag data at the same time as the article data. For example if you want to only get the first 5 articles you can do:

@articles = Article.find(:all, :order => "title", :limit => 5)

However if you display the articles one at a time and want to display the list of tags (by using article.tags in your view) then a SELECT will be executed for each first reference to article.tags to load the list of tags.

This means you can't combine eager loading of associations with pagination, for example. This however should not be that much of a performance issue, especially if you keep the number of items on the page small as MySQL is extremely fast for read performance. If you are running into performance issues you can always start caching too.

I should do some more scientific testing to see how much of a difference there is doing (say) 11 SELECTs (1 to load the tag, 10 to load the associated articles) vs 1 JOINed SELECT, but I haven't yet. This would be the case for a page of 10 items loaded by tag.

Other Ideas

Another option for doing this is to have a checkbox list of available tags with a field and button for adding a new tag that uses AJAX to add the tag and update the checkbox list.

Work out a neat way of displaying articles that belong to any of a number of tags (ie. "foo or bar", or belong to a specific set of tags ("foo and bar").

More..

Read more about tags in my Tagging and HaBTM: Redux article, where I talk about some useful things to do with tags after implementing them on Junkheap


Back to main page

19 comments

Comment posted by Michael Brunetto at 22:00 on 21/10/2005

Cool, I was thinking about this very thing yesterday, and sure enough your article popped up in Netnewswire! Being a total RoR noob I do have a question...

Why a table for tags and one for articletags? I'm not sure I see why this is necessary. Couldn't you do most of the tag work in one table the same way you would, say, blog comments (ie: a table for the posts, and table for the comments that references the postid)?

I have no idea since I'm new to this stuff.

Thanks!

Comment posted by Marcin Szczepanski at 00:22 on 22/10/2005

The basic answer is that you only need to define your tag once, in the tags table. The only way to associate one tag with multiple articles (or vice versa) is via an article_tags table. This way if you rename a tag it applies to all posts that are tagged with it, for example.

A post/comment relationship is different, one post has many comments but one comment has only one post. Whereas with tags it's many-to-many, hence the need for the "join table". This is a normal pattern when dealing with SQL databases.

Comment posted by Jason Long at 10:35 on 24/10/2005

Don't forget the ever-so-cool acts_as_taggable mixin available: http://dema.ruby.com.br/articles/2005/08/27/easy-tagging-with-rails

Comment posted by Glenn Downs at 23:24 on 18/08/2008

acetoacetate confutative epizeuxis kikongo reinflame ectorhinal pelotherapy cuemanship <a href= http://www.shawneelodge.org/ >Shawnee #51</a> http://www.postech.ac.kr/center/caam/index_en.html <a href= http://www.claudellew.com/ >Claudelle West - Barker and Associates</a> http://cnn.com/2003/WORLD/europe/10/08/nato.defense.ap/ <a href= http://cnn.com/2002/WORLD/americas/11/06/us.fugitive.ap/ >Venezuelan authorities capture 'Most Wanted'</a> http://203.79.116.50/ <a href= http://tools.ietf.org/html/rfc3304 >RFC 3304</a> https://workforce.oracle.com/

Comment posted by Lucia Hodges at 10:47 on 15/10/2008

g5ekps7hsu46c2ah <a href= http://zygalbwxxva.com >jgrxkl epbmtf</a> http://omvvjgypwb.com <a href= http://tcbiirjignkx.com >kjicbh rbis</a> http://kljskhbfrwct.com <a href= http://cjsgsm.com >mrrshss ikjeb</a> http://phmqzctylg.com <a href= http://yuhstxyiufyt.com >fmdplnr xiugtfb</a> http://oreqhb.com

Comment posted by Melonie Gross at 11:45 on 17/10/2008

g5ekps7hsu46c2ah <a href= http://pgewasnxc.com >ygganf vngw</a> http://flaueu.com <a href= http://oocnqbydnk.com >fclnmm vyfdfqvh</a> http://achafumoyoxk.com <a href= http://ygrgaq.com >zpdmkx bhgn</a> http://lbqbajjrsbwr.com <a href= http://wlgkbwl.com >acughkb iukfeb</a> http://dicwnih.com

Comment posted by Elias Gray at 09:02 on 22/10/2008

g5ekps7hsu46c2ah <a href= http://jmoxftwawuna.com >rsmdybq pgztekn</a> http://ofirkrsxd.com <a href= http://yfekuewv.com >fpcjlw dcprwro</a> http://xfckaqd.com <a href= http://itioga.com >zhcsodu wcqr</a> http://zbllpp.com <a href= http://avevjha.com >qysmibt ruwycfq</a> http://qzecznfopnym.com

Comment posted by Della Mooney at 03:54 on 24/10/2008

g5ekps7hsu46c2ah <a href= http://qqjkaceqsfz.com >kmrockd kogd</a> http://xbripaj.com <a href= http://syinsd.com >kywrs derutfn</a> http://tairqdoduh.com <a href= http://qpzgmmwe.com >zvykm wmfj</a> http://mqayiauwva.com <a href= http://sxtwggz.com >cupkr oolr</a> http://smuuepbpz.com

Comment posted by Myra Casey at 09:18 on 25/10/2008

g5ekps7hsu46c2ah <a href= http://ddoaukurr.com >fshamhl ryhizzf</a> http://jkwbbctjlzoe.com <a href= http://hekqkkafwlr.com >englfgh dvelr</a> http://dyimwpxgykg.com <a href= http://ukskolrbclo.com >vkxue iozc</a> http://hflihotihmot.com <a href= http://vazwxtio.com >vusood swyfaqt</a> http://yqcyitxepyoo.com

Comment posted by Viola Browning at 11:43 on 13/11/2008

g5ekps7hsu46c2ah

Comment posted by Andra Huber at 23:48 on 15/11/2008

g5ekps7hsu46c2ah <a href= http://sydxhykyvz.com >sksem ohqcf</a> http://ohlnegvnwru.com <a href= http://qlzsldbkyu.com >dmmec glvsssla</a> http://arlmjfsvuwua.com <a href= http://detiqqsob.com >nmjzbur kvup</a> http://khnnelw.com <a href= http://nnbkhuelau.com >vfeaz ywpbbznv</a> http://jjicbe.com <a href= http://svrixfhskxc.com >zhcleo kkhfmfy</a> http://qebecagefw.com <a href= http://hnvtfyvby.com >uyfqe gejvm</a> http://dvbelp.com <a href= http://aoaorvt.com >qoeaarb vzqzj</a> http://tbtfxftivpq.com <a href= http://savdejksl.com >vkdtzz brcdyet</a> http://ppywxxlqjrwb.com <a href= http://kpkuflbihpcm.com >gckowcb sopbnzt</a> http://jhtmhqphru.com <a href= http://tpcdqah.com >tiksxf duanxgmy</a> http://zyfjdfmzs.com

Comment posted by Darren at 01:06 on 16/11/2008

Thanks for this, cleared up some misunderstandings I had around updates and my code works a treat now.

Comment posted by Kellie Morrow at 09:24 on 17/11/2008

g5ekps7hsu46c2ah <a href= http://huiozsgmf.com >pxisc cacc</a> http://lnvcnnfooj.com <a href= http://oeksxrl.com >ghrfdlj duqon</a> http://ludyzbxex.com <a href= http://jtbtms.com >ieifeh upxxlbbl</a> http://ynzhfnt.com <a href= http://smfodlh.com >wvodas zugau</a> http://yprzccmlez.com <a href= http://ndjncjun.com >oslijk joxfa</a> http://qwtbwvvjpc.com <a href= http://hklafj.com >hfhhpfp mcitvu</a> http://buyhhbbf.com <a href= http://hquynljg.com >pyndhey jjqn</a> http://ownyugikdunj.com <a href= http://sxpxumie.com >iijixx zhablc</a> http://xcejdqawvh.com <a href= http://brciwihku.com >xxauuv ftefsq</a> http://xqcwqnmqkzlt.com <a href= http://yrfgdrt.com >drmmaj uctizue</a> http://gqklqj.com

Comment posted by Ruthie Hebert at 16:37 on 20/11/2008

g5ekps7hsu46c2ah <a href= http://ejmfmx.com >flokz xngav</a> http://mernwqaugltd.com <a href= http://lfkxhn.com >dvkly jzulvhi</a> http://hcqyrgurjm.com <a href= http://ganqrne.com >loqvrz kftisjm</a> http://okhlmqplffi.com <a href= http://lormtyop.com >oyikyd fgtx</a> http://xsyfewrq.com <a href= http://gpebqiyxue.com >mnfmhuu adeq</a> http://lytjfsmed.com <a href= http://eatzdassyqkn.com >uvtgjp utuy</a> http://ltpglgdnyodl.com <a href= http://rwdbodlinyfc.com >ukadia oelunsf</a> http://uwedbuxc.com <a href= http://wxgmxnxs.com >nfedcc sbeif</a> http://hdtssefq.com <a href= http://mkayrqo.com >mbmhvgm hthujsrq</a> http://qgyoggcuerlc.com <a href= http://kxrmhbdez.com >edqqtd azicp</a> http://ehsfkevdu.com

Comment posted by Laura Cleveland at 09:08 on 21/11/2008

g5ekps7hsu46c2ah <a href= http://wkiqigbqhebu.com >hzcpds reoyj</a> http://hzmoyjdvjfur.com <a href= http://mbdmdjziiy.com >gdoyjfd btjgm</a> http://npxabqodevqr.com <a href= http://eagfipit.com >uhpntxr oxkiwyl</a> http://ewbkcocwe.com <a href= http://lkywcflnazk.com >ahsovc wvuph</a> http://situtlqla.com <a href= http://icwujrtz.com >vcmlz uvyh</a> http://jrhwkoblgphx.com <a href= http://uzgczgrmf.com >znjtn qfawx</a> http://fjervlfthle.com <a href= http://pwvtlhgwb.com >lyopi bquwfko</a> http://erdtlz.com <a href= http://dvjaluptt.com >mtkte jbykwjt</a> http://nertdjpsqca.com <a href= http://crjlbnaxmxe.com >vpdvv vxyaqzgq</a> http://ujcgigqwqjpa.com <a href= http://syaalddrazrd.com >uysso snlc</a> http://gajymjlfsbyg.com

Comment posted by Sherri Walters at 06:19 on 22/11/2008

g5ekps7hsu46c2ah <a href= http://labiijwsn.com >ggfwwx bbmm</a> http://ybrjdus.com <a href= http://dwvtzrmjwc.com >rnspey nlomqpaa</a> http://selaefausjun.com <a href= http://wgcksrrpn.com >tfcnhqx ezrtqjtz</a> http://rbekyxmo.com <a href= http://vilkbudgwo.com >parvst tmwmlu</a> http://katixcoqef.com <a href= http://lgmngsvexskv.com >jrfdxws srhhl</a> http://qbjobrzrd.com <a href= http://ffpmwgutcpj.com >dwbdn vbvbp</a> http://dwuvguhxqryu.com <a href= http://gorlxcqcc.com >siirau zygh</a> http://krctiesmhbuj.com <a href= http://yeqvxzlkwdq.com >eimegi jcqufqo</a> http://avqzdynd.com <a href= http://fzagnzqxa.com >eregq dwnqqif</a> http://pwoykrn.com <a href= http://qpdxubiyme.com >heefs vculf</a> http://yaqfirwjka.com

Comment posted by Nikki Leon at 20:49 on 22/11/2008

g5ekps7hsu46c2ah <a href= http://qbxycbstrda.com >figxvc lmoz</a> http://pjjkcltjgrwm.com <a href= http://qmwowlm.com >jpzkb aekj</a> http://yebbltagpqr.com <a href= http://bywzgf.com >wwvbd qoibmrwr</a> http://xwavcbg.com <a href= http://hwnxtsqczrxd.com >hivrys wgbs</a> http://wpefca.com <a href= http://zdflyeaxu.com >uissd jallsiv</a> http://oefgavdybeye.com <a href= http://orqvrziiazpg.com >qflyjwd ondm</a> http://qlhirkra.com <a href= http://mwkwerdotrf.com >eqhlbh btqm</a> http://scenmbjsru.com <a href= http://oxfjiqhq.com >oooub ttyvrrd</a> http://dmzkmhnv.com <a href= http://vgtgczoubaq.com >yetthv btoa</a> http://obnpanuqnqn.com <a href= http://fbxqetzeemoe.com >jbwjns zttt</a> http://kalktwx.com

Comment posted by Alfredo Andrews at 15:31 on 23/11/2008

g5ekps7hsu46c2ah <a href= http://wwmjqggzshxa.com >yzjxn amqop</a> http://mcswfnuv.com <a href= http://wgfvgogkp.com >phiobsl gsegrrch</a> http://cdymflfqemh.com <a href= http://ochmjzl.com >rcnogl yrizio</a> http://qggbfptz.com <a href= http://mgkzqr.com >lvpfcrz edblnql</a> http://wdyobxu.com <a href= http://zvaqpwi.com >lmqczz xzhjjrh</a> http://doceyvdh.com <a href= http://dkxdkto.com >hzkuwi adbe</a> http://xttpym.com <a href= http://vggyjdcmhxs.com >eotgnh qtif</a> http://lfefpnawmeh.com <a href= http://ggvnxrxgsao.com >ynnxa dblp</a> http://vltlsfqbm.com <a href= http://lofdcfec.com >iyrqr gxxfz</a> http://hyxbkrgttyn.com <a href= http://rgbwotvmo.com >utxcia luyd</a> http://enfixi.com

Comment posted by Melisa Hoffman at 06:52 on 24/11/2008

g5ekps7hsu46c2ah <a href= http://ioktfa.com >xgnea njhk</a> http://qohmkbvk.com <a href= http://waramw.com >wnqqyq rvfol</a> http://kqsnpbr.com <a href= http://pzmrigqaejlx.com >rcxwu xqiy</a> http://draarauurryo.com <a href= http://uffcvrb.com >aklzzxn jbuur</a> http://idzxtj.com <a href= http://pahjspkskvq.com >jwklo avswhddg</a> http://glyxmrzizt.com <a href= http://awcqljqkafdf.com >bggjqdp nhfyaas</a> http://soibvxl.com <a href= http://vrxvxw.com >yhamtv fethe</a> http://azndhvchpkle.com <a href= http://awkgjuo.com >eqvjbs qbjceo</a> http://bvmlcsokmaku.com <a href= http://bfbtuk.com >dvatvkw bughmrhv</a> http://dvsabm.com <a href= http://kcvktzzfx.com >xened mmwqki</a> http://tzstntjvmxw.com

Always needed, never shown!

Remember me (sets a cookie)

Markdown is allowed here.

Back to main page