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 practice、 REST 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。以github與box為例:
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'}
這樣的做法,有哪些問題我們需要考量?
- hypermedia: 我們有辦法表達出取得狀態、關機、開機等的link嗎?
- synchronized: 在我們做開機與關機動作後,是可以立即反應的嗎?
- 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等方式的差異) 對我而言,我目前認為有幾點需要考量:
- hypermedia: 假如要表達出query string等方式,會需要使用template的方式,實作上不會比單純透過URI Path的方式容易。
- http method convention: 如果在collection上使用POST,是否會讓新增與其它action讓使用者混淆。
- 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時。
Summary
面對一個非CRUD的domain操作,我們該如何model為resource呢?
- 盡力把它變成service resource,可參考他人做法。
- 確認是否為一個procedure,是的話就把它當controller resource;可以把非同步的性質當前置條件。
- 確認是否為resource中的attribute,是的話可以使用PATCH。
- 如果不想用PATCH,可以考慮作為store resource。
以上方法可以當參考,還是要以需求為重;另外補充我看過的其它的案例。
Reference
- Why does including an action verb in the URI in a REST implementation violate the protocol?
- REST API Design RuleBook Rule: A verb or verb phrase should be used for controller names
- Confusion Between Noun vs. Verb in Rest URLs
- Do you really know why you prefer REST over RPC?
- HTTP OPTIONS method
- Understanding rest and rpc for http apis
- Patch VS PUT Store Resource
- Instangram REST API
- it is okay to use post
- Best Practices for Designing a Pragmatic RESTful API
留言
張貼留言