LoginSignup
0
0

SpringとMyBatisを使ったアプリケーション開発のメモ -その2-

Last updated at Posted at 2024-03-27

この記事について

この記事は 2011年に自分のブログに書いていた記事を発掘したものです。10年以上の文章なので、表現が稚拙だったり、時代が古いところもありますが、何かの役に立つかもしれないと考え、Qiitaに持ってくることにしました。


DIコンテナによるトランザクション管理

MyBatis単体で書いた場合と比較してみる

DIコンテナが何をしていたのか説明する前に、「もしSpringが無かったらどうやって書いていたはずだったのか」というのを示したいと思います。両者を対比をする事でDIコンテナの存在意義やDIという考え方の面白さがわかってくるかもしれません。

ではさっそく、前回作ったCard管理アプリケーションを、SpringなどのDIコンテナを使わないように直してみます。

コード中でSpringが明示的に顔を出しているのは、mainメソッドでCardServiceインスタンスを取得しているところです。ここを次のようにnewするコードに書き換える事になります。

    CardService service = new CardServiceImpl();
    // sqlSessionFactoryを手動で作る。DataSourceの設定はmybatis-config.xmlに書いておくため明示的にセットしなくても良い
    Reader reader = Resources.getResourceAsReader("jp/ohnaka/springmybatis/persistence/mybatis-config.xml");
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
    // CardMapperの実装を得る
    SqlSession session = factory.openSession();
    CardMapper mapper = session.getMapper(CardMapper.class);

これでmapperの実装が得られたので、CardServiceのインスタンスにセットしてあげれば完成、となりそうです。

ところがこれでは問題があります。DIを使った時には出てきていなかった SqlSessionの扱いです。

SqlSessionはSqlSessionFactoryから作られるもので、通常は以下のように使います。

        SqlSession session = factory.openSession();
        try {
            CardMapper mapper = session.getMapper(CardMapper.class);
            mapper.createCard();
            session.commit();
        } finally {
            session.close();
        }

SqlSessionは非MTSafeで、再利用しないようにすべきです。にもかかわらずmapperはSqlSessionから作成しなければいけません。そのため、mapperのメソッドを呼ぶ前にSqlSessionをopenし、使用後にcloseしています。

と言う事は、session.getMapper()によって得られたMapperインスタンスをCardServiceにセットしても、sessionに対するcommit()やclose()が行なわれないため、うまく動かないという事になります。

きちんと動くようにするためには次の2つの方法が考えられます。

  • CardServiceのロジックを修正して、SqlSessionFactoryを保持するようにし、CardMapparの各メソッド呼び出しの前後にSqlSessionの処理(トランザクション処理)のロジックを入れる。
  • CardMapperインターフェースを持つプロキシクラスを作り、CardMapperの各メソッドの呼び出しの前後でSqlSessionの処理(トランザクション処理)が行なわれるようにする。プロキシクラスは内部にSqlSessionFactoryを保持する。

両者を詳しく説明します。

CardServiceのロジックを書き直した場合

これは普通のMyBatisのコーディングスタイルです。CardServiceは次のようになります。

CardServiceImpl.java
package jp.ohnaka.springmybatis.service;

import java.util.UUID;

import jp.ohnaka.springmybatis.model.Card;
import jp.ohnaka.springmybatis.persistence.CardMapper;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;

public class CardServiceImpl implements CardService {
    private SqlSessionFactory factory_;

    public void setSqlSessionFactory(SqlSessionFactory factory) {
        factory_ = factory;
    }

    @Override
    public Card getCard(String id) {
        SqlSession session = factory_.openSession();
        try {
            CardMapper cardMapper = session.getMapper(CardMapper.class);
            return cardMapper.getCardById(id);
        } finally {
            session.close();
        }
    }

    @Override
    public Card createCard() {
        SqlSession session = factory_.openSession();
        try {
            CardMapper cardMapper = session.getMapper(CardMapper.class);
            Card card = new Card();
            card.setId(UUID.randomUUID().toString());
            card.setName("No Name");
            cardMapper.createCard(card);
            session.commit();
            return card;
        } finally {
            session.close();
        }
    }

    @Override
    public void updateCard(Card card) {
        SqlSession session = factory_.openSession();
        try {
            CardMapper cardMapper = session.getMapper(CardMapper.class);
            cardMapper.updateCard(card);
            session.commit();
            return;
        } finally {
            session.close();
        }
    }

    @Override
    public void deleteCard(String id) {
        SqlSession session = factory_.openSession();
        try {
            CardMapper cardMapper = session.getMapper(CardMapper.class);
            cardMapper.deleteCard(id);
            session.commit();
        } finally {
            session.close();
        }
    }

