「Spring Boot で persist()」 のあらすじ



はじめに

フレームワークも成熟しており、中身の動作を意識することも少なくなってきていますが、内部動作を大まかにでも把握しておくことは色々な意味で有益です。


大規模化してきたり、バイトコードエンハンスすることが当たり前になってきて、なかなか中身を覗く人も少なくなってきた印象がありますが、オープンソースは読まなきゃ損です。

といってもじっくり読むのも大変なので、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 と経由して、SessionImplpersist() に処理が委譲されることになります。

SessionImplpersist() は以下のようになっています。

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);
        }
    }
}

SessionImplorg.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 の実行

ActionQueueexecuteActions() は以下のように 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 の処理はかなり煩雑です。

しかし、ここで見た大まかな流れを把握することで、処理の流れを追いかけやすくなるかと思います。