MIXED_DML_OPERATIONエラー対策

今回はMIXED_DML_OPERATIONエラーについて。
Salesforce界では結構有名なエラーなので、知っている人も多いかもしれません。

スポンサーリンク

MIXED_DML_OPERATIONエラーとは?

ユーザ権限を絡めたApexコードでテストコードを組んでいた時、MIXED_DML_OPERATIONエラーと言う文字を見かけたことはないでしょうか?

例えば、以下Apexコードを実行しようとすると、次のようなエラーが発生します。

// 取引先のInsert
Account a = new Account();
a.Name = 'Hoge'+ String.valueOf(Datetime.now());
insert a;

// ロールのInsert
UserRole ur = new UserRole();
ur.Name = 'Hoge';
ur.DeveloperName = 'Hoge';
insert ur;
System.DmlException: Insert failed. First exception on row 0; first error: MIXED_DML_OPERATION, 非設定オブジェクトを更新した後の設定オブジェクト上の DML 操作 (またはその逆) は、許可されていません: UserRole、元のオブジェクト: Account: []

エラーの原因について

このエラーは、1回のトランザクションで、設定オブジェクトと非設定オブジェクトを同時にDML操作(InsertやUpdateなど)を行うと発生するようです。
なぜ、同時のDML操作がダメなのかは分かりませんが、ルールはルールなので従うしかありません。

ところで、設定オブジェクトと非設定オブジェクトって何でしょう!?
ということで調べてみました。
以下、Apex Developer Guideより抜粋。
(一部注意書きやAPIバージョンが明らかに低いものの制約については割愛しています)

  • FieldPermissions
  • Group
  • GroupMember
  • ObjectPermissions
  • PermissionSet
  • PermissionSetAssignment
  • QueueSObject
  • ObjectTerritory2AssignmentRule
  • ObjectTerritory2AssignmentRuleItem
  • RuleTerritory2Association
  • SetupEntityAccess
  • Territory2
  • Territory2Model
  • UserTerritory2Association
  • User
  • UserRole
  • UserTerritory
  • Territory

見慣れないオブジェクトも多いんですが、大抵はユーザ周りのオブジェクトが設定オブジェクトであるという認識でいいかと思います。

よく使いそうなのが、UserとGroupMemberあたりでしょうか?
他はApex上というより、設定メニューで更新をかける方が多そうです。

逆にここに記載されていないオブジェクトは非設定オブジェクトと考えてもらってOKです。

対処方法は

トランザクションを分ける

これが根本的な解決方法。
設定オブジェクトと非設定オブジェクトの同トランザクション内でのDML操作によるエラーなのだから、単純にトランザクションを分ければ解消です。

トランザクションを分けると、データの整合性が保てるかと言う別問題が出ますけどね。

一方を非同期で更新

これもトランザクションを分ける方法に近いんですが、一方を@futureアノテーションを使って非同期で更新などをかければ別トランザクション扱いになって解消します。
これはテストクラス以外で有効な対処方法になります。

System.runAsメソッドを使用して実行者を変更する

この方法はテストクラスで威力を発揮します。
System.runAsメソッドを使用して、ブロックで囲むと、その部分の設定オブジェクトと非設定オブジェクトの混合DML操作が許可されます

これは、同一ユーザで設定オブジェクトと非設定オブジェクトを同一トランザクションでDML操作しても可能で、設定オブジェクトはユーザA、非設定オブジェクトはSystem.runAsメソッドを使用してユーザBが実行するといった分け方をしなくてもOKです。

System.runAsメソッド特性についてもいくつか。

既存ユーザだけでなく、テスト用に新規作成したユーザに対しても使用可能です。
そして、その場合は作成したユーザレコードに対しInsertをわざわざかけなくても大丈夫です。
当然、ユーザとして必要な情報(ユーザ名やプロファイルなど)は設定済みである必要はあります。
Insertを必要としないので、ライセンス数にもカウントされません

以上を踏まえて、上記コードをSystem.runAsメソッドを使用して、エラーが発生しないようにします。

// 設定するプロファイルの取得
Profile p = [SELECT Id FROM Profile WHERE Name='Standard User'];

// 一意となるようなユーザ名にする
String uniqueUserName = 'standarduser' + DateTime.now().getTime() + '@testorg.com';

// テスト用ユーザの作成
User u = new User(Alias = 'standt', Email='standarduser@testorg.com',
EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
LocaleSidKey='en_US', ProfileId = p.Id,
TimeZoneSidKey='America/Los_Angeles',
UserName=uniqueUserName);
         
System.runAs(u) {
    // 取引先のInsert
    Account a = new Account();
    a.Name = 'Hoge'+ String.valueOf(Datetime.now());
    insert a;

    // ロールのInsert
    UserRole ur = new UserRole();
    ur.Name = 'Hoge';
    ur.DeveloperName = 'Hoge';
    insert ur;
}

まとめ

  • 設定オブジェクトと非設定オブジェクトの同一トランザクションDML操作でMIXED_DML_OPERATIONエラーが発生
  • 非同期またはSystem.runAsメソッドにより回避可能

コメント