Managing Content through Tagging in Grails: Part 1

0
72
8 min read

Add basic tagging

Tagging is a loose, community-based way of categorizing content. It allows a group of people to categorize by consensus. Anyone is able to tag a piece of content. The more a tag is used, the more meaning it takes on and the more widely used it becomes. This categorization by consensus has been dubbed as folksonomy (http://en.wikipedia.org/wiki/Folksonomy)

So let’s get started by building our tagging support.

Tagging domain model

When implementing tagging in our system, we need to consider the following:

  • We must be able to have many tags in our system
  • We must be able to associate a single tag with many different files and messages
  • We need to make sure that new domain objects can be easily tagged without having to change the tagging logic
  • We want to know when a domain object was tagged

To satisfy these requirements, we need to create the following new domain classes:

  • Tag—to store the name of the tag. There is one instance of this class per unique tag name in the application.
  • Tagger—to store the relationship from domain objects to a tag. This allows us to store the date a tag was added to a domain object.

Let’s create these domain classes and then write a test to prove that we can tag a message using this tagging structure.

The Tag class

We are going to separate the tagging classes out from our application domain classes. Create a folder under grails-app/domain called tagging. This is where we will put the domain model to implement tagging.

Our Tag class is extremely simple and holds only a name property:

package tagging
class Tag {
String name
static constrains = {
name( blank: false )
}
}

The Tagger class

The next class that we are going to create is the Tagger class. In relational terms, this object represents a link table between a Tag and any other domain class. It is important that the relationship between tagged domain classes and the Tagger relationship class is unidirectional. By this, we mean the domain classes are allowed to know that they can be tagged, but tags do not know which domain classes can be tagged, otherwise every tagged domain class would need a special relationship class.

Create the Tagger class as a domain class in the tagging package as follows:

package tagging
class Tagger {
Tag tag
static constraints = {
tag( nullable: false )
}
}

The basics of our tagging model are complete! We now need some logic to allow tags to be created. Create a new service class called TagService under grails-app/services/tagging, as shown below:

package tagging
class TagService {
boolean transactional = true
def createTagRelationships(String spaceDelimitedTags) {
return spaceDelimitedTags?.split(' ')?.collect { tagName ->
createTagRelationship( tagName )
}
}
def createTagRelationship(String tagName) {
def tag = Tag.findByName(tagName)?:
new Tag(name: tagName).save()
return new Tagger( tag: tag )
}

This service provides two utility methods to create new relationships by tag name or by a space delimited string of tag names. The important behavior of these two methods is that they do not allow duplicate tags to be created in the application. If a tag name already exists, the tag will be retrieved from the database and used as the tag in the relationship.

Notice that the createTagRelationships method is using the collect method to simplify what would normally take a few more lines of code to achieve. The collect method is dynamically added to any object that can be iterated over. For example, collections, arrays, strings and so on. It takes a closure as its argument and executes this closure for each item in the collection. The return value from each execution of the closure is added to a new collection that the collect method builds up and then returns once it has finished iterating the original collection.

In createTagRelationship, we are using another neat language feature of Groovy called the “Elvis operator”. It is named so, as it looks like Elvis’ hair style. This is a shorter version of the normal Java ternary operator. If the operand being checked is true then the checked operand will be returned as the default, otherwise the alternative operand will be used. So in our example:    

def tag = Tag.findByName(tagName) ?: new Tag(name: tagName).save()

If a tag can be found from the database then it is used, otherwise a new tag is created.

Tagging a message

The next step is to allow a message to be tagged. Write some integration tests to make sure the relationships are working before using tagging in the application.

In the folder test/integration/app, create the file TaggableIntegrationTests.groovy and add the following code:

package app
import tagging.Tag
class TaggableIntegrationTest extends GroovyTestCase {
User flancelot
protected void setUp() {
flancelot = User.findByUsername('flancelot')
Tag.list().each { it.delete() }
Message.list().each { it.delete() }
}
}

The code above sets up the test data needed to create messages and associate tags to messages. Remember that the flancelot user already exists because it was created by the BootStrap class.

The first test will determine that we can add tags to a message and then retrieve messages by tag. Add the following test method to your test class:

void testCanRetrieveMessagesByTags() {
Message message = new Message(user: flancelot, title: 'tagged',
detail: "I've been tagged.").save(flush: true)
Message secondMessage = new Message(user: flancelot,
title: 'other tagged',
detail: "I've been tagged.").save(flush: true)
message.addTag('urgent')
message.addTag('late')
secondMessage.addTag('urgent')
def taggedMessages = Message.withTag( 'urgent' )
assertEquals(2, taggedMessages.size())
assertEquals(2, Tag.list().size())
def secondMessages = Message.withTag( 'late' )
assertEquals(1, secondMessages.size())
assertEquals(2, Tag.list().size())
}

The test above does the following:

  • Creates two new messages
  • Adds the urgent tag to both messages
  • Adds the late tag to one message
  • Checks if we can retrieve both messages by using the urgent tag
  • Checks if only one message is returned for the late tag

Notice that the highlighted lines of code have not been implemented yet. To allow this test to pass, we need to add the following methods to the Message domain class:

  • addTag—instance method to allow a message to be tagged
  • withTag—class method to retrieve all messages with a particular tag

Add the following method to the Message class (don’t forget to import tagging.Tagger):

def addTag(String tagName) {
tags = (tags)?:[] tags << tagService.createTagRelationship( tagName )
}

This method simply delegates the creation of the tag relationship off to the TagService class, and then stores the relationship in the tags list.

Add the following method to the Message class that retrieves all messages with a given tag name:

def static withTag(String tagName) {
return Message.withCriteria {
tags {
tag {
eq('name', tagName )
}
}
}
}

This method must be static on the Message class, as it is used to load message instances for a given tag. We do not want to have to instantiate a message before we can perform the search.

Before running the test, you will notice both of these new methods assume that there is a property on the Message class called tags. This has not yet been created. We need to create a one-to-many relationship from Message to Tagger that will allow messages to be tagged. We also need to inject the TagService into new instances of the Message class so the work for creating a new tag relationship can be delegated. Add the relationship to the Message class and inject TagService as shown below:

class Message {
def tagService
static hasMany = [tags:Tagger]
...
}

Now we can run our tests by entering the following on the command line:

grails test-app

We should see some output in the command line similar to:

Running test app.TaggableTest...
testCanRetrieveMessagesByTags...SUCCESS

Tagging a file

Now that we have implemented tagging for messages, we need to make tagging available for files.

Currently the logic for creating and fetching tags is in the Message domain class. We need to extract this logic so the File domain class can reuse it. It’s time to look at how GORM supports inheritance.

GORM inheritance

The GORM supports inheritance of domain classes by default through the underlying Hibernate framework. Hibernate has a number of strategies for handling inheritance and Grails supports the following two:

  • Table-per-hierarchy—this strategy creates one database table per inheritance hierarchy. This is the default strategy in Grails.
  • Table-per-subclass—this strategy creates a database table for each subclass in an inheritance hierarchy and treats the inheritance (is a) relationship as a foreign key (has a) relationship.

Taking our domain as an example, we have two classes. They are Message and File. We are going to make them both extend a super class Taggable, which will handle all of our tagging logic and state.

Table-per-hierarchy

If we were to choose the table-per-hierarchy strategy, we would end up with one table called Taggable that contained the data for both Message and File. The database structure would look something like:

Grails 1.1 Web Application Development

The interesting side-effect of this approach is that all of the fields to be persisted must be nullable. If a File is created and persisted, it is obviously not possible for the fields from Message to be populated.

Table-per-subclass

By using the table-per-subclass strategy, we would keep two separate tables called Message and File, and both would have the tags relationship inherited from Taggable. So the Message table will look like:

Grails 1.1 Web Application Development

We can see in the diagram above that the Message and File tables have remained separate and a table representing the superclass Taggable has been created, which the subclass tables have foreign key relationships to. In the table-per-subclass strategy, a table must exist to represent the inheritance (is a) relationship.

We are going to follow the table-per-subclass strategy so that we can retain database level data integrity. The default behavior for GORM is to use the table-per-hierarchy strategy. To override this we must use the mapping property:

static mapping = {
tablePerHierarchy false
}

LEAVE A REPLY

Please enter your comment!
Please enter your name here