h2에서 @Transactional(readOnly=true) 가 안먹히는 이유 - Why @Transactional(readOnly=true) is not working in H2? (problem solved)

Table of Contents

1 레퍼런스

2 문서

일단 Transactional 문서를 보자. 그 안에 readOnly 를 설명한 글이 있다.

// Transactional#readOnly
public abstract boolean readOnly

A boolean flag that can be set to true if the transaction is effectively read-only, allowing for corresponding optimizations at runtime.
Defaults to false.

This just serves as a hint for the actual transaction subsystem; 
it will not necessarily cause failure of write access attempts. 
A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction but rather silently ignore the hint.

See Also: TransactionDefinition.isReadOnly(), TransactionSynchronizationManager.isCurrentTransactionReadOnly()

Default: false

it will not necessarily cause failure of write access attempts. readOnly라고 쓰여있다고 무조건 write를 막는 것은 아니다.

See Also: 에 있는 TransactionDefinition.isReadOnly() 을 이용해서 트랜잭션이 수행될 때, 이 메서드가 수행될 거라고 예측된다. (아직 모른다)

도큐먼트 설명을 보자.

default boolean isReadOnly()
Return whether to optimize as a read-only transaction.
The read-only flag applies to any transaction context, 
whether backed by an actual resource transaction (PROPAGATION_REQUIRED/ PROPAGATION_REQUIRES_NEW) or operating non-transactionally at the resource level (PROPAGATION_SUPPORTS). 
In the latter case, the flag will only apply to managed resources within the application, such as a Hibernate Session.

This just serves as a hint for the actual transaction subsystem; it will not necessarily cause failure of write access attempts. 
A transaction manager which cannot interpret the read-only hint will not throw an exception when asked for a read-only transaction.

Returns: true if the transaction is to be optimized as read-only (false by default)
See Also: TransactionSynchronization.beforeCommit(boolean), TransactionSynchronizationManager.isCurrentTransactionReadOnly()

이곳에도 힌트만 준다고 말한다. 트랜잭션 매니저가 read-only 힌트를 사용하지 않을 수 있다.

3 코드

3.1 TransactionDefinition#isReadOnly

인터페이스며 isReadOnly 는 기본적으로 false 이다.

public interface TransactionDefinition {
  // ...
  default boolean isReadOnly() {
    return false;
  }
  // ...
}

구현체가 있을 것이다. 찾아보니 구현체가 두개 보였다. 그중 DefaultTransactionDefinition 이것이 기본적으로 실행될 것처럼 보였다.

public class DefaultTransactionDefinition implements TransactionDefinition, Serializable {
  // ...
  @Override
  public final boolean isReadOnly() {
    return this.readOnly;
  }
}

3.2 HibernateJpaDialect#beginTransaction

당연히 트랜잭션이 수행될 때, isReadOnly() 를 사용할 것이다.

package org.springframework.orm.jpa.vendor;

public class HibernateJpaDialect extends DefaultJpaDialect {
  // ...
  @Override
  public Object beginTransaction(EntityManager entityManger, TransactionDefinition definition) 
	  throws PersistenceException, SQLException, TransactionException {
    // ...
    if (isolationLevelNeeded || definition.isReadOnly()) {
      if(this.prepareConnection) {
	preparedCon = HibernateConnectionHandle.doGetConnection(session);
	previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(preparedCon, definition);
      }
    }
    // ...

  }
}

definition을 받은 DataSourceUtils 는 무엇을 하는 걸까?

package org.springframework.jdbc.datasource;

class DataSourceUtils {
  @Nullable
  public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) {
    // Set read-only flag
    if (definition != null && definition.isReadOnly()) {
      con.setReadOnly(true);
    }
  }
}

con.setReadOnly(true) 이것은 해당 커넥션에 readOnly 플래그를 주는 것 같다. H2를 사용하고 있다면 이 커넥션은 H2구현체일 것이다.

3.3 org.h2.jdbc.JdbcConnection.java

package org.h2.jdbc;

/**
 * <p>
 * Represents a connection (session) to a database.
 * </p>
 * <p>
 * Thread safety: the connection is thread-safe, because access is synchronized.
 * However, for compatibility with other databases, a connection should only be
 * used in one thread at any time.
 * </p>
 */
public class JdbcConnection extends TraceObject implements Connection, JdbcConnectionBackwardsCompat,
	CastDataProvider {
  // ...

  /**
    * According to the JDBC specs, this setting is only a hint to the database
    * to enable optimizations - it does not cause writes to be prohibited.
    *
    * @param readOnly ignored
    * @throws SQLException if the connection is closed
    */
  @Override
  public void setReadOnly(boolean readOnly) throws SQLException {
      try {
	  if (isDebugEnabled()) {
	      debugCode("setReadOnly(" + readOnly + ");");
	  }
	  checkClosed();
      } catch (Exception e) {
	  throw logAndConvert(e);
      }
  }
}

여기 @param readOnly ignored 가 보인다.

4 결론

  • Transactional(readOnly=true)java.sql.Connection#setReadOnly 메소드에 true 인자로 호출한다.
  • 문서에 따르면 Connection 구현체들은 이것을 꼭 사용할 의무가 없다.
  • H2의 JdbcConnectionsetReadOnly 를 무시하는 것으로 구현하였다.

Date: 2022-01-10 Mon 00:00

Author: Younghwan Nam

Created: 2022-11-15 Tue 08:10

Emacs 27.2 (Org mode 9.4.4)

Validate