Den här websidan ser mycket bättre ut i en bläddrare som följer rådande webstandard, men du kan titta på sidan med andra bläddrare också.

Bluefish :: Akvariet

Bluefish logo
Logo

Java Server Faces

Java Server Faces och enhetstester

Java Server Faces och Hibernate

Hibernate och JBoss

Lunar Linux and Zepto 4200

Att delta i projekt med öppen källkod

Java-projekten i Apache

Hibernate - en introduktion

HemsideByggaren under huven

Öppen källkod på Bluefish

JavaOne 2003

Lime

OOPSLA 2002

AOP

fixafest.nu under huven

Linux From Scratch

Bluefish skänker

XSL

JDepper

OOPSLA 1999

Java Server Faces och Hibernate.

Publicerad 2004-10-12

Inledning

Detta är del 2 i en artikelserie där jag beskriver hur jag tar min testapplikation ifrån min artikel om Java Server Faces och bygger vidare på denna. I den första delen beskrev jag den grundläggande designen och hur jag fick enhetstestning och loggning att fungera.

I denna artikel går jag vidare och beskriver hur jag byter persistensmekanism från xml-fil till MySQl och Hibernate.

Trots artikelns namn innehåller den inte mycket om JavaServerFaces. Detta är väl ett tecken så gott som något att min ambition att klientkoden inte skall behövas skrivas om bara för att vi byter persistensmekanism ser ut att hålla!

Hibernate

När testerna och loggning var på plats var det dags att ta nästa steg: persistens i en RDBMS med hjälp av ett ORM verktyg, i detta fall Hibernate. Ambitionen var att klasserna i webblagret inte skulle märka att persistensen nu skedde i en "riktig" databas i stället för i en xml-fil. Då som tidigare påpekats, all kommunikation mellan webblager och persistenslager sker via interface var detta ett realistiskt mål. Persistensmekanismen ska kunna bytas ut genom en enkel omkonfigurering i web.xml.

Syftet med denna artikel är inte att beskriva Hibernate som finns utförligt beskrivet på annat håll, t ex i denna artikel av Torben Norling, men en liten beskrivning om vad Hibernate är kan ändå vara på sin plats.

Hibernate är ett ORM-verktyg där ORM står för object-relational-mapping, dvs ett verktyg som tar objekt, i vårt fall vanliga POJO:s (Plain Old Java Objects), och mappar dessa till ett format som går att persistera i en relationsdatabas. Mappningen fungerar naturligtvis även åt andra hållet där Hibernate ansvarar för att skapa objekt som motsvarar den information som läses från databasen. Själva mappningen mellan objekt och databas definieras i xml-filer, normalt en fil för varje klass som skall persisteras.

I vårt fall är en av filerna UserImpl.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="se.bluefish.jsf.example.businessobjects.UserImpl" table="user">
        <id name="id" column="id" type="long" unsaved-value="0">
            <generator class="native"/>
        </id>
        <property name="name" column="name"/>
        <property name="password" column="password"/>
        <property name="fullName" column="full_name"/>
        <property name="fromAddress" column="from_address"/>
        <property name="replyToAddress" column="reply_to_address"/>
        <set name="subscriptions" cascade="all" inverse="true" lazy="true">
          <key column="userid"/>
          <one-to-many class="se.bluefish.jsf.example.businessobjects.SubscriptionImpl"/>

</set>
    </class>
</hibernate-mapping>

Här ser vi bland annat att

  • instanser av klassen UserImpl persisteras i tabellen user
  • attributet id i UserImpl fungerar som nyckel och sparas i kolumnen med samma namn.
  • UserImpl har en kollektion med instanser av SubscriptionImpl som mappas som en en-till-många relation

Genom att skicka in våra *.hbm-xml filer i SchemaExport, ett verktyg som följer med Hibernate, genereras en ddl-fil som sedan kan användas för att skapa tabellerna i vår databas. Observera att *.hbm.xml filerna behövs även under runtime för att Hibernate skall bli korrekt konfigurerat så dessa måste ligga i CLASSPATH, lämpligen på samma ställe som respektive klass.

När databasen är uppe och snurrar återstår bara problemet att få vår applikation att prata med denna via Hibernate!

Första steget är att göra en "Hibernate-aware" implementering av MailReaderDao.

Sessionsobjektet

För att förstå problematiken kan det vara på sin plats med en förklaring av vad sessionsobjektet är och vad det används till. En Hibernate Session är ett (kortlivat) objekt som kapslar in en JDBC-koppling och därigenom representerar en "konversation" mellan applikation och databas. Innan objekt läses eller skrives till databasen så måste en session öppnas. Efter avslutade persistensoperationer skall sessionen stängas igen. En session fås via ett anrop till det "globala" session factory objektet som lämpligen skapas och sparas undan vid uppstart.

Exempel:

 Session session = sessionFactory.openSession();
 ...
 User user = (User )session.load(...);
 ...
 session.close();

All nödvändig felhantering saknas förstås!

Hibernate stödjer "lazy loading" av kollektioner , dvs det går att definiera en ett-till-många relation med lazy=true vilket innebär att Hibernate hämtar objekten i kollektionen först när de behövs. Detta måste göras "inne" i en öppen session vilket kan ställa till problem för webbapplikationer. Har du då läst in objektet i början av din requesthantering och sedan stängt sessionen innan du försöker komma åt kollektionsobjekten exempelvis när din jsp-sida skall renderas så får du smäll på fingrarna.

