Advanced Hibernate, Maps Part 1: Complex Associations

Maarten Winkels

When it comes to using Hibernate, most projects only use the very basic features. This is mostly due to naivety or unfamiliarity with the product. Hibernate is a very mature and feature rich product that can be used to solve a lot of basic or advanced problems. I think the point here is to put the complexity at the right level: You can always take a very basic approach to using Hibernate and solve the mismatch between your mapping and your model in your model, but that would put complexity in your model that is basically boilerplate code. It is only there because the persistence layer is not used correctly. If you make the persistence layer (in this case the Hibernate mapping) more complex it will be harder to maintain, but your model code will be more concise and easier to understand.

One of the features of Hibernate that is hardly ever used, but has some very interesting applications is the ability to map java.util.Maps. This blog will show an application of using a Map in your model and mapping that model with Hibernate to a Relational Database Schema.

Project Cookbook

Say one day you want to manage all your recipes in a on-line cookbook. Before digging in and churning out those lines of code, you take a step back and ask yourself: 'What is in this cookbook?'. Basically a cookbook is a collection of recipes and one of the interesting parts of a recipe is that it has ingredients. Well actually, I would say that in a recipe you use a specified amount of an ingredient. So the relationship between recipe and ingredient is not a normal parent-child or even many-to-many association. This is a relationship with an attribute: the attribute is the amount of ingredient you use in a recipe.

Model

Good, so now we have that out of the way we can start coding! Since this will be a web application we turn to Grails, of course, to be sure we can start using our cookbook tonight. So using Grails-1.0-RC-1. we find a good spot in the directory structure and type

C:\projects\grails> grails create-app cookbook

This will basically create the infrastructure structure for the grails application. A bunch of directories and file are created below the "cookbook" directory. Now we want to add a few domain classes. We have recognized the following entities: Recipe, Ingredient and Unit. The Unit entity expresses the unit in which the amount of ingredient to use in a recipe is expressed. An example would be that you use 500 grams of sugar in an apple pie recipe. The unit here is 'grams'. So lets add those entities.

C:\projects\grails\cookbook> grails create-domain-class Recipe
C:\projects\grails\cookbook> grails create-domain-class Ingredient
C:\projects\grails\cookbook> grails create-domain-class Unit

These are very simple commands that will basically create two files: one in the source directory and one in the test directory. Lets edit the Ingredient.groovy and Unit.groovy files to add the simple properties that these entities have.

Ingredient.groovy:

class Ingredient {
    String name
}

Unit.groovy

class Unit {
    String name
}

Thats all we need for now. The Recipe entity will be a bit more exiting. Thinking back to short discussion of what a recipe is it contains ingredients, but it also wants to know about the amount of each ingredient. So basically it creates a map of ingredients in which the key is an Ingredient and the value is the amount of ingredient you use in the Recipe. The amount will be a value (component, embeddable) object, since it is not an entity but it is a property of the relation between Recipe and Ingredient. The amount component has a reference to the Unit entity. To tell Grails that the Amount class is not an entity, we put the Amount.groovy file into the src/groovy directory.

Recipe.groovy

class Recipe {
    String name
    Map ingredients
}

Amount.groovy (in src/groovy)

class Amount {
    int value
    Unit unit
}

Controller and View generation

So now comes the full power of Grails. It is time to create the controllers and views for these entities.

C:\projects\grails\cookbook> grails generate-all Recipe
C:\projects\grails\cookbook> grails generate-all Ingredient
C:\projects\grails\cookbook> grails generate-all Unit

After this we start the grails container and look at the application. The main page pops up and although you will want to change this eventually it suffices to complete this example. If you look at the Ingredients and Unit controller, than they simply work out of the box. They better would, because they are basically the simplest thing possible: a thing with a name. Using these controllers you can add, list, update and remove the ingredients and the units in the system.
Basic grails application Unit is scaffolded

Now we turn to the Recipe controller. The Recipe controller is not fit for the tasks that we would like to do with it. If you open the create or edit view, you'll be able to edit the name property, but the ingredients property is not editable. The show view shows the Map, but as a simple property and not as a collection.
Unable to add ingredients to recipes Ingredients are shown as simple map

Changing the Mapping

That is not what we want. We want to add, list, update and remove ingredients for a recipe. Now we need to change the view and controller for Recipe, but I'm a bit suspicious about the data model at this point. So lets look at the schema that Hibernate generated.
Table schema when not using advanced Map mapping
All tables are disconnected! There are no foreign keys in the system. The reason is that Hibernate by default will store the keys and values in the map as single values. Both IDX and ELT columns in the RECIPE_INGREDIENTS are VARCHAR typed. Before we fix the user interface, lets try and fix this problem.

An approach that might look simple is to forget about the whole Map and use a Set of some entity or component class that will have a reference to the ingredient and an amount. But what would that class represent? It would be some sort of IngredientUse or something, representing the usage of an ingredient in a recipe. To me that doesn't really feel right. In the domain we don't normally have such a thing as a IngredientUse. We just use a certain amount of an Ingredient in a Recipe. So to stick with this model we have to make the persistence layer just a little bit smarter. Actually adding 2 standard hibernate configuration files in the grails-app/conf/hibernate directory will do just that.

hibernate.cfg.xml





	
		
	

Recipe.hbm.xml





    
        
            
        

        
        
            
            
            

                
                
            
        
    

