- はじめに
- 題材
- トランザクションインタセプタ
- リポジトリの save() 呼び出し
- 永続化イベント
- トランザクションの commit
- コミット前処理の flush()
- ActionQueue の実行
- まとめ
はじめに
フレームワークも成熟しており、中身の動作を意識することも少なくなってきていますが、内部動作を大まかにでも把握しておくことは色々な意味で有益です。
大規模化してきたり、バイトコードエンハンスすることが当たり前になってきて、なかなか中身を覗く人も少なくなってきた印象がありますが、オープンソースは読まなきゃ損です。
といってもじっくり読むのも大変なので、Spring Boot を例にして、単純な永続化の「あらすじ」を紹介します。
題材
以下のような単純なサービスを例とします。
@Service @Transactional public class CustomerService { // ... public Customer createCustomer(String firstName, String lastName) { Customer c = new Customer(firstName, lastName); repository.save(c); return c; } }
Customer
を永続化するだけのシンプルなサービスです。
リポジトリは Spring Data で以下のようなものとします。
@Repository public interface CustomerRepository extends CrudRepository<Customer, Long> { }
サービスを、以下のようなコントローラから呼び出すことを考えます。
@RestController public class CustomerController { // ... @RequestMapping("/createCustomer") public Customer createCustomer() { return service.createCustomer("Mick", "Jagger"); } }
トランザクションインタセプタ
コントローラに DI されるサービスは Proxy となっており、コントローラからサービスのメソッド呼び出しの間に各種インターセプタが挿入されます。
今回の題材のサービスには @Transactional
アノテーションがついているため、サービスのメソッドコールの前後でトランザクション処理が行われます。
トランザクションを処理する TransactionInterceptor
は以下のような実装になっています。
public class TransactionInterceptor implements MethodInterceptor { // TransactionInfo のホルダー private static final ThreadLocal<TransactionInfo> transactionInfoHolder = new NamedThreadLocal<>("Current aspect-driven transaction"); public Object invoke(MethodInvocation invocation) throws Throwable { final TransactionAttribute txAttr = ... final PlatformTransactionManager tm = ... // (1) TransactionInfo を作成(トランザクション開始)して ThreadLocal に保存 TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, ...); transactionInfoHolder.set(txInfo); Object retVal = null; try { // (2) サービスメソッドの実行 retVal = invocation.proceed(); } finally { // トランザクションの後処理 cleanupTransactionInfo(txInfo); } // (3) コミット txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); return retVal; } }
実際のコードはもっと込み入っていますが、説明用に大きく改変しています(後のコードも同じです)。
(1) ではcreateTransactionIfNecessary()
でトランザクションを取得(PlatformTransactionManager
からトランザクションを取得)し、TransactionInfo
を new して ThreadLocal
に保存しています。
(2) で次のインタセプタを呼び出し、最終的にはサービスのメソッドが呼び出されます。
(3) でトランザクションマネージャの commit()
を行います。
リポジトリの save() 呼び出し
今回のサービスメソッドでは以下のように単にリポジトリの save()
を呼ぶだけのものです。
public Customer createCustomer(String firstName, String lastName) { Customer c = new Customer(firstName, lastName); repository.save(c); return c; }
この repository
も、サービスの場合と同じく多数のインタセプタを経由します。
特に異なるのは、Spring Data を処理するインタセプタが挿まることですが、ここでは省略します。
Spring Data により、repository.save()
で以下のメソッドがコールされます。
@Repository public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> { private final EntityManager em; @Transactional public <S extends T> S save(S entity) { if (entityInformation.isNew(entity)) { em.persist(entity); return entity; } else { return em.merge(entity); } } }
em.persist()
のようにエンティティマネージャの永続化メソッドが呼び出されます。
em.persist()
の EntityManager
も Proxy化されており、SharedEntityManagerCreator
-> ExtendedEntityManagerCreator
と経由して、SessionImpl
の persist()
に処理が委譲されることになります。
SessionImpl
の persist()
は以下のようになっています。
public final class SessionImpl extends AbstractSessionImpl implements EventSource, SessionImplementor, HibernateEntityManagerImplementor { private transient ActionQueue actionQueue; private transient StatefulPersistenceContext persistenceContext; public void persist(Object object) throws HibernateException { PersistEvent event = new PersistEvent(null, object, this); for (PersistEventListener listener : listeners(EventType.PERSIST) ) { listener.onPersist(event); } } }
SessionImpl
は org.hibernate.internal.SessionImpl
であり、ここから Hibernate の世界に入っていくことになります。
ここで行っていることは、永続化イベント PersistEvent
を作成し、イベントリスナに listener.onPersist(event)
として処理を委譲しているだけです。
永続化イベント
永続化イベント PersistEvent
は以下のリスナで処理されます。
public class DefaultPersistEventListener extends AbstractSaveEventListener implements PersistEventListener { public void onPersist(PersistEvent event) throws HibernateException { final EventSource source = event.getSession(); // SessionImpl final Object entity = event.getObject(); EntityPersister persister = source.getEntityPersister(entity); // (1) EntityPersister経由で ID を採番し、entity にリフレクションでセット Serializable generatedId = persister.getIdentifierGenerator().generate(source, entity); persister.setIdentifier(entity, generatedId, source); // (2) EntityEntry を作成し、コンテキストに保存 EntityEntry entityEntry = persister.getEntityEntryFactory() .createEntityEntry(generatedId, entity, persister, ...)); source.getPersistenceContext().addEntityEntry(entity, entityEntry); // (3) EntityInsertAction を作成し、アクションキューに保存 EntityInsertAction insert = new EntityInsertAction(...); source.getActionQueue().addAction(insert); } }
event.getSession()
で取得されるのが先程の SessionImpl
です。
(1) ではコメントの通り、ID を Entity に設定しています。
(2) で Entity を EntityEntry
というホルダに入れてコンテキストに保存します。このコンテキストは、先程示した SessionImpl
のフィールドの StatefulPersistenceContext
となります。
最後に (3) で EntityInsertAction
を生成しアクションキューに保存します。こちらも SessionImpl
のフィールドの ActionQueue
が格納先です。
em.persist()
の処理は大まかに以上の処理になります。
対象の Entity を EntityEntry
としてコンテキストに保存、つまり Customer
の参照を保持し、EntityInsertAction
を Queue に登録というのが大きなあらすじです。
ここまでの処理ではデータベースへの操作は、ID の採番でシーケンスから値を取得する程度です。
Insert は続くトランザクションの commit の中で(あくまでも今回の例では)処理されます。
トランザクションの commit
save の処理が終われば、最後に TransactionInterceptor
でコミット処理が行われます。
もう一度先程の TransactionInterceptor
を見てみましょう。
public class TransactionInterceptor implements MethodInterceptor { public Object invoke(MethodInvocation invocation) throws Throwable { // ... try { retVal = invocation.proceed(); } finally { cleanupTransactionInfo(txInfo); } // (1) TransactionManager#commit() txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); return retVal; } }
(1) の箇所でトランザクションマネージャの commit をコールしています。
コミット処理は AbstractPlatformTransactionManager
で以下のように定義されています。
public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable { public final void commit(TransactionStatus status) throws TransactionException { doCommit(status); } }
doCommit()
は AbstractPlatformTransactionManager
のサブクラスである JpaTransactionManager
で以下のように定義されています。
public class JpaTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, BeanFactoryAware, InitializingBean { protected void doCommit(DefaultTransactionStatus status) { JpaTransactionObject txObject = (JpaTransactionObject) status.getTransaction(); EntityTransaction tx = txObject.getEntityManagerHolder().getEntityManager().getTransaction(); tx.commit(); } }
EntityTransaction#commit()
が呼ばれていますね。
この実態は TransactionImpl
で、以下のようになります。
public class TransactionImpl implements TransactionImplementor { public void commit() { internalGetTransactionDriverControl().commit(); } }
internalGetTransactionDriverControl().commit()
は、TransactionDriverControlImpl
で以下のような定義になっています。
public class JdbcResourceLocalTransactionCoordinatorImpl implements TransactionCoordinator { public class TransactionDriverControlImpl implements TransactionDriver { public void commit() { JdbcResourceLocalTransactionCoordinatorImpl.this.beforeCompletionCallback(); this.jdbcResourceTransaction.commit(); JdbcResourceLocalTransactionCoordinatorImpl.this.afterCompletionCallback(true); } } }
jdbcResourceTransaction.commit()
に行き着きました。
今回着目するのは、このコミット処理の直前の beforeCompletionCallback()
です。
続けて見ていきましょう。
コミット前処理の flush()
先程見た beforeCompletionCallback()
を処理するのが JdbcResourceLocalTransactionCoordinatorImpl
で、以下のようになっています。
public class JdbcResourceLocalTransactionCoordinatorImpl implements TransactionCoordinator { private void beforeCompletionCallback() { transactionCoordinatorOwner.beforeTransactionCompletion(); }
ここでのtransactionCoordinatorOwner
は、少し前に見た SessionImpl
です。
beforeTransactionCompletion()
は以下のようになっています。
public final class SessionImpl extends AbstractSessionImpl implements EventSource, SessionImplementor, HibernateEntityManagerImplementor { public void beforeTransactionCompletion() { flushBeforeTransactionCompletion(); // (1) actionQueue.beforeTransactionCompletion(); super.beforeTransactionCompletion(); } public void flushBeforeTransactionCompletion() { final boolean doFlush = ... if (doFlush) { managedFlush(); // (2) } } private void managedFlush() { doFlush(); // (3) } private void doFlush() { // (4) フラッシュイベントでリスナー呼び出し FlushEvent flushEvent = new FlushEvent(this); for (FlushEventListener listener : listeners(EventType.FLUSH)) { listener.onFlush(flushEvent); } } } }
(1) -> (2) -> (3) と来て、(4) で FlushEvent
を作成してリスナー呼び出ししています。
フラッシュリスナーは DefaultFlushEventListener
で以下となります。
public class DefaultFlushEventListener extends AbstractFlushingEventListener implements FlushEventListener { public void onFlush(FlushEvent event) throws HibernateException { final EventSource source = event.getSession(); final PersistenceContext persistenceContext = source.getPersistenceContext(); flushEverythingToExecutions(event); performExecutions(source); // (1) postFlush(source); } protected void performExecutions(EventSource session) { session.getActionQueue().prepareActions(); session.getActionQueue().executeActions(); // (2) } }
(2) で ActionQueue
を実行しています。この ActionQueue
には、先に見た DefaultPersistEventListener
で登録した EntityInsertAction
が実行されることになります。
ActionQueue の実行
ActionQueue
の executeActions()
は以下のように Action を実行します。
public class ActionQueue { public void executeActions() throws HibernateException { for (ListProvider listProvider : EXECUTABLE_LISTS_MAP.values()) { executeActions(listProvider.get(this)); } } private <E extends Executable & Comparable<?> & Serializable> void executeActions(ExecutableList<E> list) throws HibernateException { for (E e : list) { e.execute(); // Action の実行 } list.clear(); }
今回は Customer
の永続化アクションであり、以下が実行されます。
public final class EntityInsertAction extends AbstractEntityInsertAction { public void execute() throws HibernateException { final EntityPersister persister = getPersister(); // (1) final SharedSessionContractImplementor session = getSession(); final Object instance = getInstance(); final Serializable id = getId(); persister.insert(id, getState(), instance, session); // (2) PersistenceContext persistenceContext = session.getPersistenceContext(); final EntityEntry entry = persistenceContext.getEntry(instance); entry.postInsert(getState()); persistenceContext.registerInsertedKey(persister, getId()); markExecuted(); } }
データベースへの Insert は (1) で取得した EntityPersister
を使い、(2) で行われます。
persister.insert()
の中で、見慣れた PreparedStatement
による SQLが実行されます。
まとめ
単純な persist 処理のあらすじを見てみました。
SessionImpl
を中心に、EntityManager
への操作を Action
として登録し、Listener
により処理を行うことが見て取れたかと思います。
ここでは多くを省略していますが、フラッシュ時には永続化の実行順序を制御したり、カスケード処理やキャッシュ処理、Entity のProxy 化やダーティーチェックなど ORM の処理はかなり煩雑です。
しかし、ここで見た大まかな流れを把握することで、処理の流れを追いかけやすくなるかと思います。