Mit ‘Transaction’ getaggte Artikel

Wicket, Spring und Hibernate mit OpenSessionInView und das Ganze optional in der Kommandozeile

Montag, 16. März 2009

Das ist doch mal ne Überschrift.

Als fast noch euphorischer Spring+Hibernate User wollte ich eigentlich nur eine kleine Basisapplikation schreiben, welche ein simples Mapping mit Assoziationen beherbergt. Diese Applikation soll mehrere Transaktionen in einem Request (== in einer Session) beherrschen und seine Funktionalität auch in der Kommandozeile entfalten dürfen. Dann soll noch Wicket als UI Schicht drauf. Klingt einfach, für so ein ausgewachsenes Toolset, nicht war?

So lange wie ich nach einer Lösung gesucht habe, müßte ich jetzt eigentlich erstmal gefühlte 2 Stunden Bildzeitung oder schlimmeres Zitieren. Das erspare ich dem geneigten Leser.

Wir gehen davon aus, es existiert eine Webapp mit Spring 2.5.5, Hibernate 3.2 und Wicket 1.4RC.

Einfachster umzusetzender Fall:

(1) Bean mit JSP und BeanNameMapping

könnte schon alles sein, wenn heutzutage noch jemand freiwillig JSPs in einem neuen Projekt benutzen würde. Trotzdem sei die Möglichkeit hier genannt, weil sie die Minimalform des OpenSessionInViewInterceptor / OpenSessionInViewFilter darstellt.

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
    <property name="order" value="0"/>
    <property name="interceptors">
        <list>
            <ref bean="openSessionInViewInterceptor"/>
        </list>
    </property>
</bean>
<bean name="/hello.htm" class="xxx.HelloController">
	<property name="trackService" ref="trackService"></property>
</bean>

“Oh, das ist ja einfach.” - dachte ich auch. Bis ich das erste mal Versucht habe, in meiner Session mehrere Transaktionen zu benutzen. Dann merkt man nämlich, daß die Session warscheinlich nach dem Commit zu ist. “Wieso das?” fragen wir uns und lesen auf der Hibernate Seite über Sessions und Transactions nach; hier könnte man denken das ist normal so.

Because Hibernate can’t bind the “current session” to a transaction, as it does in a JTA environment, it binds it to the current Java thread. It is opened when getCurrentSession() is called for the first time, but in a “proxied” state that doesn’t allow you to do anything except start a transaction. When the transaction ends, either through commit or roll back, the “current” Session is closed automatically.

OK, denken wir uns und vergessen ganz schnell das eingangs erwähnte Session-per-Operation Anti Pattern. Wer sich jetzt keine Waffel weiter macht kommt mit der Lösung wahrscheinlich irgendwie auch zu Daten aus dem ORM, wird zwar gelegentlich ein paar LazyLoadingExceptions fangen, aber da hilft dann ein

lazy="false"

;)
Nein…

(1.a) Multi-Transaction per Session

Nun wollte ich an der Stelle doch nicht aufgeben und fragte mich die ganze Zeit, wie sich das all die schlauen Leute gedacht haben, wenn man mehrere Transaktionen haben will. Schließlich machen ja die ganze Session und deren Caching Mechanismen keinen Sinn, wenn man nur eine Transaktion darin machen könnte. Ohne Transaktionen arbeiten geht bekanntermaßen auch nicht.

So kam es, daß ich zunächst in irgendeinem JIRA Ticket von Spring in einer Antwort von Jürgen Höller laß, daß man

hibernate.current_session_context_class

nicht auf  “thread” stellen darf.

Ah Ja …

Da kann man sich denken was man will (z.B., warum das nur in diesem JIRA Ticket zu finden ist), Fakt ist, daß man direkte Hibernate Konfiguration neben Spring am besten läßt. Kommt dazu, daß IntelliJ Idea eine Excellente Spring-ApplicationContext Code-Completion hat (oder das Schema an der Stelle gut genug ist), auf jeden Fall geht das ganz gut.

