Managing Content through Tagging in Grails: Part 1

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.

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:

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:

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:

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:

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:

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}

Taggable superclass

Now that we have discussed how GORM handles domain class inheritance, it is time to implement our Taggable superclass that will allow Message and File to handle tagging. Create a domain class in the tagging package that:

Now that we have the tag-specific logic implemented in a base class, we need to remove the addTag and withTag methods from the Message class, as well as the tags relationship and the tagService property, and make the File and Message classes extend Taggable. Message, as shown below:

import tagging.Taggableclass Message extends Taggable {...}

Make the following changes to the File class:

import tagging.Taggableclass File extends Taggable {...}

Run the tests again and we can see that TaggableIntegrationTests still passes. Now add a test to verify that File objects can be tagged:

This test verifies that File instances can be tagged in exactly the same way as Message instances.

Polymorphic queries

So far so good, however, there is an additional implication to inheritance with domain classes that we need to investigate, that is, the introduction of polymorphic queries. When we query a domain superclass, we are performing a polymorphic query, which means the query will actually run over the subclasses and return all matching instances from all of the subclasses.

In more practical terms, when we call the withTag method on File or Message, we are actually going to receive all File and Message instances with the specified tag. This is because the withTag implementation exists on the Taggable class, so we are performing the query against the Taggable class:

Here we are creating a file and a message, and tagging each of them as draft. If we didn't know about polymorphic queries, we would expect to be able to retrieve one message with the 'draft' tag and one file with the 'draft' tag.

Running the tests now, we will see the following failed test in the output:

Open the tests HTML report (test/reports/html/index.html) to get more detail on the reason for the failure as shown in the following screenshot:

Our expectation was that there should be one item in the returned results; instead two items were returned—the tagged message and the tagged file.

This functionality may be useful to us in the future, but for now, we need to be able to search by a specific type. To solve this problem, we can create another withTag method that also takes a type. We end up with the following methods in Taggable:

Now we can optionally specify a type that we wish to query. The default behavior, if no type is specified, is to perform a polymorphic query against Taggable. We can now add a withTag method onto the Message and File classes that will override the default behavior of the Taggable class so that our test passes. Add the following method to Message:

Exposing tagging to the users

The domain model is up and running so we can move on to allowing users to tag messages and files. In the first instance, we will allow users to tag messages and files when they are created. To do this, we need to make the following changes:

The GSPs that render the forms to submit messages and files must allow users to enter tags

The Taggable class needs to handle adding many tags in one go

The controllers must handle tags entered by the user

The FileService class must populate user tags on the File

The home page needs to render the tags that were added to each message and file

Add the Tags input field

We are going to allow users to input tags in free text and make them delimited by spaces. So we simply need to add a new text input field to each of the Post Message and Post File screens. The fieldset element in the message create.gsp becomes:

Add multiple tags to Taggable

We can already store a list of tags against a Taggable class, but currently they must be added one at a time. For our users convenience, we really need to be able to handle adding multiple tags in one go. Let's create the addTags method on Taggable:

We need to convert the user input, a space delimited list of tags, into our structured tagging model. This means we are not able to take advantage of the Grails data binding support and must add the tags manually through the addTags method that we created earlier. When there are validation errors, we must also make sure the tags entered by the user are made available on the page model so the tags are not lost when rendering the error messages.

In FileController, we must also make the users tags are available on the model when rendering validation errors. Change the current line in the save action from:

Displaying tags

The last step for our basic tag handling is to display the tags to the users. To accomplish this we need to be able to get a list of tags as a string and then render the string representation on the home page. Add a read-only property implementation to Taggable:

def getTagsAsString() { return ((tags)?:[]).join(' ')}

We also need to override the toString implementation on Tagger:

public String toString() { return tag.name}

Open up the index.gsp file under views/home and add the following under the messagetitle div:

Create the entry for tags.display in the message bundle file under i18n/messages.properties:

tags.display=tags: {0}

Now if we run the application, we should see that users are able to add tags to their messages and files:

These tags can be displayed on the home page as shown in the following screenshot:

Summary

In this article, we have seen how to construct a domain model to allow files and messages to be tagged. We used inheritance to enable tagging for the Message and File domain classes and saw how GORM supports persistence of inheritance structures to the database. Creating an inheritance structure in our domain classes allowed us to make use of polymorphic queries. In the next part of this article, we will look at how to customize our home page and work with the templates and tags.

Alerts & Offers

Series & Level

We understand your time is important. Uniquely amongst the major publishers, we seek to develop and publish the broadest range of learning and information products on each technology. Every Packt product delivers a specific learning pathway, broadly defined by the Series type. This structured approach enables you to select the pathway which best suits your knowledge level, learning style and task objectives.

Learning

As a new user, these step-by-step tutorial guides will give you all the practical skills necessary to become competent and efficient.

Beginner's Guide

Friendly, informal tutorials that provide a practical introduction using examples, activities, and challenges.

Essentials

Fast paced, concentrated introductions showing the quickest way to put the tool to work in the real world.

Cookbook

A collection of practical self-contained recipes that all users of the technology will find useful for building more powerful and reliable systems.

Blueprints

Guides you through the most common types of project you'll encounter, giving you end-to-end guidance on how to build your specific solution quickly and reliably.

Mastering

Take your skills to the next level with advanced tutorials that will give you confidence to master the tool's most powerful features.

Starting

Accessible to readers adopting the topic, these titles get you into the tool or technology so that you can become an effective user.

Progressing

Building on core skills you already have, these titles share solutions and expertise so you become a highly productive power user.