Salesforce開発入門 Vol.2 ～ApexでREST APIエンドポイント実装編～

こんにちは！
LIFULLエンジニアの吉永です。

前回の記事に引き続き、直近でSalesforce Service Cloud（以降SFSCと略称します）関連の開発を行っているので、今回はApexを使ってカスタムオブジェクトへのCRUD操作を行うREST APIエンドポイントを実装するまでの流れを紹介したいと思います。
前回の記事は[こちら](https://qiita.com/YoshinagaYuta/items/ac70d192194f6193f728)

## Apexとは？

[公式ページ解説](https://developer.salesforce.com/docs/atlas.ja-jp.224.0.apexcode.meta/apexcode/apex_intro.htm)

ApexはSFSC上で何かしらの機能を開発する際に用いるプログラミング言語で、文法はJavaに非常に似ている為、JavaやC#での開発経験がある人は特に戸惑うことなく習得が可能だと思います。
SF関連は非常にドキュメントが充実している印象があるので、情報を探すのに苦労はしないとは思いますが、私なりの理解でApexでできることの具体例を列挙すると、

- 複合主キーを疑似的に実現する場合、入力画面では二つの別項目になっているが、API経由でレコード保存する際に連結したり、独自で入力チェックロジックを実装することができる。
- 特定のレコード作成時にその操作をトリガーとして、必ず特定のレコードを作成するなどができる。

などのように、公式ページにも書いてありますが、組織独自のビジネスロジックを実現する際の一つの手段としてApexが使えると思います。
もちろん、上記に上げた例はApex以外でも実現する方法はあると思いますし、Apexで実現することがベストプラクティスではないことも多々あるかと思いますが、Apexでできることを把握しておくと、色々な要件に柔軟に対応できると思うので、おススメです。

## 今回ApexでREST APIを実装する対象のオブジェクト

前回の[記事](https://qiita.com/YoshinagaYuta/items/ac70d192194f6193f728)で作成したカスタムオブジェクトをそのまま利用しようと思いますので、この記事から見始めていて、この後の手順を自分の環境でも試してみたい方は、[カスタムオブジェクト作成](https://qiita.com/YoshinagaYuta/items/ac70d192194f6193f728#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E4%BD%9C%E6%88%90)の手順を参考に、自分の検証用のSFSCに環境を作ってみてください。

## 実装するロジック

今回はJSON形式の注文情報をREST APIで受取り、受け取った情報からMY注文オブジェクトとMY注文明細オブジェクトへレコードを新規登録するロジックをApexで実装します。
せっかくなので、登録したレコードを取得できるロジックも合わせて実装します。
なお、今回の要件ではMY注文オブジェクトのみのレコード登録は受け付けず、必ず一件以上のMY注文明細オブジェクトのレコード情報も受け取るJSONに含めるようにして、入力チェックもその前提で実装することにします。
取得はMY注文オブジェクトの主キーをパラメーターとして受け取り、レスポンスはMY注文オブジェクトとそれに紐づくMY注文明細オブジェクトをマージした情報を返却するようにします。

## 実装するREST APIの仕様

エンドポイントの`/services/apexrest/`はSFSCで独自のREST APIエンドポイントを実装する際に必ずつくプレフィックスのようなものになっています。
詳細は公式ページの[こちら](https://developer.salesforce.com/docs/atlas.ja-jp.234.0.apexcode.meta/apexcode/apex_classes_annotation_rest_resource.htm?q=%2Fservices%2Fapexrest)を参照ください。

| エンドポイントURL  | HTTPメソッド  | 
|---|---|
| /services/apexrest/myorder/{MY注文オブジェクトの主キー} | GET |
| /services/apexrest/myorder/ | POST |

## GETのパラメータ例

```
/services/apexrest/myorder/Order0001
```

## POSTのパラメータ例

```json
{
  "orderid": "Order0003",
  "orderdetail": [
    {
      "detailid": "OrderDetail0004",
      "itemid": "ITEM0001",
      "count": 1
    },
    {
      "detailid": "OrderDetail0005",
      "itemid": "ITEM0002",
      "count": 2
    }
  ]
}
```

## 実装の流れ

ApexでREST APIを実装するには大きく分けて三つの作業が必要になります。

1. Apexでロジックを実装する。
1. 実装したApexのユニットテストコードを実装する。
1. 実装したApexに接続する為のアプリケーション設定をSFSCに追加する。

今回はApexで実装、ワークベンチで動作確認するまでをゴールとしたいので、ユニットテストの実装及びアプリケーション設定は割愛しますが、次回以降の記事でこれらのことにも触れようと思ってます。

### Apex実装

とりあえず手を動かしてみないことにはなかなか頭に入ってこないと思いますので、さっそくApexを実装するところから始めたいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/050a091e-9300-10f3-60d4-64d2f1a3db9d.png)
自分の検証環境のSFSCホーム画面から、右上のギアアイコンを押し、表示されるメニューから開発者コンソールを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/6c7c851f-d400-25d4-eeab-7fee14192bc6.png)
[File]→[New]→[Apex Class]を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/3acb6553-db9b-0ec8-5ca2-c7db0566c54c.png)
ファイル名に`MyOrderRestApi`と入力してOKを押します。

#### GETメソッド実装

```apex
@RestResource(urlMapping='/myorder/*')
global with sharing class MyOrderRestApi {
    @HttpGet
    global static void doGet() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;
        res.statusCode = 400;
        res.addHeader('Content-Type', 'application/json');
        // URLから入力パラメーターを取り出す
        String orderid = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1);
        if (orderid.length() != 9){
            // パラメーターの文字数が不正なので400エラーを返す
            return;
        }
        try {
            // SOQLを発行
            MyOrder__c result = [SELECT Name, (SELECT Name, MyItem__r.ItemName__c, Count__c FROM MyOrderDetails__r) FROM MyOrder__c WHERE Name = :orderid ORDER BY Name];
            res.statusCode = 200;
            res.responseBody = Blob.valueOf(JSON.serialize(result));
        } catch (System.QueryException e) {
            // 該当するレコードないので404エラーを返す
            res.statusCode = 404;
            return;
        }
    }
}
```

上記のコードを開発者コンソールの画面に貼り付け、保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/2f32dd8d-cec3-a2f9-f318-bb086ad3428b.png)
[ワークベンチ](https://workbench.developerforce.com/login.php)にアクセスし、`I agree to the terms of service`横のチェックボックスにチェックを入れ、`Login With Salesforce`を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/e001c6de-8015-499a-2086-1d33d3832242.png)
ログイン後、画面右上の`utilities`の`REST Explorer`を押します。

#### GETメソッド正常系確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/8a5448c7-50a5-ca40-000d-e0ff7cc0cc8f.png)
REST API動作確認用の画面に切り替わりますので、テキストボックスに`/services/apexrest/myorder/Order0001`と入力し、`Execute`ボタンを押します。
すると、自分で作成したApex REST APIのdoGetメソッドが呼び出され、SQOL実行後に処理が成功すれば上図のようなレスポンスが得られます。
参考までに、レスポンスに含まれるJsonは下記のようになります。

```json
{
	"attributes": {
		"type": "MyOrder__c",
		"url": "/services/data/v53.0/sobjects/MyOrder__c/a0A5h0000025fwrEAA"
	},
	"Name": "Order0001",
	"Id": "a0A5h0000025fwrEAA",
	"MyOrderDetails__r": {
		"totalSize": 1,
		"done": true,
		"records": [
			{
				"attributes": {
					"type": "MyOrderDetail__c",
					"url": "/services/data/v53.0/sobjects/MyOrderDetail__c/a0B5h000000F11xEAC"
				},
				"MyOrder__c": "a0A5h0000025fwrEAA",
				"Id": "a0B5h000000F11xEAC",
				"Name": "OrderDetail0001",
				"MyItem__c": "a095h000006BSKuAAO",
				"Count__c": 1,
				"MyItem__r": {
					"attributes": {
						"type": "MyItem__c",
						"url": "/services/data/v53.0/sobjects/MyItem__c/a095h000006BSKuAAO"
					},
					"Id": "a095h000006BSKuAAO",
					"ItemName__c": "りんご"
				}
			}
		]
	}
}
```

#### GETメソッド異常系確認 パラメーターエラー

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/3d992a0a-6119-cec7-80ba-abb2566c4427.png)
試しにエラーチェックも機能しているか確認します。
テキストボックスに`/services/apexrest/myorder/Order000`と入力して、`Execute`ボタンを押すと今度は上図のようなエラーが返却されていることが確認できました。

