Hibernatestödet i Spring

Detta inlägg ingår i serien Spring från början och kommer att behandla Springs stöd för Hibernate. Vi kommer inte att gå närmare in på vad Hibernate är och gör utan koncentrera oss på vad Spring kan hjälpa oss med och hur man kan jobba med HibernateTemplate och andra centrala klasser i modulen spring-orm. ORM står för Object Relational Mapping och modulen spring-orm innehåller dessutom stöd för JPA, TopLink och iBATIS.

JPA som alternativ till Hibernate

JPA (Java Persistence API) är ett standardiserat API som tagits fram bland annat av utvecklarna bakom Hibernate och är därför mycket likt Hibernates API. Hibernate 3 implementerar JPA och man kan därför som alternativ utveckla kod med hjälp av Springs JPA-stöd och använda Hibernate som implementation. Detta ger fördelen av att använda en standard men bland annat nackdelen att inte hela Hibernate API:et finns tillgängligt genom JPA. I denna artikel behandlas Springs hibernatestöd men JPA-stödet liknar till stor del hibernatestödet både funktionellt och vad gäller namnsättning.

Hibernatestöd i Spring

Spring erbjuder ett brett stöd för Hibernate:

  • Resursdefinitioner för DataSource och SessionFactory
  • Template-klass för DB-operationer; HibernateTemplate
  • Basklass för DAO (HibernateDaoSupport)
  • Transaktionshantering
  • Exceptionhantering
  • Stöd för hibernatemappning via annoteringar (AnnotationSessionFactoryBean)

Sedan får man ju på köpet alla de allmänna fördelarna med att använda Spring som DI-ramverk. Man kan till exempel konfigurera olika databaser för utveckling, test och produktion.

Förutom modulen spring-orm behövs modulerna spring-core, spring-context, spring-beans, spring-jdbc. För transaktionshantering behövs dessutom modulerna spring-tx och spring-aop.

Denna artikel är skriven utifrån Spring version 2.5+ och Hibernate version 3.x

HibernateTemplate

Denna klass utgör grundstödet för hibernateoperationer och använder ”templating” på ett sätt som påminner mycket om JdbcTemplate som beskrivs i artikeln om Spring JDBC. Hibernates API är speglat i denna klass och den låter utvecklaren arbeta med detta antingen direkt med hjälp av templatemetoderna find(), saveOrUpdate() etc eller med hjälp av metoden execute(HibernateCallback callback). Den senare ger en möjlighet att arbeta mot en hibernatesession i en callbackmetod doInHibernate(Session session) som man skapar i sin HibernateCallback-instans.
HibernateTemplate hanterar själv exceptionkonvertering, öppning och stängning av sessioner och eventuell delaktighet i pågående transaktioner.

HibernateDaoSupport

Denna basklass är till för att bygga sina klasser som utför hibernateoperationer (DAO-klasser). Den förväntar sig en SessionFactory (typiskt mha DI) och skapar då en instans av HibernateTemplate som man sedan kommer åt med getHibernateTemplate().

public class PersonDaoImpl extends HibernateDaoSupport implements PersonDao {

    public List getPersons() {
        return getHibernateTemplate().loadAll(Person.class);
    }
....
}

Ett alternativ till användning av den callbackbaserade HibernateTemplate är att med hjälp av metoden getSession() i HibernateDaoSupport arbeta direkt mot en Session. Detta ger en fördelen att även checked exceptions kan kastas i detta kodblock:

public class ExamplePersonDao extends HibernateDaoSupport
        implements PersonDao {

  public List findPersonsByLastName(String lastName)
        throws DataAccessException, NoPersonsException {
      Session session = getSession(false);
      try {
          Query query = session.createQuery("from Person where lastName=?");
          query.setString(0, lastName);
          List result = query.list();
          if (result == null) {
              throw new NoPersonsException("No Person found by lastName="+lastName);
          }
          return result;
      }
      catch (HibernateException e) {
          throw convertHibernateAccessException(e);
      }
  }
}

Det går även att koda utan beroenden mot Spring och enbart mot Hibernate genom att hämta sin Session via sessionFactory.getCurrentSession(). I detta fall skall instansen sessionFactory vara en av Springs implemenatationer av interfacet SessionFactory för att koden skall fungera som förväntat i Springs transaktioner.

Det går att blanda ren JDBC och Hibernate i samma Session och transaktions- och exceptionhantering fungerar på samma sätt.

Exceptions

På samma sätt som i Springs JDBC-stöd så konverteras hibernatespecifika undantag till Springs exceptionshierarki DataAccessException vilka är unchecked. Detta gör att man slipper en massa onödig try-catch-kod och bara behöver hantera exceptions där man verkligen kan göra något vettigt felhantering. Det finns även stöd för att explicit konvertera exceptions med hjälp av SessionFactoryUtils.convertHibernateAccessException(HibernateException);

