差異處
這裏顯示兩個版本的差異處。
java:web:restapi:http_method_cant_make_sense [2017/09/25 00:29] tony [Model the action as a service resource] |
java:web:restapi:http_method_cant_make_sense [2023/06/25 09:48] |
||
---|---|---|---|
行 1: | 行 1: | ||
- | {{tag>rest}} | ||
- | ====== 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,以[[https://pubs.vmware.com/vcd-55/index.jsp|vCloud]]為例: | ||
- | <code> | ||
- | POST https://vcloud.example.com/api/sessions | ||
- | DELETE https://vcloud.example.com/api/sessions | ||
- | </code> | ||
- | 在伺服器管理的domain中,可能有像修改firmware設定、更新firmware或掃描硬體等操作,這些該如何model為resource呢? 首先是修改firmware設定,以[[http://h20566.www2.hpe.com/hpsc/doc/public/display?docId=c04423967|HPE Server Management API]]為例,它將firmware種類model為一種resource,而設定是它的sub-resource: | ||
- | <code> | ||
- | GET /Systems/1/BIOS/Settings | ||
- | PATCH /Systems/1/BIOS/Settings | ||
- | </code> | ||
- | 接著是更新firmware,以[[http://www.dell.com/support/manuals/tw/en/twdhs1/dell-active-system-mngr-v8.1/asm%20rest%20api-v1/manageddevicefirmware-put?guid=guid-ad63b2c1-00c4-4ce3-ad7a-a498e15f6636&lang=en-us|Dell ASM REST API]]為例,使用store resource的方式去操作: | ||
- | <code> | ||
- | PUT /ManagedDevice/firmware | ||
- | </code> | ||
- | 最後是掃描硬體,以[[https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Virtualization/3.0/html-single/REST_API_Guide/index.html|RHEL Virtualization]]為例,透過post去新增一個discover的sub-resource(task): | ||
- | <code> | ||
- | 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> | ||
- | </code> | ||
- | 看起來好像很容易,但要做到這些需要經驗與想像力,否則model出來的resource可能會讓user覺得困惑。除此之外,還要考量是否有非同步需求;以更新firmware來說,雖然PUT不是不能回傳202,但比較少看到有人使用。由於以上原因,接下來我要分享比較簡單且有不少案例的方法。 | ||
- | ===== A simple way: store resource/PATCH/controller resource ===== | ||
- | 這個方法是根據[[http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api|這篇best practice]]、 | ||
- | [[http://shop.oreilly.com/product/0636920021575.do|REST API Design Rulebook]]與case studies整理出來的,給大家當一個參考方法。\\ | ||
- | 我們以下面的動作為例: | ||
- | <code> | ||
- | lock file | ||
- | unlock file | ||
- | power on | ||
- | power off system | ||
- | activate user | ||
- | deactivate user | ||
- | login | ||
- | logout | ||
- | search | ||
- | import | ||
- | update firmware | ||
- | config bios | ||
- | </code> | ||
- | ==== Classify the actions ==== | ||
- | 首先我們必須先分類;目前我分成兩類,\\ | ||
- | 1. The positive and negative action,動作有正向與反向: | ||
- | <code> | ||
- | lock/unlock file | ||
- | power on/off system | ||
- | activate/deactivate | ||
- | login/logout | ||
- | </code> | ||
- | 2. The procedural action,動作是一個執行過程: | ||
- | <code> | ||
- | search | ||
- | import | ||
- | update firmware | ||
- | config bios | ||
- | </code> | ||
- | ==== The positive and negative action ==== | ||
- | 假如你的動作是屬於這個種類,你有兩個選擇, | ||
- | * Treat it like a sub-resource. | ||
- | * Patch for the partial update. | ||
- | 如果可以是resource中的屬性,可以考慮用PATCH的方式去更新屬性值: | ||
- | <code> | ||
- | Patch /Users/12345 | ||
- | {“active”: false} | ||
- | </code> | ||
- | 另外一個選擇就是把它當sub-resource,也就是store resource的做法;positive的action使用PUT,negative的action使用DELETE。以[[https://developer.github.com/v3/issues/|github]]與[[https://developer.box.com/reference|box]]為例: | ||
- | <code> | ||
- | 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 | ||
- | </code> | ||
- | |||
- | ==== The procedural action ==== | ||
- | 這個能列舉的範例很多,只要不是CRUD的操作,你可以選擇把它model唯一個controller resource: | ||
- | <code> | ||
- | 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) | ||
- | </code> | ||
- | 如果不想濫用,你可以把動作是非同步做為前置條件。 | ||
- | ===== Case Study: Power Management ===== | ||
- | 以上述的方法,我們來討論電源管理該怎麼做。首先我們把電源當成一個resource,而狀態為其屬性;所以我們可以透過GET去取得狀態,而透過PUT或PATCH去修改狀態: | ||
- | <code> | ||
- | GET /hosts/123/power | ||
- | {'state':'on'} | ||
- | PUT /hosts/123/power | ||
- | {'state':'on'} | ||
- | PUT /hosts/123/power | ||
- | {'state':'off'} | ||
- | </code> | ||
- | 這樣的做法,有哪些問題我們需要考量? | ||
- | - hypermedia: 我們有辦法表達出取得狀態、關機、開機等的link嗎? | ||
- | - synchronized: 在我們做開機與關機動作後,是可以立即反應的嗎? | ||
- | - implementation: 這部分稍後再做說明。 | ||
- | 或許把它做成store resource會比較好: | ||
- | <code> | ||
- | GET /hosts/123/power_status | ||
- | {'state':'on'} | ||
- | PUT /hosts/123/power_on | ||
- | PUT /hosts/123/power_off | ||
- | </code> | ||
- | 這裡我並沒有使用DELETE,因為在語義上使用DELETE power_on不會比PUT power_off來得清楚。此外,在使用這種方式後,hypermedia可以很容易的使用URI去表達出不同的意義。剩下的問題就是synchronized。\\ | ||
- | \\ | ||
- | PUT回傳202的方式,我自己本身還沒看過範例,但HTTP規格書也沒說這樣是不對的。我覺得值得討論的部分是: Idempotent。假設PUT回傳202,這代表著server將產生一個asynchronized的task,每次PUT power_on所產生的task是否會相同呢? 假如不同,是不是代表違反了Idempotent? 假如相同,實作會不會蠻奇怪的呢?\\ | ||
- | \\ | ||
- | 另外一個選擇,就是把它當controller resource,使用POST去操作: | ||
- | <code> | ||
- | POST /hosts/123/power_on | ||
- | POST /hosts/123/power_off | ||
- | </code> | ||
- | 有個實際案例就是[[https://pubs.vmware.com/vcd-51/index.jsp?topic=/com.vmware.vcloud.api.reference.doc_51/doc/operations/POST-PowerOffVApp.html|vCloud]]的Power On/Off API。順便提一下,會使用這個範例是由於在Roy Fielding在2008年[[http://roy.gbiv.com/untangled/2009/it-is-okay-to-use-post|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可能會長怎樣: | ||
- | <code java> | ||
- | @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){ | ||
- | // ... | ||
- | } | ||
- | </code> | ||
- | 接著使用POST+RequestParam: | ||
- | <code java> | ||
- | @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 | ||
- | // ... | ||
- | } | ||
- | </code> | ||
- | 你喜歡哪個? 假如File的action只有lock與unlock,那真的是天下太平;但事實上,action還有move、copy、rename等。考慮一下測試、維護、擴充的話,哪一個會比較好? 以擴充與維護來說,RequestParam的方式讓所有的action都必須接受同一組參數甚至輸出,增加了修改的麻煩;而URI Path則由各別的實做去決定。測試則是因為實做已分開,根據個別操作的目的去測試即可。 | ||
- | ==== Threat the P/N action as a store resource or use PATCH? ==== | ||
- | |||
- | ===== Summary ===== | ||
- | [[.:http_method_cant_make_sense:other_case_studies|其它的案例]] | ||
- | ===== Reference ===== | ||
- | * [[https://stackoverflow.com/questions/2173721/why-does-including-an-action-verb-in-the-uri-in-a-rest-implementation-violate-th|Why does including an action verb in the URI in a REST implementation violate the protocol?]] | ||
- | * [[http://shop.oreilly.com/product/0636920021575.do|REST API Design RuleBook]] Rule: A verb or verb phrase should be used for controller names | ||
- | * [[https://stackoverflow.com/questions/27121749/confusion-between-noun-vs-verb-in-rest-urls|Confusion Between Noun vs. Verb in Rest URLs]] | ||
- | * [[https://apihandyman.io/do-you-really-know-why-you-prefer-rest-over-rpc/|Do you really know why you prefer REST over RPC?]] | ||
- | * [[https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS|HTTP OPTIONS method]] | ||
- | * [[https://www.smashingmagazine.com/2016/09/understanding-rest-and-rpc-for-http-apis/|Understanding rest and rpc for http apis]] | ||
- | * [[https://stackoverflow.com/questions/24241893/rest-api-patch-or-put|Patch VS PUT Store Resource]] | ||
- | * [[https://www.instagram.com/developer/endpoints/|Instangram REST API]] | ||
- | * [[http://roy.gbiv.com/untangled/2009/it-is-okay-to-use-post|it is okay to use post]] | ||
- | * [[http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api|Best Practices for Designing a Pragmatic RESTful API]] | ||
- | |||
- | ===== ===== | ||
- | ---- | ||
- | \\ | ||
- | ~~DISQUS~~ |