Spring Transactions

Detta inlägg ingår i serien Spring från början och kommer att behandla det transaktionsstöd som finns i springmodulen spring-tx.

Transaktioner är ett sätt att hålla ihop en eller flera operationer. Typiskt gäller principen ”allt eller inget” vilket betyder att alla operationer som ingår i en transaktion ska exekveras utan fel för att transaktionen ska gälla. Det vanligaste fallet är databastransaktioner där exempelvis flera skrivningar måste exekveras utan fel innan en så kallad commit genomförs. Ett exempel på detta är det klassiska bankkontoexemplet med överflyttning av pengar från ett konto till ett annat. En överflyttning sker genom att ett uttag först sker från ett konto och sedan insättning på ett annat konto. Bägge operationerna måste lyckas, annars ska transaktionen inte gälla det vill säga att man ”rullar tillbaka” transaktionen via en så kallad rollback.

Spring erbjuder på ett enhetligt sätt stöd för att kunna hantera transaktioner av olika slag som till exempel JTA, JDBC, Hibernate, JPA och JDO. Transaktionsstödet kan användas på två sätt, deklarativt eller programmatiskt. Det deklarativa sättet är det absolut vanligaste och innebär att metadata kring transaktionslogik inte ligger inbäddad i den faktiska javakoden utan enbart finns deklarerad ”utanför” javakoden via metadata. Metadata kan antingen bestå av externa XML-filer eller annotationer och påverkar alltså inte javakoden, den är så att säga non-intrusive.

Deklarativt transaktionsstöd

Springs deklarativa transaktionsstöd möjliggörs via användning av Spring AOP som tidigare beskrivits i artikeln Introduktion till Spring AOP. Dock krävs inte djupa kunskaper inom Spring AOP för att kunna använda Springs transaktionsstöd. AOP Proxies används för att väva ihop en PlatformTransactionManager med en så kallad TransactionInterceptor som sedan hanterar själva transaktionen, det vill säga skapande, commit, rollback och så vidare. Gränssnittet PlatformTransactionManager diskuteras senare i denna artikel.

Det deklarativa transaktionsstödet kan liknas vid vanlig EJB CMT (Container Managed Transactions). Där kan transaktionslogik specificeras ner till metodnivå via externa XML-filer som J2EE-containern kan tolka. De stora skillnaderna mellan Spring TX och EJB CMT är:

  • JTA – EJB CMT kräver att det finns en transaktionshanterare som stöder JTA. JTA-stöd brukar typiskt finnas i en applikationsserver (såsom JBoss, WebLogic och så vidare). Spring TX behöver inte JTA och följdaktligen krävs heller ej någon applikationsserver. Dock kan Spring TX användas med hjälp av JTA om så önskas.
  • POJO – Spring TX kan tillämpas på vilken springböna som helst, inte bara på EJB:s.
  • Rollback – Spring TX erbjuder ett bredare stöd för rollback-hantering som de själva kallar för ”rollback-regler”. Olika aspekter kan konfigureras för att hantera felsituationer och reglerna är dessutom deklarativa. För EJB CMT finns endast alternativet att programmatiskt hantera rollbacks via setRollbackOnly.

Standardvärden

Som vanligt när det gäller Spring försöker man sätta så vettiga standardvärden (det vill säga defaultvärden) som möjligt på attribut och liknande. Springs transaktionshantering utgör inget undantag och nedanstående tabell visar de standardvärden som gäller för transaktionsattributen.

AttributStandardvärdeBeskrivning
PropagationREQUIREDBeskriver hur transaktionen ska överföras (det vill säga propageras) mellan olika kontext. Tillåtna värden är:- REQUIRES_NEW – En ny transaktion ska alltid skapas, även om en befintlig transaktion existerar. - REQUIRED – Om en befintlig transaktion existerar så ska denna användas. Annars ska en ny transaktion skapas.
IsolationISOLATION_DEFAULTStandardvärdet innebär att det värde som är satt på den under­liggande datakällan gäller. Giltiga värden är:- ISOLATION_DEFAULT (standardvärdet) - ISOLATION_READ_UNCOMMITTED - ISOLATION_READ_UNCOMMITTED - ISOLATION_REPEATABLE_READ - ISOLATION_SERIALIZABLE
Timeout-1Specificerar hur länge en transaktion kan pågå innan en rollback sker. Standardvärdet -1 innebär att detta värde styrs av den underliggande datakällan
Read-onlyfalseAttribut som kan användas för att prestandaoptimera operationer som endast innebär läsning.
Rollback forEn lista över de checked exceptions som ger en rollback. Standardvärdet är en tom lista vilket innebär att endast unchecked exceptions ger automatisk rollback.
No rollback forEn lista över exceptions som inte ska orsaka rollback. Standardvärdet är en tom lista vilket innebär att alla unchecked exceptions ger automatisk rollback.