SessionFactory och mappning

Interfacet SessionFactory är centralt för Hibernate. I Spring behöver vi känna till följande två implementationer av detta:

  • LocalSessionFactoryBean – Grundimplementation med flexibla setters-metoder som förenklar konfigurationen
  • AnnotationSessionFactoryBean – En subklass av LocalSessionFactoryBean som stöder hibernate-annotationer

I vårt exempel nedan används AnnotationSessionFactoryBean tillsammans med en konfigurationsfil hibernate.cfg och annotationer för mappningen i modelklassen Person. Sedan behövs det bara en sak till för vår SessionFactory – en DataSource som man med fördel konfigurerar även den med hjälp av Spring:

<!-- Hibernate SessionFactory -->
<!-- Use of AnnotationSessionFactoryBean requires Hibernate3 Annotation module
and supports EJB3 and Hibernate specific annotations for our Hibernate mapping -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
  <property name="dataSource" ref="dataSource"/>
  <property name="configLocation" value="classpath:hibernate.cfg.xml"/>
  <property name="hibernateProperties">
    <value>
      hibernate.dialect=${hibernate.dialect}
      hibernate.hbm2ddl.auto=update
    </value>
  </property>
</bean>

I vårt fall anger vi en vanlig hibernate.cfg.xml som bara anger vilka klasser som ingår i Hibernate:

<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
  <session-factory>
    <mapping class="se.cygni.sample.spring.hibernate.model.Person"/>
  </session-factory>
</hibernate-configuration>

Mappning med hjälp av annotationer i klassen Person är medvetet minimal. I en riktig mappning lägger man till annat såsom relationer till andra modellklasser, kolumnlängder etc.

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Person {

    private Long id;

    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    public Long getId() {
        return id;
    }
    ...

Notera att annotationerna är JPA-annotationer, dvs i paketet javax.persistence. Man kan kombinera dessa med de så kallade ”Hibernate Extentions” för Hibernatespecifika annoteringar.

Spring kommer att sköta livscykeln för en SessionFactory i sitt ApplicationContext, dvs skapande, initiering och stängning.

Se även Springs stöd för annotationer

Transaktionshantering

Sessioner och transaktioner

En SQL-databasoperation körs alltid i en transaktion (även om den är implicit). Hibernate delegerar transaktionsoperationer till det underliggande JDBC-lagret.
Det finns många alternativa transaktionstrategier för en hibernateapplikation.
Den enklaste formen är kanske den man får då man använder HibernateTemplate utan någon transaktionshantering alls. I detta fall skapas och används en ny Session för varje templateoperation.
Det vanligaste är att man markerar vilka databasoperationer som utgör sina transaktioner. Spring synkroniserar då automatiskt transaktioner så att samma Session används under hela transaktionen. Detta för att Hibernates Session är intimt förknippad tillståndsmässigt med transaktionen.

Ett tredje fall är att man vill använda samma Session för en serie av transaktioner samt även i sin JSP-sida (vyn). Detta mönster kallas Open Session in View och stöds av ett servletfilter som naturligt heter OpenSessionInViewFilter.

Vi har alltså 3 huvudalternativ:

  1. Utan transaktioner med HibernateTemplate
  2. Med transaktioner med en session per transaktion
  3. Med transaktioner med en session för en serie av transaktioner

Normalfallet med transaktioner och HibernateTemplate är alltså alternativ nr 2.

Transaktionshantering kräver att man som utvecklare bestämmer transaktionshanterare, transaktionsmarkering (klasser/metoder samt början och slut av transaktion) samt diverse attribut för tex propagering och read-only.
Det finns stöd för två olika sorts transaktionshanterare i Spring; en för användning i JTA/container och en för ”lokala” transaktioner (hanterar bara en SessionFactory).
Transaktionshanteraren ser till att en och samma Session används under pågående transaktion samt sköter flush() av denna vid slutet av transaktionen (commit). Om ett unchecked exception kastas görs istället en rollback per default.

Deklarativt med AOP eller programmatiskt

Det vanligaste och kanske naturligaste sättet att man i Spring deklarerar transaktionshanterare och sedan med hjälp av AOP eller annotationer deklarerar vilken kod som omfattas. På detta sätt behöver utvecklaren inte hantera transaktioner explicit. Dock går det även att arbeta helt programmatiskt om man vill.
I fallet AOP kommer din implementation av de transaktionella klasserna/metoderna att bytas ut av Spring till proxifierade objekt som använder en Spring TransactionInterceptor och din angivna TransactionManager, i vårt fall en HibernateTransactionManager. Se exempelkonfigurering nedan:

<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
  <property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean id="personDao" class="se.cygni...dao.PersonDaoImpl">
  <property name="sessionFactory" ref="sessionFactory"/>
</bean>

<bean id="personManager" class="se.cygni...service.PersonManagerImpl">
  <property name="personDao" ref="personDao"/>
</bean>

<tx:advice id="txAdvice">
  <tx:attributes>
    <tx:method name="*"/>
  </tx:attributes>
</tx:advice>

<aop:config>
  <aop:advisor id="managerTx" advice-ref="txAdvice"
     pointcut="execution(* *..service.*Manager.*(..))" />
</aop:config>

<!-- Enable @Transactional support -->
<tx:annotation-driven/>

Mer om Springs stöd för AOP.

Kod som använder HibernateTemplate kommer automagiskt att vara med i en pågående transaktion (om en sådan finns – då Springs HibernateTransactionManager används – Spring kommer att leta efter en TransactionManager med <bean id=”transactionManager” ….>) . Detta kommer sig av att den binder en Session till transaktionen i en ThreadLocal – alltså i samma tråd. Detta är viktigt att tänka på när man skall testa eftersom testkoden måste emulera detta beteende, dvs använda samma Session för den kod som exekverar i samma transaktion och göra flush) annars kommer din DAO-kod som testas inte bete sig på samma sätt i testet som i din applikation eftersom tex en ny Session annars skapas för varje användning av accessmetoderna i HibernateTemplate. Som tur är så finns det stöd för detta i klassen AbstractTransactionalDataSourceSpringContextTests, se sektionen Test nedan.