#### GETメソッド異常系確認 存在しないレコード指定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/1902253e-0576-3bbb-af35-cfb48b03dc02.png)
続いて、存在しない注文IDを入力してみます。
テキストボックスに`/services/apexrest/myorder/Order0000`と入力して、`Execute`ボタンを押すと今度は上図のようなエラーが返却されていることが確認できました。

#### SOQLを変更してレスポンスにフィールドを増やしてみる

先ほど作成したApexでは商品名は分かっても、商品の単価がレスポンスに含まれておらずわかりませんでした。
Apex内で発行しているSOQLを下記のように変更して、再度実行してみます。

```sql
SELECT Name, (SELECT Name, MyItem__r.ItemName__c, MyItem__r.Price__c, Count__c FROM MyOrderDetails__r) FROM MyOrder__c WHERE Name = :orderid ORDER BY Name
```

サブクエリの中のSELECTで`MyItem__r.Price__c`を追加します。
先ほどの手順と同じように、`/services/apexrest/myorder/Order0001`を入力して実行してみると、今度はJSONが以下のようになると思います。

```json
{
	"attributes": {
		"type": "MyOrder__c",
		"url": "/services/data/v53.0/sobjects/MyOrder__c/a0A5h0000025fwrEAA"
	},
	"Name": "Order0001",
	"Id": "a0A5h0000025fwrEAA",
	"MyOrderDetails__r": {
		"totalSize": 1,
		"done": true,
		"records": [
			{
				"attributes": {
					"type": "MyOrderDetail__c",
					"url": "/services/data/v53.0/sobjects/MyOrderDetail__c/a0B5h000000F11xEAC"
				},
				"MyOrder__c": "a0A5h0000025fwrEAA",
				"Id": "a0B5h000000F11xEAC",
				"Name": "OrderDetail0001",
				"MyItem__c": "a095h000006BSKuAAO",
				"Count__c": 1,
				"MyItem__r": {
					"attributes": {
						"type": "MyItem__c",
						"url": "/services/data/v53.0/sobjects/MyItem__c/a095h000006BSKuAAO"
					},
					"Id": "a095h000006BSKuAAO",
					"ItemName__c": "りんご",
					"Price__c": 100
				}
			}
		]
	}
}
```