    @Override
    public int getNumOfCards() {
        SqlSession session = factory_.openSession();
        try {
            CardMapper cardMapper = session.getMapper(CardMapper.class);
            return cardMapper.getNumOfCards();
        } finally {
            session.close();
        }
    }
}

同じようなロジックが沢山並んでしまいました。それに、CardServiceクラスが MyBatisのクラス(SqlSessionFactoryなど)に依存するようになってしまいました。依存があると単体テストを行なうのが面倒になりますし、将来MyBatis以外のライブラリを使いたい場合は、コードからMyBatisの依存部分を引きはがす作業が必要になります。

CardMapperプロキシを使う場合

JavaのProxyクラスを使うと、上記例で示したような「各メソッドの呼び出し前後に定型処理を入れる」といった処理を簡単に書く事ができます。CardMapperに対するプロキシを作る為には、まずそのプロキシ動作を定義したInvocationHandlerを実装する必要があります。

CardMapperProxyHandler.java
package jp.ohnaka.springmybatis.service;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

import jp.ohnaka.springmybatis.persistence.CardMapper;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;

public class CardMapperProxyHandler implements InvocationHandler {
    private SqlSessionFactory factory_;

    public void setSqlSessionFactory(SqlSessionFactory factory) {
        factory_ = factory;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        SqlSession session = factory_.openSession();
        try {
            CardMapper cardMapper = session.getMapper(CardMapper.class);
            Object ret = method.invoke(cardMapper, args);
            session.commit();
            return ret;
        } finally {
            session.close();
        }
    }
}

このInvocationHandlerを使って以下のようにしてプロキシオブジェクトを得ます。

    Reader reader = Resources.getResourceAsReader("jp/ohnaka/springmybatis/persistence/mybatis-config-full.xml");
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
        CardMapperProxyHandler handler = new CardMapperProxyHandler();
        handler.setSqlSessionFactory(factory);
        CardMapper mapper = (CardMapper) Proxy.newProxyInstance(CardMapper.class.getClassLoader(), new Class[] {
            CardMapper.class
        }, handler);

このようにして得られた mapper オブジェクトはCardMapperインターフェースを実装したオブジェクトとして振る舞いますが、CardMapperImplのインスタンスではありません。mapperオブジェクトに対して何らかのメソッドを呼び出すと、先ほど示したInvocationHandler (CardMapperProxyHandlerクラスの事)の invoke()メソッドが呼び出されます。

invoke()メソッドの中では事前に与えられたSqlSessionFactoryからSqlSessionをopenし、本来のMyBatisのCardMapper実装を生成し、元々呼ばれるはずだったメソッド( createCard()など)をその実装に対して呼び出します。呼び出しに成功したら commit()を実行し、メソッドの返り値を返します。また、finally節でSqlSessionのclose処理も行なっています。

この mapperインスタンスを CardServiceにセットしてあげれば、CardServiceの実装を変える事無くトランザクション処理を挿入する事ができます。

両者の比較

先の2つの実装を比べてみて、どう感じるでしょうか。

「DBとの接続にはMyBatisを使う!」と決めているのであれば前者の方がストレートなやり方ですし、わかりやすいと思います。

後者はどうでしょう?実はこの後者のやり方は前回の記事でDIコンテナが裏でやってくれていた事に相当します(もちろんこの記事のコードは説明の為に単純化しているのでこのままのコードが動いている訳ではありませんが、プロキシによって元々の処理の流れに割り込む様子が分かってもらえれば幸いです)

依存関係を極力切りたい派の人は迷わず後者を選ぶのでしょうが、今まで前者のスタイルで書いていた人は「なぜそこまでして、、、」という感じかもしれません。

後者のメリットを一言で説得するのは難しいですが、例えば例外に対する挙動を変えたい場合などはプロキシを用いた後者の方式に明らかにメリットがあります。例えば、CardMapperのメソッドがHogeExceptionという例外を返すとしましょう。この例外はある特殊な事情で投げられるものの、DBの操作そのものは正常に行なわれているのでコミットは実行したい、という場合を考えます。すると、前者の方式だと、各メソッドを全て以下のように書き換える必要が出てきます。

    @Override
    public Card getCard(String id) {
        SqlSession session = factory_.openSession();
        try {
            CardMapper cardMapper = session.getMapper(CardMapper.class);
            return cardMapper.getCardById(id);
        } catch( HogeException e ) {
            session.commit();
            throw e;
        } finally {
            session.close();
        }
    }

これに対し、後者の方式であれば、InvocationHandlerのinvoke()メソッドの実装だけを以下のように修正すれば十分です。

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        SqlSession session = factory_.openSession();
        try {
            CardMapper cardMapper = session.getMapper(CardMapper.class);
            Object ret = method.invoke(cardMapper, args);
            session.commit();
            return ret;
        } catch( HogeException e ) {
            session.commit();
            throw e;
        } finally {
            session.close();
        }
    }