Öppen Session och Lazy Loading

Det kan noteras att de metoder i HibernateTemplate som returnerar en java.util.Iterator är tänkt att användas inom en transaktion i Spring eftersom itereringen kräver att det underliggade ResultSet:et måste vara öppet.
Samma sak gäller för så kallad Lazy Loading som definieras i hibernatemappningen. Lazy loading bygger på att data hämtas först om det verkligen behövs och då behöver följdaktligen sessionen vara öppen. Detta blir aldrig ett problem i kod som använder HibernateTemplate, dock kan det bli problem i testkod eller i en JSP-sida.

Spring tillhandahåller ett servletfilter som heter OpenSessionInViewFilter som råder bot på detta problem. Detta filter kan även konfigureras så att en ny Session skapas för varje transaktion/operation men inte stängs förrän vyn är rendrerad. På detta sätt kan ett LazyLoadingException undvikas på bekostnad av att en eller flera sessioner/connections är öppna en längre tid vilket tar resurser i anspråk ”onödigt” lång tid.

Test

Det finns gott i teststöd i modulen spring-test (f.d. spring-mock). De långa klassnamnen beskriver relativt väl vad klassen kan.
Typiskt så bryter man ut test-specifika Spring-bönor såsom DataSource etc när man testar. Detta är ju ett av Springs stora fördelar. I vårt fall används en speciell testdatabas definierad i en separat fil (applicationContext-db-test.xml).
I vår testklass finns följande springkonfigurering för detta:

protected String[] getConfigLocations() {
  return new String[] {
    "classpath:/applicationContext.xml",
    "classpath:/applicationContext-db-test.xml"
  };
}

För test av hibernatekod ingår framförallt att testa den kod gör hibernateoperationer, dvs DAO-kod.

Nedan är ett exempel på test av vår PersonDao som använder baseras på klassen AbstractTransactionalDataSourceSpringContextTests.
Denna stödklass ger bla stöd för:

  • Att ange vilka konfigurationsfiler som skall användas av Spring under testet
  • En emulerad transaktionsmiljö, dvs den är till för test av kod som normalt körs i transaktioner
  • Den gör normalt rollback(), alltså inte commit(), i slutet av testet vilket förhindar att tester påverkar varandra eller databasens tillstånd.

Ett test körs som en transaktion. Man kan manuellt sätta transanktionsgränser med endTransaction() och startNewTransaction(). Om man ändå vill att databasen skall uppdateras kan man anropa setComplete(). Den transaktionshanterare som har namnet ”transactionManager” i din springkonfiguration är den som används per default.

Vår PersonDao blir satt av Spring via ”auto-wire by type” som fungerar så länge vi bara har en springböna av denna typ.

public class PersonDaoTest extends AbstractTransactionalDataSourceSpringContextTests {
  private PersonDao dao = null;

  public void setPersonDao(PersonDao dao) {
    this.dao = dao;
  }

  /**
   * Test av att läsa befintlig Person i DB
   */
  public void testGetPerson() throws Exception {
    Person person = dao.getPerson(1L);

    assertNotNull(person);
    assertEquals("Permanent", person.getFistName());
    assertEquals("Person", person.getLastName());
    assertEquals("1234-5678", person.getPhoneNumber());
  }
}

Mer läsning: