Springを通じたTransactionは楽!Springを使ってないTransactionと比べましょう!
初めまして、Junior Back-end Developer、SIMOKITAZAWAです。
色々と足りないところがあるかと思いますがよろしくお願い致します。
私がZENNを始めた理由は、
- 私が勉強したことをまとめたい
- 勉強の内容を共有して一緒に成長したい
- 私が間違って理解している部分について先輩たちに教えてもらいたい
このような理由で始めることになりました。
たくさんの教えをお願いします。
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つの問題が生じます。
- jdbcから他のデータアクセス技術を使用する場合、Transactionコードをすべて修正。
- サービス階層にTransactionコードが繰り返されており、ロジックよりも長い。
- 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