まだ説得力に欠けるでしょうか?では次の createTwoCard() の例を見てみてください。

カードを一度に2枚作るcreateTwoCards()を考える

CardServiceに、カードを一度に2枚作る createTwoCards()メソッドを足してみたいと思います。コードイメージは次のような感じです。

    @Override
    public void createTwoCards() {
        createCard();
        createCard();
    }

これ、一見うまく動きそうですが、トランザクションが2つに分かれてしまっています。つまり、最初のcreateCard()を呼び出した時点でDBにはカードが一件追加されてコミットされてしまいます。「一度に2枚作る」というニーズからは離れてしまいます。

仕方なくベタに書くのであれば、次のようにcreateCards()の実装をcreateTwoCards()にコピーしてしまう方法もあります。

    @Override
    public void createTwoCard() {
        SqlSession session = factory_.openSession();
        try {
            CardMapper cardMapper = session.getMapper(CardMapper.class);
            // 1枚目
            Card card1 = new Card();
            card1.setId(UUID.randomUUID().toString());
            card1.setName("No Name");
            cardMapper.createCard(card1);
            // 2枚目
            Card card2 = new Card();
            card2.setId(UUID.randomUUID().toString());
            card2.setName("No Name");
            cardMapper.createCard(card2);
            // コミット
            session.commit();
        } finally {
            session.close();
        }

これでも良いのですが、こういった複数の処理をまとめたいというニーズが沢山あった場合、組み合わせの数だけメソッドが増えて大変です。

もう一方のプロキシを使うケースではどうでしょうか。こちらのケースもcreateCard()メソッドを呼ぶと都度コミットされてしまうので同じです。

トランザクションを管理するクラスについて考えてみる

ここで、どこからでもグローバルにアクセスできるTransactionという仮のクラスを作る事を考えてみます。Transactionクラスは内部にSqlSessionを保持できるクラスだと想像してください。これを使ってcreateCard()と createTwoCards()を書き換えてみます。

    @Override
    public Card createCard() {
        boolean isInTransaction = false;
        SqlSession session = Transaction.getSqlSession();
        if (session != null) {
            // すでにトランザクションが開始されているならそれを利用する
            isInTransaction = true;
        } else {
            // トランザクションが開始されていないなら新しく作る
            session = factory_.openSession();
            Transaction.setSqlSession(session);
        }
        try {
            CardMapper cardMapper = session.getMapper(CardMapper.class);
            Card card = new Card();
            card.setId(UUID.randomUUID().toString());
            card.setName("No Name");
            cardMapper.createCard(card);
            if (!isInTransaction) {
                session.commit();
            }
            return card;
        } finally {
            if (!isInTransaction) {
                session.close();
                Transaction.setSqlSession(null);
            }
        }
    }

createTwoCards()も同様にして次のように書いてみます。

    @Override
    public void createTwoCards() {
        boolean isInTransaction = false;
        SqlSession session = Transaction.getSqlSession();
        if (session != null) {
            // すでにトランザクションが開始されているならそれを利用する
            isInTransaction = true;
        } else {
            // トランザクションが開始されていないなら新しく作る
            session = factory_.openSession();
            Transaction.setSqlSession(session);
        }
        try {
            createCard();
            createCard();
            if (!isInTransaction) {
                session.commit();
            }
        } finally {
            session.close();
            Transaction.setSqlSession(null);
        }
    }

このようになっていると、createTwoCards()からcreateCard()を呼んだ場合にcreateCard()内やセッションのコミットやクローズが行なわれなくなるため、ネストした処理を行ないやすくなっています。

CardMapperにプロキシを挟んでいる場合は、この処理をInvocationHandlerに記述します。こんな感じになるでしょうか。

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        boolean isInTransaction = false;
        SqlSession session = Transaction.getSqlSession();
        if (session != null) {
            // すでにトランザクションが開始されているならそれを利用する
            isInTransaction = true;
        } else {
            // トランザクションが開始されていないなら新しく作る
            session = factory_.openSession();
            Transaction.setSqlSession(session);
        }
        try {
            CardMapper cardMapper = session.getMapper(CardMapper.class);
            Object ret = method.invoke(cardMapper, args);
            if (!isInTransaction) {
                session.commit();
            }
            return ret;
        } finally {
            session.close();
            Transaction.setSqlSession(null);
        }
    }

