読者です 読者をやめる 読者になる 読者になる

MyBatisのMapperをSpockで全自動テスト

MyBatisのSQLは2Way SQLではない上、動的SQLスニペットなどによって可読性が損なわれがちということもあり、構文エラーは早めに検知する仕組みを用意した方がよいという印象です。

そこで、Mapperを全スキャンして、SpockでData Driven Testingを行う方法がよいんじゃなかろうか?と考えました。
Spockを使うと、各SQLを一つのテストパターンとして実行できるので、SQLにバグがあった場合にそのMapper・メソッドが特定しやすくなるんじゃないかという狙いです。

というわけで、Springを使っていることを前提として、以下のような根拠の下で、実装してみました。

  • 通常Mapperインタフェースは特定のパッケージ以下にそろっているはずで、Springであれば ClassPathScanningCandidateComponentProvider を使うと、パッケージ配下のMapperインターフェース一覧を簡単に取得できる。
  • インタフェースが分かれば、対応するMapperの実体(コンポーネント)は ApplicationContext から getBean で取得できる。
  • 引数はとりあえずnullでない適当な値をセットしてしまえば、 NullPointerException にはならず、SQLクエリの実行自体は行えるはず。

で、作ってみたSpecificationは以下のような感じです。

MyBatis mappers automated test with Spock/Spring

ひとまず実行して例外が起きなければOK、という最低限のテストです。

手元のMapperで必要な最低限の型しか対応しておらず、また、デフォルトコンストラクタがない場合を考慮していなかったり、と不十分な点は多々ありますが、SQLを一通り実行してテストすることはできるようになりました。

(実行環境)

  • Spring Boot 1.4.2.RELEASE
  • mybatis-spring-boot-starter 1.2.0
  • spock-core 1.1-groovy-2.4-rc-3
  • spock-spring 1.1-groovy-2.4-rc-3

ApexでJSON.serializeした時のデータ型ごとの出力形式まとめ #salesforce

Apexでオブジェクト(SObject)を JSON.serialize した時に、データ型ごとの出力形式がどうなるのか、確認しました。

どこかにまとまっているのかもしれませんが…。

データ型が()付きのものは標準項目、それ以外はカスタム項目です。

データ型 出力時のフィールド名 出力形式 備考
(ID) Id string
(Name) Name string
(作成者) CreatedById string Salesforce ID
(作成日時) CreatedDate string ※1
(最終更新者) LastModifiedById string Salesforce ID
(最終更新日時) LastModifiedDate string ※1
(最終閲覧日時) LastViewedDate string ※1
(最終参照日時) LastReferencedDate string ※1
自動採番 {FieldName}__c string
数式 {FieldName}__c (戻り値のデータ型)
参照関係 {FieldName}__c string 参照先のID
主従関係 {FieldName}__c string 参照先のID
URL {FieldName}__c string
チェックボックス {FieldName}__c boolean
テキスト {FieldName}__c string
テキスト(暗号化) {FieldName}__c string マスク済み
テキストエリア {FieldName}__c string \r\n で改行
パーセント {FieldName}__c number
メール {FieldName}__c string
テキストエリア(リッチ) {FieldName}__c string raw HTML
\n で改行
ロングテキストエリア {FieldName}__c string \r\n で改行
数値 {FieldName}__c number
選択リスト {FieldName}__c string
選択リスト(複数選択) {FieldName}__c string 区切り文字は ;
地理位置情報 {FieldName}__c object {
“latitude”: {緯度},
“longitude”: {経度}
}
{FieldName}__Latitude__s number 緯度
{FieldName}__Longitude__s number 経度
通貨 {FieldName}__c number
電話 {FieldName}__c string
日付 {FieldName}__c string ※2
日付/時間 {FieldName}__c string ※1