MyItem__rの配下にPrice__cが追加されました。

#### GETメソッドコード解説

Apexのコードを少し解説します。
まず、ApexでREST APIエンドポイントを実装するうえで重要なのはエンドポイントの定義になります。
Apexだと、下記のRestResourceアノテーションにて定義します。

```apex
@RestResource(urlMapping='/myorder/*')
``` 

定義する際は、`/services/apexrest/`以下の独自で定義するエンドポイントの名称から記述します。
`*`はワイルドカード指定で、`/services/apexrest/myorder/hogehoge`などのように、``/services/apexrest/myorder/`にマッチすればこのApexクラスを実行するということになります。

続いて、HTTPメソッドでどのメソッドが指定されたかに応じて呼び出すクラス内のメソッドの指定をHttpGetアノテーションで指定します。
コードでは下記の部分です。

```apex
    @HttpGet
    global static void doGet() {}
```

これを例えば、

```apex
    @HttpGet
    global static void doGet() {}

    @HttpPost
    global static void doPost(String param) {}
```

などのように、そのエンドポイントでサポートするメソッド毎に対応するクラス内のメソッドの直前にアノテーションを付与して関連づけます。
上記の例ではHTTPのPOSTメソッドが指定された際に実行する`doPost`というクラス内のメソッドを定義しています。

最後に、HTTPリクエストに対するレスポンスの設定ですが、コードだと一見するとメソッドで何か値をリターンしているわけではないので分かりづらいのですが、グローバル変数（っていうと語弊がありそうですが、システム全体のリクエストでグローバルではなく、HTTPリクエスト単位でグローバルって方が正しい表現かもです）に設定しています。

```json
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;
```

まず、`RestContext.request`はHTTPリクエストの内容を取得できるオブジェクトで、`RestContext.response`がレスポンスを設定するオブジェクトです。

```apex
        res.statusCode = 400;
```

なので、doGetメソッド内の上記はレスポンスのHTTPステータスコードを400で初期化しています。

```
        if (orderid.length() != 9){
            // パラメーターエラー
            return;
        }
```

URLから取り出したパラメーターの文字数が不正だった場合は即returnしてますが、先ほどの処理で400で初期化されているので、ここでは特に何もせず早期リターンしているだけということです。

```apex
        try {
            MyOrder__c result = [SELECT Name, (SELECT Name, MyItem__r.ItemName__c, MyItem__r.Price__c, Count__c FROM MyOrderDetails__r) FROM MyOrder__c WHERE Name = :orderid ORDER BY Name];
            res.statusCode = 200;
            res.responseBody = Blob.valueOf(JSON.serialize(result));
        } catch (System.QueryException e) {
            res.statusCode = 404;
            return;
        }
