🏍️

Spring Boot + Database RiderでDB権限問題の解決方法

2024/04/18に公開

こんにちは。Nstockエンジニアのryan5500です。
今回は、DBユーザーの権限を絞っているときのDatabase Riderの使い方について書きます。

背景

現在、私達はSpring Bootを利用してWebサービスを開発しています。このサービスの統合テストのためにテストごとにDBに任意のテストデータが入った状態にしたいと考え、Database Riderというライブラリを利用しています。

Database Riderとは

https://github.com/database-rider/database-rider

Database Riderは、YAMLやCSV形式など、様々な種類のデータ形式でテストデータを読み込み、テスト実行前にDBに流し込んでくれるライブラリです。テストメソッドに@DataSetアノテーションを設定する形で利用します。

CSV形式のテストデータの場合

私達は、CSV形式のテストデータで利用しています。CSV形式で利用する場合は、2種類のファイルが必要です。

  1. テーブル名と対応するファイル名の、レコード内容を示したCSVファイル群
    • e.g. user.csv, product.csvなど
  2. テストデータのテーブルへの追加順を決める table-ordering.txt
    • こちらは必須ではありません

table-ordering.txtは、以下のようにテーブルの名前だけが順に書かれており、この順にレコード追加が実施されます。テーブルに外部キー制約を設定していて、特定の順序でデータを流し込みたい際などに便利です。

table-ordering.txt
user
product
product_metadata

これらを一つのフォルダにまとめて用意する形になります。フォルダ構成としては以下のようになります。

テストデータの配置イメージ
└── testdata-1
    ├── user.csv
    ├── product.csv
    ├── product_metadata.csv
    └── table-ordering.txt

これらのファイルを1フォルダにまとめ、以下のようにテストコードで@DataSetアノテーションを用いて指定することで、テスト実行前にDBにテストデータをセットしてくれます。

// 以下はpresentation層を統合テストする場合のイメージです。
@SpringBootTest
@DBRider // Database Riderを使うためのアノテーション
public DummyApiTest {
    ...

    @Test
    @DataSet(value = "testdata-1/user.csv") // src/test/resources/datasets以下のパスを指定
    // まるでuserのデータしか指定していないようだが、
    // 末尾の拡張子を見てcsvファイル形式でテストデータを流し込みたいという意図を理解し、
    // 同じ階層のフォルダ内にあるtable-ordering.txtを見て、
    // テーブル名に対応したcsvファイル群の内容を流し込んでくれる。
    void test_ユーザー一覧が表示できること() {
    }

    ...
}

課題:Spring Bootアプリケーションで利用するDBユーザーの権限を制限していると、Database Riderのテストデータ流し込みに失敗する

Database Riderはデフォルトの挙動ではSpring Bootアプリケーションが利用するdataSourceを利用してテストデータを流し込みます。その際に既存のデータを削除するためにDELETE文, その後テストデータを流し込むためにINSERT文のクエリを発行します。

原因:テストデータ流し込み前の、データ削除時に権限エラーとなる

私達は、DBユーザーの権限ポリシーとして必要最低限の権限を持つようにしています。そのポリシーに従って、Spring Bootアプリケーションが利用するDBユーザーの権限を、一部テーブルに対してDELETE文を発行できないように絞っていました。

そのため、Database Riderのテストデータ流し込み時に例外が出て、テストが正しく動かない状態となりました。

解決策:専用のDatabase Rider dataSourceの設定と利用

この対策として、Database Riderでテストデータをテスト用のDBに流し込む際には、より権限の強いDBユーザーをDatabase Riderが利用できる必要がありました。
そのため、Database Riderのみが利用する、より強い権限を持つDBユーザーを利用してDBアクセスを行う、dataSourceを用意しました。

dataSourceを用意するコード例

具体的には、以下のようにして、Database Rider用のdataSourceを設定しました。以後、サンプルコードのパッケージ名はcom.example とします。

test/java/com/example/config/DBRiderDataSourceConfig.java
// Database Rider用のDataSourceのBean定義
@Component
@TestConfiguration // テスト時にのみ読み込まれるコンポーネントであることを指定
public class DBRiderDataSourceConfig {
    @Bean(name = "dbRiderDataSource")
    @ConfigurationProperties("spring.db-rider-datasource")
    // application.propertiesにある特定のnamespaceの値から設定値を取得する。
    //
    // このdataSourceはテスト時にしか初期化されないため、
    // 利用するDB Roleのクレデンシャルは、
    // テスト時にのみ利用される設定ファイルに設定する。
    public DataSource dataSource() {
          return DataSourceBuilder
            .create()
            .type(TenantAwareHikariDataSource.class)
            .build();
    }
}

Database Rider用のdataSourceが利用するDB Roleのクレデンシャルは、テスト時のみに利用される設定ファイルである、test/resources/application.properties に設定しました。

test/resources/application.properties
// 通常のdataSourceだとspring.datasourceだが、違うものだとわかるよう、
// spring.db-rider-datasourceネームスペースに定義
spring.db-rider-datasource.driver-class-name=org.postgresql.Driver
spring.db-rider-datasource.url=jdbc:postgresql://localhost:5432/app
spring.db-rider-datasource.username=postgres # テーブルの内容を削除する強い権限を持つDB Roleを指定する。
spring.db-rider-datasource.password=complicated_password_replace_me
test/java/com/example/presentation/DummyApiTest.java
// 統合テストコードの中で、上記のdataSourceを指定する様子
@SpringBootTest
@DBRider(name = "dbRiderDataSource")
// DBRiderアノテーションのnameで、bean名を指定することで、
// Database Riderが利用するdataSourceを指定できる
public DummyApiTest {
    ...
}

実装詳細: @Primaryアノテーションを用いたdataSourceの優先設定

また、Database Rider用のdataSourceを追加した際に、統合テスト実行時に、Spring Bootアプリケーションが参照するdataSourceが見つからない、というエラーが出ました。

dataSourceが見つからないエラー例
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name '(クラス名)' defined in file [/workspace/target/classes/com/example/usecase/(クラス名).class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'com.example.domain.SomeRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

これは、DI時に、同じ型のデータが存在するので、どちらをDIすべきかわからないために起こっていました。

そこで、通常のdataSource設定の側に、@Primaryというアノテーションを追加し、DI時の優先度を設定する形にしました。

src/main/java/com/example/config/DataSourceConfig.java
@Component
@Configuration
public class DataSourceAdminConfig {
    @Bean
    @Primary // これを追加
    @ConfigurationProperties("spring.datasource")
    public DataSource dataSource() {
          return DataSourceBuilder
            .create()
            .type(TenantAwareHikariDataSource.class)
            .build();
    }
}

注意点: 強い権限のDBユーザーについてはテスト時にのみ利用する等管理が必要

今回は、Spring Bootアプリケーションで利用するDBユーザーよりも権限の強いDBユーザーを用意しました。
このDBユーザーは、権限が強いので、テスト時にのみ利用するように管理する必要があります。

まとめ

今回は、DBユーザーの権限を絞っているときのDatabase Riderの使い方について書きました。

Nstockではエンジニアを募集しています!
👨‍👩‍👧‍👦カジュアル面談から気軽にお話しましょう🤞

https://speakerdeck.com/nstock/we-are-hiring
https://nstock.co.jp/recruit

Nstock Tech Blog

Discussion