コンピュータクワガタ

かっぱのかっぱによるコンピュータ関連のサイトです

Mirageの例外をSpringのDataAccessExceptionにする

2WaySQLが使えるシンプルなデータベースアクセスライブラリーのMirageとSpring Frameworkの組み合わせで、Mirageが吐く例外をSpringのDataAccessExceptionに変換する方法です。

Mirageプロジェクトは以下のページを参照して下さい。
http://amateras.sourceforge.jp/site/mirage/welcome.html

Spring Frameworkでは@Repositoryアノテーションを付けた場合に、その中で発生した例外をDataAccessExceptionに変換する働きを持っています。

今回は、Mirageの中で例外が発生した場合に、自動でDataAccessException系の例外に変更する方法を紹介します。

普通に書いてみる

最初に、普通にキーの重複エラーが出るようなプログラムを書いてみます。

今回はPostgreSQLで確認しています。

テストで利用するテーブルのDDLです。

CREATE TABLE emp(
  emp_no INTEGER,
  name VARCHAR(40),
  age INTEGER,
  PRIMARY KEY(emp_no)
);

Spring側のソースはSpringの設定ファイルからです。適当にbeans.xmlという名前にしています。

<?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:context="http://www.springframework.org/schema/context"
 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-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
 <context:annotation-config />
 <context:component-scan base-package="*" />

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

 <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
  <tx:attributes>
   <tx:method name="select*" read-only="true" />
   <tx:method name="*" propagation="REQUIRED" isolation="READ_COMMITTED"
    timeout="10" read-only="false" />
  </tx:attributes>
 </tx:advice>
 <aop:config>
  <aop:advisor advice-ref="transactionAdvice"
   pointcut="execution(* com.example.spring.*Service.*(..))" />
 </aop:config>
 <bean id="dataSource"
  class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  <property name="driverClassName" value="org.postgresql.Driver" />
  <property name="url" value="jdbc:postgresql://127.0.0.1:5432/miragedb" />
  <property name="username" value="mirage" />
  <property name="password" value="mirage" />
 </bean>

 <bean id="connectionProvider"
  class="jp.sf.amateras.mirage.integration.spring.SpringConnectionProvider">
  <property name="transactionManager" ref="transactionManager" />
 </bean>

 <bean id="dialect" class="jp.sf.amateras.mirage.dialect.HyperSQLDialect" />

 <bean id="sqlManager" class="jp.sf.amateras.mirage.SqlManagerImpl">
  <property name="connectionProvider" ref="connectionProvider" />
  <property name="dialect" ref="dialect" />
 </bean>
</beans>

EntityのEmpクラスです。

package com.example.spring;

import jp.sf.amateras.mirage.annotation.PrimaryKey;
import jp.sf.amateras.mirage.annotation.PrimaryKey.GenerationType;

public class Emp {
    @PrimaryKey(generationType = GenerationType.APPLICATION)
    private int empNo;
    private String name;
    private int age;

    public int getEmpNo() {
        return empNo;
    }

