標準APIのカスタマイズ例

TemPlat Consoleでは、ERDを定義してビルドすることで、ERDで定義したテーブルへのアクセスをするためのRestfulなAPIを自動生成することができます。そしてこれらのAPIは開発者がカスタマイズすることができるので、ここではその例を説明します。

標準APIの仕様に関しては 標準APIの仕様に関して を参考にしてください。

APIカスタマイズのパターン

APIをカスタマイズするにもいくつかパターンがありますので、ここではそのパターン別に説明していきたいと思います。サンプルの説明は順次追加していきます。

カスタマイズパターン
GET系で別テーブルのデータを返したい・親子テーブルで親テーブルと一緒に子テーブルの一覧も取得したい
・親子テーブルで親テーブルの一覧検索時に子テーブルの項目も一緒に取得したい
複数のテーブルにまとめて登録したい・親テーブルと一緒に子テーブルも登録したい
・複数のテーブルを同期を取って更新したい

パターン1:GET系で別テーブルのデータを返したい

標準APIはテーブル単位の操作のみとなっていますので、まずカスタマイズが必要になってくるのが、他のテーブルのデータも合わせてAPIで取得するというケースがかと思います。

例を見ながら進めていきたいと思います。例えばERDで他のTableと関連線を引いたようなケースで必要になるかと思います。以下のERDの例は商品(Product)と商品詳細(ProductDetail)をTableとして分けて定義した場合となります。

標準APIの確認

ここで、ProductをID指定でGETする際、標準APIではProductテーブルの該当レコードのみが取得されますが、関連するProductDetailの複数レコードも一緒に取得したいケースもあるかと思います。ここではその例で説明していきます。

ID指定でProductを取得するAPIの自動生成のソースコードは以下のようになっています。
ソースファイルは「/src/app/handler/product_handler.go」です。

func (h *productHandler) getProduct(c echo.Context) error {
	id := c.Param("id")
	if len(id) == 0 {
		result := &model.Response{}
		result.Error = constant.InvalidRequestParameters
		util.Logger.Error(result.Error)
		return c.JSON(http.StatusBadRequest, result)
	}
	intID, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		result := &model.Response{}
		result.Error = constant.InvalidRequestParameters
		util.Logger.Error(result.Error, err)
		return c.JSON(http.StatusBadRequest, result)
	}
	product, err := h.pr.Get(c, intID)
	if err != nil {
		result := &model.Response{}
		result.Error = constant.DataBaseErrorOnSelect
		util.Logger.Error(result.Error, err)
		return c.JSON(http.StatusBadRequest, result)
	}
	if product == nil {
		result := &model.Response{}
		result.Error = constant.DataBaseErrorNoData
		util.Logger.Error(result.Error)
		return c.JSON(http.StatusNotFound, result)
	}
	return c.JSON(http.StatusOK, product)
}

ここではProductテーブルからリクエストで指定されたIDで1レコードを取得して返しています。ここをカスタマイズして、親のProductのレコードに加えて、子供となるProductDetailテーブルの関連するレコードを一緒に返せるようにしたいと思います。

APIのレスポンスデータの追加

カスタマイズとしてはまずAPIのレスポンスデータ(戻り値)を追加します。非標準ではProduct1レコードを返すようになっていますが、それにに加えてProductDetail複数レコードを返せるように拡張します。

以下が標準APIで返しているデータ構造のモデル(product.go)のソースとなります。
ソースパス: /src/app/model/product.go
ここで見て頂きたいのが、テーブルの項目に加えて「ProductEmbed」というデータが含まれています。この「ProductEmbed」を使って、返却死体データを拡張できます。

type Product struct {
	Kind        string  `datastore:"-" boom:"kind,Product" json:"-" example:""`
	ID          int64   `datastore:"-" boom:"id" json:"id" example:""`
	ProductName *string `datastore:"ProductName" json:"productName" example:""`
	ProductCode *string `datastore:"ProductCode" json:"productCode" example:""`
	Description *string `datastore:"Description" json:"description" example:""`
	ProductType *string `datastore:"ProductType" json:"productType" example:""`
	ProductEmbed
	Entity
}

この「ProductEmbed」は以下のソースに定義されています。
ソースパス: /src/app/model/product_condition.go
このソースは自動生成の上書き対象外となっているので、開発者が修正しても問題ありません。逆に「product.go」の方は基本自動生成で上書きされますので修正しないでください。このあたりの説明は APIカスタマイズと自動生成での上書き禁止設定 にて説明しているので参考にしてください。

では「ProductEmbed」を使って、ProductDeitalを複数返せるようにしましょう。初期のソースは以下のようにtypeのみ定義されており中身は空になっています。

type ProductEmbed struct {
}

ここに追加したいProductDetailを配列(実際はスライス、Slices)を定義します。以下が定義したソースになります。「ProductDetails」という変数名で、型はProductDeitalモデル(ポインター)のスライス型としています。変数名は「パスカルケース(またはアッパーキャメルケース)」で指定してください。

コメントの部分は、データベースである、GCP Datastoreおよび、RESTで返却する際のJSONの項目名との関連を紐付けています。ここではDBアクセスでは使わないので、JSON項目名のみ定義しています。基本は変数名の先頭を小文字にした「キャメルケース」で指定してください。

type ProductEmbed struct {
	ProductDetails []*ProductDetail `datastore:"-" json:"productDetails" example:""`
}

標準APIに子テーブルの取得処理を追加する

APIのレスポンスに項目が追加できたら、続いてはその追加項目にProductDetailのデータを詰める処理をHandlerに追加していきます。Handlerも自動生成処理では上書きされないソースファイルなので開発者が修正して問題無いソースです。

