ChatGPTにApexトリガーを書かせてみたらすごかった

今話題の対話型AIチャットのChatGPTにApexトリガーを書かせてみました。

https://chat.openai.com/chat

# ChatGPTとは何ですか？

ChatGPTとは何かChatGPTに聞いてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27267/9f9657d1-db04-818a-be73-4b9f9afbfb8b.png)

このように会話のコンテキストを理解して返事をしてくれます。本当に人間と会話しているレベルの返答を返してくれて衝撃です。

ネットでもかなり話題になっています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ChatGPTっていうチャットボットAIで遊んでるんだけど有能すぎてやばい<br>エンジニア終了の日も近いかも <a href="https://t.co/6ObDabuJkQ">pic.twitter.com/6ObDabuJkQ</a></p>&mdash; 銀河P (@galaxy14141356) <a href="https://twitter.com/galaxy14141356/status/1598159792601976832?ref_src=twsrc%5Etfw">December 1, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">AIチャットの新しい使い方見つけた！<br>AI同士にディベートさせると、メジャーな論点が洗い出せる。<br>これ、いろんなことの予行練習に使えそう。素晴らしい。 <a href="https://t.co/6LGU1tFlOO">pic.twitter.com/6LGU1tFlOO</a></p>&mdash; 深津 貴之 / THE GUILD / note.com (@fladdict) <a href="https://twitter.com/fladdict/status/1599977277869420545?ref_src=twsrc%5Etfw">December 6, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

詳しくはたくさん記事があるので他の記事を読んでください。

# Apexを書かせてみよう

これはSalesforceの開発もできるのではないかと、とりあえずApexトリガーを書かせてみました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/ChatGPT?src=hash&amp;ref_src=twsrc%5Etfw">#ChatGPT</a> さんすごい。「取引先の請求先住所が変更されたときにGoogle Geocoding APIを実行し緯度経度を取得し保存するApexトリガーのコードを教えて下さい。」って聞いたらけっこうちゃんとしたコード書いてくれた。<br>コード全体は以下。<a href="https://t.co/iyzROlWDVY">https://t.co/iyzROlWDVY</a> <a href="https://t.co/Ra5gtpVWSm">pic.twitter.com/Ra5gtpVWSm</a></p>&mdash; Atsuhiko Kimura (@atskimura) <a href="https://twitter.com/atskimura/status/1599322825407725568?ref_src=twsrc%5Etfw">December 4, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

すっごい。

# 再度、緯度経度取得Apexトリガーを書いてもらう

質問する度に回答は異なるので、記事を書くために再度同じ質問をしてみました。今回はさらに対話してコードをよりよくしてもらってみます。

## Q1. 取引先の請求先住所が変更されたときにGoogle Geocoding APIを実行し緯度経度を取得し保存するApexトリガーのコードを教えてください。

## A1. ChatGPTの回答

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27267/14cd7e90-d5b3-6080-ae17-368f62bfaced.png)


> Google Geocoding APIを使用するためには、Google Maps APIキーが必要です。まずは、Google Cloud Platformでプロジェクトを作成し、Google Maps Geocoding APIを有効化してAPIキーを取得する必要があります。
>
> 次に、Apexトリガーのコードを書く前に、Google Geocoding APIを呼び出すためのApexクラスを作成します。下記のようなコードで実装できます。

```apex
// GoogleGeocodingAPI.cls

public with sharing class GoogleGeocodingAPI {
  public static String GEOCODING_API_KEY = 'YOUR_API_KEY';
  public static String GEOCODING_API_ENDPOINT = 'https://maps.googleapis.com/maps/api/geocode/json';

  public static GeocodingResponse getGeocodingResponse(String address) {
    Http h = new Http();
    HttpRequest req = new HttpRequest();
    req.setEndpoint(GEOCODING_API_ENDPOINT + '?address=' + EncodingUtil.urlEncode(address, 'UTF-8') + '&key=' + GEOCODING_API_KEY);
    req.setMethod('GET');
    HttpResponse res = h.send(req);
    return (GeocodingResponse) JSON.deserialize(res.getBody(), GeocodingResponse.class);
  }
}
```

> 次に、GeocodingResponseクラスを定義します。このクラスは、Google Geocoding APIから取得したレスポンスを表すためのクラスです。

```apex
// GeocodingResponse.cls

public class GeocodingResponse {
  public String status;
  public List<Result> results;
  public String error_message;

  public class Result {
    public List<AddressComponent> address_components;
    public String formatted_address;
    public Geometry geometry;
  }

  public class AddressComponent {
    public String long_name;
    public String short_name;
    public List<String> types;
  }

  public class Geometry {
    public Location location;
  }

  public class Location {
    public Double lat;
    public Double lng;
  }
}
```