※1 日時(日付+時間)の場合、YYYY-MM-DDThh:mm:ss.SSS+0000 のフォーマットです(例: 2017-01-28T16:17:31.000+0000

※2 日付の場合、YYYY-MM-DD のフォーマットです。

というわけで、基本的には違和感のない出力形式となっていました。留意するのは地理位置情報型、選択リスト(複数選択)、テキストエリアの改行コードくらいですかね。

ちなみに、カスタムオブジェクトの型情報は attributes 項目の type 項目として取得できます。

  "attributes": {
    "type": "Sample__c",
    "url": "/services/data/v38.0/sobjects/Sample__c/a0D2800000xxxxxXXX"
  }

GradleでMyBatis Generatorを使う

MyBatisを使ったことがなかったので、評価中です。

MyBatis Generator用のGradleプラグインがあるみたいなので、試してみました。

Gradle - Plugin: com.arenagod.gradle.MybatisGenerator

build.gradle

build.gradle は以下のような感じです。

plugins {
    id "com.arenagod.gradle.MybatisGenerator" version "1.3"
}

mybatisGenerator {
    verbose = true
    configFile = "${projectDir}/src/main/resources/autogen/generatorConfig.xml"
}

generatorConfig.xml

generatorConfig.xml に自動生成のための設定、ルールを書いていきます。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" >
<generatorConfiguration >
    <classPathEntry location="/path/to/mysql-connector-java-5.1.40.jar" />
    <context id="MySQLTables" targetRuntime="MyBatis3">
        <commentGenerator>
            <property name="suppressDate" value="true" />
        </commentGenerator>
        <jdbcConnection
                driverClass="com.mysql.jdbc.Driver"
                connectionURL="jdbc:mysql://localhost:3306/sampledb"
                userId="root"
                password="" />
        <javaModelGenerator
                targetPackage="sample.domain.model"
                targetProject="src/main/java">
            <property name="enableSubPackages" value="true" />
        </javaModelGenerator>
        <sqlMapGenerator
                targetPackage="sample.domain.mapper"
                targetProject="src/main/resources">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>
        <javaClientGenerator
                targetPackage="sample.domain.mapper"
                targetProject="src/main/java" type="MIXEDMAPPER">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>
        <table tableName="%"
               enableInsert="true"
               enableSelectByPrimaryKey="true"
               enableSelectByExample="false"
               enableUpdateByPrimaryKey="true"
               enableUpdateByExample="false"
               enableDeleteByPrimaryKey="true"
               enableDeleteByExample="false"
               enableCountByExample="false"
               selectByExampleQueryId="false"
               modelType="flat">
        </table>
    </context>
</generatorConfiguration>

設定のリファレンスは公式を参照してください。

MyBatis Generator – MyBatis Generator XML Configuration File Reference

なるべく余計なものは生成されないようにする、という方針のもとで、以下を設定しています。

  • javaClientGeneratorのtypeはMIXEDMAPPER

マッピングファイル(XML)とアノテーションによる実装が混在した状態で生成されます。
混在というと複雑なように思われますが、ざっくり言うと、1件のINSERT, 主キーを指定したUPDATE, DELETEに関しては、アノテーションでの定義、それ以外がXMLになりました。

  • tableのxxxByExampleはfalse

⇒ 柔軟な条件によるSELECT, UPDATE, DELETEを行なうためのマッピングを自動生成するオプションですが、いったん無効化しました。
理由として、この類のものは、アプリケーション側の要件に応じて、必要な時に作っていった方がよいように思ったからです。また、MyBatisを使った場合の(JPAと比較しての)メリットとして、SQLを直接扱えることによるわかりやすさがあると思うのですが、 XML中に <if>, <choose>, <set> などの非SQLな構文が混ざって、可読性を損ねている(メリットを潰している)気がします。

ただ、 selectByExample="false" を指定しても、Example系のコードが生成されてしまいました…。

  • tableのmodelTypeはflat

⇒ hierarchicalを指定した場合、主キーは別のクラスとして生成されますが、flatを指定すると、テーブルとクラスが1:1に対応します。

実行

$ ./gradlew mbGenerator

(タスク名が分かりにくいなと思いました…)

疑問

  • 生成されるモデルをLombokで装飾したい
    • MyBatis Generatorはプラグインによって処理を挟めるみたいなので、それでできそう。
  • 生成されるMapperをGroovyコードで出力したい(ヒアドキュメントが使えるため、アノテーションSQLを記述しても、読みやすい)
  • スキーマ変更に追随する場合のベストプラクティス
    • 変更の度に毎回自動生成して上書き、でもよさそうだけど、自動生成後に変更を加えた部分が潰されてしまうリスクがある。

Spring Bootでキャッシュ(Redis)からのデータ取得エラー時のカスタマイズ

(この前書いた以下の記事に近い話ですが、今回はSpring Sessionに限らない話です)

Spring Bootでキャッシュ機構としてRedis( org.springframework.boot:spring-boot-starter-data-redis )を使っている場合、デフォルトでは Redis への書き込み、Redis からの読み込みは、JdkSerializationRedisSerializer によるJavaのオブジェクトシリアライズ・デシリアライズが行われ、キャッシュの取得時にデシリアライズに失敗すると、「 @Cacheable をつけたメソッドの呼び出しそのものがエラー 」になってしまいます。

この挙動を、デシリアライズに失敗したら、 「 リザルトキャッシュを利用せずに対象メソッドを実行し、その結果をキャッシュに書き込む 」といったものに変更したいと思います。キャッシュにhitしなかった時と同じように扱うということですね。

結論として、Springの場合、 CacheErrorHandler というインタフェースが用意されているので、カスタマイズしたエラーハンドラを使うようにしてあげればよいです。

やり方は、例えば以下の記事にて紹介されています。

qiita.com

ポイントだけをまとめたconfigurationは次のような感じです。

@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {

    @Override
    public CacheErrorHandler errorHandler() {

        return new SimpleCacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                // キャッシュ取得時のエラーハンドラの内容を書く
            }
        };
    }
}