これはCardMapperに対するプロキシですが、createTwoCards()はCardServiceのメソッドなので、CardServiceに対しても同様のプロキシを用意します。こちらは次のようなハンドラになります。

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        boolean isInTransaction = false;
        SqlSession session = Transaction.getSqlSession();
        if (session != null) {
            // すでにトランザクションが開始されているならそれを利用する
            isInTransaction = true;
        } else {
            // トランザクションが開始されていないなら新しく作る
            session = factory_.openSession();
            Transaction.setSqlSession(session);
        }
        try {
            Object ret = method.invoke(new CardServiceImpl(), args);
            if (!isInTransaction) {
                session.commit();
            }
            return ret;
        } finally {
            session.close();
            Transaction.setSqlSession(null);
        }
    }

もしトランザクション処理を開始するのがCardServiceよりももっと上位にある場合は、そちらのクラスも同様にして上のような InvocationHandlerを挟めばOKです。

うーん。

だんだん手作業でやるのが大変になってきましたよ!!!!

SpringのTransaction Management機構を使えばもっと簡単

Springはまさにこういったプロキシの処理を突っ込むのが大得意なフレームワークです。前回の記事で少し説明した「アスペクト指向プログラミング(AOP)」に相当します。

しかも、このようなトランザクション管理はみんな使いたい機能なので、Springがはじめから想定しており、より簡単に使えるようになっています。わざわざ自前でコーディングする必要はありません。

具体的には CardServiceに @Transactional というアノテーションを付けるだけです。

@Transactional()
public class CardServiceImpl implements CardService {
    private CardMapper cardMapper_;

    public void setCardMapper(CardMapper cardMapper) {
        cardMapper_ = cardMapper;
    }

    @Override
    public Card getCard(String id) {
        return cardMapper_.getCardById(id);
    }

    @Override
    public Card createCard() {
        Card card = new Card();
        card.setId(UUID.randomUUID().toString());
        card.setName("No Name");
        cardMapper_.createCard(card);
        return card;
    }

    @Override
    public void updateCard(Card card) {
        cardMapper_.updateCard(card);
    }

    @Override
    public void deleteCard(String id) {
        cardMapper_.deleteCard(id);
    }

    @Override
    public int getNumOfCards() {
        return cardMapper_.getNumOfCards();
    }

    @Override
    public void createTwoCards() {
        createCard();
        createCard();
    }

ただし、この@TransactionalアノテーションをSpringに認識してもらう為には、spring-setting.xmlに少し記述を追加します。

<!-- アノテーションベースのトランザクション管理を有効にする -->
 <tx:annotation-driven transaction-manager="txManager" />

 <!-- トランザクションマネージャの指定。データソースを使っているのでDataSourceTransactionManagerを使います -->
 <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
 </bean>

txというXMLのネームスペースが増えているので、以下の宣言も追加しておいてください。

xmlns:tx="http://www.springframework.org/schema/tx"

それと、spring-tx.jarというモジュールを有効にする必要があります。Mavenを使っているのであれば、以下のdependencyを追加してください。

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-tx</artifactId>
   <version>3.0.5.RELEASE</version>
  </dependency>

@Transactionalを付けるとDIコンテナは何をしてくれるのか?

もう一度CardServiceImplの実装を見てください。createTwoCardsには特にトランザクションを開始する為のコードは一切書いてありません。しかし、クラスに@Transactionalが付いているので、このメソッドを呼び出した時にトランザクションが無ければ自動的に開始されます。createCard()についても同様です。

どうしてそんな事が起こるかと言うと、DIコンテナがCardServiceをインスタンス化する際に、@Transactionalが付いている事を認識して、自動的にプロキシクラスを挿入してくれる為です。そのプロキシクラスによって「必要な時にトランザクションを開始する」というロジックが自動的に呼ばれるようになります。

一方、createCard()が呼び出しているCardMapperの実装はMapperFactoryBeanによって作られます。実はMapperFactoryBeanによって作られたインスタンスは標準でSpringのトランザクションマネージャの指示に従うように作られているため、CardMapperの各メソッドも「トランザクションが無ければ作る、あればそれを利用する」という動きをするようになります。

createTwoCards()から createCard()を呼び出した場合は、createTwoCards()の頭で既にトランザクションが開始されているため新たなトランザクションは作られず、コミットもされません。コミットはcreateTwoCards()が終了する時に自動的に行なわれるのです。そしてcreateCard()を単体で呼び出した場合はcreateCard()の終了時にコミットされます。

なんて素敵なのでしょう。

おわりに

どうでしょう。ちょっとの苦労でDIコンテナのお世話になると多大なメリットが得られるのがだんだん分かってきたでしょうか。

次回は CardServiceのフロントに RESTEasyというコンポーネントを接続し、CardServiceの機能をJAX-RS (RESTful)経由で利用できるようにしてみたい と思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0