> 最後に、Apexトリガーを定義します。このトリガーは、取引先の請求先住所が変更されたときに実行され、Google Geocoding APIを呼び出して、請求先住所の緯度経度を取得し保存します。

```apex
// GeocodingTrigger.apxt

trigger GeocodingTrigger on Account (after update) {
  Set<Id> accountIds = new Set<Id>();
  for (Account account : Trigger.new) {
    if (account.BillingStreet != Trigger.oldMap.get(account.Id).BillingStreet) {
      accountIds.add(account.Id);
    }
  }

  if (accountIds.isEmpty()) {
    return;
  }

  List<Account> accounts = [
    SELECT Id, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry
    FROM Account
    WHERE Id IN :accountIds
  ];

  for (Account account : accounts) {
    String address = account.BillingStreet + ', ' + account.BillingCity + ', ' + account.BillingState + ' ' + account.BillingPostalCode + ' ' + account.BillingCountry;
    GeocodingResponse response = GoogleGeocodingAPI.getGeocodingResponse(address);
    if (response.status == 'OK') {
      Result result = response.results[0];
      account.BillingLatitude = result.geometry.location.lat;
      account.BillingLongitude = result.geometry.location.lng;
    }
  }

  update accounts;
}
```

> このトリガーは、取引先の請求先住所が変更されたときに、その住所の緯度経度を取得して、取引先の請求先住所の緯度経度を更新します。

## A1.の感想

Geocoding API実行クラスが分かれて、さらによくなっている。

しかし、住所が変更されたかの確認に `BillingStreet` しか見ていないので、直してもらいましょう。

## Q2. 住所が変更されたかの確認はBillingStreetのチェックだけでは不足しています。Apexトリガーを修正してください。

## A2. ChatGPTの回答
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27267/4c500888-0c83-4777-d530-4fa94adf6ee1.png)


> そうですね。上記のApexトリガーでは、BillingStreet以外のフィールドも更新された場合に、住所の緯度経度を更新するようにする必要があります。
>
> 修正するには、トリガー内で更新されたフィールドを確認し、BillingStreet以外のフィールドも更新されている場合は、その住所の緯度経度を更新するようにします。
> 
> 次のように修正します。

```apex
// GeocodingTrigger.apxt

trigger GeocodingTrigger on Account (after update) {
  Set<Id> accountIds = new Set<Id>();
  for (Account account : Trigger.new) {
    Account oldAccount = Trigger.oldMap.get(account.Id);
    if (account.BillingStreet != oldAccount.BillingStreet || account.BillingCity != oldAccount.BillingCity || account.BillingState != oldAccount.BillingState || account.BillingPostalCode != oldAccount.BillingPostalCode || account.BillingCountry != oldAccount.BillingCountry) {
      accountIds.add(account.Id);
    }
  }

  if (accountIds.isEmpty()) {
    return;
  }

  List<Account> accounts = [
    SELECT Id, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry
    FROM Account
    WHERE Id IN :accountIds
  ];

  for (Account account : accounts) {
    String address = account.BillingStreet + ', ' + account.BillingCity + ', ' + account.BillingState + ' ' + account.BillingPostalCode + ' ' + account.BillingCountry;
    GeocodingResponse response = GoogleGeocodingAPI.getGeocodingResponse(address);
    if (response.status == 'OK') {
      Result result = response.results[0];
      account.BillingLatitude = result.geometry.location.lat;
      account.BillingLongitude = result.geometry.location.lng;
    }
  }

  update accounts;
}
```

## A2.の感想

すごい。いい感じに直った。

しかし、Apexトリガーに直接コードを書きたくないので、Apexクラスに分離してもらいましょう。

## Q3. Apexトリガーに直接コードを書くのではなく、Apexクラスを呼び出すようにしたいです。

## A3. ChatGPTの回答
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27267/a33d48c0-54bd-42e3-4dc6-d2d4bdd91894.png)

> そうですね。Apexトリガーでは、トリガーのロジックを直接書くことが多いですが、Apexクラスを呼び出すことで、トリガーのロジックを分離することができます。
> 
> Apexクラスを呼び出すためには、Apexトリガー内でApexクラスをインスタンス化し、メソッドを呼び出す必要があります。
>
> 具体的には、次のように修正します。