CacheErrorHandlerは get, put, evict, clear のそれぞれにおけるエラー時のハンドラメソッドを実装しなければならないインタフェースですが、あらかじめSimpleCacheErrorHandlerという何もしない(デフォルトと同じ挙動をさせる)実装が用意されているので、必要なメソッドだけoverrideすればよいです。

今回は次のように実装しました。

@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
    Throwable cause = exception.getCause();
    if (cause != null && cause instanceof SerializationFailedException) {
        return;
    }
    throw exception;
}

ハンドラメソッドの中で例外をスローせずに return すれば、直接対象メソッドが実行されます。
また、結果はキャッシュに書き込まれ、その際、デシリアライズに失敗する古いキャッシュは上書きされる形になります(Spring Boot 1.4.2.RELEASEで検証)。そのため、ハンドラの中で cache#evict を呼んだりしなくてもよいようです。

小ネタ) SpockのData Driven Testingでテスト観点をUnrollのメソッド名に記述するとよいかもしれないと思った話

Spockのデータ駆動テスト(Data Driven Testing)では、メソッド名としてデータ変数を含んだ文字列を使うことで、テスト結果を見た時にどこで失敗したか、わかりやすくすることができます。

データ駆動テスト — Spock 1.0-SNAPSHOT

さて、この時に利用するデータ変数は、テストで利用するパラメータと期待値を使うことが基本だと思います(実際ほとんどのケースではそれで十分だと思いますが)。

しかし、イテレーション(テストのパターン数)が増えてきたり、複雑なロジックのテストの場合、パラメータと期待値だけでは、そのパターンのテスト理由・意図が一見してわからないこともあります。その時、テスト観点も含めると便利だと思ったので、紹介します。

デフォルト

そもそもデータ変数をメソッド名として使わない場合、例えば、こんな感じのテストがあった時、

    @Unroll
    def "maximum of two numbers"() {
        expect:
        max(a, b) == ans

        where:
        a  | b   || ans
        3  | 4   || 4
        -3 | -4  || -3
        10 | 10  || 10
        10 | -20 || 10
        0  | 5   || 5
        -5 | 0   || 0
    }

個々のメソッド名はイテレーションのインデックスだけで区別されます。

f:id:jappy:20170114073900p:plain

パラメータと期待値