Exempel – Deklarativt transaktionsstöd via XML

Följande exempel belyser många olika delar av Springs deklarativa transaktionsstöd – både via XML-konfiguration och via annotationer. Vi utgår från en tabell i databasen som kallas person. DDL:en för denna tabell ser ut enligt följande för MySQL.

CREATE TABLE person (
  id bigint(20) NOT NULL,
  firstName varchar(255) NOT NULL,
  lastName varchar(255) NOT NULL,
  phoneNumber varchar(255) NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB;

Tabellen person innehåller alltså ett id, förnamn, efternamn och telefonnummer – inte direkt raketforskning men det räcker gott som exempel.

Vi vill bygga en enkel tjänst som erbjuder CRUD-metoder mot person-tabellen. Dessa metoder ska vara transaktionshanterade. Gränssnittet för denna tjänst ser ut som i figuren nedan.

package se.cygni.sample.spring.tx;

public interface PersonService {
    void create(Person person);
    Person retrieve(long id);
    void update(Person person);
    void delete(long id);
}

Observera att ingen transaktionsspecifik kod finns i gränssnittet. Implementationen av denna tjänst är en enkel POJO som andvänder SimpleJdbcTemplate för att kommunicera med databasen. Klassen ärver springklassen SimpleJdbcDaoSupport för att enkelt kunna få tag på en instans av typen SimpleJdbcTemplate via metoden getSimpleJdbcTemplate. Se även inlägget om Spring JDBC för mer information om just SimpleJdbcTemplate och SimpleJdbcDaoSupport.

package se.cygni.sample.spring.tx;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcDaoSupport;
import org.springframework.transaction.annotation.Transactional;

public class PersonServiceImpl
        extends SimpleJdbcDaoSupport implements PersonService {

    public void create(Person person) {
        getSimpleJdbcTemplate().update(
                "insert into person " +
                    "(id, firstName, lastName, phoneNumber) " +
                    "values(?, ?, ?, ?)",
                person.getId(),
                person.getFirstName(),
                person.getLastName(),
                person.getPhoneNumber());
    }

    public Person retrieve(long id) {
        return getSimpleJdbcTemplate().queryForObject(
                "select id, firstName, lastName, phoneNumber " +
                    "from person where id = ?",
                new ParameterizedRowMapper<Person>() {
                    public Person mapRow(ResultSet rs, int rowNum)
                            throws SQLException {
                        Person person = new Person();
                        person.setId(rs.getLong("id"));
                        person.setFirstName(rs.getString("firstName"));
                        person.setLastName(rs.getString("lastName"));
                        person.setPhoneNumber(
                            rs.getString("phoneNumber"));
                        return person;
                    }
                },
                id);
    }

    public void update(Person person) {
        getSimpleJdbcTemplate().update(
                "update person firstName = ?, lastName = ?, " +
                    "phoneNumber = ? where id = ?",
                person.getFirstName(),
                person.getLastName(),
                person.getPhoneNumber(),
                person.getId());
    }

    public void delete(long id) {
        getSimpleJdbcTemplate().update(
            "delete from person where id = ?", id);
    }
}

Klassen PersonServiceImpl innehåller, liksom gränssnittet PersonService, inte heller någon transaktions­specifik kod. Nu gäller det bara att deklarera denna klass som en springböna och knyta ihop det hela med Spring TX. Applikationskontextet visas nedan.

<?xml version="1.0" encoding="UTF-8"?>
<beans
   xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:aop="http://www.springframework.org/schema/aop"
   xmlns:tx="http://www.springframework.org/schema/tx"
   xsi:schemaLocation=
     "http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
      http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
      http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

    <bean id="personService"
          class="se.cygni.sample.spring.tx.PersonServiceImpl">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="dataSource"
          class="org.apache.commons.dbcp.BasicDataSource"
          destroy-method="close">

      <property name="driverClassName" value="com.mysql.jdbc.Driver" />
      <property name="url" value="jdbc:mysql://localhost:3306/person" />
      <property name="username" value="root" />
      <property name="password" value="" />
    </bean>

    <bean id="txManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource" />
    </bean>

    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="retrieve*" read-only="true" />
            <tx:method name="*" />
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut
          id="personServiceOperation"
          expression=
            "execution(* se.cygni.sample.spring.tx.PersonService.*(..))" />

        <aop:advisor
            advice-ref="txAdvice"
            pointcut-ref="personServiceOperation" />
    </aop:config>

</beans>

Observera att förutom schemat beans så används också http://www.springframework.org/schema/tx och http://www.springframework.org/schema/aop. Dessa scheman erbjuder förenklad XML-syntax för både transaktions­hanteringen och aspekt­konfigurationen.

Bönan personService länkas ihop med en vanlig dataSource. I exemplet används BasicDataSource från commons-dbcp men det går att använda andra implementationer som till exempel ett JNDI-uppslag eller liknande.

Datakällan länkas dessutom ihop med bönan txManager som är av typen DataSourceTransactionManager. Denna klass implementerar gränssnittet PlatformTransactionManager som är ett av grundgränssnitten i modulen spring-tx. Det finns ett flertal implementationer av PlatformTransactionManager som är värda att nämna:

  • DataSourceTransactionManager – Används när man vill transaktionshantera en vanlig JDBC-datakälla som i detta exempel
  • HibernateTransactionManager – Denna transaktionshanterare kopplas ihop med en SessionFactory från Hibernate och kan därmed hantera hibernatetransaktioner
  • JpaTransactionManager – Används för att kunna hantera JPA-transaktioner
  • JtaTransactionManager – När JTA används så är denna transaktionshanterare lämplig. Det finns dessutom specifika transaktionshanterare för olika applikationsservrar som till exempel WebSphere.
  • JmsTransactionManager – Används för JMS-operationer som kräver transaktionsstöd.

De ovan nämnda transaktionshanterarna implementerar alltså gränssnittet PlatformTransactionManager. Vid deklarativa transaktioner behöver dock utvecklaren aldrig känna till detta gränssnitt eller använda hanteraren direkt, detta är helt inkapslat via spring-tx. PlatformTransactionManager kan däremot användas direkt för programmatisk transaktionshantering som visas senare i denna artikel.

Nu när vi länkat ihop vår tjänst med en transaktionshanterare så måste vi se till att de metoder som exponeras via tjänsten blir transaktionshanterade. Detta sker via den aspekt som visas i applikationskontextet. Den första delen, det vill säga advicet txAdvice, specificerar semantiken för olika metoder, i exemplet så kommer alla metoder som börjar på retrieve att hanteras som read-only och kan därmed optimeras av den underliggande datakällan. Alla övriga metoder är inte read-only och får spring-tx standardbetende som tidigare visas i tabellen ovan. Advicet länkas ihop med den aktuella transaktions­hanteraren via attributet transaction-manager.

Figuren nedan som är den sista delen i kontextet visar hur aspekten konfigureras. Advicet länkas ihop med en pointcut som ska gälla för alla metoder i gränssnittet PersonService enligt standardsyntax för Spring AOP.

<aop:config>
    <aop:pointcut
      id="personServiceOperation"
      expression=
        "execution(* se.cygni.sample.spring.tx.PersonService.*(..))" />

    <aop:advisor
        advice-ref="txAdvice"
        pointcut-ref="personServiceOperation" />
</aop:config>

Här skapas en pointcut vid namn personServiceOperation som triggar på alla metodanrop till gränssnittet se.cygni.sample.spring.tx.PersonService. Detta kan även generaliseras ytterligare genom att exempelvis ange *Service.*(..) vilket då innebär aspekten kommer att triggas för alla metoder på alla gränssnitt som slutar på just ordet Service.

Raden med advisor länkar ihop transaktionsadvicet med pointcut:en och vi har därmed en fullständig konfiguration för spring-tx.

För att anropa tjänsten och använda transaktionsstödet krävs inga speciella åtgärder. Sätt upp en vanlig referens till personService-bönan och använd den rakt av.

Exempel – Deklarativt transaktionsstöd via annotationer

Spring erbjuder även deklarativt transaktionsstöd via annotationer. Annotationerna sätts på den konkreta implementationen av tjänsten och alltså inte på själva gränssnittet. En enkel omskrivning av ovanstående exempel ser ut som följer.

@Transactional
public class PersonServiceImpl
        extends SimpleJdbcDaoSupport implements PersonService {

    public void create(Person person) {
        getSimpleJdbcTemplate().update(
                "insert into person " +
                    "(id, firstName, lastName, phoneNumber) " +
                    "values(?, ?, ?, ?)",
                person.getId(),
                person.getFirstName(),
                person.getLastName(),
                person.getPhoneNumber());
    }

    @Transactional(readOnly = true)
    public Person retrieve(long id) {
        ...

Först och främst är klassen annoterad med @Transactional. Detta innebär att alla publika metoder i klassen kommer att kunna hanteras av spring-tx och de de transaktionsattribut som gäller är de standardvärden som visats i tabellen ovan. Metoden retrieve har en separat annotation som överrider transaktionsattributet readOnly (standardvärdet är ju egentligen readOnly=false).

Dessa annotationer är den enda kodförändring som krävs. Nu fattas bara konfigurationen i kontextet.

<?xml version="1.0" encoding="UTF-8"?>
<beans
  xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:tx="http://www.springframework.org/schema/tx"
  xsi:schemaLocation=
    "http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
     http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">

    <bean id="personService"
          class="se.cygni.sample.spring.tx.PersonServiceImpl">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="dataSource"
          class="org.apache.commons.dbcp.BasicDataSource"
          destroy-method="close">

      <property name="driverClassName" value="com.mysql.jdbc.Driver" />
      <property name="url" value="jdbc:mysql://localhost:3306/person" />
      <property name="username" value="root" />
      <property name="password" value="" />
    </bean>

    <bean id="txManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource" />
    </bean>

    <tx:annotation-driven transaction-manager="txManager"/>
</beans>

Observera att bönorna personService, dataSource och txManager är samma som i det tidigare exemplet. Notera även att schemat för Spring AOP inte behövs längre eftersom ingen aspekt konfigureras i kontextet.

Den stora skillnaden gäller den sista bönan som visas i figuren nedan. Här deklareras att annotationsstöd för spring-tx ska användas och resten hanterar spring-tx åt dig. Ingen aspektkonfiguration krävs vilket leder till kortare XML-filer, färre potentiella felkällor och mindre krångel. Peka bara ut vilken transaktionshanterare som ska användas och sen är det bara att köra så det ryker. Faktum är att ifall transaktionshanterarens namn är "transactionManager" så behövs inte detta attribut eftersom standardvärdet på attributet transaction-manager är just "transactionManager".

<tx:annotation-driven transaction-manager="txManager"/>

Programmatiskt transaktionsstöd

Det programmatiska transaktionsstödet kan användas på två sätt. Antingen genom att använda PlatformTransactionManager direkt vilket innebär en en väldigt låg abstraktionsnivå. Det andra sättet är att använda klassen TransactionTemplate som följer templatingmönstret som tidigare diskuterats i artikeln om Spring JDBC.

Det första sättet – att använda PlatformTransactionManager direkt – kommer inte att beskrivas i denna artikel och är ett arbetssätt som inte rekommenderas. Om programmatisk transaktionshantering ska användas rekommenderas användning av TransactionTemplate. Observera att det absolut vanligaste sättet är dock att använda det deklarativa transaktionsstödet.

Exemplet nedan visar hur det programmatiska stödet kan användas. Vi antar att samma tjänst som i tidigare exempel finns tillgänglig – PersonServiceImpl. Vi kapslar in denna tjänst via klassen TxAwarePersonServiceImpl vars uppgift är att hantera själva transaktionen med hjälp av TransactionTemplate. Den faktiska logiken, det vill säga JDBC-operationerna – sker fortfarande i originaltjänsten.

package se.cygni.sample.spring.tx;

import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

public class TxAwarePersonServiceImpl implements PersonService {
    private TransactionTemplate transactionTemplate;
    private PersonService personService;

    public void setTransactionTemplate(
            TransactionTemplate transactionTemplate) {
        this.transactionTemplate = transactionTemplate;
    }

    public void setPersonService(PersonService personService) {
        this.personService = personService;
    }

    public void create(final Person person) {
        transactionTemplate.execute(
            new TransactionCallbackWithoutResult() {
                protected void doInTransactionWithoutResult(
                        TransactionStatus status) {
                    personService.create(person);
                }
            });
    }

    public Person retrieve(final long id) {
        Person person = (Person) transactionTemplate.execute(
            new TransactionCallback() {
                public Object doInTransaction(
                        TransactionStatus status) {
                    return personService.retrieve(id);
                }
            });
        return person;
    }

    public void update(final Person person) {
        transactionTemplate.execute(
                new TransactionCallbackWithoutResult() {
            protected void doInTransactionWithoutResult(
                    TransactionStatus status) {
                personService.update(person);
            }
        });
    }

    public void delete(final long id) {
        transactionTemplate.execute(
                new TransactionCallbackWithoutResult() {
            protected void doInTransactionWithoutResult(
                    TransactionStatus status) {
                personService.delete(id);
            }
        });
    }
}

Klassen TxAwarePersonServiceImpl delegerar alla anrop vidare till originaltjänsten. Alla dessa anrop sker dock med hjälp av TransactionTemplate. Applikationskontextet visas nedan, där kan man se att bönan personService kapslar in originaltjänsten och behöver tillgång till en TransactionTemplate. Själva template-klassen deklareras som en vanlig springböna med en referens till transaktionshanteraren och visas längst ner i filen.

<?xml version="1.0" encoding="UTF-8"?>
<beans
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation=
      "http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
       http://www.springframework.org/schema/tx
         http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">

    <bean id="personService"
          class="se.cygni.sample.spring.tx.TxAwarePersonServiceImpl">

        <property name="transactionTemplate" ref="transactionTemplate"/>

        <property name="personService">
            <bean class="se.cygni.sample.spring.tx.PersonServiceImpl">
                <property name="dataSource" ref="dataSource" />
            </bean>
        </property>
    </bean>

    <bean id="dataSource"
          class="org.apache.commons.dbcp.BasicDataSource"
          destroy-method="close">

      <property name="driverClassName" value="com.mysql.jdbc.Driver" />
      <property name="url" value="jdbc:mysql://localhost:3306/person" />
      <property name="username" value="root" />
      <property name="password" value="" />
    </bean>

    <bean id="txManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="transactionTemplate"
      class="org.springframework.transaction.support.TransactionTemplate">
        <property name="transactionManager" ref="txManager"/>
    </bean>
</beans>

TransactionTemplate använder – som namnet antyder – templatingmönstret, som tidigare diskuterats. All transaktionshantering sker via denna klass och den callback som måste implementeras heter TransactionCallback och ser ut som i figuren nedan.

package org.springframework.transaction.support;

import org.springframework.transaction.TransactionStatus;

public interface TransactionCallback {
    Object doInTransaction(TransactionStatus status);
}

För operationer som inte har något returvärde kan klassen TransactionCallbackWithoutResult ärvas istället för att direkt implementera gränssnittet TransactionCallback. Metoderna i klassen TransactionCallbackWithoutResult och gränssnittet TransactionCallback tar en para­meter av typen TransactionStatus som visas i figuren nedan.

package org.springframework.transaction;

public interface TransactionStatus extends SavepointManager {
    boolean isNewTransaction();
    boolean hasSavepoint();
    void setRollbackOnly();
    boolean isRollbackOnly();
    boolean isCompleted();
}

TransactionStatus kan bland annat användas för att markera att en transaktion måste rullas tillbaka via metoden setRollbackOnly.

Sammanfattning

Spring erbjuder ett kraftfullt stöd för transaktioner via modulen spring-tx. På ett enhetligt sätt erbjuds stöd för att kunna hantera transaktioner av olika slag som till exempel JTA, JDBC, Hibernate, JPA och JDO.

Det deklarativa transaktionsstödet är det absolut vanligaste och kan användas på två sätt, via XML eller via annotationer. Det senare alternativet innebär att väldigt lite konfiguration krävs, vilket är väldigt smakfullt.

Det programmatiska transaktionsstödet är också kraftfullt och där rekommenderas användning av templatingklassen TransactionTemplate som hanterar all boilerplate-kod som utvecklaren annars typiskt måste skriva.