Enterprise applications typically persist data in relational databases. In object-oriented systems such as those written in Java, saving data equates to persist object states into SQL databases. One advantage of OOP is encapsulation, and hiding the database operations from the application code is only natural in an OO environment. ORM (Object-Relational Mapping) frameworks are designed to make this as easy, seamless and painless as possible. Mordern ORM frameworks, such as Hibernate, are very close to this ideal. Hibernate has emerged to be one of the best ORM frameworks for Java. In Hibernate, you write an object models with totally normal Java objects with accompanying meta data in XML format, and the framework takes care of creating SQLs for persisting object state into the database. Hibernate also defines a query language, the Hibernate Query Language (HQL), for retrieving objects from database according to specific query conditions. Another important feature is, object models in Hibernate can be used both in containers and in plain Java software. Judo chooses to natively support Hibernate, because Hibernate is clearly an ORM done right. Judo's Hibernate scripting support allows programmers to query the object model easily just like Judo's JDBC scripting (chapter 22. JDBC (SQL) Scripting). Also provided is a set of abstract commands for manipulating Hibernate persistent objects. This Hibernate scripting support is significant for systems implemented with Hibernate as its persistence layer: you can use Judo to manipulate data through the same object model that the system uses, so the data integrity and business rules are best maintained.
|
Listing 24.1 TestSimpleTypes.hbm.xml |
---|
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> <hibernate-mapping> <class name="TestSimpleTypes" discriminator-value="N"> <id name="id" type="long"> <generator class="native"/> </id> <property name="theFloat" type="float"/> <property name="theString" type="java.lang.String"/> <property name="theDate" type="java.util.Date"/> </class> </hibernate-mapping> |
Run the hbm2java
tool, and the Java source file is generated:
Listing 24.2 TestSimpleTypes.java |
---|
import java.util.Date; import org.apache.commons.lang.builder.ToStringBuilder; /** @author Hibernate CodeGenerator */ public class TestSimpleTypes implements java.io.Serializable { private Long id; private Float theFloat; private String theString; private Date theDate; public TestSimpleTypes(Float theFloat, String theString, Date theDate) { this.theFloat = theFloat; this.theString = theString; this.theDate = theDate; } public TestSimpleTypes() {} public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public Float getTheFloat() { return this.theFloat; } public void setTheFloat(Float theFloat) { this.theFloat = theFloat; } public String getTheString() { return this.theString; } public void setTheString(String theString) { this.theString = theString; } public Date getTheDate() { return this.theDate; } public void setTheDate(Date theDate) { this.theDate = theDate; } public String toString() { return new ToStringBuilder(this).append("id", getId()).toString(); } } |
Note that the Java class doesn't have to implement any Hibernate-specific interfaces! Because of this, Hibernate is deemed one of the least intrusive ORM framework. This generated code simply contains property getters and setters. You can add business logic methods and other convenience methods, and there are ways to define composite or calculated properties; this falls in the realm of Hibernate ORM practices and is out of the scope of this chapter.
Compile the Java source file and bundle the descirptor hbm.xml
file in the same classpath, there you have a working Hibernate object model.
Once you have the object model ready, you can use Hibernate to retrieve persistent objects from database and write the object state back to database via calls to Hibernate APIs.
The reason Judo provides native support for any domain is to hide the details of the domain APIs. While this holds true with Hibernate scripting as well, it is a little different. Because Hibernate object models are predominantly used in Java systems, understanding the API itself is a necessity to anyone who uses Hibernate. Here we summarize the most important Hibernate API classes and interfaces for using Hibernate object models. In the rest of this chapter, we will refer to these classes and interfaces when discussing Judo's Hibernate scripting support.
Hibernate versions and API names
Hibernate version 2 API used prefix of net.sf.hibernate
; later versions use the prefix of org.hibernate
. Judo can script both and you are not exposed with this issue. In this documentation, however, we have to occasionally refer to the Hibernate API classes and interfaces. For the convenience of this discussion, we simply use the prefix of org.hibernate
, but keep in mind that if you are working with Hibernate version 2, the prefix should be net.sf.hibernate
.
In Java Hibernate programs, these are the most basic classes and interfaces:
org.hibernate.cfg.Configuration
org.hibernate.SessionFactory
org.hibernate.Session
org.hibernate.Transaction
For Hibernate transactions, these classes and interfaces are critical:
org.hibernate.Query
org.hibernate.Criteria
org.hibernate.Lock
In Hibernate object models, you may very likely encounter user-defined types, which are implementations of one of these interfaces:
org.hibernate.UserType
org.hibernate.CompositeUserType
There are other classes and interfaces in Hibernate API that allow various kinds of extensions to the framework, which are not that important to our purpose of scripting Hibernate object models.
For any applications using Hibernate, the Hibernate environment must be initialized once. This means to initialize an instance of the Configuration
, from which to derive a singleton instance of SessionFactory
. Hibernate object persistence is done in units of work called sessions, represented by the Session
interface. Each session is created by the session factory.
The Hibernate configuration is extremely important; it contains information about the whole ORM environment as well as the object model itself. There are two ways to configure Hibernate. The first way is to use initialization properties, which can be either from System.getProperties()
or stored in a hibernate.properties
file in the classpath. The second way is the more versatile configraution XML document, hibernate.cfg.xml
, also in the classpath. Using hibernate.cfg.xml
, you can contain object model as well; if you use properties, you would have to programmatically add each class in the object model via Configuration.addResource()
or Configuration.addClass()
calls.
Commonly-used Hibernate configuration attributes
The most important information to configure is the database connection. In standalone applications, you probably need these attributes:
hibernate.connection.driver_class
hibernate.connection.url
hibernate.connection.username
hibernate.connection.password
hibernate.connection.pool_size
hibernate.dialect
hibernate.connection.datasource
hibernate.jndi.url
hibernate.jndi.class
hibernate.show_sql
hibernate.hbm2dll.auto
Steps of using Hibernate in Java
To use Hibernate in a Java applications, including Judo programs, these are the steps:
hbm.xml
files.Configuration
instance with the configuration information and register the object model classes.SessionFactory
singleton.Session
to carry out a unit of work of CRUD persistent objects, in one or more transactions.Judo provides a number of abstract constructs to make it very easy to script objects in Hibernate object models. These are mechanisms for initializing and accessing Hibernate environment:
hib::setup
statement:hib::addClass
and hib::addResource
statements:hib::get
function:Judo provides these mechanisms for persisting objects:
hib::get
(
JavaClass,
ObjectID [ ,
lock ] )
function:hib::save
, hib::update
, hib::saveOrUpdateCopy
, hib::delete
and hib::lock
and hib::unlock
functions:hib::txBegin
, hib::txEnd
and hib::txAbort
statements:Judo provides these mechanisms for HQL querying and deleting multiple objects:
hib::query
, hib::iterate
and hib::delete
statements:db::query
statement.The hib::setup
statement abstracts the Hibernate configuration. You can specify everything in this single statement. Its syntax is:
Hibernate_Setup | ::= | hib::setup [ attributes ] [ ( JavaClassName | STRING_LITERAL ),* ] ; |
attributes | ::= | ( ( PROPERTY_NAME = Expr ),* ) |
Let's look at an example.
import com.foo.bar.Auction.*; hib::setup ( hibernate.connection.url = 'jdbc:mysql://localhost/test', hibernate.connection.username = 'james', hibernate.connection.password = 'james', hibernate.hbm2ddl.auto = 'update', hibernate.show_sql = true, judoscript.echo = true // echo the eventual config values ) 'com/foo/bar/Auction/Item', // add a resource com.foo.bar.Auction.Category, // add a class with qualified class name Bid // add a class qualified via the import. ;
The attributes in the parentheses are the same as those in the hibernate.proeprties
file; the Java class names and/or mapping hbm.xml
files are listed at the end. Look closely at the database connection attributes, and you may find that there are some important attributes missing, such as hibernate.connection.driver_class
and hibernate.dialect
. This is because Judo knows many relational databases; based on the JDBC URL , Judo can figure out the driver class (see JDBC Drivers) and Hibernate dialects. The ultimate properties used by Hibernate may have more attributes than what are specified in the hib::setup
statement. Judo provides an extra boolean attribute, judoscript.echo
; when it is , Judo will print out all the attributes before Hibernate is actually initialized. So when you run it, you may first see something like this:
[hib::setup] hibernate.connection.username = james [hib::setup] judoscript.echo = true [hib::setup] hibernate.connection.password = james [hib::setup] hibernate.dialect = net.sf.hibernate.dialect.MySQLDialect [hib::setup] hibernate.connection.url = jdbc:mysql://localhost/test [hib::setup] hibernate.connection.driver_class = com.mysql.jdbc.Driver [hib::setup] hibernate.hbm2ddl.auto = update [hib::setup] judoscript.hibernate.version = 2
The last property, judoscript.hibernate.version
, is a Judo internal indicator. In the above output, it is "2" because we ran with Hibernate version 2. Judo figures out the Hibernate version based on the classes it founds; if Hibernate version 3 classes are in the classpath instead, it would yield the following result:
[hib::setup] hibernate.connection.username = james [hib::setup] judoscript.echo = true [hib::setup] hibernate.connection.password = james [hib::setup] hibernate.dialect = org.hibernate.dialect.MySQLDialect [hib::setup] hibernate.connection.url = jdbc:mysql://localhost/test [hib::setup] hibernate.connection.driver_class = com.mysql.jdbc.Driver [hib::setup] hibernate.hbm2ddl.auto = update [hib::setup] judoscript.hibernate.version = 3
You can choose to store configuration information in the configuration file instead. For example, the following statement initializes Hibernate:
hib::setup;
In this case, you need to specified everything in a hibernate.cfg.xml
document that can be found in the classpath, which contains something like this:
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd"> <hibernate-configuration> <session-factory name="java:/hibernate/HibernateFactory"> <property name="show_sql">true</property> <property name="connection.datasource">java:/comp/env/jdbc/AuctionDB</property> <property name="dialect">org.hibernate.dialect.MySQLDialect"</property> <mapping resource="auction/Item.hbm.xml"/> <mapping resource="auction/Category.hbm.xml"/> <mapping resource="auction/Bid.hbm.xml"/> </session-factory> </hibernate-configuration>
You can also put configuration attributes in the hibernate.properties
file, in which case all the classes in the object model must be specified explicitly in hib::setup
. All attributes must be specified, such as hibernate.connection.driver_class
and hibernate.dialect
.
Programmatically add classes and/or resources
There may be situations where you prefer to programmatically add classes of the object model to the Hibernate environment instead of declaring them in hib::setup
statement. Judo provides two functions for this purpose: hib::addClass()
and hib::addResource()
. For instance, perhaps you want to add all the hbm.xml
mapping files in a folder within a jar file:
hib::setup;
listFiles '*.hbm.xml' in 'myobjmdl.jar' recursive
{
hib::addResource $_; // $_ holds the current file path
}
Hibernate system objects
After initialization, Judo maintains a singleton of Configuration
and a SessionFactory
instances. When any of the persisting operations (discussed later) are invoked, a new session is created (if it doesn't already exist); this session will be there until hib::close
is called.
The hib::get()
function can take an argument of "config"
, "factory
or "session"
and return one of these Hibernate system objects. For example, you may want to get the session object and invoke its criteria API to perform a query-by-example (QBE) operation:
import org.hibernate.expression.Example;
hib::setup;
exampleUser = new java::User;
exampleUser.firstName = 'Max';
sess = hib::get('session');
crit = sess.createCriteria(java::User);
crit.add( Example::create(exampleUser) );
result = crit.list(); // result is a List.
for x in result {
println x.firstName, ' ', x.lastName;
}
This example is almost purely Java scripting except it gets the session object via hib::get()
.
Note that hib::get()
function is dual-purpose: it can also be used to retrieve individual persistent objects from database, as you will see in the next section.
Once Hibernate is initialized properly, you can create and manipulate instances in the object model using the Hibernate functions and operators.
Hibernate sessions and transactions
As we know, all the object persistence operations in Hibernate occur within a session. In Judo, a session is started automatically whenever any persisting operation starts, per each thread. The session is open until hib::close
is called.
More accurately, Hibernate object persisting operations happen in transactions, and multiple transactions can happen in a session. In Judo, you can start a transaction via a hib::txBegin
call, and commit and finish it via a hib::txEnd
call, or roll back the changes by a hib::txAbort
call. If a transaction is not started for a session, operations are in the auto-commit mode, where each operation starts its own transaction, commits it once done. Because the session instance is per-thread, there can be only one transaction per session at any moment.
Persistent object life-cycles and manipulation
In Hibernate, persistent objects can be in one of the following states: transient, persistent and detached. An object is in the transient state when it is either newly created, or deleted. An object with a valid database identifier is said to be persistent. Persistent objects are associated with a Session
instnace; some API calls can disassociate persistent instances from the Session
, and such instances are said to be in the detached state. Detached instances' changes will not be automatically picked up unless they are explicitly updated by a Session
.
In Judo, Hibernate persistent objects' life-cycles are managed with these built-in functions and operators:
hib::get(
InstanceClass ,
Identifier [ ,
LockMode ] )
:hib::save
Instance:hib::update
Instance:hib::saveOrUpdateCopy
Instance:hib::delete
Instance:hib::lock
Instance ,
LockMode:hib::unlock
Instance:hib::lock(instance, "none")
or hib::lock(instance, null)
.If you have worked with the Hibernate API, you probably recognize that most of them are methods of the Session
interface. This is indeed true, as these functions and operators are delegates to the methods of the underlying per-thread Session
instance. All parameters are natural to the API, except for the LockMode used in hib::get()
and hib::lock()
, which is normally a case-insensititive name of "none", "read", "upgrade", "upgrade_nowait" and "write", or an instance of org.hibernate.LockMode
(such as one of its static final instances like NONE
, READ
, UPGRADE
, UPGRADE_NOWAIT
and WRITE
).
Let's take a look at an example. The following script uses the simple object model we saw earlier, which comprises of a single class, TestSimpleTypes
.
Listing 24.3 SimplestORM.judo |
---|
// // 1. First of all, initialize the Hibernate environment using MySQL. // hib::setup ( hibernate.connection.url = 'jdbc:mysql://localhost/test', hibernate.connection.username = 'james', hibernate.connection.password = 'james', hibernate.hbm2ddl.auto = 'update', judoscript.echo = true ) TestSimpleTypes ; // // 2. Create a new object, and save it. // x = new java::TestSimpleTypes(10.5, 'abcdefg', Date()); println 'Before save: x.id = ', x.id; hib::save(x); println 'After save: x.id = ', x.id; // // 3. Modify the object and update it. // x.theFloat = 100.8; hib::update(x); println 'Updated x.'; // // 4. Find that object. // y = hib::get(java::TestSimpleTypes, x.id); println 'Found: y.id = ', y.id, ' y.theFloat = ', y.theFloat; // // 5. Delete the object. // hib::delete y; z = hib::get(java::TestSimpleTypes, y.id); if (z != null) println 'Found: z.id = ', z.id; else println 'Object[id=', y.id, '] has been deleted.'; // // 6. Reinstate the object (obtains a different ID). // println 'Re-save y... currently, y.id = ', y.id; hib::saveOrUpdateCopy y; // put it back in. println 'Now, y.id = ', y.id; |
In this script, we conduct six experiments. The first experiment is for hib::setup
, where we simply specify a MySQL database connection, and turn on the judoscript.echo
option to see the "real" properties at runtime. The output for this part is:
[hib::setup] hibernate.connection.username = james [hib::setup] judoscript.echo = true [hib::setup] hibernate.connection.password = james [hib::setup] hibernate.dialect = org.hibernate.dialect.MySQLDialect [hib::setup] hibernate.connection.url = jdbc:mysql://localhost/test [hib::setup] hibernate.connection.driver_class = com.mysql.jdbc.Driver [hib::setup] hibernate.hbm2ddl.auto = update
The second experiment creates a new instance and saves it to the database. We simply print out the instance's identifier before and after the save, and you can see the identifier is filled in during the persisting process:
Before save: x.id = After save: x.id = 1
The third experiment then changes one of its property and updates it in the database. Then, the forth experiment gets that instance from the database and verifies that the changed property is indeed as expected:
Updated x. Found: y.id = 1 y.theFloat = 100.80000305175781
The fifth experiment deletes that instance (note this is auto-committed), and then tries to get it from the database, which should return null
:
Object[id=1] has been deleted.
By now, the instance referenced by y
is in transient state, that is, it does not exist in the database, and its identifier, though not null, is bogus. Finally, the sixth experiment calls hib::saveOrUpdateCopy
to save it again into the database. We print out its identifier for proof:
Re-save y... currently, y.id = 1 Now, y.id = 2
Summary
To summarize, Judo provides the aforementioned statements, functions and operators to hide the details of manipulating Hibernate persistent objects. These operations are terse, intuitive and convenient, and the resultant code is much less noisy than the Java counterpart.
Many of the object life-cycle management functions and operations are delegates to the org.hibernate.Session
methods. But the org.hibernate.Session
has more methods that you may want to call, and you may even need to call methods of the org.hibernate.SessionFactory
and org.hibernate.cfg.Configuration
instances. Judo doesn't stop you from doing this: you can get these objects via a call to hib::get('session')
, hib::get('factory')
or hib::get('config')
. Such API calls are considered far less likely to happen as compared to the hib::
operations. Citing the 80-20 rule, Judo's Hibernate scripting makes it easy for the >80% chance of sripting Hibernate while leaving it possible for the <20% chance of Hibernate API calls.
Hibernate defines a SQL-like object query language, Hibernate Query Language (HQL for short), to find a collection of objects from the database or from a collection of objects. The found objects can be returned or deleted. HQL can also return a collection of values rather than objects; this is sometimes called report queries. Like, JDBC, the Hibernate HQL API allows users to dynamically bind parameters to a query.
Judo supports HQL scripting in a way similar to chapter 22. JDBC (SQL) Scripting. Judo provides a unified syntax for querying and deleting objects using HQL:
HQL | ::= | ( hib::query | hib::iterate | hib::delete ) [ IDENTIFIER ] [ options ] : HQL ; [ with BindVariableList ; ] |
options | ::= | ( ( from | limit | in ) Expr )* |
BindVariableList | ::= | ( IDENTIFIER [ : HibernateType ] = Expr ),* |
In this unitified syntax, the options are for pagination and query in a collection; they obviously don't apply to hib::delete
. The hib::iterate
is functionally the same as hib::query
, but Hibernate does the query in two steps: for non-report queries, it first finds the identifiers of entities and then fetches the entities on demand; this is primarily to allow applications to take advantage of the second-level cache. Let's see an example of query.
Listing 24.4 Queries.judo |
---|
hib::setup ( hibernate.connection.url = 'jdbc:mysql://localhost/test', hibernate.connection.username = 'james', hibernate.connection.password = 'james', hibernate.hbm2ddl.auto = 'update' ) TestSimpleTypes ; // // Query for objects: // hib::query qry: // or hib::iterate. from TestSimpleTypes o where o.id > 9 and o.id < 15 ; // now, qry is a List of TestSimpleTypes's -- for o in qry { println o; } // // Report query: // hib::query qry: select o.id, o.theFloat, o.theString from TestSimpleTypes o where o.id > :startIdx and o.id < :endIdx ; with startIdx:Long = 9, endIdx:Long = 15; // now, qry is a List of Object[] -- for o in qry { println o; } |
The first query returns a collection of TestSimpleTypes
instances. It is the same as:
hib::query qry: // or hib::iterate. select o from TestSimpleTypes o where o.id > 9 and o.id < 15 ;
The second query is the so-called report query that returns a collection of Object[]
arrays. This one actually uses bind variables, startIdx
and endIdx
; we will discuss bind variables later. The running result is:
TestSimpleTypes@ff94b1[id=10] TestSimpleTypes@7c3885[id=12] TestSimpleTypes@162e295[id=14] [10,100.8,abcdefg] [12,100.8,abcdefg] [14,100.8,abcdefg]
The reason that the ids are all even numbers is because the operations done in code listing 24.3.
Delete objects via HQL
You can delete objects with the same HQL syntax:
Listing 24.5 DeleteObjects.judo |
---|
hib::setup ( hibernate.connection.url = 'jdbc:mysql://localhost/test', hibernate.connection.username = 'james', hibernate.connection.password = 'james', hibernate.hbm2ddl.auto = 'update' ) TestSimpleTypes ; // // Delete objects: // hib::delete: from TestSimpleTypes o where o.id > :startIdx and o.id < :endIdx ; with startIdx:Long = 11, endIdx:Long = 13; // // Verify: // hib::query qry: select o.id, o.theFloat, o.theString from TestSimpleTypes o where o.id > :startIdx and o.id < :endIdx ; with startIdx:Long = 9, endIdx:Long = 15; for o in qry { println o; } |
And the result is:
[10,100.8,abcdefg] [14,100.8,abcdefg]
Bind variables
If you have worked with JDBC scripting (chapter 22. JDBC (SQL) Scripting), then bind variable is not a stranger. In Judo JDBC scripting, you can embed use question marks or names prefixed with colons :
to represent bind variables with SQL statements. When the SQLs are run, you can bind variables to them with index numbers (starting at 1) or by name. The SQL binding variables can be assigned with a type; if no type specified, by default it is VARCHAR
.
For HQL statements, it is slightly different. First of all, only named bind variables are supported in HQL statements. Secondly, the bind variable type must always be specified. The types are those defined by Hibernate, which include built-in types that are mostly Java and JDBC types, or custom types. Keep in mind that, with Judo's Java scripting support (chapter 11. Java Scripting), you can use import
just like in Java, and also, java.lang.*
, java.util.*
and java.io.*
are imported automatically. For instance, in code listing 24.4, the second query bound values to startIdx
and endIdx
with type Long
, which is really java.lang.Long
.
In this section, we are going to explore some of the common situations in using Hibernate. TODO: to be done.
Table per concrete class
.
Table per class hierarchy
.
Table per subclass
.
One-to-one relationship
.
Many-to-one unidirectional relationship
.
Many-to-one bidirectional relationship
.
Many-to-many relationship
.
Polymorphic associations
.
Why Judo support a proprietary API to such an extent for Hibernate? We believe that Hibernate is one of the best solutions in the problem domain of ORM, and it has a promising future. Hibernate may be somehow absorbed into some standards, at which time Judo will evolve accordingly to support that standard. For now, natively support Hibernate seems good enough.
Judo's native Hibernate support makes much sense. One of the biggest selling points of Hibernate is that object models written in Hibernate can be used in and out of containers; the same object model can be used in a web application and by a command-line tool, say. Judo is designed to be a powerful data manipulation tool; by using Hibernate object models (rather than, say, using raw SQL) in Judo programs, business rules and data integrity will be easily maintained throughout the system. Conversely, during system design phase, Judo is an ideal tool for prototyping and testing the object model, which may ultimately lead to automated testing package for the object model, probably as a part of the overall system test suite.