目錄表

Http Method無法表達出某些動作

Introduction

在設計RestAPI時,並非所有domain操作都符合CRUD;例如,model中資料的同步(sync)、有transaction的行如轉換(transfer)、搬移(move)、複製(copy)等;因此做些案例研究看是否能找尋到自己能滿意的做法。本篇文章是根據研究結果,分享針對操作(action)做resource modeling的心得。

Model the action as a service resource

action本身就是一種service,如果能把它model成一個service resource,是再好不過的。最常見的例子是login/logout,可以model為sessions,以vCloud為例:

POST https://vcloud.example.com/api/sessions 
DELETE https://vcloud.example.com/api/sessions
在伺服器管理的domain中,可能有像修改firmware設定、更新firmware或掃描硬體等操作,這些該如何model為resource呢? 首先是修改firmware設定,以HPE Server Management API為例,它將firmware種類model為一種resource,而設定是它的sub-resource:
GET /Systems/1/BIOS/Settings
PATCH /Systems/1/BIOS/Settings
接著是更新firmware,以Dell ASM REST API為例,使用store resource的方式去操作:
PUT /ManagedDevice/firmware
最後是掃描硬體,以RHEL Virtualization為例,透過post去新增一個discover的sub-resource(task):
POST /api/hosts/2ab5e1da-b726-4274-bbf7-0a42b16a0fc3/iscsidiscover HTTP/1.1
Accept: application/xml
Content-Type: application/xml

<action>
    <iscsi>
        <address>mysan.example.com</address>
    </iscsi>
</action>

HTTP/1.1 202 Accept
Content-Type: application/xml

<action id="e9126d04-0f74-4e1a-9139-13f11fcbb4ab"
  href="/api/hosts/2ab5e1da-b726-4274-bbf7-0a42b16a0fc3/iscsidiscover/
  e9126d04-0f74-4e1a-9139-13f11fcbb4ab">
    <iscsi_target>iqn.2009-08.com.example:mysan.foobar</iscsi_target>
    ...
<action>
看起來好像很容易,但要做到這些需要經驗與想像力,否則model出來的resource可能會讓user覺得困惑。除此之外,還要考量是否有非同步需求;以更新firmware來說,雖然PUT不是不能回傳202,但比較少看到有人使用。由於以上原因,接下來我要分享比較簡單且有不少案例的方法。

A simple way: store resource/PATCH/controller resource

這個方法是根據這篇best practiceREST API Design Rulebook與case studies整理出來的,給大家當一個參考方法。
我們以下面的動作為例:

lock file
unlock file
power on
power off system
activate user
deactivate user
login 
logout
search
import
update firmware
config bios

Classify the actions

首先我們必須先分類;目前我分成兩類,
1. The positive and negative action,動作有正向與反向:

lock/unlock file
power on/off system
activate/deactivate
login/logout
2. The procedural action,動作是一個執行過程:
search
import
update firmware
config bios

The positive and negative action

假如你的動作是屬於這個種類,你有兩個選擇,

如果可以是resource中的屬性,可以考慮用PATCH的方式去更新屬性值:

Patch /Users/12345
{“active”: false}
另外一個選擇就是把它當sub-resource,也就是store resource的做法;positive的action使用PUT,negative的action使用DELETE。以githubbox為例:
lock/unlock (github)
    PUT /repos/:owner/:repo/issues/:number/lock
    DELETE /repos/:owner/:repo/issues/:number/lock
apply/remove the watermark (box)
    PUT https://api.box.com/2.0/files/file_id/watermark
    DELETE https://api.box.com/2.0/files/file_id/watermark

The procedural action

這個能列舉的範例很多,只要不是CRUD的操作,你可以選擇把它model為controller resource:

Search
    POST /v1/users/search (instagram)
    POST /1/indexes/{indexName}/query (algolia)
    POST /indexes/hotels/docs/search (azure)
    POST facebook, twitter, box, github, etc..
POST /videos/reportAbuse (youtube)
如果不想濫用,你可以把動作是非同步做為前置條件。

Case Study: Power Management

以上述的方法,我們來討論電源管理該怎麼做。首先我們把電源當成一個resource,而狀態為其屬性;所以我們可以透過GET去取得狀態,而透過PUT或PATCH去修改狀態:

GET /hosts/123/power
{'state':'on'}
PUT /hosts/123/power
{'state':'on'}
PUT /hosts/123/power
{'state':'off'}
這樣的做法,有哪些問題我們需要考量?

  1. hypermedia: 我們有辦法表達出取得狀態、關機、開機等的link嗎?
  2. synchronized: 在我們做開機與關機動作後,是可以立即反應的嗎?
  3. implementation: 如果使用PATCH的話,我比較不是那麼喜愛,原因稍後再做說明。