    public void setEmpNo(int empNo) {
        this.empNo = empNo;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Test [emp_no=" + empNo + ", name=" + name + ", age=" + age
                + "]";
    }
}


キーの重複を起こすServiceクラスです。

package com.example.spring;

import jp.sf.amateras.mirage.SqlManager;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class TransactionSpringService {
    @Autowired
    private SqlManager sqlManager;

    @Transactional
    public void insert(Emp emp) {
        // キーの重複をわざと起こす
        sqlManager.insertEntity(emp);
        sqlManager.insertEntity(emp);
    }
}

最後に、Serviceを呼び出すmainメソッドです。

package com.example.spring;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TransactionSpringConnect {

    public static void main(String... args) {
        ApplicationContext context = new ClassPathXmlApplicationContext(
                "/spring/beans.xml");
        TransactionSpringService tss = (TransactionSpringService) context
                .getBean("transactionSpringService");
        Emp emp = new Emp();
        emp.setEmpNo(11);
        emp.setName("あいうえお");
        emp.setAge(30);
        tss.insert(emp);
    }
}

この状態で実行すると以下の様なエラーが出力されます。

Exception in thread "main" jp.sf.amateras.mirage.exception.SQLRuntimeException: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "emp_pkey"
	at jp.sf.amateras.mirage.SqlExecutor.executeUpdateSql(SqlExecutor.java:288)
	at jp.sf.amateras.mirage.SqlManagerImpl.insertEntity(SqlManagerImpl.java:316)
	at com.example.spring.TransactionSpringService.insert(TransactionSpringService.java:18)
	at com.example.spring.TransactionSpringService$$FastClassByCGLIB$$e353229a.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:710)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:643)
	at com.example.spring.TransactionSpringService$$EnhancerByCGLIB$$becf7388.insert(<generated>)
	at com.example.spring.TransactionSpringConnect2.main(TransactionSpringConnect2.java:17)
Caused by: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "emp_pkey"
	at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2077)
	at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1810)
	at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:257)
	at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:498)
	at org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2Statement.java:386)
	at org.postgresql.jdbc2.AbstractJdbc2Statement.executeUpdate(AbstractJdbc2Statement.java:332)
	at jp.sf.amateras.mirage.SqlExecutor.executeUpdateSql(SqlExecutor.java:277)
	... 15 more

MirageのSQLRuntimeExceptionが出力されているのがわかるかと思います。もちろんこのままでも、この例外となったSQLExceptionを取得でき、エラーコードなりから重複エラーということはわかります。

DataAccessExceptionをつかう

Springで標準的にサポートされていないDBアクセスライブラリーを用いてDataAccessExceptionを出力するためには、SpringのSQLErrorCodeSQLExceptionTranslatorを使用します。

 <bean
  class="org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator">
  <constructor-arg ref="dataSource" />
 </bean>
 <bean class="com.example.spring.MiragePersistenceExceptionTranslator" />
 <bean
  class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

設定の中で使用しているMiragePersistenceExceptionTranslatorは以下になります。

package com.example.spring;

import java.sql.SQLException;

import jp.sf.amateras.mirage.exception.SQLRuntimeException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;

public class MiragePersistenceExceptionTranslator implements
        PersistenceExceptionTranslator {
    @Autowired
    private SQLErrorCodeSQLExceptionTranslator translator;

    @Override
    public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
        if (ex instanceof SQLRuntimeException) {
            Throwable t;
            while ((t = ex.getCause()) != null) {
                if (t instanceof SQLException) {
                    return translator.translate("", "", (SQLException) t);
                }
            }
        }
        return null;
    }

}

コンポーネントとして、PersistenceExceptionTranslatorを実装したクラスを作成することで、内部的にDataAccessExceptionに変更して例外を送出できます。

実際に動作させると以下のようになります。

Exception in thread "main" org.springframework.dao.DuplicateKeyException: ; SQL []; ERROR: duplicate key value violates unique constraint "emp_pkey"; nested exception is org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "emp_pkey"
	at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:239)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
	at com.example.spring.MiragePersistenceExceptionTranslator.translateExceptionIfPossible(MiragePersistenceExceptionTranslator.java:23)
	at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:59)
	at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:213)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:147)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:643)
	at com.example.spring.TransactionSpringService$$EnhancerByCGLIB$$51c3aa23.insert(<generated>)
	at com.example.spring.TransactionSpringConnect2.main(TransactionSpringConnect2.java:17)
