ラベル mybatis の投稿を表示しています。 すべての投稿を表示
ラベル mybatis の投稿を表示しています。 すべての投稿を表示

2012年1月10日火曜日

Guice3 + MyBatis3

前回のSpring3 + MyBatis3のSpringをGuiceに置き換えてみます。

User.java、UserDao.java、UserDao.xml、UserService.javaは前回と同じ物をそのまま利用します。

Guice-MyBatisプラグインの標準のトランザクションインターセプターではreadOnlyの設定ができない(rollbackOnlyは可能)ため、 readOnlyに対応したトランザクションインターセプターを自作します。
ソースコードはGuice-MyBatisプラグインの標準のトランザクションインターセプターを参考にしています。

※PooledDataSourceProviderではなく、BasicDataSourceProviderを使用すればdefaultReadOnlyの設定が可能でした。
ただし、その場合には標準のトランザクションインターセプターではreadOnlyをfalseに設定しなおす事がないようで、更新系の処理ができません。

TransactionalMethodInterceptor.java
package com.brightgenerous.extension.ibatis;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;

import javax.inject.Inject;

import com.brightgenerous.extension.logging.Log;
import com.brightgenerous.extension.logging.LogFactory;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionManager;
import org.apache.ibatis.session.TransactionIsolationLevel;

public final class TransactionalMethodInterceptor implements MethodInterceptor {

    private static final Class<?>[] CAUSE_TYPES = new Class[] { Throwable.class };

    private static final Class<?>[] MESSAGE_CAUSE_TYPES = new Class[] {
            String.class, Throwable.class };

    private static final Log log = LogFactory
            .getLog(TransactionalMethodInterceptor.class);

    @Inject
    private SqlSessionManager sqlSessionManager;

    private final boolean readOnly;

    private final ExecutorType executorType;

    private final TransactionIsolationLevel isolationLevel;

    private final boolean rollbackOnly;

    private final boolean force;

    private final Class<? extends Throwable> rethrowExceptionsAs;

    private final String exceptionMessage;

    public TransactionalMethodInterceptor() {
        this(false);
    }

    public TransactionalMethodInterceptor(boolean readOnly) {
        this(readOnly, ExecutorType.SIMPLE, null, false, false,
                Exception.class, "");
    }

    public TransactionalMethodInterceptor(boolean readOnly,
            ExecutorType executorType,
            TransactionIsolationLevel isolationLevel, boolean rollbackOnly,
            boolean force, Class<? extends Throwable> rethrowExceptionsAs,
            String exceptionMessage) {
        this.readOnly = readOnly;
        this.executorType = executorType;
        this.isolationLevel = isolationLevel;
        this.rollbackOnly = rollbackOnly;
        this.force = force;
        this.rethrowExceptionsAs = rethrowExceptionsAs;
        this.exceptionMessage = exceptionMessage;
    }