```apex
// GeocodingTrigger.apxt

trigger GeocodingTrigger on Account (after update) {
  Set<Id> accountIds = new Set<Id>();
  for (Account account : Trigger.new) {
    Account oldAccount = Trigger.oldMap.get(account.Id);
    if (account.BillingStreet != oldAccount.BillingStreet || account.BillingCity != oldAccount.BillingCity || account.BillingState != oldAccount.BillingState || account.BillingPostalCode != oldAccount.BillingPostalCode || account.BillingCountry != oldAccount.BillingCountry) {
      accountIds.add(account.Id);
    }
  }

  if (accountIds.isEmpty()) {
    return;
  }

  List<Account> accounts = [    SELECT Id, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry    FROM Account    WHERE Id IN :accountIds  ];

  Geocoding geocoding = new Geocoding();
  geocoding.updateGeocoding(accounts);
}
```

```apex
// Geocoding.cls

public with sharing class Geocoding {
  public void updateGeocoding(List<Account> accounts) {
    for (Account account : accounts) {
      String address = account.BillingStreet + ', ' + account.BillingCity + ', ' + account.BillingState + ' ' + account.BillingPostalCode + ' ' + account.BillingCountry;
      GeocodingResponse response = GoogleGeocodingAPI.getGeocodingResponse(address);
      if (response.status == 'OK') {
        Result result = response.results[0];
        account.BillingLatitude = result.geometry.location.lat;
        account.BillingLongitude = result.geometry.location.lng;
      }
    }

    update accounts;
  }
}
```

> 上記のように修正することで、Apexトリガー内でApexクラスを呼び出すことができます。Apexクラスを使用することで、トリガーのロジックを分離することができ、コードの見通しがよくなります。

## A4.の感想

分けてくれた。すごい。

でも、条件分岐もクラスに分けて欲しい。

## Q4. ApexトリガーからはApexクラスを呼び出すだけにして欲しいです。

## A4. ChatGPTの回答
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27267/3bbad0fa-8e81-eb8d-24f4-2f6dc7188a26.png)


> そうですね。ApexトリガーからはApexクラスを呼び出すだけにすることができます。
>
> Apexクラスを呼び出すだけにするためには、Apexトリガーではトリガーのロジックを記述せず、Apexクラスのみでトリガーのロジックを実装するようにします。
>
> 具体的には、次のように修正します。

**コードは変わらなかったので省略😢**

## A4.の感想

コード変わらなかった…

依頼文を変えてみよう。

## Q5. 住所が変更された取引先レコードを取得するロジックもApexクラスに分離して欲しいです。

## A5. ChatGPTの回答
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27267/5befff7a-37c0-163a-1f59-cdd880eab74b.png)

> そうですね。上記のApexトリガーでは、住所が変更された取引先レコードを取得するロジックがApexクラスに分離されていません。
>
> Apexクラスに分離するためには、Apexトリガーでは住所が変更された取引先レコードを取得するロジックを実装せず、Apexクラスのみで実装するようにします。
>
> 具体的には、次のように修正します。

```apex
// GeocodingTrigger.apxt

trigger GeocodingTrigger on Account (after update) {
  List<Account> accounts = Geocoding.getAccountsForGeocoding(Trigger.new, Trigger.oldMap);
  if (accounts.isEmpty()) {
    return;
  }

  Geocoding geocoding = new Geocoding();
  geocoding.updateGeocoding(accounts);
}
```

```apex
// Geocoding.cls

public with sharing class Geocoding {
  public static List<Account> getAccountsForGeocoding(List<Account> newAccounts, Map<Id, Account> oldAccountMap) {
    List<Account> accounts = new List<Account>();
    for (Account account : newAccounts) {
      Account oldAccount = oldAccountMap.get(account.Id);
      if (account.BillingStreet != oldAccount.BillingStreet || account.BillingCity != oldAccount.BillingCity || account.BillingState != oldAccount.BillingState || account.BillingPostalCode != oldAccount.BillingPostalCode || account.BillingCountry != oldAccount.BillingCountry) {
        accounts.add(account);
      }
    }
    return accounts;
  }

  public static void updateGeocoding(List<Account> accounts) {
    for (Account account : accounts) {
      String address = account.BillingStreet + ', ' + account.BillingCity + ', ' + account.BillingState + ' ' + account.BillingPostalCode + ' ' + account.BillingCountry;
      GeocodingResponse response = GoogleGeocodingAPI.getGeocodingResponse(address);
      if (response.status == 'OK') {
        Result result = response.results[0];
        account.BillingLatitude = result.geometry.location.lat;
        account.BillingLongitude = result.geometry.location.lng;
      }
    }

    update accounts;
  }
}
```

## A5.の感想

うーん。。。期待したのとはちょっと違うけど、まあこんなところかな。終わろう。

# 終わりに

すごくない？

でも、全然ひどいコードを書いて、いくら言っても直らない時もありましたｗ