こんにちは!
LIFULLエンジニアの吉永です。
前回の記事に引き続き、直近でSalesforce Service Cloud(以降SFSCと略称します)関連の開発を行っているので、今回はApexを使ってカスタムオブジェクトへのCRUD操作を行うREST APIエンドポイントを実装するまでの流れを紹介したいと思います。
前回の記事はこちら
Apexとは?
ApexはSFSC上で何かしらの機能を開発する際に用いるプログラミング言語で、文法はJavaに非常に似ている為、JavaやC#での開発経験がある人は特に戸惑うことなく習得が可能だと思います。
SF関連は非常にドキュメントが充実している印象があるので、情報を探すのに苦労はしないとは思いますが、私なりの理解でApexでできることの具体例を列挙すると、
- 複合主キーを疑似的に実現する場合、入力画面では二つの別項目になっているが、API経由でレコード保存する際に連結したり、独自で入力チェックロジックを実装することができる。
- 特定のレコード作成時にその操作をトリガーとして、必ず特定のレコードを作成するなどができる。
などのように、公式ページにも書いてありますが、組織独自のビジネスロジックを実現する際の一つの手段としてApexが使えると思います。
もちろん、上記に上げた例はApex以外でも実現する方法はあると思いますし、Apexで実現することがベストプラクティスではないことも多々あるかと思いますが、Apexでできることを把握しておくと、色々な要件に柔軟に対応できると思うので、おススメです。
今回ApexでREST APIを実装する対象のオブジェクト
前回の記事で作成したカスタムオブジェクトをそのまま利用しようと思いますので、この記事から見始めていて、この後の手順を自分の環境でも試してみたい方は、カスタムオブジェクト作成の手順を参考に、自分の検証用のSFSCに環境を作ってみてください。
実装するロジック
今回はJSON形式の注文情報をREST APIで受取り、受け取った情報からMY注文オブジェクトとMY注文明細オブジェクトへレコードを新規登録するロジックをApexで実装します。
せっかくなので、登録したレコードを取得できるロジックも合わせて実装します。
なお、今回の要件ではMY注文オブジェクトのみのレコード登録は受け付けず、必ず一件以上のMY注文明細オブジェクトのレコード情報も受け取るJSONに含めるようにして、入力チェックもその前提で実装することにします。
取得はMY注文オブジェクトの主キーをパラメーターとして受け取り、レスポンスはMY注文オブジェクトとそれに紐づくMY注文明細オブジェクトをマージした情報を返却するようにします。
実装するREST APIの仕様
エンドポイントの/services/apexrest/
はSFSCで独自のREST APIエンドポイントを実装する際に必ずつくプレフィックスのようなものになっています。
詳細は公式ページのこちらを参照ください。
エンドポイントURL | HTTPメソッド |
---|---|
/services/apexrest/myorder/{MY注文オブジェクトの主キー} | GET |
/services/apexrest/myorder/ | POST |
GETのパラメータ例
/services/apexrest/myorder/Order0001
POSTのパラメータ例
{
"orderid": "Order0003",
"orderdetail": [
{
"detailid": "OrderDetail0004",
"itemid": "ITEM0001",
"count": 1
},
{
"detailid": "OrderDetail0005",
"itemid": "ITEM0002",
"count": 2
}
]
}
実装の流れ
ApexでREST APIを実装するには大きく分けて三つの作業が必要になります。
- Apexでロジックを実装する。
- 実装したApexのユニットテストコードを実装する。
- 実装したApexに接続する為のアプリケーション設定をSFSCに追加する。
今回はApexで実装、ワークベンチで動作確認するまでをゴールとしたいので、ユニットテストの実装及びアプリケーション設定は割愛しますが、次回以降の記事でこれらのことにも触れようと思ってます。
Apex実装
とりあえず手を動かしてみないことにはなかなか頭に入ってこないと思いますので、さっそくApexを実装するところから始めたいと思います。
自分の検証環境のSFSCホーム画面から、右上のギアアイコンを押し、表示されるメニューから開発者コンソールを選択します。
[File]→[New]→[Apex Class]を選択します。
ファイル名にMyOrderRestApi
と入力してOKを押します。
GETメソッド実装
@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;
}
}
}
上記のコードを開発者コンソールの画面に貼り付け、保存します。
ワークベンチにアクセスし、I agree to the terms of service
横のチェックボックスにチェックを入れ、Login With Salesforce
を押します。
ログイン後、画面右上のutilities
のREST Explorer
を押します。
GETメソッド正常系確認
REST API動作確認用の画面に切り替わりますので、テキストボックスに/services/apexrest/myorder/Order0001
と入力し、Execute
ボタンを押します。
すると、自分で作成したApex REST APIのdoGetメソッドが呼び出され、SQOL実行後に処理が成功すれば上図のようなレスポンスが得られます。
参考までに、レスポンスに含まれる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メソッド異常系確認 パラメーターエラー
試しにエラーチェックも機能しているか確認します。
テキストボックスに/services/apexrest/myorder/Order000
と入力して、Execute
ボタンを押すと今度は上図のようなエラーが返却されていることが確認できました。
GETメソッド異常系確認 存在しないレコード指定
続いて、存在しない注文IDを入力してみます。
テキストボックスに/services/apexrest/myorder/Order0000
と入力して、Execute
ボタンを押すと今度は上図のようなエラーが返却されていることが確認できました。
SOQLを変更してレスポンスにフィールドを増やしてみる
先ほど作成したApexでは商品名は分かっても、商品の単価がレスポンスに含まれておらずわかりませんでした。
Apex内で発行しているSOQLを下記のように変更して、再度実行してみます。
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が以下のようになると思います。
{
"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アノテーションにて定義します。
@RestResource(urlMapping='/myorder/*')
定義する際は、/services/apexrest/
以下の独自で定義するエンドポイントの名称から記述します。
*
はワイルドカード指定で、/services/apexrest/myorder/hogehoge
などのように、``/services/apexrest/myorder/`にマッチすればこのApexクラスを実行するということになります。
続いて、HTTPメソッドでどのメソッドが指定されたかに応じて呼び出すクラス内のメソッドの指定をHttpGetアノテーションで指定します。
コードでは下記の部分です。
@HttpGet
global static void doGet() {}
これを例えば、
@HttpGet
global static void doGet() {}
@HttpPost
global static void doPost(String param) {}
などのように、そのエンドポイントでサポートするメソッド毎に対応するクラス内のメソッドの直前にアノテーションを付与して関連づけます。
上記の例ではHTTPのPOSTメソッドが指定された際に実行するdoPost
というクラス内のメソッドを定義しています。
最後に、HTTPリクエストに対するレスポンスの設定ですが、コードだと一見するとメソッドで何か値をリターンしているわけではないので分かりづらいのですが、グローバル変数(っていうと語弊がありそうですが、システム全体のリクエストでグローバルではなく、HTTPリクエスト単位でグローバルって方が正しい表現かもです)に設定しています。
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
まず、RestContext.request
はHTTPリクエストの内容を取得できるオブジェクトで、RestContext.response
がレスポンスを設定するオブジェクトです。
res.statusCode = 400;
なので、doGetメソッド内の上記はレスポンスのHTTPステータスコードを400で初期化しています。
if (orderid.length() != 9){
// パラメーターエラー
return;
}
URLから取り出したパラメーターの文字数が不正だった場合は即returnしてますが、先ほどの処理で400で初期化されているので、ここでは特に何もせず早期リターンしているだけということです。
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メソッドを実装します。
下記のコードを開発者コンソールの画面に貼り付け、保存します。
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正常系確認
テキストボックスに/services/apexrest/myorder/
と入力し、ラジオボタンをPOSTに切り替え、Request Bodyに下記JSONを入力して、Execute
ボタンを押します。
{
"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がレスポンスで確認できると思います。
{
"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をデシリアライズしてもらって受け取る方法と、自分でリクエストボディをデシリアライズする方法があります。
今回は後者を選択しましたが、パラメーターが単純でプリミティブ型の場合は前者の方法を選択した方が楽だと思います。
// リクエストBODYをデシリアライズ
String reqString = req.requestBody.toString();
// JSON文字列をParamOrderクラスへデシリアライズ
ParamOrder param = (ParamOrder) System.JSON.deserialize(reqString, ParamOrder.class);
上記の方法では事前にインナークラスでリクエストボディをデシリアライズするようのクラス定義をしてあります。
public class OrderDetail {
public String detailid;
public String itemid;
public Integer count;
}
global class ParamOrder {
public String orderid;
public List<OrderDetail> orderdetail;
}
カスタムオブジェクトへ新規レコードを登録する場合はinsert
を用います。
// 注文オブジェクトへ新規レコード登録
MyOrder__c order = new MyOrder__c();
order.Name = param.orderid;
insert order;
insertするには、まず対象となるカスタムオブジェクトのインスタンスを生成し、そのインスタンスにリクエストボディの値を反映してからinsertします。
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開発体験をするには、こんな形で雑に実装してしまうのもありかなと思います。
最後までご覧いただき、ありがとうございました。
それではまた次の記事でお会いしましょう。