Apex

取引先作成時に履歴へ同名項目をコピーするApexトリガの書き方

オブジェクト構成を用意し、Database.insertとSaveResultで安全に履歴レコードを作るトリガ処理を説明します。

2023.03.01

なぜトリガでコピーするのか

取引先の作成時と更新時に、その時点の値をカスタムオブジェクトへ複製して残したいことがあります。標準の項目履歴機能は文字数やオブジェクトの制約があり、任意の項目をまとめて複製する用途には向きません。Apexトリガでコピーすれば、取引先のカスタム項目が増えても取得項目を書き足すだけで済みます。

オブジェクトの構成

まず入れ物を用意します。

  1. 取引先履歴オブジェクトを作る

    設定のオブジェクトマネージャで、カスタムオブジェクト AccountHistory__c を作成します。

  2. 取引先への参照項目を作る

    AccountHistory__c に、取引先を指す参照関係項目 Account__c を作成します。

  3. 複製したい項目を同名で作る

    取引先側のカスタム項目のうち、履歴として残したいものを、AccountHistory__c 側に同じAPI参照名で作成します。

取引先と取引先履歴の2つのオブジェクトの関係図。履歴から取引先へ矢印が伸びている
用意するオブジェクトの構成です。履歴側から取引先へ参照関係を張る向きになっている点を見てください。

⚠️ 同名でなければコピーは行われません。トリガは取引先側のカスタム項目名で、履歴側に同名の項目があるかどうかを見ています。

Apexトリガとハンドラー

after insertとafter updateで、作成・更新された取引先ぶんの履歴レコードをまとめて作ります。

trigger AccountTrigger on Account (after insert, after update) {
    if (Trigger.isAfter && (Trigger.isInsert || Trigger.isUpdate)) {
        new AccountTriggerHandler().afterUpsert(Trigger.new);
    }
}

取引先の後処理をハンドラークラスへ委譲します。DMLはこの中に置きません。

public class AccountTriggerHandler {

    public void afterUpsert(List<Account> accounts) {
        List<AccountHistory__c> histories = buildHistories(accounts);
        if (histories.isEmpty()) {
            return;
        }
        Database.SaveResult[] results = Database.insert(histories, false);
        for (Database.SaveResult result : results) {
            if (!result.isSuccess()) {
                for (Database.Error err : result.getErrors()) {
                    System.debug(LoggingLevel.ERROR, '取引先履歴の作成に失敗: ' + err.getMessage());
                }
            }
        }
    }

    private List<AccountHistory__c> buildHistories(List<Account> accounts) {
        Map<String, Schema.SObjectField> sourceFields = Account.SObjectType.getDescribe().fields.getMap();
        Map<String, Schema.SObjectField> targetFields = AccountHistory__c.SObjectType.getDescribe().fields.getMap();
        List<String> copyFieldNames = new List<String>();
        for (String fieldName : sourceFields.keySet()) {
            if (sourceFields.get(fieldName).getDescribe().isCustom() && targetFields.containsKey(fieldName)) {
                copyFieldNames.add(fieldName);
            }
        }

        List<AccountHistory__c> histories = new List<AccountHistory__c>();
        for (Account acc : accounts) {
            AccountHistory__c history = new AccountHistory__c();
            for (String fieldName : copyFieldNames) {
                history.put(fieldName, acc.get(fieldName));
            }
            history.Name = acc.Name;
            history.Account__c = acc.Id;
            histories.add(history);
        }
        return histories;
    }
}

履歴レコードをまとめて作ります。Schemaの取得はループの外で1回だけ行います。

避けたい書き方

このコードでは、次の3つの書き方を避けています。

避けたい書き方避ける理由
upsert upsertAccHistoryList Account.Id;参照型の項目は外部ID項目にできません。履歴は毎回新規作成でよいのでinsert(Database.insert)を使います
Schema.describeSObjectsを取引先1件ごとに呼ぶ説明(Schema describe)はレコードの件数に関係なく同じ結果です。ループの外で1回だけ呼びます
Database.setSavepointとtry-catchで失敗を握りつぶすDatabase.insert(list, false)を使えば、SaveResultで失敗したレコードだけを個別に確認できます。取引先本体の登録を巻き込みません