ソースの修正は簡単です。APIで受け取ったProductIDを使って、ProductDetailを検索すればいいのです。この検索は「productdeital_handler.go」の「searchProductDeital」関数を参考になります。以下ProductDetailを検索する部分のコードになります。

	// Productに紐づくProductDetailを取得する
	params := &model.ProductDetailSearchCondition{}
	params.ProductID = &intID
	productDetails, err := h.rm.ProductDetail.GetAll(c, params)
	if err != nil {
		result := &model.Response{}
		result.Error = constant.DataBaseErrorOnSelect
		util.Logger.Error(result.Error, err)
		return c.JSON(http.StatusInternalServerError, result)
	}
	product.ProductDetails = productDetails

検索に使うProductID(int64型)は、intIDという形で既にProductのGETで使われていますので、これをProductDetailの検索用の定義にセットしてあげて、ProductDeitalリポジトリのGetAll()関数を呼んであげるだけです。

Handlerにて他のテーブル用のリポジトリを使用したい場合は「h.rm」からアクセスできます。このrmは「Repository Master」という意味で全テーブルのリポジトリへのアクセスができるようになっています。

一応、関数全体のソースも載せておきます。

func (h *productHandler) getProduct(c echo.Context) error {
	id := c.Param("id")
	if len(id) == 0 {
		result := &model.Response{}
		result.Error = constant.InvalidRequestParameters
		util.Logger.Error(result.Error)
		return c.JSON(http.StatusBadRequest, result)
	}
	intID, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		result := &model.Response{}
		result.Error = constant.InvalidRequestParameters
		util.Logger.Error(result.Error, err)
		return c.JSON(http.StatusBadRequest, result)
	}
	product, err := h.pr.Get(c, intID)
	if err != nil {
		result := &model.Response{}
		result.Error = constant.DataBaseErrorOnSelect
		util.Logger.Error(result.Error, err)
		return c.JSON(http.StatusBadRequest, result)
	}
	if product == nil {
		result := &model.Response{}
		result.Error = constant.DataBaseErrorNoData
		util.Logger.Error(result.Error)
		return c.JSON(http.StatusNotFound, result)
	}

	// ---- ★★★★start customize★★★★ ---- //
	// Productに紐づくProductDetailを取得する
	params := &model.ProductDetailSearchCondition{}
	params.ProductID = &intID
	productDetails, err := h.rm.ProductDetail.GetAll(c, params)
	if err != nil {
		result := &model.Response{}
		result.Error = constant.DataBaseErrorOnSelect
		util.Logger.Error(result.Error, err)
		return c.JSON(http.StatusInternalServerError, result)
	}
	product.ProductDetails = productDetails
	// ---- ★★★★★end customize★★★★★ ---- //

	return c.JSON(http.StatusOK, product)
}

ソース修正終了後の進め方

ソースの修正が完了したら、CSR(ソースリポジトリ)に反映させます。ここもgitによる操作としては以下の流れとなります。

① 開発PCのローカルリポジトリに修正をcommitする
基本ブランチはmasterのままで問題ありません。
② GCPのリモートリポジトリ(GSRのこと)へpushする
cloneしてきた時点でリモートリポジトリとしてGSRが登録されていますので、そこへgit pushします。

以下修正後のgitコマンドでの操作サンプルを載せておきます。
まずは、修正後のローカルリポジトリのステータスを確認します。修正した2つのソースが未コミット状態になっています。

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   src/app/handler/product_handler.go
        modified:   src/app/model/product_condition.go

これらのソースをまずはローカルリポジトリにコミットして、リモートリポジトリへpushします。

$ git commit -M "getProduct時にProductDeitalも取得"
$ git push origin master

ソースがGSRにpushされると、GCP側で自動的に修正されたソースでCloudRun環境へのデプロイを実行してくれます。これはCloudBuildという自動ビルドの設定がされているからです。

ただ、今回はAPIのレスポンスに項目を追加しているので、上記デプロイとは別に、TemPlat Consoleでのサーバビルドを実施しておく必要があります。これはAPIの仕様が変更されているので、TemPlat Consoleが自動生成しているSwaggerファイルや、そのSwaggerファイルから自動生成されるクライアント用のソースコードに変更内容を反映しておきたいためです。

TemPlat Consoleからのビルドは以下から実施できます。手順は TemPlatのビルド方法 を参考にしてください。

Swagger UIでの確認

カスタマイズして、TemPlat Consoleでビルドの完了したら、早速SwaggerUI経由でAPIの確認をしてみましょう。TemPlat Consoleでビルドが完了するとプロジェクトのトップページに画面が戻ります。そこにSwaggerUIページへのリンクがありますので、早速開いてみましょう。

Swagger UIページに移動したら今回カスタマイズした「ProductのgetProduct」を試したいですが、その前にテスト用のデータを登録しておきましょう。SwaggerUIから登録用のAPIも呼び出せます。

確認手順は以下となります。
①Productを1件登録する
②登録したProductに紐づくProductDetailレコードを数件登録する
③ProductのgetProductを呼び出せてProductDetailも合わせて取得できるか確認する

SwaggerUIの基本的な操作に関しては SwaggerUIの使い方 を参考にしてください。

まずはProductの登録からです。bodyとして登録するデータを設定しますが、初期で表示される項目にはTableの全項目と、カスタマイズで追加した項目が表示されますが、以下の項目は登録時には不要なので項目毎消してしまいます。


登録が成功すると、レスポンスにProductの「id」が返ってくるので、それを記録しておいて、ProductDetailの登録で使います。

続いてProductDetailも登録します。要領はProductと一緒です。先程記録したProductIDをここで指定します。

ProductDetailは複数件登録しておきましょう。最後にProductのGETをしてみましょう。

以下のような結果が返ってくれば成功です。