Det finns flera tekniker för att hantera detta problem. Vår testapplikation använder ett servletfilter, HibernateFilter, som öppnar en session innan requesten skickas vidare och sedan stänger sessionen på tillbakavägen.

   ...
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
   {
    try
    {
     HibernateSession.currentSession();
    }
    catch (HibernateException e)
    {
     throw new ServletException("Unable to access current Hibernate session");
    }
    try
    {
     chain.doFilter(request, response);
    }
    finally
    {
       try
       {
        HibernateSession.closeSession();
       }
           catch (HibernateException e) {}
    }
    }
    ...

HibernateSession är en hjälpklass som säkerställer att vi endast öppnar en session per requesttråd genom att vid första åtkomst från en tråd spara undan sessionen i en ThreadLocal och sedan returnera samma session vid ytterligare åtkomstförsök från samma tråd.

Filtret ser till att HibernateSession initieras korrekt med en SessionFactory som är konfigurerad med hjälp av applikationens hibernate.cfg.xml.

En DAO-klass som använder Hibernate måste enligt vår princip om att klientkod inte skall behöva bekymra sig om persistensmekanism, fånga alla Hibernatespecifika undantag som kastas och omvandla dessa till vår generella DataAccessExeption. Dessutom måste felhanteringen se till att sessioner stängs, samt göra rollback på eventuella transaktioner.

Exempel:

  ...
  Session session = null;
  Transaction tx  = null;
  try
  {
    session = sessionfactory.openSession();
    tx      = session.beginTransaction();
    ...
    // Gör något persistensaktigt här
    tx.commit();
  }
  catch (HibernateException e)
  {
    if (tx != null)
    {
     try
     {
    tx.rollback();
     }
     catch (HibernateException e1)
     {
    throw new DataAccessException("...",e1);
     }
    }
    throw new DataAccessException("...",e);
  }
  finally
  {
    if (session != null)
    {
      try
      {
        session.close();
      }
      catch (HibernateExecption e)
      {
      }
    }
  }
  ...

Mycket kod blir det! Detta måste dessutom upprepas i varje metod som använder Hibernate. Jag angrep detta genom att skapa en liten klasshierarki av Callback-klasser:

Uploaded Image:HibernateMailReader.png

Tanken är att ett callback-objekt skall implementera själva persistensoperationen i sin execute-metod. Objektet skapas och skickas in en generell metod i DAO-klassen som kapslar in anropet till execute i en felhantering som ovan.

    ...

    public void addUser(final User user)throws DataAccessException
    {
    doInTransaction(new TransactionCallback("Unable to save user ["+user.getName()+"] ") {

      public void execute(Session session) throws HibernateException
      {
         session.save(user);
         log.debug(" Added: "+user);
      }

    });
    }

    ...

    protected void doInTransaction(TransactionCallback callback) throws DataAccessException
    {
        Session     session = null;
        Transaction tx      =  null;
        try
        {
            session = openSession();
            tx = session.beginTransaction();
            callback.execute(session);
            tx.commit();
        }
        catch (Exception e)
        {
            handleRollback(tx);
            throw new DataAccessException(callback.getErrorMessage(),e);
        }
        finally
        {
            closeSession(session);
        }
    }

   private void closeSession(Session session)
    {
        if (session != null)
        {
            try
            {
                 HibernateSession.closeSession();
            }
            catch (HibernateException e)
            {
                log.error("Exception when closing session!",e);
            }
        }
    }

    private Session openSession() throws HibernateException
    {
        return HibernateSession.currentSession();
    }
    ...

Värt att notera är, trots att servletfiltret som tidigare beskrivits ser till att en session är öppen under den tiden som en request behandlas, att en session till synes öppnas och stängs även i vårt DAO-objekt. DAO-objektet måste ha tillgång till en session för att kunna anropa dess persistensmetoder. Det korrekta sättet att få tillgång till en session i vår design är att anropa metoden currentSession i HibernateSession. För att denna klass skall kunna ha koll på när en session slutligen skall stängas på riktigt så måste ett anrop till currentSession alltid följas av ett anrop till closeSession.

Konfigurering i web.xml

Att nu persistera i exempelvis MySQL via Hibernate i stället för i en xml-fil fixades genom att ändra DAO-klass i web.xml:

    ...
    <context-param>
      <param-name>dao</param-name>
<param-value>se.bluefish.jsf.example.dao.hibernate.HibernateMailReaderDaoImpl</param-value>
      <description>
        Read by the MailReaderDaoInitializer to create the
        right type of data access implementation.
      </description>
    </context-param>
    <listener>
          <listener-class>se.bluefish.jsf.example.dao.MailReaderDaoInitializer</listener-class>
    </listener>
    ...

I den avslutande delen av artikelserien beskriver de problem jag stötte på när jag ville använda Hibernate tillsammans med JBoss.

/Lars Svadängs

Bluefish logo

Bluefish AB :: Sturegatan 34 :: 114 36 Stockholm :: tel 08 459 93 30 :: fax 08 459 93 39 :: e-post info@bluefish.se