    public void setSqlSessionManager(SqlSessionManager sqlSessionManager) {
        this.sqlSessionManager = sqlSessionManager;
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method interceptedMethod = invocation.getMethod();

        String debugPrefix = null;
        if (log.isDebugEnabled()) {
            debugPrefix = String.format("[Intercepted method: %s]",
                    interceptedMethod.toGenericString());
        }

        boolean isSessionInherited = sqlSessionManager
                .isManagedSessionStarted();

        if (isSessionInherited) {
            if (log.isDebugEnabled()) {
                log.debug(String.format(
                        "%s - SqlSession already set for thread: %s",
                        debugPrefix, Thread.currentThread().getId()));
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug(String
                        .format("%s - SqlSession not set for thread: %s, creating a new one",
                                debugPrefix, Thread.currentThread().getId()));
            }

            sqlSessionManager.startManagedSession(executorType, isolationLevel);
            sqlSessionManager.getConnection().setReadOnly(readOnly);
        }

        Object object = null;
        try {
            object = invocation.proceed();

            if (!isSessionInherited && !rollbackOnly) {
                sqlSessionManager.commit(force);
            }
        } catch (Throwable t) {
            sqlSessionManager.rollback(force);

            for (Class<?> exceptionClass : interceptedMethod
                    .getExceptionTypes()) {
                if (exceptionClass.isAssignableFrom(t.getClass())) {
                    throw t;
                }
            }

            if (rethrowExceptionsAs.isAssignableFrom(t.getClass())) {
                throw t;
            }

            String errorMessage;
            Object[] initargs;
            Class<?>[] initargsType;

            if (exceptionMessage.length() != 0) {
                errorMessage = String.format(exceptionMessage,
                        invocation.getArguments());
                initargs = new Object[] { errorMessage, t };
                initargsType = MESSAGE_CAUSE_TYPES;
            } else {
                initargs = new Object[] { t };
                initargsType = CAUSE_TYPES;
            }

            Constructor<? extends Throwable> exceptionConstructor = getMatchingConstructor(
                    rethrowExceptionsAs, initargsType);
            Throwable rethrowEx = null;
            if (exceptionConstructor != null) {
                try {
                    rethrowEx = exceptionConstructor.newInstance(initargs);
                } catch (Exception e) {
                    errorMessage = String
                            .format("Impossible to re-throw '%s', it needs the constructor with %s argument(s).",
                                    rethrowExceptionsAs.getName(),
                                    Arrays.toString(initargsType));
                    log.error(errorMessage, e);
                    rethrowEx = new RuntimeException(errorMessage, e);
                }
            } else {
                errorMessage = String
                        .format("Impossible to re-throw '%s', it needs the constructor with %s or %s argument(s).",
                                rethrowExceptionsAs.getName(),
                                Arrays.toString(CAUSE_TYPES),
                                Arrays.toString(MESSAGE_CAUSE_TYPES));
                log.error(errorMessage);
                rethrowEx = new RuntimeException(errorMessage);
            }

            throw rethrowEx;
        } finally {
            if (!isSessionInherited) {
                if (rollbackOnly) {
                    if (log.isDebugEnabled()) {
                        log.debug(debugPrefix + " - SqlSession of thread: "
                                + Thread.currentThread().getId()
                                + " was in rollbackOnly mode, rolling it back");
                    }

                    sqlSessionManager.rollback(true);
                }

                if (log.isDebugEnabled()) {
                    log.debug(String
                            .format("%s - SqlSession of thread: %s terminated its life-cycle, closing it",
                                    debugPrefix, Thread.currentThread().getId()));
                }

                sqlSessionManager.close();
            } else if (log.isDebugEnabled()) {
                log.debug(String
                        .format("%s - SqlSession of thread: %s is inherited, skipped close operation",
                                debugPrefix, Thread.currentThread().getId()));
            }
        }

        return object;
    }

    private static <E extends Throwable> Constructor<E> getMatchingConstructor(
            Class<E> type, Class<?>[] argumentsType) {
        Class<? super E> currentType = type;
        while (Object.class != currentType) {
            for (Constructor<?> constructor : currentType.getConstructors()) {
                if (Arrays.equals(argumentsType,
                        constructor.getParameterTypes())) {
                    return (Constructor<E>) constructor;
                }
            }
            currentType = currentType.getSuperclass();
        }
        return null;
    }
}

Guiceでは、Spring3 + MyBatis3で使用したapplicationContext.xml、sqlMapConfig.xml、database.propertiesが不要になります。
かわりに、Guiceの場合はソースコードに設定を記述します。
以下が実行するテストコードです。beforeメソッド内の処理が設定になります。

UserServiceTestGuice.java
package com.brightgenerous.sample.application.service;

import java.lang.reflect.Method;
import java.util.Properties;
import java.util.Set;

import com.brightgenerous.extension.ibatis.TransactionalMethodInterceptor;
import com.brightgenerous.sample.application.bean.User;
import com.brightgenerous.sample.application.dao.UserDao;

import org.aopalliance.intercept.MethodInterceptor;
import org.apache.ibatis.io.ResolverUtil;
import org.apache.ibatis.session.AutoMappingBehavior;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.junit.Before;
import org.junit.Test;
import org.mybatis.guice.MyBatisModule;
import org.mybatis.guice.datasource.builtin.PooledDataSourceProvider;
import org.mybatis.guice.transactional.Transactional;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Scopes;
import com.google.inject.matcher.AbstractMatcher;
import com.google.inject.name.Names;

public class UserServiceTestGuice {

    private Injector injector;