Caused by: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "emp_pkey"
	at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2077)
	at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1810)
	at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:257)
	at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:498)
	at org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2Statement.java:386)
	at org.postgresql.jdbc2.AbstractJdbc2Statement.executeUpdate(AbstractJdbc2Statement.java:332)
	at jp.sf.amateras.mirage.SqlExecutor.executeUpdateSql(SqlExecutor.java:277)
	at jp.sf.amateras.mirage.SqlManagerImpl.insertEntity(SqlManagerImpl.java:316)
	at com.example.spring.TransactionSpringService.insert(TransactionSpringService.java:18)
	at com.example.spring.TransactionSpringService$$FastClassByCGLIB$$e353229a.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:710)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136)
	... 10 more

解説

PersistenceExceptionTranslationPostProcessorでは、PersistenceExceptionTranslationAdvisorを設定しています。

@Override
public void setBeanFactory(BeanFactory beanFactory) {
    if (!(beanFactory instanceof ListableBeanFactory)) {
        throw new IllegalArgumentException(
                "Cannot use PersistenceExceptionTranslator autodetection without ListableBeanFactory");
    }
    this.advisor = new PersistenceExceptionTranslationAdvisor(
            (ListableBeanFactory) beanFactory, this.repositoryAnnotationType);
}

PersistenceExceptionTranslationAdvisorでは、adviceとして、PersistenceExceptionTranslationInterceptorを設定しています。

public PersistenceExceptionTranslationAdvisor(
        PersistenceExceptionTranslator persistenceExceptionTranslator,
        Class<? extends Annotation> repositoryAnnotationType) {

    this.advice = new PersistenceExceptionTranslationInterceptor(persistenceExceptionTranslator);
    this.pointcut = new AnnotationMatchingPointcut(repositoryAnnotationType, true);
}

PersistenceExceptionTranslationInterceptorでは、@Repositoryの付いたコンポーネントのメソッドを呼び出し、例外をcatchして処理しています。

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    try {
        return mi.proceed();
    }
    catch (RuntimeException ex) {
        // Let it throw raw if the type of the exception is on the throws clause of the method.
        if (!this.alwaysTranslate && ReflectionUtils.declaresException(mi.getMethod(), ex.getClass())) {
            throw ex;
        }
        else {
            if (this.persistenceExceptionTranslator == null) {
                this.persistenceExceptionTranslator = detectPersistenceExceptionTranslators(this.beanFactory);
            }
            throw DataAccessUtils.translateIfNecessary(ex, this.persistenceExceptionTranslator);
        }
    }
}

例外自体は、以下の処理で適切な例外に変換しています。

protected PersistenceExceptionTranslator detectPersistenceExceptionTranslators(ListableBeanFactory beanFactory) {
    // Find all translators, being careful not to activate FactoryBeans.
    Map<String, PersistenceExceptionTranslator> pets = BeanFactoryUtils.beansOfTypeIncludingAncestors(
            beanFactory, PersistenceExceptionTranslator.class, false, false);
    ChainedPersistenceExceptionTranslator cpet = new ChainedPersistenceExceptionTranslator();
    for (PersistenceExceptionTranslator pet : pets.values()) {
        cpet.addDelegate(pet);
    }
    return cpet;
}

ChainedPersistenceExceptionTranslatorで使用できるTranslatorで例外をチェックしていき、DataAccessExceptionのインスタンスになるまで変換します。変換できない場合にはnullを返します。

@Override
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
    for (PersistenceExceptionTranslator pet : this.delegates) {
        DataAccessException translatedDex = pet.translateExceptionIfPossible(ex);
        if (translatedDex != null) {
            return translatedDex;
        }
    }
    return null;
}

ここで使用されるTrnslatorはPersistenceExceptionTranslatorを実装したクラスを作成し、それをBeanとして設定しておくことで、自動的に使用されます。

<bean class="com.example.spring.MiragePersistenceExceptionTranslator" />

Spring3入門 ――Javaフレームワーク・より良い設計とアーキテクチャ

Spring3入門 ――Javaフレームワーク・より良い設計とアーキテクチャ

現場で使えるJavaライブラリ

現場で使えるJavaライブラリ