Hibernate Infrastructure
Overview
I'm no Hibernate expert, by any means, but just in case anyone is curious or could use a helpful push in getting started, here is another example of using Hibernate (with JBoss and MySQL).
Just so the curious needn't waste their time if I've come from a direction they aren't interesting in, here are some characteristics of the approach outlined below:
- No build tools like Ant or Maven are used.
- Top-level Hibernate configuration is done thru code rather than hibernate.cfg.xml.
- An entire request is covered by a single transaction (resulting in an open session during view rendering).
I won't go into detail on all the necessary steps, as better documentation exists elsewhere, but here's a broad outline of the necessary tasks:
- Install MySQL, setup a schema for your application, and an ID/PWD that the application will use to connect
- Configure your JBoss datasource for MySQL
- Create a HbnConfigUtil to handle common configuration tasks
- Create a HbnSessionUtil to handle common Session tasks
- Create some intercepting filter logic to manage Hibernate resources during the course of a request
MySQL Setup
Go here to download MySQL. Go here to download MySQL's GUI Tools. Install them. Use the admin tool to create a user and schema.
Configure a JBoss datasource for MySQL
Here's an example mysql-ds.xml to drop in your [JBoss root]/server/default/deploy directory:
<?xml version="1.0" encoding="UTF-8"?> <!-- $Id: mysql-ds.xml,v 1.3.2.3 2006/02/07 14:23:00 acoliver Exp $ --> <!-- Datasource config for MySQL using 3.0.9 available from: http://www.mysql.com/downloads/api-jdbc-stable.html --> <datasources> <local-tx-datasource> <jndi-name>jdbc/HbnDataSource</jndi-name> <connection-url> jdbc:mysql://localhost/stripes_newb </connection-url> <driver-class>com.mysql.jdbc.Driver</driver-class> <user-name>stripes_app</user-name> <password>zebra</password> <valid-connection-checker-class-name> org.jboss.resource.adapter.jdbc.vendor.MySQLValidConnectionChecker </valid-connection-checker-class-name> <exception-sorter-class-name> org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter </exception-sorter-class-name> <!-- corresponding type-mapping in the standardjbosscmp-jdbc.xml (optional) --> <metadata> <type-mapping>mySQL</type-mapping> </metadata> </local-tx-datasource> </datasources>
You will of course have to replace the above values with the ones you configured using the MySQL tools.
Hibernate Configuration
I don't particularly like XML configuration (one of the reasons I'm drawn to Stripes), so if given a choice between XML config and code config, I choose code config. Check out this article from martinfowler.comwhere Martin discusses some pros and cons of code config vs XML config. The HbnConfigUtil that you'll see below replaces the need for a hibernate.cfg.xml file, and is more flexible in some ways. First off, it provides an easy way to reuse configuration that is common to all the ways your application will use Hibernate. For example, it is very common for your app to use a datasource to connect in a web situation, but use the non-datasource way when running tests, during prototyping, or using any other classes invoked by calling main(). Also, some of your stand-alone classes may want Hibernate to build the schema for you as part of execution, and some may want SQL to be sent to System.out. In all these cases, you probably still want Hibernate to manage the same set of Entity classes, so you would want that configuration reused. Code configuration makes all of this a breeze. Here is a HbnConfigUtil that I created to make this sort of thing a little easier:
package com.techlunch.newb.model; import java.util.*; import org.hibernate.*; import org.hibernate.cfg.*; public class HbnConfigUtil { private static final String DSN = "java:comp/env/jdbc/HbnDataSource"; private static SessionFactory sesFac = null; private HbnConfigUtil() {} static SessionFactory getSessionFactory() { if (sesFac == null) { System.out.println( "Creating Ses Fac" ); sesFac = createDatasourceFactory( false ); } return sesFac; } public static void useLocalConfig() { sesFac = createFdcDevFactory( false, false ); } public static void useLocalConfig( boolean showSql, boolean createSchema ) { sesFac = createLocalMySqlFactory( createSchema, showSql ); } private static SessionFactory createDatasourceFactory( boolean showSql ) { List<Class> classes = buildFullModelClassList(); return buildFactory( classes, DSN, false, showSql ); } private static SessionFactory createFdcDevFactory( boolean createSchema, boolean showSql ) { List<Class> classes = buildFullModelClassList(); LocalJdbcConfig jdbcCfg = LocalJdbcConfig.createFdcDevConfig(); return buildFactory( classes, jdbcCfg, createSchema, showSql ); } private static SessionFactory createLocalMySqlFactory( boolean createSchema, boolean showSql ) { List<Class> classes = buildFullModelClassList(); LocalJdbcConfig jdbcCfg = LocalJdbcConfig.createLocalMySqlConfig(); return buildFactory( classes, jdbcCfg, createSchema, showSql ); } private static List<Class> buildFullModelClassList() { List<Class> lst = new ArrayList<Class>(); lst.add( HrProfile.class ); lst.add( Manufacturer.class ); lst.add( OssComponent.class ); lst.add( OssLicenseDoc.class ); lst.add( Part.class ); lst.add( Widget.class ); return lst; } public static SessionFactory buildFactory( List classes, LocalJdbcConfig jdbcCfg, boolean createSchema, boolean showSql ) { return buildFactory( classes, jdbcCfg, null, createSchema, showSql ); } public static SessionFactory buildFactory( List classes, String datasource, boolean createSchema, boolean showSql ) { return buildFactory( classes, null, datasource, createSchema, showSql ); } private static SessionFactory buildFactory( List classes, LocalJdbcConfig jdbcCfg, String datasource, boolean createSchema, boolean showSql ) { try { Configuration config = new Configuration(); for (int i = 0; i < classes.size(); i++) config.addClass( (Class)classes.get( i ) ); if (jdbcCfg != null) { config.setProperty( "hibernate.connection.driver_class", jdbcCfg.getDriver() ); config.setProperty( "hibernate.connection.url", jdbcCfg.getUrl() ); config.setProperty( "hibernate.connection.username", jdbcCfg.getUid() ); config.setProperty( "hibernate.connection.password", jdbcCfg.getPwd() ); config.setProperty( "hibernate.connection.pool_size", jdbcCfg.getPoolSize() ); } if (datasource != null) { config.setProperty( "hibernate.connection.datasource", datasource ); } config.setProperty( "hibernate.dialect", "org.hibernate.dialect.MySQLDialect" ); config.setProperty( "hibernate.current_session_context_class", "thread" ); config.setProperty( "hibernate.cache.provider_class", "org.hibernate.cache.NoCacheProvider" ); if (showSql) config.setProperty( "hibernate.show_sql", "true" ); else config.setProperty( "hibernate.show_sql", "false" ); if (createSchema) config.setProperty( "hibernate.hbm2ddl.auto", "create" ); return config.buildSessionFactory(); } catch (Throwable ex) { // Make sure you log the exception, as it might be swallowed System.err.println( "Initial SessionFactory creation failed." ); ex.printStackTrace(); throw new ExceptionInInitializerError( ex ); } } }
HbnConfigUtil.getSessionFactory() defaults to using a DataSource looked up thru JNDI to acquire Connection resources. Stand alone classes will call the HbnConfigUtil.useLocalConfig() method with various parameters, depending on whether they want the schema to be generated or SQL to be sent to System.out. A stand alone class that doesn't need the schema to be generated and doesn't want SQL sent to System.out would do something like: (btw, HbnSessionUtil.beginTransaction() calls HbnConfigUtil.getSessionFactory())
public static void main( String[] args ) { try { HbnConfigUtil.useLocalConfig( false, false ); HbnSessionUtil.beginTransaction(); ManufacturerDAO manDao = DAOFactory.DEFAULT.getManufacturerDAO(); Manufacturer.dump( manDao.findInactives() ); Manufacturer.dump( manDao.findActives() ); } catch (Exception e) { e.printStackTrace(); HbnSessionUtil.rollbackOnly(); } finally { HbnSessionUtil.resolveTransaction(); HbnSessionUtil.closeSession(); } }
It is occasionally very useful to have a class that you can run that will initialize the schema for you and populate it with some initial data that you can use for either testing, or simply to initialize some data that you know needs to be added. An example of this (as well as having SQL sent to System.out) using my code config approach is:
public static void main( String[] args ) { try { HbnConfigUtil.useLocalConfig( true, true ); HbnSessionUtil.beginTransaction(); initializeModelData(); } catch (Exception e) { e.printStackTrace(); HbnSessionUtil.rollbackOnly(); } finally { HbnSessionUtil.resolveTransaction(); HbnSessionUtil.closeSession(); } }
Hibernate Session Management
The heart of using Hibernate is the Session. In Hibernate, the Session manages JDBC resources, transactions, the identity map, and unit of work, so you don't have to. This still means that you have to manage the Session, tho. To make these common session-management tasks easier, I created HbnSessionUtil. It handles creating at most a single Session which is stored in a ThreadLocal registry, as well as common Session tasks like beginning a transaction, rolling back, and comitting. Here it is:
package com.techlunch.newb.model; import java.util.*; import org.hibernate.*; public class HbnSessionUtil { private static final ThreadLocal<Session> registry; private static final ThreadLocal<Boolean> rollbackReg; static { registry = new ThreadLocal<Session>(); rollbackReg = new ThreadLocal<Boolean>(); } private HbnSessionUtil() {} public static void beginTransaction() { Session ses = getCurrentSession(); if (ses == null) { registry.set( HbnConfigUtil.getSessionFactory().openSession() ); rollbackReg.set( false ); ses = getCurrentSession(); } ses.beginTransaction(); } public static void rollbackOnly() { rollbackReg.set( true ); getCurrentSession().clear(); } public static Session getCurrentSession() { return registry.get(); } public static void evict( Object entity ) { getCurrentSession().evict( entity ); } public static void evict( List<Object> entities ) { for (Object o : entities) evict( o ); } private static void commitTransaction() { Session ses = getCurrentSession(); Transaction trn = ses.getTransaction(); trn.commit(); } private static void rollbackTransaction() { System.out.println( "Rolling back transaction!" ); getCurrentSession().getTransaction().rollback(); } public static void resolveTransaction() { if (rollbackReg.get()) rollbackTransaction(); else commitTransaction(); } public static void closeSession() { getCurrentSession().close(); registry.set( null ); rollbackReg.set( null ); } }
Your ActionBeans won't normally have to do much direct interaction with HbnSessionUtil, because I use a couple intercepting filters to manage the Session.
Using Filters/Interceptors to manage the Session
To make sure that every request to your web application is covered by a Hibernate Session, I create a servlet filter that makes sure a Session is available at all times during the course of a request. Here is my web.xml configuration for this filter:
<filter> <display-name>Hbn Filter</display-name> <filter-name>HbnFilter</filter-name> <filter-class> com.techlunch.newb.interceptor.HbnFilter </filter-class> </filter> <filter-mapping> <filter-name>HbnFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> </filter-mapping>
This filter is actually quite simple:
package com.techlunch.newb.interceptor; import com.techlunch.newb.model.*; import java.io.*; import javax.servlet.*; public class HbnFilter implements Filter { private FilterConfig filterConfig = null; public void init( FilterConfig filterConfig ) throws ServletException { this.filterConfig = filterConfig; } public void destroy() { this.filterConfig = null; } public void doFilter( ServletRequest req, ServletResponse resp, FilterChain chain ) throws IOException, ServletException { try { HbnSessionUtil.beginTransaction(); chain.doFilter( req, resp ); } catch (Exception e) { HbnSessionUtil.rollbackOnly(); e.printStackTrace(); req.setAttribute( "e", e ); req.getRequestDispatcher( "/error/appException.jsp" ).forward( req, resp ); } finally { HbnSessionUtil.resolveTransaction(); HbnSessionUtil.closeSession(); } } }
The filter simply begins a transaction, let's the rest of the request proceed normally, sets the transaction to rollback in the event of any exception, and finally resolves the transaction (commit unless rollbackOnly() was called) and closes the session.
Code those ActionBeans!
At this point, you have about everything in place necessary to have your ActionBeans, DAOs, and Entities do their thing without having to worry about Hibernate resource lifecycle issues.
Some choices I favor regarding ActionBeans include having my JSPs and ActionsBeans bind submitted form values directly to my Entities with no intermediate value holder, and then evicting these bound entities from the Hibernate Session if I want to ignore form binding. You may want to ignore any form binding that Stripes has performed if validation failed, or if you have cancel- or skip- style event handlers where you don't want any submitted form values processed.
A choice I currently favor regarding my Data Access Layer utilizes the approach layed out in the Generic Data Access Objects article at the Hibernate Wiki.