    @Before
    public void before() {

        injector = Guice.createInjector(new MyBatisModule() {

            @Override
            protected void initialize() {

                MethodInterceptor updateInterceptor = new TransactionalMethodInterceptor();
                binder().requestInjection(updateInterceptor);
                binder().bindInterceptor(new AbstractMatcher<Class<?>>() {

                    @Override
                    public boolean matches(Class<?> x_arg0) {
                        return x_arg0.getName().matches("^.*Service$");
                    }
                }, new AbstractMatcher<Method>() {

                    @Override
                    public boolean matches(Method x_arg0) {
                        return (x_arg0.getAnnotation(Transactional.class) == null)
                                && x_arg0.getName().matches(
                                        "^(regist|update|delete).*$");
                    }
                }, updateInterceptor);
                MethodInterceptor readInterceptor = new TransactionalMethodInterceptor(
                        true);
                binder().requestInjection(readInterceptor);
                binder().bindInterceptor(new AbstractMatcher<Class<?>>() {

                    @Override
                    public boolean matches(Class<?> x_arg0) {
                        return x_arg0.getName().matches("^.*Service$");
                    }
                }, new AbstractMatcher<Method>() {

                    @Override
                    public boolean matches(Method x_arg0) {
                        return (x_arg0.getAnnotation(Transactional.class) == null)
                                && !x_arg0.getName().matches(
                                        "^(regist|update|delete).*$");
                    }
                }, readInterceptor);

                environmentId("test");

                bindDataSourceProviderType(PooledDataSourceProvider.class);
                bindTransactionFactoryType(JdbcTransactionFactory.class);

                Properties properties = new Properties();
                properties.setProperty("JDBC.url",
                        "jdbc:postgresql://127.0.0.1:5432/scheme");
                properties.setProperty("JDBC.driver", "org.postgresql.Driver");
                properties.setProperty("JDBC.username", "username");
                properties.setProperty("JDBC.password", "password");
                properties.setProperty("JDBC.autoCommit", "false");
                Names.bindProperties(binder(), properties);

                useCacheEnabled(true);
                lazyLoadingEnabled(true);
                aggressiveLazyLoading(true);
                autoMappingBehavior(AutoMappingBehavior.PARTIAL);
                useColumnLabel(true);
                useGeneratedKeys(false);
                failFast(true);
                multipleResultSetsEnabled(true);

                addSimpleAliases("com.brightgenerous.sample.application.bean");

                addMapperClasses(new ResolverUtil<Object>().find(
                        new ResolverUtil.Test() {

                            @Override
                            public boolean matches(Class<?> x_arg0) {
                                return x_arg0.getName().matches("^.*Dao$");
                            }
                        }, "com.brightgenerous.sample.application.dao")
                        .getClasses());
            }
        },

        new AbstractModule() {

            @Override
            protected void configure() {
                Set<Class<? extends Object>> services = new ResolverUtil<Object>()
                        .find(new ResolverUtil.Test() {

                            @Override
                            public boolean matches(Class<?> x_arg0) {
                                return x_arg0.getName().matches("^.*Service$");
                            }
                        }, "com.brightgenerous.sample.application.service")
                        .getClasses();
                for (Class<? extends Object> service : services) {
                    bind(service).in(Scopes.SINGLETON);
                }
            }
        });
    }

    @Test
    public void test01() {
        UserService service = injector.getInstance(UserService.class);
        User user = new User();
        user.setId(Integer.valueOf(1));
        user = service.get(user);
        System.out.println(user.getName());
        user.setName(user.getName() + "-");
        service.update(user);
        System.out.println(service.get(user).getName());
        System.out.println(service.equals(injector
                .getInstance(UserService.class)));
        System.out.println(injector.getInstance(UserDao.class).equals(
                injector.getInstance(UserDao.class)));
    }
}

使ってみると、Guiceもかなり便利でした。
今回は、あえてSpringの場合と同じ設定にするためにトランザクションインターセプターを自作しましたが、 標準のトランザクションインターセプターとTransactionalアノテーションを使う方法が一般的だと思います。

設定をソースコードに記述するフレームワークとして、Wicketと相性が良さそうです。

2012年1月9日月曜日

Spring3 + MyBatis3

MyBatis3がMyBatis2に比べて結構使いやすくなっています。
特に実装クラスを必要としないMapperの機能が便利です。
他には、XMLでognl式が使えたり、使いやすいタグが増えています。

Spring3 + MyBatis3の実装例で試してみます。

まず、データクラスを作成します。

User.java
package com.brightgenerous.sample.application.bean;

import java.io.Serializable;

public class User implements Serializable {

    private static final long serialVersionUID = -1056270813521861315L;

    private Integer m_id;

    private String m_name;

    private String m_email;

    public Integer getId() {
        return m_id;
    }

    public void setId(Integer x_id) {
        m_id = x_id;
    }

    public String getName() {
        return m_name;
    }

    public void setName(String x_name) {
        m_name = x_name;
    }

    public String getEmail() {
        return m_email;
    }

    public void setEmail(String x_email) {
        m_email = x_email;
    }
}

次に、Dao(Mapper)インターフェースを作成します。

UserDao.java
package com.brightgenerous.sample.application.dao;

import com.brightgenerous.sample.application.bean.User;

public interface UserDao {

    User select(User x_user);

    int insert(User x_user);

    int update(User x_user);

    int delete(User x_user);
}

