Grails 1.1 Web Application Development

«»

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:

  • Contains a set of Tagger instances
  • Defines the hibernate inheritance mapping strategy to use
  • Implements the addTag method from Message
  • Implements the withTag method from Message

The implementation for the base class looks like:


	package tagging
	class Taggable {
		def tagService
		static hasMany = [tags: Tagger]
		static mapping = {
			tablePerHierarchy false
		}
		def addTag(String tagName) {
			tags = (tags)?:[]
			tags << tagService.createTagRelationship( tagName )
		}
		def static withTag(String tagName) {
			return Taggable.withCriteria {
				tags {
					tag {
						eq('name', tagName )
					}
				}
			}
		}
	}

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.Taggable
	class Message extends Taggable {
	…
	}

Make the following changes to the File class:


	import tagging.Taggable
	class 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:


	void testFileCanBeTagged() {
		def fileData = new FileData(data: [0])
		def aVersion = new FileVersion(name: name, fileData: fileData,
				description: 'foo', extension: 'pdf', user: fred )
		def firstFile = new File( currentVersion: aVersion )
				.save(flush: true)
		def secondFile = new File( currentVersion: aVersion )
				.save(flush: true)
		firstFile.addTag('draft')
		secondFile.addTag('draft')
		secondFile.addTag('released')

		def draftFiles = File.withTag('draft')
		assertEquals(2, draftFiles.size())
		assertEquals(2, Tag.list().size())

		def releasedFiles = File.withTag('released')
		assertEquals(1, releasedFiles.size())
		assertEquals(2, Tag.list().size())
	}

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:


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

Let's write a test to prove this:


	void testTaggedObjectsCanBeRetrievedByType() {
		def fileData = new FileData( data: [0] )
		def aVersion = new FileVersion( name: 'v1', fileData: fileData,
				description: 'foo', size: 101, extension: 'pdf',
				user: fred )
		def firstFile = new File( currentVersion: aVersion )
				.save(flush: true)
		def message = new Message(user: fred, title: 'tagged',
				detail: "I've been tagged.").save(flush: true)

		firstFile.addTag('draft')
		message.addTag('draft')

		assertEquals(1, Message.withTag('draft').size())
		assertEquals(1, File.withTag('draft').size())
	}

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:


	Running test app.TaggableTest...
				testCanRetrieveMessagesByTags...SUCCESS
				testFileCanBeTagged...SUCCESS
				testTaggedObjectsCanBeRetrievedByType...FAILURE

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:


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

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:


	def static withTag(String tagName) {
		return Taggable.withTag(tagName, Message)
	}

And add the following method to File:


	def static withTag(String tagName) {
		return Taggable.withTag(tagName, File)
	}

Now run the tests again and all should pass.

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:


	<fieldset>
		<dl>
			<dt>Title</dt>
			<dd><g:textField name="title" value="${message.title}"
					size="35"/>
			</dd>
			<dt>Message detail</dt>
			<dd><g:textArea name="detail" value="${message.detail}"/></dd>
			<dt>Tags</dt>
			<dd><g:textField name="userTags" value="${userTags}"
					size="35"/></dd>
		</dl>
	</fieldset>

While the fieldset for the file create.gsp becomes:


	<fieldset>
		<dl>
			<dt>Title</dt>
			<dd><g:textField name="name" value="${file.name}"
					size="35"/></dd>
			<dt>File</dt>
			<dd><input type="file" name="data"/></dd>
			<dt>Message detail</dt>
			<dd><g:textArea name="description"
					value="${file.description}"/></dd>
			<dt>Tags</dt>
			<dd><g:textField name="userTags" value="${userTags}"
					size="35"/></dd>
		</dl>
	</fieldset>

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:


	def addTags(String spaceDelimitedTags) {
		tags = (tags)?:[]
		tags.addAll(tagService.createTagRelationships(spaceDelimitedTags))
	}

Once again, here we just delegate the logic to the TagService class.

Saving the users tags

The next step is to handle the new user input so that the tags will be persisted. The
save action on MessageController is updated as shown below:


	def save = {
		def message = new Message(params)
		message.addTags( params.userTags )
		message.user = userService.getAuthenticatedUser()
		if( !message.hasErrors() && message.save() ) {
			flash.toUser = "Message [${message.title}] has been added."
			redirect(action: 'create')
		} else {
			render(view: 'create',
			model: [message: message, userTags: params.userTags])
		}
	}

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:


	render(view: 'post', model: [file: file.currentVersion])

to:


	render(view: 'post', model: [file: file.currentVersion,
			userTags: params.userTags])

The call to addTags takes place in the saveNewVersion method in FileService:


	def saveNewVersion( params, multipartFile ) {
		def version = createVersionFile( params, multipartFile )
		def file = applyNewVersion( params.fileId, version )
		file.addTags( params.userTags )
		file.save()
		return file
	}

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:


	<div class="tagcontainer">
		<g:message code="tags.display" args="${[message.tagsAsString]}" />
	</div>

Then add the following under the filename panel:


	<div class="tagcontainer">
		<g:message code="tags.display" args="${[file.tagsAsString]}" />
	</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:

«»

Comments

comments

Pages: 1 2 3 4

About Krishna Srinivasan

He is Founder and Chief Editor of JavaBeat. He has more than 8+ years of experience on developing Web applications. He writes about Spring, DOJO, JSF, Hibernate and many other emerging technologies in this blog.

Speak Your Mind

*

Close
Please support the site
By clicking any of these buttons you help our site to get better