Die Konfiguration der Hibernate-Session via Spring führt dazu, daß die “Haupt Session”, welche am Request hängt nicht nach einer Transaktion schließt.

Man gelangt zu einer solchen SessionFactory:

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
	<property name="dataSource" ref="dataSource"/>
	<property name="mappingResources">
		<list>
			<value>xx/Track.hbm.xml</value>
		</list>
	</property>
	<property name="hibernateProperties">
		<props>
			<prop key="hibernate.hbm2ddl.auto">update</prop>
			<prop key="hibernate.dialect">${hibernate.dialect}</prop>
			<prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
			<prop key="hibernate.format_sql">${hibernate.format_sql}</prop>
			<prop key="hibernate.cache.use_second_level_cache">${hibernate.cache.use_second_level_cache}</prop>
			<prop key="hibernate.cache.use_query_cache">${hibernate.cache.use_query_cache}</prop>
			<prop key="hibernate.cache.provider_class">${hibernate.cache.provider}</prop>
			<prop key="hibernate.connection.pool_size">10</prop>
			<prop key="hibernate.jdbc.batch_size">1000</prop>
			<prop key="hibernate.bytecode.use_reflection_optimizer">${hibernate.bytecode.use_reflection_optimizer}</prop>
		</props>
	</property>
	<!--property name="schemaUpdate" value="${hibernate.schemaUpdate}"/-->
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="driverClassName" value="${hibernate.connection.driver_class}"/>
	<property name="url" value="${hibernate.connection.url}"/>
	<property name="username" value="${hibernate.connection.username}"/>
	<property name="password" value="${hibernate.connection.password}"/>
</bean>
<bean name="openSessionInViewInterceptor"
	  class="org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor">
	<property name="sessionFactory" ref="sessionFactory"/>
</bean>

Damit kann man nun schonmal halbwegs was anfangen, zumindest in dem o.g.

xxx.HelloController

welcher auf “/hello.htm” mappt. Innerhalb dessen - oder innerhalb der Methoden, die dieser Controller verwendet - kann man nun lustig Transaktionen auf- und zu machen.

(1.b) Multi-Transaction by Annotation

Damit man das Transactionhandling nicht manuell tun muß (und auch nicht sollte), kann man unter dem Block SessionFactory gleich noch mit spezifizieren, daß Spring die Transaktionen automatisch nach Annotation auf und zu macht:

<!-- Tell Spring it should use @Transactional annotations -->
<tx:annotation-driven/>
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
	<property name="sessionFactory" ref="sessionFactory"/>
	<property name="nestedTransactionAllowed" value="true"/>
</bean>

Mit dieser Konfiguration kann man jetzt in einer verwendeten Service-Klasse schreiben:

@Transactional(readOnly = true)
public Track findTrackByFilename(String filename) {
    // ...
}

@Transactional(rollbackFor = Exception.class)
public Album findAlbumByNameOrCreate(String name) {
    // ...
}

und Spring macht den Rest. Das ist doch schonmal ganz sexy.

(2) Einbindung Wicket