In addition we have to add an id property (and optionally a version property for optimistic locking) to the Recipe class.
If we now restart the server and look at the newly created database schema, it will look like the image below, now containing the foreign keys as expected and with some more readable column names.
Adjusted Hibernate mapping

Changing the Controller

Now to use enable editting the map via the web application, we have to add some methods to the RecipeController. The methods will change the contents of the ingredients map in a specfic recipe.

    def addIngredient = {
        doWith( params.id, params.ingredientId ) { Recipe recipe, Ingredient ingredient ->
            def unit = Unit.get( params.unitId )
            def amount = new Amount(value : Integer.parseInt(params.amount), unit : unit)
            recipe.ingredients.put(ingredient, amount)
        }
    }

    def updateIngredient = {
        doWith( params.id, params.ingredientId ) { Recipe recipe, Ingredient ingredient ->
            recipe.ingredients.get(ingredient).value = Integer.parseInt(params.amount)
        }
    }

    def deleteIngredient = {
        doWith( params.id, params.ingredientId ) { Recipe recipe, Ingredient ingredient ->
            recipe.ingredients.remove(ingredient)
        }
    }

    private doWith (recipeId, ingredientId, cl) {
        def recipe = Recipe.get( recipeId )
        if (recipe) {
            def ingredient = Ingredient.get( ingredientId )
            if (ingredient) {
                cl.call(recipe, ingredient)
                redirect(action:show,id:recipe.id)
            }
            else {
                flash.message = "Ingredient not found with id ${ingredientId}"
                redirect(action:edit,id:recipeId)
            }
        }
        else {
            flash.message = "Recipe not found with id ${recipeId}"
            redirect(action:edit,id:recipeId)
        }
    }

The controller methods are pretty straight forward: The fetch the necessary entities, do some validation and finally manipulate the ingredients map. The changes are persisted using Hibernate transparantly at commit time.

Changing the View

As far as the model and the Hibernate mapping is concerned we could stop here, but we'll finish the view part of the application just for fun!! In the edit.gsp, create.gsp, list.gsp and show.gsp for recipe we will remove the elements that show the ingredients property, since the representation is not very useful. There are several places that we could use to provide a User Interface to manipulate the map. I've choosen to add the components to the show view of the recipe, turning that essentially in a show view of the recipe and an edit view for the ingredients collection. This could be moved to the edit view, but that would result in a view with a lot of buttons that manipulate different entities. Anyway, either user interface design would be possible with this model and controller design.

fragment of show.gsp


  Ingredients:
  
    
      
      
      
${entry.value.unit.name} of ${entry.key.name}
of  

The <g:actionSubmit> tags will turn in to buttons, that will trigger the updateIngredient, deleteIngredient and addIngredient methods on the RecipeController. This enables the user to manipulate the ingredient map for the recipe.
Recipe view after adding Hibernate mapping

Now this application can be used to easily manage a collection of recipes with ingredients. You can find all sources of this application in the attached Zip file.

Conclusion

When finding a mismatch between a simple Hibernate mapping and an expressive model, try to stay true to the model. Using a little more complex mapping will keep the model simple, expressive and easy to use. In some cases a Map can be used to express a complex association between entities that has properties.
In the process we have seen that it is easy and fun to develop web applications in Grails. Hibernate mappings in XML files can be added to Grails to enhance the mapping features that Grails provides out of the box.

Comments (4)

  1. Lars Vonk - Reply

    October 22, 2007 at 10:50 pm

    Hi Maarten,

    Great stuff. I agree that we should focus on the model. Using these hints will allow us to do so.

    I am only thinking if I would choose to create a Unit table, it feels a bit like over-normalizing to me. Instead you could also choose to create an enum for Units and just store it as text in the RECIPE_INGREDIENTS table. The hbm file will then be something like this:

    instead of:

    I think this will do it:

    See http://www.hibernate.org/265.html for an example of how to implement the UnitUserType.

    BTW I could not download the attached zip file, so I could test my proposed solution (I didn't feel like copy-pasting ;-))

    Lars

  2. Maarten Winkels - Reply

    October 22, 2007 at 11:35 pm

    Hi Lars,

    Thanks for your comment! I fixed the download link for the Zip file, it should work now.

    Regarding your comment on Enum vs. Table, I would say that it depends on your requirements. You could certainly do as you suggest and use a CustomUserType to implement persisting an enum. I could for see in this case the need to add Units add run time (who ever though you needed both tablespoon and teaspoon as units for receipts ;)). It would be quite a pain if you would have to change a .java file and recompile and rebuild the whole app... o wait, this is Grails... nevermind 😉

    Cheers!

    -Maarten

  3. andy - Reply

    February 5, 2009 at 9:25 pm

    great!
    I'm have been googling at how to store a map from an entity to a value, like you are doing here, but with hibernate-annotations.
    if you could add that it would be great!

  4. Adam Dyga - Reply

    August 4, 2009 at 3:05 pm

    That's true that hibernate offers a lot of functionality that can map really advanced data structures to database schema. Unfortunately my experience is that if you run into some problems or find bugs in corner-case situations, you'll get very weak support from both the community and Hibernate developers (bug fixes). If I built my application again, I would redesign and avoid using a couple of advanced hibernate features in some areas (such as ternary associations, lazy loading combined with detached objects, etc). Also, the "implicit updates" functionality (criticized by many, but in the opinion of hibernate delevopers, it is a feature, not a bug) is often pain in the *ss and makes you develop weird workarounds.

Add a Comment