kagamihogeの日記

kagamihogeの日記です。

Testcontainers + Oracle Database 21c XEの初期化SQL実行

基本形

以降の記述は以下の基本形をベースに進めていく。

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.0'
    id 'io.spring.dependency-management' version '1.1.5'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'

    runtimeOnly 'com.oracle.database.jdbc:ojdbc11'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.boot:spring-boot-testcontainers'
    testImplementation 'org.testcontainers:junit-jupiter'
    testImplementation 'org.testcontainers:oracle-xe'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.testcontainers.containers.OracleContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@SpringBootTest
@Testcontainers
class IntegrationTestSimple {

  @Container
  @ServiceConnection
  static OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21.3.0-slim-faststart");

  @Autowired
  DataSource ds;

  @Test
  void test() {
    Integer v = JdbcClient.create(ds).sql("select 1 from dual").query(Integer.class).single();
    System.out.println(v);
  }
}

以下は挙動確認用のsrc\test\resources\logback-test.xml

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="STDOUT"/>
  </root>

  <logger name="org.testcontainers" level="DEBUG"/>
  <!-- The following logger can be used for containers logs since 1.18.0 -->
  <logger name="tc" level="INFO"/>
  <logger name="com.github.dockerjava" level="WARN"/>
  <logger name="com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire" level="OFF"/>
</configuration>

Initialization scripts

詳細は https://hub.docker.com/r/gvenzl/oracle-xe のInitialization scriptsにある。

コンテナの /container-entrypoint-initdb.dディレクトリに*.sql*.shを配置するとDB起動時にそれらを自動的に実行する。次に、TestcontainersのOracleContainer#withCopyFileToContainerなどでホストのファイルをコンテナの任意ディレクトリにコピーできる。これらを組み合わせて初期化SQLの実行を実現する。

以下例はクラスパスのsqlディレクトリ(src\test\resources\sql)を/container-entrypoint-initdb.dとしてコピーする。

import org.testcontainers.utility.MountableFile;

  @Container
  @ServiceConnection
  static OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21.3.0-slim-faststart")
      .withCopyFileToContainer(
          MountableFile.forClasspathResource("sql"), "/container-entrypoint-initdb.d");

以下例はファイルシステムのパスを使用する場合。また、ファイルの実行順はアルファベット順なので、これを制御したい場合は例えばprefixに001_, 002_を付与するのがオススメ、と公式ドキュメントにある。

  @Container
  @ServiceConnection
  static OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21.3.0-slim-faststart")
      .withCopyFileToContainer(
          MountableFile.forHostPath(Paths.get("C:\\sample\\sql\\create_tablespace.sql")), "/container-entrypoint-initdb.d/001_init.sql")
      .withCopyFileToContainer(
          MountableFile.forHostPath(Paths.get("C:\\sample\\sql\\create_table.sql")), "/container-entrypoint-initdb.d/002_init.sql");

これらスクリプトの実行はSYSとしてXEインスタンスSQL*Plusを介して実行される。

Oracle XEでアプリケーションが基本的に使用するのはXEPDB1なので、これら初期化スクリプトは以下のようにPDBに切り替えてから、がほとんどになると思われる。

alter session set container = XEPDB1;

Startup scripts

詳細は https://hub.docker.com/r/gvenzl/oracle-xe のStartup scriptsにある。

使用方法はInitialization scriptsとほぼ同じでターゲットディレクトリが異なり/container-entrypoint-startdb.dとなる。スクリプトの実行タイミングが異なり大雑把にはInitialization -> Startup の順で、前者がAfter the database setup is completed,、後者がAfter the database is up and ready for requests, との記述がある。

withInitScript

上述まではdockerの機能で、これはOracleContainer(の親クラスのJdbcDatabaseContainer)の機能。コンテナ起動後にクラスパス下のSQLを実行する。DB接続は自分自身つまりOracleContainerの設定を使用する。

以下例はsrc\test\resources\create_table.sqlを実行する。

  @Container
  @ServiceConnection
  static OracleContainer oracle = new OracleContainer("gvenzl/oracle-xe:21.3.0-slim-faststart")
      .withInitScript("create_table.sql")
      ;

該当箇所のログは以下のとおり。DATABASE IS READY TO USE! -> Container is startedのあと、かつ、spring-bootの起動開始前にSQLが実行されているのが分かる。

DEBUG org.testcontainers.containers.output.WaitingConsumer - STDOUT: #########################
DEBUG org.testcontainers.containers.output.WaitingConsumer - STDOUT: DATABASE IS READY TO USE!
INFO  tc.gvenzl/oracle-xe:21.3.0-slim-faststart - Container gvenzl/oracle-xe:21.3.0-slim-faststart started in PT14.390841S
INFO  tc.gvenzl/oracle-xe:21.3.0-slim-faststart - Container is started (JDBC URL: jdbc:oracle:thin:@localhost:32776/xepdb1)
INFO  org.testcontainers.ext.ScriptUtils - Executing database script from create_table.sql
(略)
INFO  (略) - Starting IntegrationTest using Java 17.0.9 with ..(略)

spring-bootの各種DB初期化機能

詳細は省略するが @Sql とか色々ある。各テストケースに必要なデータ投入はこちら、のような使い分けをすると思われる。

参考URL