by Pavel Simakov on 2006-05-07
Introduction
Hibernate {1} is a leading object-relational mapping solution widely used in the Java community. The main goal of Hibernate is to eliminate 95% of all programming tasks related to data persistence. This goal has already been realized in many software development teams that fully rely on Hibernate and do not have to write a single line of SQL code by hand. In addition to hiding database access details, more importantly, Hibernate encourages an object-oriented view of artifacts stored in the database. It encourages object-oriented over database-centric software design.
The Problem
It is possible to understand the object-model and database schema of your application simply by reading the XML mapping files. But this is tedious and quite difficult to do. Object-relational mapping XML files get quite bulky and XML is difficult for humans to read. Hibernate includes a set of Apache Ant tasks and other classes that generate HTML documentation for the database schema and object-relational mapping. But there is no convenient tool for generating UML-like entity-relation diagrams. A diagram is a quick and effective way to understand domain objects that are expressed in XML mapping files.
Linguine Maps visualization library {2} has support for Hibernate and creates UML-style entity-relation diagrams from Hibernate mapping files. In the releases prior to 1.3, it did not use any of the Hibernate code base and generated diagrams by parsing and analyzing XML mapping files. While quite successful, this approach had a significant limitation. Many relations between classes were not present in the Hibernate mapping files and therefore could not represented on the diagram. In fact, Hibernate, in addition to XML mapping-files, use Java reflection to discover the names and types of class members.
The Solution
To improve the quality of the diagrams it is better to allow Hibernate to process the XML mapping files. After that, one can use Hibernate runtime metadata to discover entities and relations and to create a diagram. In this article you will learn about various metadata elements stored in the org.hibernate.cfg.Configuration object. You will also discover how easy it is to use this information to create UML-style entity-relation diagrams with Linguine Maps. Through small code examples we will slowly develop a complete HBMCtoGRAPH.java class which can produce the following diagram:
Diagram 1.Diagram produced by HBMCtoGRAPH class for animal.hbm.xml Hibernate mapping file.
For the purposes of this article we will assume that you have a basic understanding of object-relational mapping methodology and the Hibernate 3.0 tool set. You will need to be familiar with several specific classes from the org.hibernate.mapping package to effectively work with Hibernate metadata. Many of the internal metadata-related Hibernate objects are associated with nodes and attributes in the XML mapping files, but others are not. While you have probably used the Configuration object directly, most of the other classes mentioned in this article are internally used by Hibernate and might need some introduction. The most important ones are mentioned below. For the full JavaDoc of Hibernate packages please use the official Hibernate web site or a great book {3}.
org.hibernate.cfg.Configuration
A Configuration approximately corresponds to a <hibernate-mapping> node of the XML mapping file. The Configuration object is used to specify properties and mapping documents to be used by the Hibernate. All of the content of your XML mapping files is contained in the Configuration object. If you have been using Hibernate, you already know how to create and use a Configuration object and use it to store and retrieve objects. A valid instance of a Configuration object can also be used to access metadata about business objects declared in the mapping files. All mapped persistent classes can be retrieved from the Configuration object. Listing 1 shows how to add two mapping files to a Configuration object and build mappings.
Listing 1.Configuration object example.
private void listing1(){ Configuration conf = new Configuration(); conf.addFile("c:\\test\\Animal.hbm.xml"); conf.addFile("c:\\test\\Multi.hbm.xml"); conf.buildMappings(); listing2(conf); }
org.hibernate.mapping.PersistentClass
A PersistentClass approximately corresponds to a <class> node of the XML mapping file. Each persistent class declared in the XML mapping files has a corresponding instance of a PersistentClass. You can use PersistentClass to obtain basic information about your persistent classes including its name, sub-classes, superclasses, properties, etc. All declared properties can be retrieved from the PersistentClass object. Listing 2 shows how to list all Persistent classes in the Configuration and determine their class names and direct subclasses.
Listing 2. PersistentClass object example.
private void listing2(Configuration conf){ Iterator myClasses = conf.getClassMappings(); while(myClasses.hasNext()){ PersistentClass myClass = (PersistentClass) myClasses.next(); Iterator myClassSubs = myClass.getDirectSubclasses(); while(myClassSubs.hasNext()){ PersistentClass myClassSub = (PersistentClass) myClassSubs.next(); System.out.println(myClassSub.getClassName() + " inherits-from " + myClass.getClassName()); } listing3(myClass.getPropertyIterator()); } }
org.hibernate.mapping.Property & org.hibernate.mapping.Value
Property does not correspond to any one specific node in the XML mapping file. A Property contains a Value and other metadata including property name, column name, getters, setters, etc. A property Value in turn is anything that is persisted by value, instead of by reference. This includes a primitive value, a relation, a collection or a component. There can be many strategies of how to inspect Properties, including casting, which seems to be ugly, but the most straight forward. First of all, deal with one-to-many, many-to-one, and component Properties. Then, fish out all collection types. And finally, fish out primitive values like String, int, long, etc. This order of processing is important since Component and ToOne have SimpleValue as a superclass. We will review the most common kinds of Values next. Listing 3 shows how to list all the properties of a class and inspect values of various types.Listing 3. Property & Value objects example.
private void listing3(Iterator myProps){ while(myProps.hasNext()){ Property myProp = (Property) myProps.next(); Value val = myProp.getValue(); // one-to-one/many-to-one if (val instanceof ToOne){ System.out.println(myProp.getName() + " is-a ToOne " + val.getClass().getName()); listing4(myProp, (ToOne) val); continue; } // component if (val instanceof Component){ System.out.println(myProp.getName() + " is-a Component " + val.getClass().getName()); listing5((Component) val); continue; } // collections if (val instanceof Collection){ System.out.println(myProp.getName() + " is-a Collection " + val.getClass().getName()); listing6((Collection) val); continue; } // primitive if (val instanceof SimpleValue){ System.out.println(myProp.getName() + " is-a primitive " + val.getClass().getName()); continue; } // QueryList and other things will be taken care of elsewhere continue; } }
org.hibernate.mapping.ToOne
A ToOne represents a relation or a reference to an entity and has two concrete subclasses OneToOne and ManyToOne. The OneToOne assumes cardinality of a relation 0..1 at the "from" association end. The ManyToOne assumes cardinality of a relation 0..n at the "from" association end. As per other SQL-like relations, ToOne can support cascading deletes. Listing 4 shows how to get a name and a cardinality for a ToOne relation.
Listing 4. ToOne object example.
private void listing4(Property prop, ToOne toOne){ boolean cascadeDelete = toOne.isCascadeDeleteEnabled(); String cardinality = "one"; if (toOne instanceof ManyToOne){ cardinality = "many"; } System.out.println("toOne " + prop.getType().getName() + " "+ cardinality); }
org.hibernate.mapping.Component
A Component approximately corresponds to a <component> node of the XML mapping file. From a metadata perspective a Component is similar to a PersistentClass. A Component has properties, but these properties are mapped to the columns of a table of the parent class where Component resides. Listing 5 shows how to get a component class name and list all component properties.
Listing 5. Component object example.
private void listing5(Component comp){ System.out.println("component " + comp.getComponentClassName()); listing3(comp.getPropertyIterator()); }
org.hibernate.mapping.Collection
A Collection represents any of the supported Hibernate persistent collections including set, array, list, and map. There is a specific Hibernate type for each type of a collection in the package org.hibernate.mapping. The common trait of all collections is that they contain collection elements. A Collection element is of type Value with all of its possible subclasses as discussed above. Listing 6 shows how to determine class name and element type for a collection element.
Listing 6. Collection object example.
private void listing6(Collection col){ Value elem = col.getElement(); String name = col.getClass().getName(); if (elem instanceof OneToMany){ System.out.println(name + " has-element OneToMany"); return; } if (elem instanceof ManyToOne){ System.out.println(name + " has-element ManyToOne"); return; } if (elem instanceof Component){ System.out.println(name + " has-element Component"); return; } if (elem instanceof SimpleValue){ System.out.println(name + " has-element SimpleValue"); return; } }
org.hibernate.mapping.SimpleValue
SimpleValue is the most basic type of Value. We have specifically considered ToOne, Component and Collections already. If a Value is not of any of these specific types it is likely to be a primitive or to refer to a class without a Hibernate mapping. For the purposes of diagramming and documentation we will treat SimpleValue as a primitive.Working with metadata might seem quite intimidating at first, but in practice, you will quickly discover that metadata support in many languages, frameworks, modeling and meta-modeling packages is quite similar. Now having sufficient knowledge of Hibernate metadata we can start building a UML class diagram.
The Fun Part
Drawing a diagram usually requires shapes and arrows that hold text, can be colored and arranged on the page. Linguine Maps supports many different kinds of shapes and arrows and allows their styles to be changed. In order to create a UML-like class diagram with classes and their relations one needs the following graphical objects:- a shape for a Class with a place for class name, stereotype and attributes
- an arrow with a specific line style (solid, dashed, etc.) and line caption for the role name
- line head and tail decorator (full diamond, hollow diamond, filled arrow, etc.)
- text label for head and tail cardinality (0..1, 1..n)
The full source code for building a diagram from Hibernate Configuration with Linguine Maps can be found in HBMCtoGRAPH.java file, which is part of Linguine Maps release 1.3. This class processes metadata from a Configuration object and simultaneously creates shapes and arrows on the diagram. The implementation is very similar to the code samples reviewed in Listings 1-6. Hibernate metadata elements are recursively inspected in the following order:
- iterate all PersistentClasses; function processClasses()
- for each PersistentClass create a shape
- for each PersistentClass create an arrow towards superclass (use hollow arrow head style); function processSubclasses()
- iterate over all Properties; function processProperties()
- for each Property Value of type ToOne make an arrow towards PersistentClass specified in the property Value; specify proper tail label to reflect cardinality (0..1 for OneToOne, 0..n for ManyToOne); select desired tail shape and line style to reflect cascading/non-cascading deletes as solid/dashed line correspondingly; function addToOneProp()
- treat Property Value of type Component similar to ToOne above; keep in mind that components imply cascading deletes and cardinality of 1
- for each Property Value of type Collection make an arrow towards a PersistentClass specified by the collection element; specify cardinality 0..n; select desired line style to reflect whether collection cascade-deletes its elements; function addCollectionProp()
- for each Property Value of type SimpleValue add a new line of text to the shape that represents PersistentClass; function addSimpleProp()
Once shapes and arrows have been added to a diagram we have to perform a graph layout. In general, the layout algorithm moves shapes around so all of the shapes are equally spaced and arrows do not cross each other. This is a very difficult task and is superiorly handled by Graphviz {4}. A Graphviz functionality is included and seamlessly integrated with the Linguine Maps distribution. After the graph layout, the diagram is ready to be rendered into an image file. Just select an output format (GIF, PNG, JPG, etc.) and generate a file with the image.
In a simple example below we create an instance of a Hibernate Configuration object and convert it first to a generic in-memory graph and finally to a GIF image. The animals.hbm.xml file included in Hibernate 3.0 is used as an example mapping file.
Listing 7.Example of HBMCtoGRAPH class use with Hibernate Configuration.
private void exampleOfUse() throws Exception { // new Hibernate config, add one file Configuration conf = new Configuration(); conf.addFile( "hibernate-3.0\\test\\org\\hibernate\\test\\hql\\animal.hbm.xml" ); conf.buildMappings(); // new options, set caption and colors TaskOptions opt = new TaskOptions(); opt.caption = "My Test"; opt.colors = "#FF5937, black, blue"; // create generic graph IGraphModel graph = HBMCtoGRAPH.load(opt, conf); // convert graph into image file GRAPHtoDOTtoGIF.transform( graph, "myTest.dot", "myTest.gif", "c:/graphviz/bin/dot.exe" ); }
For this example to run, the following files should be available on the classpath:
- oy-lm-1.3.jar (Linguine Maps programmatic visualization library)
- all of the class files required by your hbm.xml schema
- hibernate3.jar
- commons-collections-2.1.1.jar
- commons-logging-1.0.4.jar
- dom4j-1.6.jar
- junit-3.8.1.jar
Final Word
This was quite an exciting project! Instead of drawing UML diagrams by hand and constantly trying to remember what parts have changed in the real system - we now have the ability to generate all diagrams automatically. Not only is visualization with Linguine Maps cross-platform, but it also requires very little coding. The Hibernate mapping file visualization only requires 300 lines of Java code. Apache Ant builds, XML DTDs, or Apache ObJectRelationBridge mapping files all can be visualized in about 200 lines of Java code. With Linguine Maps you can quickly create custom visualizations for other metadata sources and XML files used in your projects.
The old approach still had a couple of benefits. We could allow users to paste the XML into a web form and generate diagrams online. This is no longer possible unless they are willing to upload multiple XML and class files for all of their classes. The old approach was useful at build time, while the new one is most likely to be used at runtime.
About Linguine Maps
Linguine Maps is an open-source Java library for programmatic visualization. It analyzes various structured text files, generating from them easy-to-understand entity-relation diagrams. Released under LGPL in August of 2005, it currently supports programmatic visualization for WSDL web services, Apache ANT build files, Document Type Definition (DTD) for XML, Apache ObJectRelationBridge (OJB) and Hibernate mapping files. For Hibernate specifically, it produces easy to read UML-style entity-relation diagrams. The diagrams show all the entities defined in the mapping files along with their attributes and relationships.
The motivation behind Linguine Maps was to generate documentation automatically - in the form of UML diagrams. Many documentation tools create textual reports, Linguine Maps creates diagrams and drawings. In the several months from the date of its release, over 1000 copies of Linguine Maps were downloaded from the web site. Many of the web site visitors interested in Hibernate used an online submission form that accepts text of a Hibernate mapping file and instantly generates a GIF or a PNG file with a diagram. Over 3000 diagrams were generated from this online form for Hibernate mapping files, some of which contained close to one hundred classes. In the vast majority of cases Linguine Maps produced accurate UML class diagrams automatically, with absolutely no manual work!
References
- http://www.hibernate.org
- Linguine Maps
- Christian Bauer, Gavin King: Hibernate in Action.
- Graphviz