🍣

Springを通じたTransactionは楽!Springを使ってないTransactionと比べましょう!

2022/10/29に公開

初めまして、Junior Back-end Developer、SIMOKITAZAWAです。
色々と足りないところがあるかと思いますがよろしくお願い致します。

私がZENNを始めた理由は、

  1. 私が勉強したことをまとめたい
  2. 勉強の内容を共有して一緒に成長したい
  3. 私が間違って理解している部分について先輩たちに教えてもらいたい

このような理由で始めることになりました。
たくさんの教えをお願いします。

Transaction使ってみる前に復習

Transactionが何か今まで勉強しました、これからはSpringなしに使ってみてSpringがあったら何が良くなるか勉強してみましょう。

もう一回簡単に復習してみましょう
Transactionを使用するにはConnectionが必要です。Connectionを作ってConnection Poolに入れます。使用するたびにConnection PoolからConnectionを持ってきて使用します。Connectionを通じてDatabasesにSQL文を伝達するためです。

Transactionは、autocommitがfalseである時点からTransactionが開始されると考え、commitを行う前にはupdateを行う当該Sessionにのみデータが表示されます。
commitをするとDatabase Session関係なくcommitしたデータがすべてに表示されるようになり、rollbackをする際にデータは元に戻ります。
また、Database Lockを通じてロックを獲得したSessionのみupdateができ、Lockを返還すれば返還されたrowのデータは他のセッションがupdateできます。

Transaction使ってみましょう

まず、Transactionはどこの階層で使ったら良いでしょうか
ControllerはClientからRequestをもらうドアみたいところですようね
そうしたら、ServiceかDAO(Repository)どっちかです。

考えてみましょう、Trasactionはデータの整合性を検証するのに役立つのであって、sql文を送ってくれるのではありません。なので、ビジネスロジックがあるService階層で使います。

Connection作る方法

簡単にapplication.properties、またはapplication.ymlで設定しても良いです。

public class ConnectionConst {
    public static final String URL = "jdbc:h2:tcp://localhost/~/test";
    public static final String USERNAME = "sa";
    public static final String PASSWORD = "";
}

h2 Databaseを使うのでConnectionのaddressをSettingしましょう。

@Slf4j
public class DBConnectionUtil {

    public static Connection getConnection() {

        try {
            Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            log.info("get connection = {}, class = {}",connection, connection.getClass());
            return connection;
        } catch (SQLException e) {
            throw  new IllegalArgumentException(e);
        }
    }
}

DriverManagerからH2 Databaseと繋ぐConnectionをとってURL、USERNAME、PASSWORDを入れます。新しい商品のItemNameとPriceを作ろうとしましょう。

@RequiredArgsConstructor
@Slf4j
public class ItemService {

    private final DataSource dataSource;
    private final ItemRepository itemRepository;

    public void itemSetting(String itemId, String itemName, int price) throws SQLException {

        Connection con = dataSource.getConnection();
        try {
            con.setAutoCommit(false); → Transaction スタートします。
            bizLogic(con, itemId, iteName, price); → ここはビジネスロジックです。
            con.commit();  → Transactionが成功したらCommitします。

        } catch (Exception e) {
            con.rollback(); → Transactionが失敗したらRollbackになります。
            throw new IllegalStateException(e);
        } finally {
            release(con); 
        }
    }
    
    private void release(Connection con) {
        if (con != null) {
            try {
                con.setAutoCommit(true); //connection pool true
                con.close();
            } catch (Exception e) {
                log.info("error", e);
            }
        }
    }

DataSourceとは?
DBに関するConnection情報が含まれており、Beanに登録して因子として渡します。この過程を通じてSpringはDataSourceでDBとの接続を獲得します。JDBC技術を使用するため、JDBC用Transactionマネージャ(DataSourceTransactionManager)を選択してサービスに注入します。
上のServiceコードを見るとDataSourceを通じてConnectionをGetします。GetしたConnectionを使ってTransactionを始めます。ロジックが成功したらCommitになり、失敗したらRollbackになります。使って終わったConntionはConntion Pool考慮してAutoCommitをTrueにします。

@Slf4j
@RequiredArgsConstructor
public class ItemRepository {

