これは、なにをしたくて書いたもの?
Javaでデータベースを使うテストで、Database Riderが便利なのでちょいちょいと使うのですが。
外部キーをたくさん使っていると実行が遅くなるという問題に悩まされたことがあったので、メモしておきます。
@DataSetのuseSequenceFiltering
Database Riderは、テストメソッドに@DataSet
アノテーションを指定することでテスト実行前にデータのセットアップができます。
Database Rider / Rider Core / Configuration / DataSet Configuration
@DataSet
アノテーションにはuseSequenceFiltering
という属性があり、こちらをtrue
にすることでテーブル間の依存関係を調べて
データを正しい順番で登録してくれます。
この機能は外部キーを使用している場合、データの登録順をテーブル間の依存関係を調べて明示的に指示しなくていいので便利です。
デフォルトで有効(true
)になっています。
機能そのものは、DbUnitのもののようです。
なのですが、外部キーを使ったテーブルが多くなってくると実行速度が気になってきます。
issueが作成されたこともあるようです。
Slow Sequence Filtering on real database · Issue #242 · database-rider/database-rider · GitHub
テーブル間の依存関係を解析する機能はメモリー上にキャッシュされるので、テスト全体で実行される回数は少なくなります。
なのですが、IDE上でテストメソッドを実行する時などはキャッシュがないので、都度この負担が見えるようになります。
この挙動をちょっと確認しておきましょう。ソースコードの作成にはSpring Bootを使うことにします。
環境
今回の環境はこちら。
$ java --version openjdk 21.0.1 2023-10-17 OpenJDK Runtime Environment (build 21.0.1+12-Ubuntu-222.04) OpenJDK 64-Bit Server VM (build 21.0.1+12-Ubuntu-222.04, mixed mode, sharing) $ mvn --version Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 21.0.1, vendor: Private Build, runtime: /usr/lib/jvm/java-21-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-91-generic", arch: "amd64", family: "unix"
データベースにはMySQLを使用しました。172.17.0.2で動作しているものとします。
MySQL localhost:3306 ssl practice SQL > select version(); +-----------+ | version() | +-----------+ | 8.0.35 | +-----------+ 1 row in set (0.0011 sec)
Spring Bootプロジェクトを作成し、Database Riderへの依存関係を加える
まずはSpring Bootプロジェクトを作成します。依存関係にはjdbc
、mysql
を追加しました。
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=3.2.1 \ -d javaVersion=21 \ -d type=maven-project \ -d name=database-rider-resolve-table-dependencies \ -d groupId=org.littlewings \ -d artifactId=database-rider-resolve-table-dependencies \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.spring.dbrider \ -d dependencies=jdbc,mysql \ -d baseDir=database-rider-resolve-table-dependencies | tar zxvf -
作成されたプロジェクト内へ移動。
$ cd database-rider-resolve-table-dependencies
Maven依存関係など。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>org.littlewings</groupId> <artifactId>database-rider-resolve-table-dependencies</artifactId> <version>0.0.1-SNAPSHOT</version> <name>database-rider-resolve-table-dependencies</name> <description>Demo project for Spring Boot</description> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
生成されたソースコードは削除しておきます。
$ rm src/main/java/org/littlewings/spring/dbrider/DatabaseRiderResolveTableDependenciesApplication.java src/test/java/org/littlewings/spring/dbrider/DatabaseRiderResolveTableDependenciesApplicationTests.java
そして、pom.xml
にはSpring向けのDatabase Rider(rider-spring
)への依存関係を追加。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.github.database-rider</groupId> <artifactId>rider-spring</artifactId> <version>1.41.0</version> <scope>test</scope> </dependency> </dependencies>
今回はテストだけ実行できればいいので、@SpringBootApplication
だけを付与したダミーのクラスを作成しておきました。
src/main/java/org/littlewings/spring/dbrider/App.java
package org.littlewings.spring.dbrider; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { }
application.properties
はこのように設定。
src/main/resources/application.properties
spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin spring.datasource.username=kazuhira spring.datasource.password=password
dbunit.yml
も作成しました。
src/test/resources/dbunit.yml
cacheConnection: false properties: caseSensitiveTableNames: true
外部キーを定義したテーブルを大量に作成して実行してみる
では、外部キーを定義したテーブルをたくさん作って、@DataSet
のuseSequenceFiltering
のtrue
/false
でDatabase Riderの挙動が
どのように変わるのか見てみましょう。
create_table.py
import sys table_num = int(sys.argv[1]) for i in range(table_num): num = i + 1 if num == 1: print(f"""create table t{num} ( id varchar(10), primary key(id) ); """) continue print(f"""create table t{num} ( id varchar(10), t{num-1}_id varchar(10), primary key(id), foreign key(t{num-1}_id) references t{num-1}(id) on delete no action on update no action ); """)
引数で3を指定すると、こんな結果になります。
$ python3 create_table.py 3 create table t1 ( id varchar(10), primary key(id) ); create table t2 ( id varchar(10), t1_id varchar(10), primary key(id), foreign key(t1_id) references t1(id) on delete no action on update no action ); create table t3 ( id varchar(10), t2_id varchar(10), primary key(id), foreign key(t2_id) references t2(id) on delete no action on update no action );
今回は500を指定して、テーブルを作成しました。
$ python3 create_table.py 500
MySQL側での結果。
MySQL localhost:3306 ssl practice SQL > show tables; +--------------------+ | Tables_in_practice | +--------------------+ | t1 | | t10 | | t100 | | t101 | | t102 | | t103 | | t104 | | t105 | | t106 | | t107 | | t108 | | t109 | | t11 | | t110 | | t111 | | t112 | | t113 | | t114 | | t115 | | t116 | | t117 | | t118 | | t119 | | t12 | | t120 | | t121 | | t122 | | t123 | | t124 | | t125 | | t126 | | t127 | | t128 | | t129 | | t13 | | t130 | | t131 | | t132 | | t133 | | t134 | | t135 | | t136 | | t137 | | t138 | | t139 | | t14 | | t140 | | t141 | | t142 | | t143 | | t144 | | t145 | | t146 | | t147 | | t148 | | t149 | | t15 | | t150 | | t151 | | t152 | | t153 | | t154 | | t155 | | t156 | | t157 | | t158 | | t159 | | t16 | | t160 | | t161 | | t162 | | t163 | | t164 | | t165 | | t166 | | t167 | | t168 | | t169 | | t17 | | t170 | | t171 | | t172 | | t173 | | t174 | | t175 | | t176 | | t177 | | t178 | | t179 | | t18 | | t180 | | t181 | | t182 | | t183 | | t184 | | t185 | | t186 | | t187 | | t188 | | t189 | | t19 | | t190 | | t191 | | t192 | | t193 | | t194 | | t195 | | t196 | | t197 | | t198 | | t199 | | t2 | | t20 | | t200 | | t201 | | t202 | | t203 | | t204 | | t205 | | t206 | | t207 | | t208 | | t209 | | t21 | | t210 | | t211 | | t212 | | t213 | | t214 | | t215 | | t216 | | t217 | | t218 | | t219 | | t22 | | t220 | | t221 | | t222 | | t223 | | t224 | | t225 | | t226 | | t227 | | t228 | | t229 | | t23 | | t230 | | t231 | | t232 | | t233 | | t234 | | t235 | | t236 | | t237 | | t238 | | t239 | | t24 | | t240 | | t241 | | t242 | | t243 | | t244 | | t245 | | t246 | | t247 | | t248 | | t249 | | t25 | | t250 | | t251 | | t252 | | t253 | | t254 | | t255 | | t256 | | t257 | | t258 | | t259 | | t26 | | t260 | | t261 | | t262 | | t263 | | t264 | | t265 | | t266 | | t267 | | t268 | | t269 | | t27 | | t270 | | t271 | | t272 | | t273 | | t274 | | t275 | | t276 | | t277 | | t278 | | t279 | | t28 | | t280 | | t281 | | t282 | | t283 | | t284 | | t285 | | t286 | | t287 | | t288 | | t289 | | t29 | | t290 | | t291 | | t292 | | t293 | | t294 | | t295 | | t296 | | t297 | | t298 | | t299 | | t3 | | t30 | | t300 | | t301 | | t302 | | t303 | | t304 | | t305 | | t306 | | t307 | | t308 | | t309 | | t31 | | t310 | | t311 | | t312 | | t313 | | t314 | | t315 | | t316 | | t317 | | t318 | | t319 | | t32 | | t320 | | t321 | | t322 | | t323 | | t324 | | t325 | | t326 | | t327 | | t328 | | t329 | | t33 | | t330 | | t331 | | t332 | | t333 | | t334 | | t335 | | t336 | | t337 | | t338 | | t339 | | t34 | | t340 | | t341 | | t342 | | t343 | | t344 | | t345 | | t346 | | t347 | | t348 | | t349 | | t35 | | t350 | | t351 | | t352 | | t353 | | t354 | | t355 | | t356 | | t357 | | t358 | | t359 | | t36 | | t360 | | t361 | | t362 | | t363 | | t364 | | t365 | | t366 | | t367 | | t368 | | t369 | | t37 | | t370 | | t371 | | t372 | | t373 | | t374 | | t375 | | t376 | | t377 | | t378 | | t379 | | t38 | | t380 | | t381 | | t382 | | t383 | | t384 | | t385 | | t386 | | t387 | | t388 | | t389 | | t39 | | t390 | | t391 | | t392 | | t393 | | t394 | | t395 | | t396 | | t397 | | t398 | | t399 | | t4 | | t40 | | t400 | | t401 | | t402 | | t403 | | t404 | | t405 | | t406 | | t407 | | t408 | | t409 | | t41 | | t410 | | t411 | | t412 | | t413 | | t414 | | t415 | | t416 | | t417 | | t418 | | t419 | | t42 | | t420 | | t421 | | t422 | | t423 | | t424 | | t425 | | t426 | | t427 | | t428 | | t429 | | t43 | | t430 | | t431 | | t432 | | t433 | | t434 | | t435 | | t436 | | t437 | | t438 | | t439 | | t44 | | t440 | | t441 | | t442 | | t443 | | t444 | | t445 | | t446 | | t447 | | t448 | | t449 | | t45 | | t450 | | t451 | | t452 | | t453 | | t454 | | t455 | | t456 | | t457 | | t458 | | t459 | | t46 | | t460 | | t461 | | t462 | | t463 | | t464 | | t465 | | t466 | | t467 | | t468 | | t469 | | t47 | | t470 | | t471 | | t472 | | t473 | | t474 | | t475 | | t476 | | t477 | | t478 | | t479 | | t48 | | t480 | | t481 | | t482 | | t483 | | t484 | | t485 | | t486 | | t487 | | t488 | | t489 | | t49 | | t490 | | t491 | | t492 | | t493 | | t494 | | t495 | | t496 | | t497 | | t498 | | t499 | | t5 | | t50 | | t500 | | t51 | | t52 | | t53 | | t54 | | t55 | | t56 | | t57 | | t58 | | t59 | | t6 | | t60 | | t61 | | t62 | | t63 | | t64 | | t65 | | t66 | | t67 | | t68 | | t69 | | t7 | | t70 | | t71 | | t72 | | t73 | | t74 | | t75 | | t76 | | t77 | | t78 | | t79 | | t8 | | t80 | | t81 | | t82 | | t83 | | t84 | | t85 | | t86 | | t87 | | t88 | | t89 | | t9 | | t90 | | t91 | | t92 | | t93 | | t94 | | t95 | | t96 | | t97 | | t98 | | t99 | +--------------------+ 500 rows in set (0.0309 sec)
次は、テストを作成していきます。
まずはデータセットを用意。
src/test/resources/org/littlewings/spring/dbrider/dataset.yml
t1: - id: 0000100001 - id: 0000100002 - id: 0000100003 - id: 0000100004 - id: 0000100005 t2: - id: 0000200001 t1_id: 0000100001 - id: 0000200002 t1_id: 0000100002 - id: 0000200003 t1_id: 0000100003 - id: 0000200004 t1_id: 0000100004
このデータセットは、@DataSet
のuseSequenceFiltering
の値に関わらず同じものを使います。
useSequenceFiltering
をtrue
にしたテストを用意。実行時間だけが見たいので、テストの中身は空です。
src/test/java/org/littlewings/spring/dbrider/DatabaseRiderUseSequenceFilteringTest.java
package org.littlewings.spring.dbrider; import com.github.database.rider.core.api.dataset.DataSet; import com.github.database.rider.spring.api.DBRider; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; @DBRider @SpringBootTest class DatabaseRiderUseSequenceFilteringTest { @DataSet( value = "org/littlewings/spring/dbrider/dataset.yml", useSequenceFiltering = true // default ) @Transactional @Test void test() { // dummy } }
繰り返しになりますが、useSequenceFiltering
はデフォルトでtrue
なので、明示的に指定なくても同じ状態になります。
このテストクラスを実行してみます。
$ mvn test -Dtest=DatabaseRiderUseSequenceFilteringTest
すると、以下の表示が出たところで動作が止まります。
2024-01-13T19:45:50.014+09:00 INFO 26309 --- [ main] c.g.d.r.c.dataset.DataSetExecutorImpl : DBUnit configuration for dataset executor 'default':cacheConnection: false cacheTableNames: true caseInsensitiveStrategy: UPPERCASE columnSensing: false leakHunter: false mergeDataSets: false mergingStrategy: METHOD disableSequenceFiltering: false alwaysCleanBefore: false alwaysCleanAfter: false raiseExceptionOnCleanUp: false disablePKCheckFor: com.github.database.rider.core.configuration.DBUnitConfig@e640772f schema: tableType: [TABLE] allowEmptyFields: false fetchSize: 100 qualifiedTableNames: false prologTimeout: 1000 batchSize: 100 batchedStatements: false caseSensitiveTableNames: true replacers: [com.github.database.rider.core.replacers.DateTimeReplacer@7e0986c9, com.github.database.rider.core.replacers.UnixTimestampReplacer@1377b1a0, com.github.database.rider.core.replacers.NullReplacer@48cf8414]
しばらく待つと動き始め、自分の環境では1分近くかかってテストが終了しました。
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 51.51 s -- in org.littlewings.spring.dbrider.DatabaseRiderUseSequenceFilteringTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 56.425 s [INFO] Finished at: 2024-01-13T19:46:39+09:00 [INFO] ------------------------------------------------------------------------
この止まっている時にスレッドダンプを取ると、こんな感じになっていることが確認できます。
at com.mysql.cj.jdbc.DatabaseMetaDataUsingInfoSchema.executeMetadataQuery(DatabaseMetaDataUsingInfoSchema.java:71) at com.mysql.cj.jdbc.DatabaseMetaDataUsingInfoSchema.getExportedKeys(DatabaseMetaDataUsingInfoSchema.java:404) at com.zaxxer.hikari.pool.ProxyDatabaseMetaData.getExportedKeys(ProxyDatabaseMetaData.java:177) at com.zaxxer.hikari.pool.HikariProxyDatabaseMetaData.getExportedKeys(HikariProxyDatabaseMetaData.java) at org.dbunit.database.search.AbstractMetaDataBasedSearchCallback.getNodes(AbstractMetaDataBasedSearchCallback.java:194) at org.dbunit.database.search.AbstractMetaDataBasedSearchCallback.getNodes(AbstractMetaDataBasedSearchCallback.java:149) at org.dbunit.database.search.AbstractMetaDataBasedSearchCallback.getNodesFromExportedKeys(AbstractMetaDataBasedSearchCallback.java:119) at org.dbunit.database.search.ExportedKeysSearchCallback.getEdges(ExportedKeysSearchCallback.java:54) at org.dbunit.util.search.DepthFirstSearch.reverseSearch(DepthFirstSearch.java:264) at org.dbunit.util.search.DepthFirstSearch.reverseSearch(DepthFirstSearch.java:273) at org.dbunit.util.search.DepthFirstSearch.reverseSearch(DepthFirstSearch.java:273) 〜省略〜 at org.dbunit.util.search.DepthFirstSearch.reverseSearch(DepthFirstSearch.java:273) at org.dbunit.util.search.DepthFirstSearch.reverseSearch(DepthFirstSearch.java:273) at org.dbunit.util.search.DepthFirstSearch.reverseSearch(DepthFirstSearch.java:273) at org.dbunit.util.search.DepthFirstSearch.search(DepthFirstSearch.java:148) at org.dbunit.util.search.DepthFirstSearch.search(DepthFirstSearch.java:104) at org.dbunit.database.search.TablesDependencyHelper.getDirectDependsOnTables(TablesDependencyHelper.java:232) at com.github.database.rider.core.api.dataset.RiderSequenceFilter.getDependencyInfo(RiderSequenceFilter.java:183) at com.github.database.rider.core.api.dataset.RiderSequenceFilter.sortTableNames(RiderSequenceFilter.java:68) at com.github.database.rider.core.api.dataset.RiderSequenceFilter.<init>(RiderSequenceFilter.java:34) at com.github.database.rider.core.dataset.DataSetExecutorImpl.performSequenceFiltering(DataSetExecutorImpl.java:324) at com.github.database.rider.core.dataset.DataSetExecutorImpl.createDataSet(DataSetExecutorImpl.java:129) at com.github.database.rider.core.RiderRunner.runBeforeTest(RiderRunner.java:44) at com.github.database.rider.spring.DBRiderTestExecutionListener.beforeTestMethod(DBRiderTestExecutionListener.java:24) at org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:320) at org.springframework.test.context.junit.jupiter.SpringExtension.beforeEach(SpringExtension.java:240) 〜省略〜
このあたりが、テーブル間の依存関係を調べている処理です。
at org.dbunit.database.search.TablesDependencyHelper.getDirectDependsOnTables(TablesDependencyHelper.java:232) at com.github.database.rider.core.api.dataset.RiderSequenceFilter.getDependencyInfo(RiderSequenceFilter.java:183) at com.github.database.rider.core.api.dataset.RiderSequenceFilter.sortTableNames(RiderSequenceFilter.java:68) at com.github.database.rider.core.api.dataset.RiderSequenceFilter.<init>(RiderSequenceFilter.java:34) at com.github.database.rider.core.dataset.DataSetExecutorImpl.performSequenceFiltering(DataSetExecutorImpl.java:324)
もうひとつ、useSequenceFiltering
をfalse
にしたテストを用意。
src/test/java/org/littlewings/spring/dbrider/DatabaseRiderDisableSequenceFilteringTest.java
package org.littlewings.spring.dbrider; import com.github.database.rider.core.api.dataset.DataSet; import com.github.database.rider.spring.api.DBRider; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; @DBRider @SpringBootTest class DatabaseRiderDisableSequenceFilteringTest { @DataSet( value = "org/littlewings/spring/dbrider/dataset.yml", useSequenceFiltering = false, tableOrdering = {"t1", "t2"} ) @Transactional @Test void test() { // dummy } }
こちらはuseSequenceFiltering
をfalse
にしているので、tableOrdering
も指定しています。
テストを実行してみます。
$ mvn test -Dtest=DatabaseRiderDisableSequenceFilteringTest
同じデータベースに対するテストですが、こちらはすぐに終わります。
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.641 s -- in org.littlewings.spring.dbrider.DatabaseRiderDisableSequenceFilteringTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 4.562 s [INFO] Finished at: 2024-01-13T19:48:59+09:00 [INFO] ------------------------------------------------------------------------
けっこう差が歴然と出ますね。
今回は単純なテーブル構造なので、わかりやすく差が出るところまでテーブルを作成(500個)しましたが、ちゃんとした開発で
外部キーを使うともっと複雑、複数のテーブルに外部キーを作成したりといろいろ入り組んだりすると思います。
そうすると、ここまでの数のテーブルに到達する前に、もっとわかりやすく速度劣化します。
おわりに
Database Riderで、useSequenceFiltering
をtrue
にしたまま外部キーを大量に使っているデータベースを対象にすると実行が遅くなる
ということを見てみました。
この状態になるとテストをIDE上で実行するのがだんだん嫌になってくるので、useSequenceFiltering
をfalse
にしてtableOrdering
を
指定するのが良いと思います。
一方で、tableOrdering
を指定するということは依存関係にしたがった順で書いていく必要があるので、面倒なのですが…。
というわけで、その対応案も考えてみました。
Spring Test × Database Riderで、データを作成する時にテーブル間の依存関係を記録する - CLOVER🍀