そして、UserDao.javaと同じパッケージにUserDao.xmlを置きます。(拡張子以外のファイル名を同じにする)
それだけでインターフェースとXMLファイルが関連付けられます。

UserDao.xml





 

 
  
  
  
 

 

 
  
   select coalesce((select max(id) from t_user),0) + 1 as id
  
  insert into
   t_user (
    id,
    name,
    email
   ) values (
    #{id},
    #{name},
    #{email}
   )
 

 
  update
   t_user
  set
   name = #{name},
   email = #{email}
  where
   id = #{id}
 

 
  delete from
   t_user
  where
   id = #{id}
 


最後に、トランザクション境界となるServiceクラスを作成します。

UserService.java
package com.brightgenerous.sample.application.service;

import javax.inject.Inject;

import com.brightgenerous.sample.application.bean.User;
import com.brightgenerous.sample.application.dao.UserDao;

public class UserService {

    @Inject
    private UserDao m_userDao;

    public User get(User x_user) {
        return m_userDao.select(x_user);
    }

    public void update(User x_user) {
        m_userDao.update(x_user);
    }
}

あとは設定ファイルを書くだけです。
注入先フィールドのInjectアノテーションはjavax.inject.Injectです。
実は、上記のすべてのクラスでMyBatisやSpringに関するインポートがないのです。

applicationContext.xml



 
 
  
 

 
 
  
  
  
  
  
  
 

 
 
  
 
 
  
   
    PROPAGATION_REQUIRED
    PROPAGATION_REQUIRED
    PROPAGATION_REQUIRED
    PROPAGATION_REQUIRED,readOnly
   
  
 
 
  
  
 
 
  
  
 

 
 
  
  
 

 
 
  
 
 
  
 


applicationContext.xmlには com.brightgenerous.extension.spring3.FqcnBeanNameGeneratorというクラスがありますが、
これは、ただFQCNを返すだけのSpringのBeanNameGenerator実装です。
database.propertiesファイルは特にここで書くほどでもないので省略します。

sqlMapConfig.xml





 
  
  
  
  
  
  
  
  
  
  
 

 
  
 


実行方法は以下のようになります。

UserServiceTestSpring
package com.brightgenerous.sample.application.service;

import com.brightgenerous.sample.application.bean.User;
import com.brightgenerous.sample.application.dao.UserDao;

import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class UserServiceTestSpring {

    private ApplicationContext context;

    @Before
    public void before() {
        context = new ClassPathXmlApplicationContext("applicationContext.xml");
    }

    @Test
    public void test01() {
        UserService service = context.getBean(UserService.class);
        User user = new User();
        user.setId(Integer.valueOf(1));
        user = service.get(user);
        user.setName(user.getName() + "-");
        service.update(user);
    }
}

これ以上ソースコードの記述量を減らせないんじゃないのかと思えるほど、簡単になりました。
設定ファイルは一度書いてしまえば、以降は編集しないので、 実際に作成するファイルはデータクラス、Mapperインターフェース、MapperのXMLファイル、サービスクラスの4つです。

次回は、SpringをGuiceに置き換えて同じ機能を実現してみようと思います。
具体的には、Springでの設定ファイルにあたる部分をソースで書くだけで、Mapperやサービスクラスには手を加えません。

2011年3月21日月曜日

Struts2 + Spring + MyBatis (その2)

Struts2 + Spring + MyBatis (その1)の続きです。

WEBアプリケーション開発で使用するフレームワークをStruts2.2 + Spring2.5 + MyBatis2.3 で構成してみます。
(その2)では、各xmlファイルとpropertiesファイルを揃えます。