    private final DataSource dataSource;

    public Item save(Item item) throws SQLException {

        String sql = "insert into member(itme_id, itemName, price) values(?,?,?)";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection(); //Conntioncをget
            pstmt = con.prepareStatement(sql); → ConntioneにSQL入れる
            pstmt.setString(1, item.getItemId()); → ID Setting
            pstmt.setString(2, item.getItemName()); → ItemName Setting
            pstmt.setInt(3, item.getPrice()); → Price Setting
            pstmt.executeUpdate(); → SettingのSQLをDBに送る
            return item;
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null); //終わったらConnectionを閉じる
        }
    }
    
    private void close(Connection con, Statement stmt, ResultSet re) {
        JdbcUtils.closeResultSet(re);
        JdbcUtils.closeStatement(stmt);
        JdbcUtils.closeConnection(con);
    }

    private Connection getConnection() throws SQLException {
        Connection connection = dataSource.getConnection();
        log.info("get connection = {}, class = {}", connection, connection.getClass());
        return connection;
    }

Reposiotyでは、ServiceのビジネスロジックからもらったDataを使ってSQLを作ります。
ConnectionをGETして、そのConnectionにSQLを入れてDBに伝達します。その後Connectionを閉じてConnection Poolに返還します。

Springを使ってないTransaction問題

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

上のコードを作成した後、Serviceのimportを見ると純粋なService階層がJDBCに従属しています。
そうすると3つの問題が生じます。

  1. jdbcから他のデータアクセス技術を使用する場合、Transactionコードをすべて修正。
  2. サービス階層にTransactionコードが繰り返されており、ロジックよりも長い。
  3. SQLExceptionはJDBC専用技術で例外処理をやり直さなければならない。

実装技術ごとにトランザクションの使用方法が異なります。
JDBC : con.setAutoCommit(false)
JPA : transaction.begin()

この問題を解決するためにSpringはPlatformTransactionManagerというTransaction interfaceを提供します。

PlatformTransactionManagerのおかげてDatabase Access技術を変える時に全てのTracsationコードを修正しなくても大丈夫です。

PlatformTransactionManagerはTransactionMagerをExtendしています。

このTransactionMagerは2つの役割があります。一つ目は上に書いたinterfaceを提供すること、二つ目はTransactionを維持するために、Transactionの開始から最後まで同じデータベースConnectionを維持することです。

今までのコードを見てるTransactionを維持するために、ConnectionをGetしてGetしたそのConnectionをずっとparameterで渡しました。
parameterでConnectionを伝達する方法はコードが汚れるのはもちろん、Connectionを渡すMethodと渡さないMethodを重複して作らなければならないなど、様々な短所が多いです。
これを解決できるようにSprigはTransactionMager提供して解決します。

@Slf4j
@RequiredArgsConstructor
public class ItemService {

    private final PlatformTransactionManager transactionManager;

    public void itemSetting(String itemId, String itemName, int price) throws SQLException {

        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); → Transaction スタート
        try {
            bizLogic(itemId, iteName, price); → ここはビジネスロジック
            transactionManager.commit(status); → Transactionが成功Commit

        } catch (Exception e) {
            transactionManager.rollback(status); → Transactionが失敗Rollback
            throw new IllegalStateException(e);
        } 
    } 

最初のServiceコードと比べたら短くなり、繰り返し作らなくてはいけないコードがなくなりました。

Springを使ってTransaction

ここでもう一度Springはもっと簡単にしてくれます。Service階層にTransaction適用コードを使わなくても良い魔法の@Transactionalを提供します。

@Transactional → Proxy投入
    public void itemSetting(String itemId, String itemName, int price) {
        bizLogic(fromId, toId, money);
    }

SpringがProxyを提供することによって、ProxyにTracsactionを開始し、commit、rollbackを持ち、service Logicを呼び出します。私たちはservice階層にTransactionに関連するコードを作成しなくてもよいです。ただ@Transactionalアノテーション一つだけで終わらせることができます。
@Transactionalの意味はitemSettingのProxyを作って、そのProxyがTransactionを処理する意味です。

絵で全体的な流れを理解しましょう

Discussion