06 Adding tagging support

簡介

由於BLOG涵蓋各種不同類型的文章,導致會越來越難以尋找。為了加快速度查詢,我們將按照分類來排序,這個時候,必須增加標籤(Tagging)選項功能。


The Tag model object

我們將新增Tag Class model,這個方式很容易:

package models; import java.util.*; import javax.persistence.*; import play.db.jpa.*; @Entity public class Tag extends Model implements Comparable<Tag> { public String name; private Tag(String name) { this.name = name; } public String toString() { return name; } public int compareTo(Tag otherTag) { return name.compareTo(otherTag.name); } }


因為我們總想要運用更方式的方式來建立標籤,我們總是會運用findOrCreateByName(String name)工廠方法(factory method)。因此,試著新增到Tag Class:

public static Tag findOrCreateByName(String name) { Tag tag = Tag.find("byName", name).first(); if(tag == null) { tag = new Tag(name); } return tag; }

Tagging posts

現在,就將Tag model與Post做多對多關聯。讓我們增加正確的關聯模式在Post class中:

... @ManyToMany(cascade=CascadeType.PERSIST) public Set<Tag> tags; public Post(User author, String title, String content) { this.comments = new ArrayList<Comment>(); this.tags = new TreeSet<Tag>(); this.author = author; this.title = title; this.content = content; this.postedAt = new Date(); } ...

注意,我們在這裡使用一個TreeSet是為了保持在有序的排列方式(按字母順序排序,事實上,之前已經實作compareTo)。

我們將保持這種單向關係。為此,增加了一些輔助方法,使標籤管理更容易。首先,標記文章一組標籤:

... public Post tagItWith(String name) { tags.add(Tag.findOrCreateByName(name)); return this; } ...

然後,可以獲取所有特定文章標籤:

... public static List<Post> findTaggedWith(String tag) { return Post.find( "select distinct p from Post p join p.tags as t where t.name = ?", tag ).fetch(); } ...


A little more difficult now

我們現在還不會在BLOG使用,但是如果我們想要檢索文章有幾組標籤,它看起來更加困難。試著嘗試JPQL查詢,因為你可能會用到它在多個Web專案中:

... public static List<Post> findTaggedWith(String... tags) { return Post.find( "select distinct p from Post p join p.tags as t where t.name in (:tags) group by p.id, p.author, p.t         itle, p.content,p.postedAt having count(t.id) = :size" ).bind("tags", tags).bind("size", tags.length).fetch(); } ...

最棘手的部分是我們必須使用一個計數過濾器(count statement filter)可過濾出文章所有標籤並加入至網頁。

注意,我們不能使用Post.find(“…”, tags, tags.count)。這是因為標籤已經是一個vararg


The tag cloud

我們必須要一個標籤雲(tag cloud)。讓我們增加一個方法在Tag Class來產生標籤雲。

public static List<Map> getCloud() { List<Map> result = Tag.find( "select new map(t.name as tag, count(p.id) as pound) from Post p join p.tags as t group by t.name order 
by t.name" ).fetch(); return result; }


Adding tags to the Blog UI

我們可以使用標籤的方式來瀏覽BLOG。一如往常,為了提高工作效率,我們需要增加一個標籤,修改 /yabe/conf/initial-data.yml檔案,以增加一些標籤,例如:

... Tag(play): name: Play Tag(architecture): name: Architecture Tag(test): name: Test Tag(mvc): name: MVC ...

然後增加它們來聲明文章:

... Post(jeffPost): title: The MVC application postedAt: 2009-06-06 author: jeff tags: - play - architecture - mvc content: > A Play ...

增加標籤聲明在YAML檔案上方,因為他們需要建立在Post引用它們之前被產生出來做關聯。你需要重新啟動你的應用程式載入YAML檔案並初始化。注意,Play會告訴你有關YAML檔案的相關問題:




然後修改#{display /}來設定文章標籤為full時顯示。編輯/yabe/app/views/tags/display.html:

... #{if _as != 'full'} <span class="post-comments"> &nbsp;|&nbsp; ${_post.comments.size() ?: 'no'} comment${_post.comments.size().pluralize()} #{if _post.comments} , latest by ${_post.comments[0].author} #{/if} </span> #{/if} #{elseif _post.tags} <span class="post-tags"> - Tagged #{list items:_post.tags, as:'tag'} <a href="#">${tag}</a>${tag_isLast ? '' : ', '} #{/list} </span> #{/elseif} ...




The new ‘tagged with’ page

現在我們可以增加新的方式來依照標籤列出BLOG相關文章。我們將更改超連結來對應listTagged action:

... - Tagged #{list items:_post.tags, as:'tag'} <a href="@{Application.listTagged(tag.name)}">${tag}</a>${tag_isLast ? '' : ', '} #{/list} ...

在Application controller建立一組action方法:

... public static void listTagged(String tag) { List<Post> posts = Post.findTaggedWith(tag); render(tag, posts); } ...

一如往常,我們必須建立一個具體的URL路徑:

GET /posts/{tag} Application.listTagged

接著,這裡出現一個問題,因為現有的URL路徑發生衝突,這兩條路徑已匹配相同的URL路徑:

GET /posts/{id} Application.show GET /posts/{tag} Application.listTagged

但是,由於我們假設一個ID是數字而一個Tag則不是,我們可以很容易解決這種情況並使用正則表達式來限制第一條路徑:

GET /posts/{<[0-9]+>id} Application.show GET /posts/{tag} Application.listTagged

最後,我們需要建立/yabe/app/views/Application/listTagged.html 模版來搭配listTagged action:

#{extends 'main.html' /} #{set title:'Posts tagged with ' + tag /} *{********* Title ********* }* #{if posts.size() > 1} <h3>There are ${posts.size()} posts tagged '${tag}'</h3> #{/if} #{elseif posts} <h3>There is 1 post tagged '${tag}'</h3> #{/elseif} #{else} <h3>No post tagged '${tag}'</h3> #{/else} *{********* Posts list *********}* <div class="older-posts"> #{list items:posts, as:'post'} #{display post:post, as:'teaser' /} #{/list} </div>




Comments