必要な設定ファイルを記載しておきます。
(省略)と書かれているファイルの内容については、一例を記述する程度に留まるので説明しません。詳細について知りたい場合は、解説している他サイトを参照することをお勧めします。
設定ファイル
web.xmlServletの配備記述子
struts.xmlStruts2の設定ファイル(省略)
struts.propertiesStruts2のパラメータファイル(省略)
applicationContext.xmlSpringの設定ファイル
detabase.propertiesデータベースのパラメータファイル(省略)
sqlMapConfig.xmlMyBatisの設定ファイル(省略)
tiles.xmlTilesの設定ファイル(省略)
web.xml
「WEB-INF」フォルダ内にある配備記述子です。これが無いと始まりません。
どのプロジェクトでも共通した記述なので、ほぼそのままコピーすれば使えます。
Servlet2.5の構文で書いてみます。



 
  contextConfigLocation
  classpath:applicationContext*.xml
 

 
  struts2
  org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
  
   actionPackages
   sample.action
  
 

 
  struts2
  /*
 

 
  JspSupportServlet
  org.apache.struts2.views.JspSupportServlet
  1
 

 
  JspSupportServlet
  /*
 

 
  org.springframework.web.context.ContextLoaderListener
 
 
  org.apache.struts2.tiles.StrutsTilesListener
 

 
  30
 

 
  index.html
  index.jsp
 

 
  
   http://tiles.apache.org/tags-tiles
   /WEB-INF/tld/tiles-jsp.tld
  
 


Struts2とSpringを使用するには、上記のように記述します。
画面(View)の実装では、JspではなくFreeMarkerを使用します。 FreeMarker内でJspのtaglib(tiles等)を使用するためには、「JspSupportServlet」を動作させておく必要があります。
applicationContext.xml
applicationContex.xmlはweb.xmlに記述した「classpath:applicationContext*.xml」で参照される場所に配置します。
以下の内容は記述例です。



 

 
 

 
  
   
    classpath:database.properties
   
  
 

 
 
  
  
  
  
  
  
 

 
 
  
 
 
  
   
    PROPAGATION_REQUIRED
    PROPAGATION_REQUIRED
    PROPAGATION_REQUIRED
    PROPAGATION_REQUIRED,readOnly
   
  
 
 
  
  
 
 
  
   
    transactionInterceptor
   
  
  
   
    *ServiceImpl
   
  
 

 
 
  
  
 


MyBatis(iBatis)をSpringの管理下に置きます。
Springがトランザクションの管理(Begin、Rollback、Commit等)を、MyBatisがSQLの実行のみを担当するといった役割分担を可能にするためです。
そうすることで、MyBatisをSlim3に置き換えた場合にも同じ設計で実装できます。

大半を省略しましたが、設定ファイルについては以上です。
次回は、Google App Engine上でSlim3と連携させてみます。

2011年3月20日日曜日

Struts2 + Spring + MyBatis (その1)

WEBアプリケーション開発で使用するフレームワークをStruts2.2 + Spring2.5 + MyBatis2.3 で構成してみます。
最終的には、MyBatisをSlim3に置き換えてGoogle App Engine上で動作させるまでを予定しているので、そのための手順も含まれています。
(その1)では、必要なjarファイルの選定をします。

使用するフレームワークとそのバージョンを記載しておきます。
フレームワークとバージョン
Struts2Struts2.2.1.1
SpringSpring2.5.6
MyBatisMyBatis2.3.6

空のWebアプリケーションのプロジェクトを作成しておきます。
「Eclipse IDE for Java EE Developers」の「Dynamic Web Project」から新規作成を行うと簡単です。

Struts2
公式サイトからStruts2.2.1.1をダウンロードします。
ダウンローが完了したらファイルを解凍し、使用するjarファイルを選定してプロジェクトの「WEB-INF/lib」フォルダ内にコピーします。
使用するjarファイル
aopalliance-1.0
commons-beanutils-1.7.0
commons-digester-2.0
commons-fileupload-1.2.1
commons-io-1.3.2
commons-lang-2.3
commons-logging-1.0.4
commons-validator-1.3.1
freemarker-2.3.16
ognl-3.0
struts2-convention-plugin-2.2.1.1
struts2-core-2.2.1.1
struts2-json-plugin-2.2.1.1
struts2-spring-plugin-2.2.1.1
struts2-tiles-plugin-2.2.1.1
tiles-api-2.0.6
tiles-core-2.0.6
tiles-jsp-2.0.6
xwork-core-2.2.1.1
開発を進めていくうちに上記以外のjarファイルが必要になる事がありますが、その時点で追加します。
struts2にはspringのjarファイルも含まれていますが、springのjarファイルは別途用意します。
javassist
source forgeサイトからJavassist3.14をダウンロードします。
ダウンローが完了したらファイルを解凍し、javassist.jarファイルをプロジェクトの「WEB-INF/lib」フォルダ内にコピーします。
Spring
公式サイトからSpring2.5.6をダウンロードします。
ダウンローが完了したらファイルを解凍し、使用するJarファイルを選定してプロジェクトの「WEB-INF/lib」フォルダ内にコピーします。
使用するJarファイル
spring-aop
spring-beans
spring-context
spring-core
spring-tx
spring-web
MyBatis
公式サイトからMyBatis2.3.5をダウンロードします。
ダウンローが完了したらファイルを解凍し、mybatis-2.3.5.jarファイルをプロジェクトの「WEB-INF/lib」フォルダ内にコピーします。

以上でjarファイルの選定は終わりです。
(その2)では、各フレームワークの設定ファイルであるxmlについて書きます。