Http Method無法表達出某些動作

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

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,但比較少看到有人使用。由於以上原因,接下來我要分享比較簡單且有不少案例的方法。

這個方法是根據這篇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

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

  • Treat it like a sub-resource.
  • Patch for the partial update.

如果可以是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)

如果不想濫用,你可以把動作是非同步做為前置條件。

以上述的方法,我們來討論電源管理該怎麼做。首先我們把電源當成一個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為不同狀態,這使我思考為什麼他會這樣想。

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

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。主要原因有以下:

  • 合適的media type: PATCH所能使用的media type種類相當多,對於同時支援xml與json格式的我們,要選哪一條路還無定論。PATCH種類有機會再介紹。
  • 不容易寫好維護的程式: 由於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);
}
  • 符合多種情況的request參數: 面對不同的傳遞參數方式,目前我還沒找到一個共用的方法。

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

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

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

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