```

そして、入力値が正常だった時はSOQLを発行してレコードを取得しますが、もしSOQLの結果が0件だった場合は`System.QueryException`という例外が発生するので、キャッチ処理のなかで、`res.statusCode`を404にしています。
レコードが無事取得できた場合はステータスコードを200にして、`res.responseBody`にMyOrder__cオブジェクトをJSONにシリアライズした結果を格納しています。

#### POSTメソッド実装

続いて、POSTメソッドを実装します。
下記のコードを開発者コンソールの画面に貼り付け、保存します。

```apex
    public class OrderDetail {
        public String detailid;
        public String itemid;
        public Integer count;
    }

    global class ParamOrder {

        public String orderid;
        public List<OrderDetail> orderdetail;

    }

    @HttpPost
    global static void doPost() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;
        res.statusCode = 400;
        res.addHeader('Content-Type', 'application/json');
        // リクエストBODYをデシリアライズ
        String reqString = req.requestBody.toString();
        // JSON文字列をParamOrderクラスへデシリアライズ
        ParamOrder param = (ParamOrder) System.JSON.deserialize(reqString, ParamOrder.class);

        if (param.orderid.length() != 9){
            // パラメーターエラー
            return;
        }
        if (param.orderdetail.size() == 0){
            // パラメーターエラー
            return;
        }
        // 注文オブジェクトへ新規レコード登録
        MyOrder__c order = new MyOrder__c();
        order.Name = param.orderid;
        insert order;

        for(OrderDetail detail : param.orderdetail){
            try {
                // 商品情報取得
                MyItem__c result = [SELECT ID FROM MyItem__c WHERE Name = :detail.itemid];
                // 注文明細へ新規レコード登録
                MyOrderDetail__c orderDetail = new MyOrderDetail__c();
                orderDetail.MyOrder__c = order.ID;
                orderDetail.MyItem__c = result.ID;
                orderDetail.Count__c = detail.count;
                orderDetail.Name = detail.detailid;
                insert orderDetail;
            } catch (System.QueryException e) {
                res.statusCode = 404;
                return;
            }            
        }
        res.statusCode = 200;
    }
```

#### POST正常系確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/690018/f6abda9f-7565-451c-02f9-2e0b7cb28725.png)

テキストボックスに`/services/apexrest/myorder/`と入力し、ラジオボタンをPOSTに切り替え、Request Bodyに下記JSONを入力して、`Execute`ボタンを押します。

```json
{
  "orderid": "Order0003",
  "orderdetail": [
    {
      "detailid": "OrderDetail0004",
      "itemid": "ITEM0001",
      "count": 1
    },
    {
      "detailid": "OrderDetail0005",
      "itemid": "ITEM0002",
      "count": 2
    }
  ]
}
```

成功すると`HTTP/1.1 200 OK`が返ってきますので、本当に登録できたかをGETメソッドで確認します。

テキストボックスに`/services/apexrest/myorder/Order0003`と入力し、ラジオボタンをGETに切り替え、`Execute`ボタンを押します。
すると下記のようなJSONがレスポンスで確認できると思います。

```json
{
	"attributes": {
		"type": "MyOrder__c",
		"url": "/services/data/v53.0/sobjects/MyOrder__c/a0A5h000005lr9IEAQ"
	},
	"Name": "Order0003",
	"Id": "a0A5h000005lr9IEAQ",
	"MyOrderDetails__r": {
		"totalSize": 2,
		"done": true,
		"records": [
			{
				"attributes": {
					"type": "MyOrderDetail__c",
					"url": "/services/data/v53.0/sobjects/MyOrderDetail__c/a0B5h000003HHx4EAG"
				},
				"MyOrder__c": "a0A5h000005lr9IEAQ",
				"Id": "a0B5h000003HHx4EAG",
				"Name": "OrderDetail0004",
				"MyItem__c": "a095h000006BSKuAAO",
				"Count__c": 1,
				"MyItem__r": {
					"attributes": {
						"type": "MyItem__c",
						"url": "/services/data/v53.0/sobjects/MyItem__c/a095h000006BSKuAAO"
					},
					"Id": "a095h000006BSKuAAO",
					"ItemName__c": "りんご",
					"Price__c": 100
				}
			},
			{
				"attributes": {
					"type": "MyOrderDetail__c",
					"url": "/services/data/v53.0/sobjects/MyOrderDetail__c/a0B5h000003HHx5EAG"
				},
				"MyOrder__c": "a0A5h000005lr9IEAQ",
				"Id": "a0B5h000003HHx5EAG",
				"Name": "OrderDetail0005",
				"MyItem__c": "a095h000006BSKzAAO",
				"Count__c": 2,
				"MyItem__r": {
					"attributes": {
						"type": "MyItem__c",
						"url": "/services/data/v53.0/sobjects/MyItem__c/a095h000006BSKzAAO"
					},
					"Id": "a095h000006BSKzAAO",
					"ItemName__c": "ばなな",
					"Price__c": 150
				}
			}
		]
	}
}
```

リクエスト時に指定した値で注文レコードも注文明細レコードも作成されていることが確認できました。

#### POSTメソッドコード解説

まずPOSTはリクエストの受け取り方が2通りあり、事前にSFSC側でJSONをデシリアライズしてもらって受け取る方法と、自分でリクエストボディをデシリアライズする方法があります。
今回は後者を選択しましたが、パラメーターが単純でプリミティブ型の場合は前者の方法を選択した方が楽だと思います。

```apex
        // リクエストBODYをデシリアライズ
        String reqString = req.requestBody.toString();
        // JSON文字列をParamOrderクラスへデシリアライズ
        ParamOrder param = (ParamOrder) System.JSON.deserialize(reqString, ParamOrder.class);