メソッド名を次のように変えると、

    def "maximum of two numbers (#a, #b) -> #ans"() {

どのケースで失敗したかが一目瞭然になります。

f:id:jappy:20170114074230p:plain

(このキャプチャではすべてpassしていますが)

テスト観点を加える

今度は、以下のようにしました。

    @Unroll
    def "maximum of two numbers (#a, #b) -> #ans (#description)"() {
        expect:
        max(a, b) == ans

        where:
        description << [
                'positive & positive',
                'negative & negative',
                'same',
                'positive & negative',
                'zero & positive',
                'zero & negative'
        ]

        a  | b   || ans
        3  | 4   || 4
        -3 | -4  || -3
        10 | 10  || 10
        10 | -20 || 10
        0  | 5   || 5
        -5 | 0   || 0
    }

ポイントは2つで、

  • データ変数として、テスト内で直接使うわけではないが、テスト観点・意図を表す description を追加する
  • テスト観点は長くなりがちなので、データテーブルの可読性を下げないように、データテーブルの列ではなく << オペレータで列挙する

実行結果は想像がつくと思いますが、以下のようになります。

f:id:jappy:20170114075047p:plain

おわりに

上の例だと単純過ぎてあまりメリットが見えにくいですが、実際に業務でテストコードをレビューしていてこんなことがありました。

  • テスト対象のロジックが複雑(パラメータの組み合わせ)なため、データテーブル中にコメントを書いて、テスト目的を分かるようにしていた。
    (コメントに書くくらいなら、テストメソッド名になっていた方が、CIでのレポート結果などでもすぐに分かるので、有用だと思います)
  • パラメータや期待値が複数の項目から成るリストで、データ変数としてそのまま展開するとものすごい長いメソッド名になってしまい、テスト結果の一覧性を悪くしていた。

なので、その辺りをちょっとだけ改善するために、テストの意図を表す文字列をメソッド名に含めるとよいのでは?と思ったのでした。

マルチデータソースにおけるGradleでのFlywayタスク実行

マルチデータソース(2つのRDB)を使っている場合に、FlywayのGradle PluginでflywayMigrateなどのタスクを実行し、両方のDBにマイグレーションを実行したい場合。

flyway {
    user = 'user1'
    password = 'pass1'
    url = 'jdbc:xxx://host1/db1'
}

def flyway2 = new org.flywaydb.core.Flyway()
flyway2.setDataSource('jdbc:xxx://host2/db2', 'user2', 'pass2')
flyway2.setLocations('filesystem:./src/main/resources/db_migration2')

tasks['flywayMigrate'].doLast {
    flyway2.migrate()
}

// 他タスク(flywayClean, flywayInfo, ...)も同様

プラグインの機能としては提供されていないので、Gradle(Groovy)側のカスタマイズで対応してみました。

Spring Sessionのバージョンアップとデシリアライズエラー

Spring Security + Spring Sessionを使っている場合、通常、セッションの中身はシリアライズされてセッションストアに保存されます(実際に確認したのはRedisのみ)。

シリアライズされる org.springframework.security.core.context.SecurityContextImpl には serialVersionUID が定義されているのですが、その値は SpringSecurityCoreVersion の中にあります。
https://github.com/spring-projects/spring-security/blob/4.1.x/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java#L35-L41

コードの通り、Spring Securityのバージョンごとに決まった値になります。

したがって、Spring Securityをバージョンアップした場合、それまで使っていたセッションがリクエストされると、デシリアライズに失敗します。
issueがGitHubやstarckoverflowに挙がってます。

シリアライズに失敗した場合、サーバ側エラーとなって500エラーを返してしまうので、本来はセッションの有効期限切れと見なして403エラー辺りを返したいところです。

対象方法についてのメモです。

対処1: いったんセッションの中身をクリアする

乱暴ではありますが、ソースコメントにも「Classes are not intended to be serializable between different versions.」とある通り、シンプルに、バージョンアップの際にセッションストア側をクリアしてしまうのが確実だと思います。

Redisの場合は、キーが spring:session:* にマッチするものを削除すればOKです。

対処2: 独自の SessionRepository を作ってデフォルトの挙動をカスタマイズする

以下に紹介されていました。

sdqali.in

シリアライズエラーをcatchして、該当のセッションをRedisから削除しています。
また、getSessionnull を返すことで、有効なセッションが見つからなかった時と同じ振る舞いにさせているっぽいです。

方法1と比較すると、追加のコードが必要ではあるものの、バージョンアップの度にRedisの中身をメンテする必要はなくなるので、保守性では優れている気がします。

対処3: セッションの中身をマイグレートする

方法1と方法2も、バージョンアップ前のセッションを破棄するアプローチでした。
Spring Sessionのバージョンアップの頻度はそう高くはないとは思いますが、依存ライブラリのバージョンアップというサーバ側の都合で、セッションが破棄されてしまうのが許容できないケースもあるかもしれません。

そういうったケースの対応方法を考えてみました。一つのプログラム内で新旧の SecurityContextImpl が共存できないので、2つのステップ(プログラム)に分けています。

  1. 既存のセッションの中身をエクスポートする。エクスポートする際は旧バージョンのSpring Securityを利用してデシリアライズし、 Javaシリアライズに依存しない形 (例えばJSONなど)で出力する。
  2. エクスポートした内容を読み取り、新バージョンのSpring Securityを利用してセッションストアに戻す。

うーん、無理やりな感じが否めないので、オススメはできそうにないですね。