考慮以上原因,或許把它做成store resource會比較好:

GET /hosts/123/power_status
{'state':'on'}
PUT /hosts/123/power_on
PUT /hosts/123/power_off
這裡我並沒有使用DELETE,因為在語義上使用DELETE power_on不會比PUT power_off來得清楚。此外,在使用這種方式後,hypermedia可以很容易的使用URI去表達出不同的意義。剩下的問題就是synchronized。

PUT回傳202的方式,目前有看過HPE OneView REST API存在這樣的設計;HTTP規格書也沒說這樣是不對的。我覺得值得討論的部分是: idempotent。假設PUT回傳202,這代表著server將產生一個asynchronized的task,每次PUT power_on所產生的task是否會相同呢? 假如不同,是不是代表違反了idempotent? 假如相同,實作會不會蠻奇怪的呢? 這個我目前沒有答案。

另外一個選擇,就是把它當controller resource,使用POST去操作:
POST /hosts/123/power_on
POST /hosts/123/power_off
有個實際案例就是vCloud的Power On/Off API。順便提一下,會使用這個範例是由於在Roy Fielding在2008年It is okay to use POST文章中,留言區有人提及此範例;而Roy Fielding也是傾向於將狀態與操作model為不同狀態,這使我思考為什麼他會這樣想。

Some questions

針對以上提到的方法,我還思考著幾個問題:

Why don't you use the query string or formdata to pass the action?

(這裡先撇開query string、formdata或request body等方式的差異) 對我而言,我目前認為有幾點需要考量:

  1. hypermedia: 假如要表達出query string等方式,會需要使用template的方式,實作上不會比單純透過URI Path的方式容易。
  2. http method convention: 如果在collection上使用POST,是否會讓新增與其它action讓使用者混淆。
  3. implementation: 我們看看如果使用URI的Path來實做lock與unlock可能會長怎樣:

	@RequestMapping(value = "/files/{fid}/lock", method = RequestMethod.PUT, produces = {"application/json"})
	public OutputData<HostRestBean> lockFile(@PathVariable("fid") String aFid, HttpServletResponse  aResponse){
		// ...
	}
 
	@RequestMapping(value = "/files/{fid}/unlock", method = RequestMethod.DELETE, produces = {"application/json"})
	public OutputData<HostRestBean> unlockFile(@PathVariable("fid") String aFid, HttpServletResponse  aResponse){
		// ...
	}
接著使用POST+RequestParam:
	@RequestMapping(value = "/files/{fid}", method = RequestMethod.POST, produces = {"application/json"})
	public OutputData<HostRestBean> opFile(@PathVariable("fid") String aFid, 
			@RequestParam(value="action", required=true) String action,
			HttpServletResponse  aResponse) {
		if("lock".equalsIgnoreCase(action)) {
			// ...
		} else if("unlock".equalsIgnoreCase(action)){
			// ...
		} else
			// ...
	}
你喜歡哪個? 假如File的action只有lock與unlock,那真的是天下太平;但事實上,action還有move、copy、rename等。考慮一下測試、維護、擴充的話,哪一個會比較好? 以擴充與維護來說,RequestParam的方式讓所有的action都必須接受同一組參數甚至輸出,增加了修改的麻煩;URI Path則由各別的實做去決定。測試則是因為實做已分開,根據各別操作的目的去測試即可。

Threat the P/N action as a store resource or use PATCH?

在我們的專案中,P/N action我傾向使用store resource大於PATCH。主要原因有以下:

@PatchMapping(value = "/files/{id}")
public ResponseEntity<String> editFile(@RequestBody FileBean updateFile, 
		@PathVariable("id") String fileId){
	File file = FileDao.getFile(fileId);
	if( file == null ) {
		return new ResponseEntity<String>("File doesn't exist",  HttpStatus.NOT_FOUND);
	}
 
	if( updateFile.locked != null )
		file.locked = Boolean.parseBoolean(updateFile.locked);
	if( updateFile.name != null )
		file.name = updateFile.name;
	return new ResponseEntity<String>("good",  HttpStatus.OK);
}

這樣抉擇的發生,是建立在動作屬於Resource中的一個attribute時。

Summary

面對一個非CRUD的domain操作,我們該如何model為resource呢?

  1. 盡力把它變成service resource,可參考他人做法。
  2. 確認是否為一個procedure,是的話就把它當controller resource;可以把非同步的性質當前置條件。
  3. 確認是否為resource中的attribute,是的話可以使用PATCH。
  4. 如果不想用PATCH,可以考慮作為store resource。

以上方法可以當參考,還是要以需求為重;另外補充我看過的其它的案例

Reference