【Apex】cloneメソッド解説

# はじめに

SObjectクラスのcloneメソッドには4つの引数が設定できますが、なんとなく`.clone(false, true)`で覚えている方が多いのではないでしょうか。
そこで今回はcloneメソッドの基本とそれぞれの引数について深堀りしたいと思います。

[Apex 開発者ガイド SObjectクラス](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_methods_system_sobject.htm)

# 基本的な使い方
```java
// CustomerNo__cは自動採番型項目
Account acc = [SELECT Id, Name, CustomerNo__c, createdDate FROM Account WHERE Name = 'オリジナル' LIMIT 1];
Account clonedAcc = acc.clone();
insert clonedAcc;
clonedAcc = [SELECT Id, Name, CustomerNo__c, createdDate FROM Account WHERE Id = :clonedAcc.Id LIMIT 1];

System.debug(acc);
System.debug(clonedAcc);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/c832bb2c-7ede-9871-716d-e47fac1cded2.png)

引数無しでコールすると、`.clone(false, false, false, false)`と同義になります（APIバージョン23以降）。
そのため、ただ単にレコードをコピーしてinsertするだけならば（変更を加えないのであれば）、単に`clone()`で問題ありません。
しかし、デバッグ内容を確認すると、自動採番型のCustomerNo__cやcreatedDateがオリジナルと違うことが分かります。
以降、各引数の意味と共に解説していきます。

# 各引数について
## 1. preserveId
trueに設定した場合はコピー元のレコードIDが引き継がれます。
そのため`.clone(true, false, false, false)`でコピーしたレコードのinsertを試みると、
`INVALID_FIELD_FOR_INSERT_UPDATE, cannot specify Id in an insert call: [Id]`とDmlExceptionが発生します。
一見trueに設定する機会はなさそうですが、有用なケースも存在します。

[Apex開発で押さえておきたいポイント](https://qiita.com/tyoshikawa1106/items/64e1dde75de245018647#%E3%83%AD%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E3%81%A8id%E9%A0%85%E7%9B%AE)

```java
Account upsertAccount = this.objAccount.clone(true, false, true, true);
upsert upsertAccount;
```

変数にIDをセットしてinsertした後、insert前までロールバックを行ったとしてもIDはクリアされません。
一度cloneしてからupsertを実行することにより、

- 更新時 → update対象のIDが渡される
- 作成時 → insert対象のIDをnullに設定

ロールバックの有無に関係なくID項目を設定され、どちらの状況でも正常にupsertが行われることが担保されています。

## 2. isDeepClone
trueに設定することで、全く同じレコードのコピーが可能です。
falseに設定した場合は実態ではなく参照をコピーすることになるので、コピーしたレコード・コピー元のレコードに変更を加えるともう一方のレコードにも変更が反映されてしまいます。

> メソッドで SObject 項目の完全コピーを作成するか参照のみを作成するかを決定します。
true に設定すると、メソッドは SObject の完全コピーを作成します。リレーション項目など、SObject のすべての項目はメモリ内に複製されます。その結果、コピーした SObject の項目に変更を行っても、元の SObject は影響されません。
false に設定すると、メソッドは SObject 項目の浅いコピーを作成します。コピーされたすべてのリレーション項目は元の SObject を使用します。その結果、コピーされた SObject でリレーション項目を変更すると、元の SObject の対応する項目も変更され、元の SObject で変更するとコピーされた SObject も変更されます。デフォルトは、false です。

※開発者ガイドより

そのためcloneメソッドの使用頻度が高そうな「レコードをコピーして参照項目だけ変更する」といった使用方法の際には第2引数をtrueに設定する必要があります。

## 3. preserveReadonlyTimestamps
trueに設定すると、`CreatedById` `CreatedDate` `LastModifiedById` `LastModifiedDate`の4項目がコピーされます。

```java
Account acc = [SELECT Name, createdDate FROM Account WHERE Name = 'オリジナル' LIMIT 1];
Account clonedAcc = acc.clone(false, false, true, false);
insert clonedAcc;
System.debug(clonedAcc);
System.debug(acc);
clonedAcc = [SELECT Name, createdDate FROM Account WHERE Id = :clonedAcc.Id LIMIT 1];
System.debug(clonedAcc);
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/84132bf9-78fd-c69f-e033-cb947b0547cd.png)

しかしご覧の通り、登録される`createdDate`は実際の登録日時（clone元とは別）になってしまうため正直使い所は分かりません。

## 4. preserveAutonumber
trueに設定すると、自動採番型の項目値をコピーすることが可能です。
もちろん一意になるよう制御されている項目が存在する際にinsertを行うとエラーになるのでご注意下さい。

```java
Account acc = [SELECT Name, CustomerNo__c FROM Account WHERE Name = 'オリジナル' LIMIT 1];
Account clonedAcc = acc.clone(false, false, false, true);
insert clonedAcc;
System.debug(clonedAcc);
System.debug(acc);
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573865/d0148e1e-d621-c8b5-d112-b58db65d33a1.png)

# まとめ
~~ここまで説明しておいてなんですが、正直`.clone(false, true)`の丸覚えでほぼ問題ありません。~~

最後に最も大切な点を1つ。
**SOQLで取得していない項目はコピーされません。**

そのため、必須項目をクエリに含めない状態でレコードをコピーし、そのままinsertを実行すると失敗します。
レコードの完全コピーを作成したい場合は、[こちらの記事](https://qiita.com/arufian/items/dd376e23dd3e568b9f1a)などを参考に、全項目を取得してからcloneする必要があることを覚えておいて下さい。

# 参考
Apex 開発者ガイド
https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_methods_system_sobject.htm
Apex開発で押さえておきたいポイント
https://qiita.com/tyoshikawa1106/items/64e1dde75de245018647
Apex上でSelect *を実装してみた
https://qiita.com/arufian/items/dd376e23dd3e568b9f1a