Salesforceの開発を行っていると、トリガを作る機会が多くあります。
作るたびに調べたりするのですが、oldやnewの使い方や使いどころなどを一旦きちんとまとめておいた方が自分でも迷わないと思い、まとめておくことにしました。
実行のタイミング
まずは、基本的な実行のタイミングから。
beforeトリガ
レコードがDBに格納される前のタイミングで起動されます。DBに保存する前に何か前処理を行いたい場合に使うことが多いです。例えば、入力チェックなどが多いですね。
afterトリガ
レコードがDBに保存された後に起動されます。ObjectIdや自動採番などが振られた状態になりますので、それらの値が決定してからの処理を行うのに適しています。ただ、まだコミットされていないとはいえ、DBに格納されているのと同じ状態なので、その値を変更するには、DML文を使う必要があります。
実際に実行のタイミングをデバッグして確かめてみる
実際に、以下のようなApex文を用いて、どのような値が入っているのか、どういう操作をしたらどのようなエラーが出るのかを確かめてみました。
trigger testAccountTrigger on Account (before update, after update, before insert, after insert) { if (Trigger.isBefore) { System.debug('********トリガーの値isBefore***********'); System.debug('***SFDC: Trigger.old は: ' + Trigger.old); System.debug('***SFDC: Trigger.oldMap は: ' + Trigger.oldMap); System.debug('***SFDC: Trigger.new は: ' + Trigger.new); System.debug('***SFDC: Trigger.newMap は: ' + Trigger.newMap); } if (Trigger.isAfter) { System.debug('********トリガーの値isAfter***********'); System.debug('***SFDC: Trigger.old は: ' + Trigger.old); System.debug('***SFDC: Trigger.oldMap は: ' + Trigger.oldMap); System.debug('***SFDC: Trigger.new は: ' + Trigger.new); System.debug('***SFDC: Trigger.newMap は: ' + Trigger.newMap); } }
上記のようなコードでデバッグしてみると、oldとnewには以下のような値が入ります。
基本的に、トリガーの中で用いられるoldとnewは、変更前の値と変更後の値が入るのですが、isBeforeなのか、isAfterなのかのタイミングによって、変更できるか、変更できるとしてもその方法が異なります。
insert系
では、Insertの場合は、oldとnewには何が入っているでしょうか。beforeとafterで確認してみると、以下のようになります。なお、before insertの場合のみ、Trigger.newとTrigger.newMapで結果が異なりました。
ちなみに、Trigger.newはList配列で、順番に処理するのに適していますが、Trigger.newMapはMap配列型で、順番は保証されません。
before insertの場合
Trigger.old・・・null
Trigger.oldMap・・・null
Trigger.new・・・新規登録する直前のオブジェクト(ただしDB格納後に決定する、Id=null,自動採番系=null,CreatedById等のシステム系=null)
Trigger.newMap・・・null
Apex上で、oldの値を変更すると、
execution of BeforeInsert caused by: System.NullPointerException: Attempt to de-reference a null object: ()
が発生します。
newは変更可能です。
after insertの場合
Trigger.old(oldMap)・・・null
Trigger.new(newMap)・・・新規にDBに登録されたオブジェクト(ただしDB格納後に決定する、Id=あり,自動採番系=あり,CreatedById等のシステム系=あり)
Apex上で、newの値を変更すると
execution of AfterInsert caused by: System.FinalException: Record is read-only: ()
が発生します。
oldを変更すると、
execution of AfterInsert caused by: System.NullPointerException: Attempt to de-reference a null object: ()
となります。
なので、after insertではDML文で変更必要があります(変更可)。ただし、条件を if (Trigger.isInsert && Trigger.isAfter)のように変更しないと、
execution of AfterInsert caused by: System.DmlException: Update failed. First exception on row 0 with id 00128000018cHWpAAM; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, testAccountTrigger: maximum trigger depth exceeded Account trigger event AfterInsert for [xxxxx] Account trigger event AfterUpdate for [xxxx] Account trigger event AfterUpdate for [xxxxxxx] Account trigger event AfterUpdate
のようなエラーになります。
これは、Insertの後、Updateが行われ、その後、さらにUpdateのトリガが走ってしまうので、無限ループになってしまうためです。
update系
before updateの場合
Trigger.old(oldMap)・・・新規登録する直前の、変更前のオブジェクト(DBに格納されている)
Trigger.new(newMap)・・・新規登録する直前の、変更予定のオブジェクト
oldを変更すると、
execution of BeforeUpdate caused by: System.FinalException: Record is read-only:
のエラーになり、変更できません。
newは変更可能で、その後、その値でDBにupdateされることになります。
after updateの場合
Trigger.old(oldMap)・・・変更前のオブジェクト(すでに変更されている)
Trigger.new(newMap)・・・DBに登録された、変更後のオブジェクト(コミット前)
oldを変更すると、
execution of AfterUpdate caused by: System.FinalException: Record is read-only
になります。newを変更すると、
execution of AfterUpdate caused by: System.FinalException: Record is read-only
となります。ただし、DMLでは値の変更が可能です。でも、Updateのトリガなので、うまく考えないと、無限ループになってしまいます。
「うまく」というのは、条件にフラグ項目を入れ、そこが変わればトリガを発動させないなどの工夫をする必要があるということです。
エラーの回避例
ListlistAcc=new List (); for (Account a : Trigger.new) { Account updacc=[select id,Name,Fax from account where id = :a.id]; if(a.<フラグ項目>!='22442222'){ updacc.<フラグ項目>='22442222'; listAcc.add(updacc); } } update listAcc; }
のように、フラグ項目を持たせ、一旦変更されたら、変更されたことを検知して、それ以降、Updateは行わないようにするというようなことです。