Technical Articles
SAP Cloud Application Programming Model(CAP):カスタムロジックの実装 – Part 2
はじめに
Node.jsを用いたカスタムロジックの定義方法を紹介します。本ブログはこちらのブログの続編です。
1. カスタムロジック定義
CAPではサービスおよびDBを定義すると自動的にそれらを扱うためのCRUDサービスが生成されますが、そのサービスを拡張することも可能です。主な拡張可能な箇所はイベントとして用意されており、それらは次の3か所です。HTTPリクエストがCRUDサービスを呼び出すとき、1、2、および3のイベントが順に呼び出されます。
- before
- on
- after
CAPが自動で生成するCRUDサービスは2. onイベントに配置されています。開発者はこのイベントを置き換えて自由に処理を記述することも可能です。生成されたCRUDサービスを利用した上で拡張することもできます。その場合にはbeforeおよびafterのイベントを使用します。beforeイベントでは主に入力チェックなどの処理を実装し、afterイベントでCRUDイベント後の後処理を実装します。技術的にはonイベントにすべての処理を実装することも可能ですが、コードの可読性やポータビリティを考えるとonイベントを実装する場合でもbeforeおよびafterイベントに必要な処理を分割して記述すべき場合が多い※でしょう。
※アプリケーションに関わる要件や制約を考慮して決めてください。
- カスタムロジックを実装するファイルを作成
srvディレクトリ配下にcat-service.jsを作成します。cat-service.cdsに対応するカスタムハンドラは拡張子を.jsにした同じ名前のファイルを作成することで用意できます。他にもアノテーションでサービスから対応するカスタムハンドラのファイルを指定する方法などがあります。
- カスタムロジックの記述
以下の内容を作成したcat-service.jsに書き込みます。
module.exports = (srv) => {
const {Books} = cds.entities ('my.bookshop')
// Reduce stock of ordered books
srv.before ('CREATE', 'Orders', async (req) => {
const order = req.data
if (!order.amount || order.amount <= 0) return req.error (400, 'Order at least 1 book')
const tx = cds.transaction(req)
const affectedRows = await tx.run (
UPDATE (Books)
.set ({ stock: {'-=': order.amount}})
.where ({ stock: {'>=': order.amount},/*and*/ ID: order.book_ID})
)
if (affectedRows === 0) req.error (409, "Sold out, sorry")
})
// Add some discount for overstocked books
srv.after ('READ', 'Books', each => {
if (each.stock > 111) each.title += ' -- 11% discount!'
})
}
コーディングした内容を紹介します。
module.exports = (srv) => {
}
まずはmodule.exportsに渡すオブジェクトを用意します。この関数オブジェクトには引数を一つ用意しています。この引数のオブジェクトを用いてbeforeイベントなどの登録メソッドを呼び出します。
module.exports = (srv) => {
const {Books} = cds.entities ('my.bookshop')
}
名前空間のmy.bookshopに定義しているBooksエンティティを用意します。このエンティティはデータベースへアクセスするときのエンティティ指定に使用します。
module.exports = (srv) => {
const {Books} = cds.entities ('my.bookshop')
srv.before ('CREATE', 'Orders', async (req) => {
})
}
srv.beforeを呼び出し、次の3つの引数を与えます。これによって”Orders”エンティティの”CREATE”処理がonイベントにおいて実行される前に実行される関数オブジェクトを登録することができます。
- 第一引数:割り当てるリクエストの種類の文字列
- 第二引数:割り当てるエンティティの文字列
- 第三引数:カスタムロジックを定義した関数オブジェクト
第一引数および第二引数は文字列だけでなく、文字列の配列を渡すこともできます。例えば[‘CREATE’, ‘UPDATE’, ‘DELETE’]のように記述し、更新系の処理をまとめて定義することもできます。beforeに渡す関数オブジェクトで受け取れる第一引数には、リクエストとして受け取ったデータ本体やサービス呼び出し時のURLを解釈したオブジェクト、またエラーハンドリングのためのメソッドなどが含まれています。
module.exports = (srv) => {
const {Books} = cds.entities ('my.bookshop')
srv.before ('CREATE', 'Orders', async (req) => {
const order = req.data
})
}
リクエストボディとして受け取ったデータをreq.dataから取り出します。
module.exports = (srv) => {
const {Books} = cds.entities ('my.bookshop')
srv.before ('CREATE', 'Orders', async (req) => {
const order = req.data
if (!order.amount || order.amount <= 0) return req.error (400, 'Order at least 1 book')
})
}
リクエストとして送られてきたレコードのうち、amountの値がない場合もしくは0以下の場合に400のHTTPステータスコードでイベントを終了するように記述しています。req.errorメソッドでは第一引数にHTTPステータスコードの数値、第二引数にHTTPステータスメッセージを文字列として渡すことができます。第二引数は省略可能です。
module.exports = (srv) => {
const {Books} = cds.entities ('my.bookshop')
srv.before ('CREATE', 'Orders', async (req) => {
const order = req.data
if (!order.amount || order.amount <= 0) return req.error (400, 'Order at least 1 book')
const tx = cds.transaction(req)
})
}
トランザクションの箱を用意します。cds.transactionメソッドにイベント登録メソッドの第三引数から受け取れる引数(ここではreq)を渡すことで生成します。
module.exports = (srv) => {
const {Books} = cds.entities ('my.bookshop')
srv.before ('CREATE', 'Orders', async (req) => {
const order = req.data
if (!order.amount || order.amount <= 0) return req.error (400, 'Order at least 1 book')
const tx = cds.transaction(req)
await tx.run (
UPDATE (Books)
.set ({ stock: {'-=': order.amount}})
.where ({ stock: {'>=': order.amount},/*and*/ ID: order.book_ID})
)
})
}
トランザクションの箱に実行するクエリを登録します。txオブジェクトにはSQLライクなクエリを記述することができるCQN(Query Notation)を用いてトランザクションの箱に登録するクエリを表現できます。ここではwhereメソッドの中に書いた条件に該当するレコードに対し、setメソッドに与えた変更を加えています。UPDATEメソッドに渡した引数を用いてクエリの実行対象であるエンティティを指定しています。
module.exports = (srv) => {
const {Books} = cds.entities ('my.bookshop')
srv.before ('CREATE', 'Orders', async (req) => {
const order = req.data
if (!order.amount || order.amount <= 0) return req.error (400, 'Order at least 1 book')
const tx = cds.transaction(req)
const affectedRows = await tx.run (
UPDATE (Books)
.set ({ stock: {'-=': order.amount}})
.where ({ stock: {'>=': order.amount},/*and*/ ID: order.book_ID})
)
if (affectedRows === 0) req.error (409, "Sold out, sorry")
})
}
UPDATEメソッドは返り値として実行した結果反映されたレコード数を返します。その値を受け取りエラーをハンドリングします。もし変更されたレコードがない場合に409のHTTPステータスコードを返しています。以上で”Orders”エンティティの”CREATE”処理における”before”イベントの定義は終了です。
module.exports = (srv) => {
const {Books} = cds.entities ('my.bookshop')
// Reduce stock of ordered books
srv.before ('CREATE', 'Orders', async (req) => {
const order = req.data
if (!order.amount || order.amount <= 0) return req.error (400, 'Order at least 1 book')
const tx = cds.transaction(req)
const affectedRows = await tx.run (
UPDATE (Books)
.set ({ stock: {'-=': order.amount}})
.where ({ stock: {'>=': order.amount},/*and*/ ID: order.book_ID})
)
if (affectedRows === 0) req.error (409, "Sold out, sorry")
})
srv.after ('READ', 'Books', each => {
})
}
続いて”Books”エンティティの”READ”処理における”after”イベントに関数オブジェクトを登録します。afterメソッドの第三引数では実行結果として返される各レコードを受け取ることができます。したがって、今回のようなREAD処理の結果、複数のレコードが返ってきている場合はそのレコードの数だけ登録した関数オブジェクトが呼び出されます。
module.exports = (srv) => {
const {Books} = cds.entities ('my.bookshop')
// Reduce stock of ordered books
srv.before ('CREATE', 'Orders', async (req) => {
const order = req.data
if (!order.amount || order.amount <= 0) return req.error (400, 'Order at least 1 book')
const tx = cds.transaction(req)
const affectedRows = await tx.run (
UPDATE (Books)
.set ({ stock: {'-=': order.amount}})
.where ({ stock: {'>=': order.amount},/*and*/ ID: order.book_ID})
)
if (affectedRows === 0) req.error (409, "Sold out, sorry")
})
srv.after ('READ', 'Books', each => {
if (each.stock > 111) each.title += ' -- 11% discount!'
})
}
stockの値が111以上であった場合に各レコードのtitle末尾に” — 11% discount!”の文字列を追記しています。
おわりに
Part 3に続きます。Part 3ではUIの追加を扱う予定です。
ご共有ありがとうございます。
Part 3をお待ちしております!
よろしくお願い致します。