Nachdem nun das Spring-”Minimal” Beispiel funktioniert, sollte man mal weiter nach Vorne im Schichtenmodell gehen. Wir wollen einen Wicket Controller mit OpenSessionInView bestücken. Da fällt auf, daß die Wicket Requests nicht über das Spring Mapping laufen und damit auch nicht von dem HibernateSessionInViewInterceptor greifbar sind. Hier hat Jürgen und sein Team vorgesorgt und gleich einen HibernateOpenSessionInViewFilter mitgeliefert. Den kann man ganz trivial auf die Wicket Requests klemmen.

    <servlet>
        <servlet-name>WicketApplication</servlet-name>
        <servlet-class>org.apache.wicket.protocol.http.WicketServlet</servlet-class>
        <init-param>
            <param-name>applicationClassName</param-name>
            <param-value>xxx.wicket.Application</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>WicketApplication</servlet-name>
        <url-pattern>/wicket/*</url-pattern>
    </servlet-mapping>

    <!-- this will be used by wicket only as spring requests use osivInterceptor -->
    <filter>
        <filter-name>openSessionInViewFilter</filter-name>
        <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>openSessionInViewFilter</filter-name>
        <url-pattern>/wicket/*</url-pattern>
    </filter-mapping>

Am Ende passiert hier drin das Gleiche wie in dem Interceptor.

Vielleicht gibt es ja auch eine Variante, wie man Wicket (welches am Ende auch wieder mit Spring bestückt wird) über den OSIV Interceptor filtern kann.  Dann hätte man nämlich auch nicht die dumme Notwendigkeit einen extra URL Level (hier: “wicket”) aufzumachen. Comments appreciated…

Mehr zu der eigentlichen Spring-Wicket Integration gibt es übrigens bei DZone, soll aber nicht Inhalt dieses Artikels sein.

(3) Die Kommandozeile

Als ob das nicht schon genug Text wäre, kommt nun hier (meiner Meinung nach) der interessanteste Teil. Wie oben gesagt ist Anforderung an die Applikation, daß man quasi alles auch per CMD bedienen kann. Beispiele wären Batch-Processing (Import) oder Integration Test. Man kann sich jetzt schnell denken, daß es hier keinen OSIV Filter oder Interceptor gibt. Mist. An der Stelle bekommt man nochmal schmerzlich zu spüren, daß Spring-Hibernate nicht gleich Hibernate ist, denn einfach eine Session öffnen reicht hier nicht aus. Spring proxied hier und da noch etwas.

Wenn man jetzt eine CMD Applikation starten würde bekommt man entweder eine solche Exception:

org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session

oder was ich noch verwirrender finde:

java.lang.IllegalStateException: No Hibernate Session bound to thread,
and configuration does not allow creation of non-transactional one here

Haben wir nicht weiter oben gelesen, daß man die Session nicht an den Thread binden darf?

Zwei Links haben mich hier zu einer Lösung gebracht: dieser junge Mann hier und der Thread. Dabei kommt eine schlichte SessionKlasse heraus, die man z.B. in einer main-Methode aufrufen könnte oder per AOP an die richtige Stelle binden könnte.

/**
 * pretty much a clone of what the hibernate session in view filter does
 * User: ab
 * Date: Mar 8, 2009
 * Time: 9:37:19 AM
 */
public class Session extends HibernateAccessor {
    private final Logger log = Logger.getLogger(Session.class);

    public void startSession() {
        org.hibernate.classic.Session session = getSessionFactory().openSession();
        setFlushMode(FLUSH_NEVER);

        //Bind the Session to the current thread/transaction
        TransactionSynchronizationManager.bindResource(getSessionFactory(), new SessionHolder(session));
        //Activate transaction synchronization for the current thread.
        TransactionSynchronizationManager.initSynchronization();
    }

    public void endSession() throws HibernateException {
        SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager.getResource(getSessionFactory());
        log.debug("Flushing single Hibernate Session");

        try {
            flushIfNecessary(sessionHolder.getSession(), false);
        }
        catch (HibernateException ex) {
            throw convertHibernateAccessException(ex);
        }

        sessionHolder = (SessionHolder) TransactionSynchronizationManager.unbindResource(getSessionFactory());
        log.debug("Closing single Hibernate Session");
        SessionFactoryUtils.releaseSession(sessionHolder.getSession(), getSessionFactory());
    }
}

Bei exzessiven Import-Operationen muß man lediglich beachten, daß man regelmäßig die Session flush()t und clear()t.  Ansonsten hat man in allen Belangen eine äußerst praktische und vielseitige Basis.

Damit sind alle Anforderungen erfüllt und ich kann mich nun um das (eigentlich popelige) Problem des Mappings kümmern. Wenn das mal kein erfolgreiches Wochenende ist.

Oh Yeah…