AWS SDK for Java (DynamoDBMapper)を使った条件付き書き込み

DynamoDBMapperの概要については以下に書かれています。

docs.aws.amazon.com

DynamoDBMapper はタイプセーフに扱えて便利な反面、素のAWS API(オペレーション)が色々とラップされてしまっているので、以下によく使う利用法を逆引きリファレンスとして整理しました。

(注)確認は AWS SDK for Java のバージョン 1.10.62 で行っています。古いバージョンなので、最新のバージョンでは API が変更されている可能性もあるのでご注意ください。

すべての属性を置き換える

項目を更新する際に、既存の項目を丸ごと置き換える(不要な属性は削除)か、特定の属性だけ書き換えたいかを使い分けたいケースがあります。
通常、前者はputItem、後者はupdateItemといった形でオペレーションを使い分けます。

引数1つだけの save メソッドは putItem 相当の動作になります。

mapper.save(item);

指定した属性だけを更新する(他の属性はそのまま)

保存時の振る舞いについては DynamoDBSaveExpression を使って変更が可能です。

DynamoDBMapperConfig config = new DynamoDBMapperConfig(DynamoDBMapperConfig.SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES);
mapper.save(item, config);

item中で非NULLの属性だけが更新されます。

ちなみに、 SaveBehavior には以下の4種類のパターンがあります。詳細はJavaDocを参照してください。

  • UPDATE(デフォルト)
  • UPDATE_SKIP_NULL_ATTRIBUTES
  • CLOBBER
  • APPEND_SET

項目が存在しない場合はエラーにする

putItem, updateItemのどちらも、基本的にはupsert相当の動作になります(キーがあれば更新、なければ追加)。

中途半端な属性だけをもつ項目が追加されてしまっても困るので、項目が存在する場合に限り、更新処理を行いたい場合があります。

その場合、条件式 ( DynamoDBSaveExpression )を利用します。

AttributeValue hashValue = new AttributeValue().withS("Key1");
AttributeValue rangeValue = new AttributeValue().withN(Long.toString(123L));

DynamoDBSaveExpression expression= new DynamoDBSaveExpression()
        .withConditionalOperator(ConditionalOperator.AND)
        .withExpectedEntry("partitionKeyName", new ExpectedAttributeValue()
                .withValue(hashValue)
                .withComparisonOperator(ComparisonOperator.EQ))
        .withExpectedEntry("sortKeyName", new ExpectedAttributeValue()
                .withValue(rangeValue)
                .withComparisonOperator(ComparisonOperator.EQ));

mapper.save(item, expression);

// DynamoDBMapperConfigと組み合わせる場合は以下。
// mapper.save(item, expression, config);

プライマリキー “Key1”, パーティションキー “123” の項目が存在しない場合、 ConditionalCheckFailedException がスローされます。

項目が存在する場合はエラーにする

上の例と逆に、項目が存在しない場合に限り登録を行いたい場合の例です。

DynamoDBSaveExpression expression = new DynamoDBSaveExpression()
        .withExpectedEntry("partitionKeyName", new ExpectedAttributeValue().withExists(false))
        .withConditionalOperator(ConditionalOperator.AND)
        .withExpectedEntry("sortKeyName", new ExpectedAttributeValue().withExists(false));

mapper.save(item, expression);

こちらも違反時は ConditionalCheckFailedException がスローされます。

その他の条件式

上で書いた以外にも、色々な条件式が利用できます。

docs.aws.amazon.com

DynamoDBMapperでも、上記の条件式がすべて利用できるはずですので、必要に応じて DynamoDBSaveExpression を作成していくことになると思います。

(続)活動を意図的にアーカイブしてテストしたい #salesforce

この前(というか日付的には昨日)、こんな記事を書きました。

jappy.hatenablog.com

「期日が1年以上前の活動を作ったら、そのうちアーカイブされると思ったが、1日待ってもアーカイブされなかった」というのが要旨ですが、改めて確認するとアーカイブされていました

というわけで、改めてわかったことをまとめると、

活動がアーカイブされるには、1日以上かかる。

今回のレコードは、期日が「2016/04/20 15:00」、作成日時が「2017/06/15 10:04」でしたが、アーカイブされたのは 6/17 12:00~6/18 22:20のどこか、なようです。

アーカイブ時刻ってArchivedDateみたいな項目でわかるんでしたっけ…?)

活動がアーカイブされるのに、レコード数は(おそらく)関係ない

今回、DE環境で確認したので、ディスク容量は5.0MBです。

確認時点で使用量は約79%(4.0MB)、そのうち、活動は441件(=882KB)でした。

そんなにキリがよい数字でもないので、レコード数や空き容量はおそらく関係ないのでしょう。

FROM Eventだけだとアーカイブ済みの活動はクエリされない

開発者コンソール上のQuery Editorで FROM Event として活動をクエリしていましたが、アーカイブ済みの活動はクエリ結果として返らないようです。

SOQLで提供されている ALL ROWS というキーワードを使うと、アーカイブ(や削除)済みのレコードもクエリできますが、これはQuery Editor上では使えないようです(なぜでしょうね…?)

Apexからは利用できるみたいなので、Anonymous Windowで実行しました。

List<Event> events = [
  SELECT Id, ActivityDate, IsArchived, Subject FROM Event ORDER BY ActivityDate ASC ALL ROWS    
];

System.debug(events[0]);

活動を意図的にアーカイブしてテストしたい #salesforce

Salesforceにおける活動のアーカイブとしては、

  • 期日が365日以上前の活動はアーカイブされる
  • この365日という期限に関しては、サポートデスクに依頼することで(必要であれば)延長できる

ことまでは書かれています。

さて、1件も活動が登録されていないクリーンなSalesforce環境で、テストのためにアーカイブ済みの活動を作りたい、というのは、稀によくあるケースだと思います。
そこで、調査してみました。

が、結論から言うと、残念ながら作れないようです。もし方法知っている方がいたら教えてください…。

やったこと

期日が1年以上前の活動を手動で登録してみました。

予想

活動のアーカイブ処理は何時かは分からないが、毎日どこかのタイミングで行われているはず。 なので、24時間以上待てば、アーカイブされているだろう。

結果

優に24時間は待ってみましたが、特にアーカイブされた痕跡はありません。
ただ、もしかするとアーカイブ処理は実は日次ではなく、週次とか月次とか不定期とか、そんなタイミングで実施されているのかもしれないので、そのうちしれっとアーカイブされているかもしれません。
もしくは、一定以上の数のレコードがないとアーカイブ対象にならないとか。

ちなみに、アーカイブされたかどうかの確認は、開発者コンソール上のQuery Editorで、以下のクエリを実行して行っています。

SELECT Id, ActivityDate, IsArchived, Subject FROM Event ORDER BY ActivityDate ASC

また、API、Apex、設定などで、明示的にアーカイブする手段があるだろうと思って調べてみたのですが、見つかりませんでした。

salesforce.stackexchange.com

このページの回答を見るに、やっぱりできなそうな感じ…。

追記

続きを書きました。

jappy.hatenablog.com

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 を呼んだりしなくてもよいようです。