⚠️ insertやupsert(allOrNoneを指定しない形)を使うと、履歴の作成に1件でも失敗したとき、取引先の作成・更新そのものが例外で止まります。afterトリガの中のDML例外は、同じトランザクションの中にある取引先の変更も巻き込んでロールバックします。付随的に作る履歴レコードは、Database.insert(list, false)で失敗を許容し、SaveResultを見て記録するだけにするのが安全です。

同じトランザクションで2回走らせない

このトリガは取引先自身を更新しないので、いま書いた形では再帰は起きません。ただし、同じafterトリガの中で取引先側の項目も更新するように直すと、その更新が同じトリガをもう一度呼び出し、再帰実行になります。静的変数で「このトランザクションではもう実行した」を覚えておくのが定番の防ぎ方です。

public class AccountTriggerHandler {
    private static Boolean hasRun = false;

    public void afterUpsert(List<Account> accounts) {
        if (hasRun) {
            return;
        }
        hasRun = true;
    }
}

クラス変数はトランザクションの間だけ生きています。2回目の呼び出しをここで止めます。

動作確認

  1. 取引先を新規作成する

    カスタム項目に値を入れて保存します。

  2. 取引先履歴を確認する

    AccountHistory__cのタブか関連リストから、直後に1件作られていることを確認します。取引先側のカスタム項目の値が、同名の項目へ複製されているかを見ます。

  3. 取引先を更新する

    複製対象の項目を書き換えて保存します。

  4. 更新後の履歴を確認する

    更新のたびに履歴レコードが追加されることを確認します。既存の履歴が上書きされないことも見ます。

新規取引先の編集画面。取引先名とカスタム項目に値を入力している
取引先を新規作成するところです。複製したいカスタム項目に値を入れてから保存することを見てください。
取引先履歴の詳細画面。取引先への参照と同名項目に同じ値が入っている
保存の直後に作られた履歴レコードです。取引先の値が同名の項目へそのまま複製されていることを確認してください。
既存の取引先の編集画面。複製対象のカスタム項目の値を書き換えている
同じ取引先を更新するところです。複製対象の項目の値だけを書き換えて保存します。
更新後に作られた2件目の取引先履歴の詳細画面。書き換えた値が入っている
更新したあとに増えた2件目の履歴です。既存の履歴が上書きされず、新しい行として追加されることを見てください。

ここで間違えやすい

間違い何が起きるか
参照型の項目を外部ID指定にしてupsertするコンパイルも実行もできません。外部IDにできるのはテキスト・数値・メールなど値型の項目だけです
Schema.describeSObjectsをレコードごとに呼ぶ結果は同じなのに毎回呼ぶだけ無駄です。件数が増えると素直に遅くなります
afterトリガの中のDMLにallOrNoneを指定しない付随的な処理の失敗が、本体のレコード操作ごと巻き戻します
同名でない項目を複製対象だと思い込むこのコードは項目名の一致だけを見ます。名前が違えばコピーされません

確認した環境

  • 2026年9月/Salesforce Summer '26(APIバージョン67.0)時点の公式リファレンスで、DML文とDatabaseクラスの違い、allOrNoneとSaveResultの仕様を確認しています

まとめ

  • 参照型の項目は外部IDにできません。upsertで使う前に項目タイプを確認してください
  • Database.insert(list, false)とSaveResultを使うと、付随的な処理の失敗を本体の操作から切り離せます
  • Schema.describeSObjectsなどの重い呼び出しは、レコードごとではなくループの外で1回だけ行います
  • afterトリガが自分自身を再度呼び出す可能性があるときは、静的変数で2回目以降の実行を止めます
  • コピーは項目名が完全一致しているときだけ働きます。取引先側に項目を増やしたら、履歴側にも同名で作成してください

Salesforceの導入・運用についてご相談ください

導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。