```

上記の方法では事前にインナークラスでリクエストボディをデシリアライズするようのクラス定義をしてあります。

```apex
    public class OrderDetail {
        public String detailid;
        public String itemid;
        public Integer count;
    }

    global class ParamOrder {

        public String orderid;
        public List<OrderDetail> orderdetail;

    }
```

カスタムオブジェクトへ新規レコードを登録する場合は`insert`を用います。

```apex
        // 注文オブジェクトへ新規レコード登録
        MyOrder__c order = new MyOrder__c();
        order.Name = param.orderid;
        insert order;
```

insertするには、まず対象となるカスタムオブジェクトのインスタンスを生成し、そのインスタンスにリクエストボディの値を反映してからinsertします。

```apex
        for(OrderDetail detail : param.orderdetail){
            try {
                // 商品情報取得
                MyItem__c result = [SELECT ID FROM MyItem__c WHERE Name = :detail.itemid];
                // 注文明細へ新規レコード登録
                MyOrderDetail__c orderDetail = new MyOrderDetail__c();
                orderDetail.MyOrder__c = order.ID;
                orderDetail.MyItem__c = result.ID;
                orderDetail.Count__c = detail.count;
                orderDetail.Name = detail.detailid;
                insert orderDetail;
            } catch (System.QueryException e) {
                res.statusCode = 404;
                return;
            }            
        }
```

注文明細オブジェクトは注文オブジェクトと商品オブジェクトに依存しているので、それぞれのオブジェクトのレコードへのID情報を設定する必要があります。
今回は注文オブジェクトのIDは直前にinsertしており、このinsert処理後は対象のインスタンスにIDが格納されているので、そちらの値を利用します。
対して、商品オブジェクトは現時点では商品オブジェクトレコードへのID情報がないので、事前にSOQLで取得してから格納する必要があります。

## まとめ

いかがでしたでしょうか？
今回はApexでカスタムオブジェクトに対する独自処理を実装する為に、独自のREST APIエンドポイントを実装しました。
SFSC側で事前におぜん立てしてくれており、かなり短いコードで要件を満たすことができるという特徴があります。
特に、カスタムオブジェクトのクラス定義を自分で行う必要がなく、またコード内にSOQLをそのまま埋め込めるといった利便性もあり、Javaっぽい文法でありながら、SFSCで必要になるものは直感的に記述できるというのはとても良いと思います。
今回はコントローラークラスに全ての処理を書いてしまっていますが、できればSpring Bootのような形で、サービス（ビジネスロジック）とリポジトリ（SOQL発行してデータ取得する）に責務を分離して、コードをすっきりさせた方が良いとは思いますが、ひとまずハンズオンでApex開発体験をするには、こんな形で雑に実装してしまうのもありかなと思います。

最後までご覧いただき、ありがとうございました。
それではまた次の記事でお会いしましょう。