dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

[Salesforce][Apex] CreatedDate はアテにならないという話

テストをしていてハマったので、メモ。
CreatedDate がアテにならないというか、SOQL で ORDER BY に CreatedDate を使うとき は注意した方がいいです。

やってたこと

商談 (Opportunity) のクローズ日が条件を満たしたらコピーを生成するようなクラスを作成した。
クラスはバッチとして定義し、Apex スケジュール実行により定期的に実行されるようにした。

このバッチクラスのテストを次のように書いていた。

@isTest
public class OpportunityBatchTest {

    public static testMethod void run() {
        // Opportunity を1個作成、insert
        Opportunity opp = new Opportunity(Name=...);
        insert opp;

        Test.startTest();
        // バッチ実行
        Test.stopTest();

        // 作成された Opportunity を取得
        Opportunity result = [SELECT Id FROM Opportunity ORDER BY CreatedDate DESC];
        System.assertEquals( ... );
    }
}

ところが、このテストがなかなか成功しない。
もっと言うと、

テスト単体だとうまくいくんだけど、複数のテストと同時に実行すると失敗する。
また「並列 Apex テストを無効化」すると通るようになる

といった奇妙な現象が。

原因

Discussion Forum でこんな記事を見つけた。

Ordering SOQL by CreatedDate - Salesforce Developer Community

CreatedDate でソートしたはずなのに目的のレコードが取得できない、みたいな話。

ほんとかなと思ってリンク先にもあるような以下のコードを書いて試してみた。

で、結果は Forum にも書いているように、18行目のアサーションでエラー。
この時取得した result は1個目の Account のよう。

というわけで、CreatedDate を使わずにレコードを取得するように変更したら
並列実行だろうが他のテストと一緒にだろうがテストが通るようになりました。

@isTest
public class OpportunityBatchTest {

    public static testMethod void run() {
        // Opportunity を1個作成、insert
        Opportunity opp = new Opportunity(Name=...);
        insert opp;

        Test.startTest();
        // バッチ実行
        Test.stopTest();

        // 作成された Opportunity を取得 (CreatedDate は使わない)
        Opportunity result = [SELECT Id FROM Opportunity WHERE Id NOT IN (:opp.id)];
        System.assertEquals( ... );
    }
}

(おまけ) どうして CreatedDate はアテにならない?

Forum の投稿者自身が書いてたけど、

  1. CreatedDate の fieldType は dateTime 型
  2. dateTime の精度は1秒 (ミリ秒まで保証してない)

なので、今回のサンプルコードのようにほぼ同時に2つのレコードを insert した場合
CreatedDate は同じ値となり、順番はアテにならなくなるみたい。

特に 2 については リファレンス にも次のように記載があります。

Regular dateTime fields are full timestamps with a precision of one second.

ためしに、先ほどのサンプルコードで insert a の直後に

for (Integer i = 0; i < 1000000; i++) {}

とかってやるとテストが通るようになります。
このことからも、テストの実行時間に左右されるテストにならないよう CreatedDate は使わない方がいいですね。
(もちろん、今回のような用途に限ってですが)