Thực tiễn tốt nhất để cập nhật một phần trong dịch vụ RESTful


208

Tôi đang viết một dịch vụ RESTful cho một hệ thống quản lý khách hàng và tôi đang cố gắng tìm ra cách thực hành tốt nhất để cập nhật hồ sơ một phần. Ví dụ, tôi muốn người gọi có thể đọc bản ghi đầy đủ với yêu cầu GET. Nhưng để cập nhật, chỉ một số thao tác nhất định trong hồ sơ được cho phép, như thay đổi trạng thái từ ENABLED sang DISABLED. (Tôi có nhiều kịch bản phức tạp hơn thế này)

Tôi không muốn người gọi gửi toàn bộ hồ sơ chỉ với trường được cập nhật vì lý do bảo mật (nó cũng có cảm giác như quá mức cần thiết).

Có cách nào được đề xuất để xây dựng các URI không? Khi đọc sách REST, các cuộc gọi kiểu RPC dường như được tán thành.

Nếu cuộc gọi sau trả về hồ sơ khách hàng đầy đủ cho khách hàng với id 123

GET /customer/123
<customer>
    {lots of attributes}
    <status>ENABLED</status>
    {even more attributes}
</customer>

Tôi nên cập nhật trạng thái như thế nào?

POST /customer/123/status
<status>DISABLED</status>

POST /customer/123/changeStatus
DISABLED

...

Cập nhật : Để tăng thêm câu hỏi. Làm thế nào để kết hợp 'cuộc gọi logic kinh doanh' vào api REST? Có một cách đồng ý để làm điều này? Không phải tất cả các phương pháp là CRUD theo bản chất. Một số phức tạp hơn, như ' sendEmailToCustomer (123) ', ' mergeCustomers (123, 456) ', ' CountCustomers () '

POST /customer/123?cmd=sendEmail

POST /cmd/sendEmail?customerId=123

GET /customer/count 

3
Để trả lời câu hỏi của bạn về "các cuộc gọi logic kinh doanh", đây là một bài viết về chính POSTRoy Fielding: roy.gbiv.com/untangled/2009/it-is-okay-to-use-post trong đó ý tưởng cơ bản là: nếu có 't một phương pháp (chẳng hạn như GEThoặc PUT) phù hợp lý tưởng với việc sử dụng hoạt động của bạn POST.
rojoca

Đây là khá nhiều những gì tôi đã kết thúc làm. Thực hiện các cuộc gọi REST để truy xuất và cập nhật các tài nguyên đã biết bằng cách sử dụng GET, PUT, DELETE. POST để thêm tài nguyên mới và POST với một số URL mô tả cho các cuộc gọi logic nghiệp vụ.
magiconair

Dù bạn quyết định điều gì, nếu hoạt động đó không phải là một phần của phản hồi GET, bạn không có dịch vụ RESTful. Tôi không nhìn thấy điều đó ở đây
MStodd

Câu trả lời:


69

Về cơ bản, bạn có hai lựa chọn:

  1. Sử dụng PATCH(nhưng lưu ý rằng bạn phải xác định loại phương tiện của riêng mình chỉ định chính xác điều gì sẽ xảy ra)

  2. Sử dụng POSTcho tài nguyên phụ và trả về 303 Xem Khác với tiêu đề Vị trí trỏ đến tài nguyên chính. Mục đích của 303 là nói với khách hàng: "Tôi đã thực hiện POST của bạn và hiệu quả là một số tài nguyên khác đã được cập nhật. Xem tiêu đề Vị trí để biết tài nguyên đó là gì." POST / 303 được dành cho việc bổ sung lặp lại vào một tài nguyên để xây dựng trạng thái của một số tài nguyên chính và nó phù hợp hoàn hảo cho các cập nhật một phần.


OK, POST / 303 có ý nghĩa với tôi. PATCH và MERGE Tôi không thể tìm thấy trong danh sách các động từ HTTP hợp lệ để yêu cầu thử nghiệm nhiều hơn. Làm cách nào để tạo URI nếu tôi muốn hệ thống gửi email cho khách hàng 123? Một cái gì đó giống như một cuộc gọi phương thức RPC thuần túy không thay đổi trạng thái của đối tượng. Cách thức RESTful để làm điều này là gì?
magiconair

Tôi không hiểu câu hỏi URI email. Bạn có muốn triển khai một cổng mà bạn có thể POST để gửi email hay bạn đang tìm mailto: customer.123@service.org?
Jan Algermissen

15
Cả REST và HTTP đều không liên quan gì đến CRUD ngoài một số người đánh đồng các phương thức HTTP với CRUD. REST là về thao tác trạng thái tài nguyên bằng cách chuyển các biểu diễn. Bất cứ điều gì bạn muốn đạt được bạn làm bằng cách chuyển một đại diện sang một tài nguyên với ngữ nghĩa phù hợp. Cảnh giác với các thuật ngữ 'gọi phương thức thuần túy' hoặc 'logic kinh doanh' vì chúng quá dễ dàng ám chỉ 'HTTP là dành cho vận chuyển'. Nếu bạn cần gửi email, POST đến tài nguyên cổng, nếu bạn cần hợp nhất với tài khoản, hãy tạo một đại diện mới và POST của hai người kia, v.v.
Jan Algermissen

9
Xem thêm cách Google thực hiện: googlecode.blogspot.com/2010/03/ Khăn
Marius

4
williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot PATCH [{"op": "test", "path": "/ a / b / c", "value" : "foo"}, {"op": "remove", "path": "/ a / b / c"}, {"op": "thêm", "đường dẫn": "/ a / b / c" , "value": ["foo", "bar"]}, {"op": "thay thế", "đường dẫn": "/ a / b / c", "value": 42}, {"op": "di chuyển", "từ": "/ a / b / c", "đường dẫn": "/ a / b / d"}, {"op": "sao chép", "từ": "/ a / b / d "," đường dẫn ":" / a / b / e "}]
intotecho

48

Bạn nên sử dụng POST để cập nhật một phần.

Để cập nhật các trường cho khách hàng 123, hãy tạo POST cho / khách hàng / 123.

Nếu bạn chỉ muốn cập nhật trạng thái, bạn cũng có thể PUT tới / khách hàng / 123 / trạng thái.

Nói chung, các yêu cầu GET không được có bất kỳ tác dụng phụ nào và PUT là để ghi / thay thế toàn bộ tài nguyên.

Điều này diễn ra trực tiếp từ HTTP, như được thấy ở đây: http://en.wikipedia.org/wiki/HTTP_PUT#Request_methods


1
@John Saunders POST không nhất thiết phải tạo một tài nguyên mới có thể truy cập được từ URI: tools.ietf.org/html/rfc2616#section-9.5
wsorenson

10
@wsorensen: Tôi biết rằng nó không cần phải tạo ra một URL mới, nhưng vẫn nghĩ rằng một POST /customer/123sẽ tạo ra điều rõ ràng là hợp lý theo khách hàng 123. Có thể là một đơn đặt hàng? PUT /customer/123/statusdường như có ý nghĩa tốt hơn, giả sử POST sẽ /customersngầm tạo ra một status(và giả sử đó là REST hợp pháp).
John Saunders

1
@ John Saunders: thực tế mà nói, nếu chúng tôi muốn cập nhật một trường trên tài nguyên tại một URI nhất định, POST có ý nghĩa hơn PUT và thiếu CẬP NHẬT, tôi tin rằng nó thường được sử dụng trong các dịch vụ REST. POST cho / khách hàng có thể tạo ra một khách hàng mới và trạng thái PUT cho / khách hàng / 123 / có thể phù hợp hơn với từ đặc tả, nhưng đối với các thực tiễn tốt nhất, tôi không nghĩ có bất kỳ lý do nào để không POST cho / khách hàng / 123 để cập nhật một lĩnh vực - nó ngắn gọn, hợp lý và không hoàn toàn đi ngược lại bất cứ điều gì trong đặc tả.
wsorenson

8
Không nên POST yêu cầu không phải là idempotent? Chắc chắn việc cập nhật một mục là bình thường và do đó nên là một PUT thay thế?
Martin Andersson

1
@MartinAndersson POST-requests không cần phải không bình thường. Và như đã đề cập, PUTphải thay thế toàn bộ tài nguyên.
Halle Knast

10

Bạn nên sử dụng PATCH để cập nhật một phần - sử dụng tài liệu json-patch (xem http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-08 hoặc http://www.mnot.net/ blog / 2012/09/05 / patch ) hoặc khung vá XML (xem http://tools.ietf.org/html/rfc5261 ). Theo ý kiến ​​của tôi, json-patch là phù hợp nhất cho loại dữ liệu kinh doanh của bạn.

PATCH với các tài liệu vá JSON / XML có ngữ nghĩa rất cao để cập nhật một phần. Nếu bạn bắt đầu sử dụng POST, với các bản sao đã sửa đổi của tài liệu gốc, đối với các cập nhật một phần, bạn sẽ sớm gặp phải các vấn đề khi bạn muốn thiếu giá trị (hoặc, đúng hơn là giá trị null) để thể hiện "bỏ qua thuộc tính này" hoặc "đặt thuộc tính này thành giá trị trống "- và điều đó dẫn đến một lỗ thỏ của các giải pháp bị hack mà cuối cùng sẽ dẫn đến loại định dạng bản vá của riêng bạn.

Bạn có thể tìm thấy câu trả lời sâu hơn tại đây: http://soabits.blogspot.dk/2013/01/http-put-patch-or-post-partial-updates.html .


Xin lưu ý rằng trong khi đó RFC cho json-patchxml-patch đã được hoàn thành.
botchniaque

8

Tôi đang chạy vào một vấn đề tương tự. PUT trên tài nguyên phụ dường như hoạt động khi bạn chỉ muốn cập nhật một trường duy nhất. Tuy nhiên, đôi khi bạn muốn cập nhật một loạt điều: Hãy nghĩ về một biểu mẫu web đại diện cho tài nguyên với tùy chọn để thay đổi một số mục. Việc gửi biểu mẫu của người dùng không được dẫn đến nhiều PUT.

Đây là hai giải pháp mà tôi có thể nghĩ ra:

  1. làm một PUT với toàn bộ tài nguyên. Về phía máy chủ, xác định ngữ nghĩa rằng PUT với toàn bộ tài nguyên bỏ qua tất cả các giá trị không thay đổi.

  2. làm một PUT với một phần tài nguyên. Về phía máy chủ, xác định ngữ nghĩa của điều này là hợp nhất.

2 chỉ là tối ưu hóa băng thông là 1. Đôi khi 1 là tùy chọn duy nhất nếu tài nguyên xác định một số trường là các trường bắt buộc (nghĩ bộ đệm proto).

Vấn đề với cả hai cách tiếp cận này là làm thế nào để xóa một trường. Bạn sẽ phải xác định một giá trị null đặc biệt (đặc biệt đối với bộ đệm proto vì giá trị null không được xác định cho bộ đệm proto) sẽ gây ra xóa trường.

Bình luận?


2
Điều này sẽ hữu ích hơn nếu được đăng dưới dạng một câu hỏi riêng biệt.
intotecho 7/12/2015

6

Để sửa đổi trạng thái, tôi nghĩ rằng cách tiếp cận RESTful là sử dụng tài nguyên con logic để mô tả trạng thái của các tài nguyên. IMO này khá hữu ích và sạch sẽ khi bạn có một bộ trạng thái giảm. Nó làm cho API của bạn biểu cảm hơn mà không buộc các hoạt động hiện có cho tài nguyên khách hàng của bạn.

Thí dụ:

POST /customer/active  <-- Providing entity in the body a new customer
{
  ...  // attributes here except status
}

Dịch vụ POST sẽ trả về khách hàng mới được tạo với id:

{
    id:123,
    ...  // the other fields here
}

GET cho tài nguyên đã tạo sẽ sử dụng vị trí tài nguyên:

GET /customer/123/active

Một GET / khách hàng / 123 / không hoạt động sẽ trả về 404

Đối với hoạt động PUT, không cung cấp thực thể Json, nó sẽ chỉ cập nhật trạng thái

PUT /customer/123/inactive  <-- Deactivating an existing customer

Cung cấp một thực thể sẽ cho phép bạn cập nhật nội dung của khách hàng và cập nhật trạng thái cùng một lúc.

PUT /customer/123/inactive
{
    ...  // entity fields here except id and status
}

Bạn đang tạo một tài nguyên phụ khái niệm cho tài nguyên khách hàng của bạn. Nó cũng phù hợp với định nghĩa về tài nguyên của Roy Fielding: "... Tài nguyên là ánh xạ khái niệm đến một tập hợp các thực thể, không phải là thực thể tương ứng với ánh xạ tại bất kỳ thời điểm cụ thể nào ..." Trong trường hợp này ánh xạ khái niệm là khách hàng đang hoạt động với khách hàng có trạng thái = HOẠT ĐỘNG.

Đọc hoạt động:

GET /customer/123/active 
GET /customer/123/inactive

Nếu bạn thực hiện các cuộc gọi đó ngay sau khi một trong số chúng phải trả lại trạng thái 404, thì đầu ra thành công có thể không bao gồm trạng thái như ẩn. Tất nhiên bạn vẫn có thể sử dụng GET / khách hàng / 123? Status = ACTIVE | INACTIVE để truy vấn trực tiếp tài nguyên của khách hàng.

Hoạt động DELETE rất thú vị vì ngữ nghĩa có thể gây nhầm lẫn. Nhưng bạn có tùy chọn không xuất bản thao tác đó cho tài nguyên khái niệm này hoặc sử dụng nó theo logic kinh doanh của bạn.

DELETE /customer/123/active

Người ta có thể đưa khách hàng của bạn đến trạng thái BỊ XÓA / BỊ XÓA hoặc sang trạng thái ngược lại (HOẠT ĐỘNG / KHÔNG THỰC HIỆN).


Làm thế nào để bạn có được tài nguyên phụ?
MStodd

Tôi đã cấu trúc lại câu trả lời đang cố gắng làm cho nó rõ ràng hơn
raspacorp

5

Những điều cần thêm vào câu hỏi tăng cường của bạn. Tôi nghĩ bạn thường có thể thiết kế hoàn hảo các hành động kinh doanh phức tạp hơn. Nhưng bạn phải từ bỏ phương pháp / thủ tục suy nghĩ và suy nghĩ nhiều hơn về tài nguyên và động từ.

gửi thư


POST /customers/123/mails

payload:
{from: x@x.com, subject: "foo", to: y@y.com}

Việc thực hiện tài nguyên này + POST sau đó sẽ gửi thư. nếu cần, sau đó bạn có thể cung cấp một cái gì đó như / khách hàng / 123 / hộp thư đi và sau đó cung cấp các liên kết tài nguyên đến / khách hàng / thư / {mailId}.

số lượng khách hàng

Bạn có thể xử lý nó như một tài nguyên tìm kiếm (bao gồm siêu dữ liệu tìm kiếm với thông tin phân trang và tìm thấy số, cung cấp cho bạn số lượng khách hàng).


GET /customers

response payload:
{numFound: 1234, paging: {self:..., next:..., previous:...} customer: { ...} ....}


Tôi thích cách phân nhóm logic các trường trong tài nguyên phụ POST.
gertas

3

Sử dụng PUT để cập nhật tài nguyên không đầy đủ / một phần.

Bạn có thể chấp nhận jObject làm tham số và phân tích giá trị của nó để cập nhật tài nguyên.

Dưới đây là chức năng mà bạn có thể sử dụng làm tài liệu tham khảo:

public IHttpActionResult Put(int id, JObject partialObject)
{
    Dictionary<string, string> dictionaryObject = new Dictionary<string, string>();

    foreach (JProperty property in json.Properties())
    {
        dictionaryObject.Add(property.Name.ToString(), property.Value.ToString());
    }

    int id = Convert.ToInt32(dictionaryObject["id"]);
    DateTime startTime = Convert.ToDateTime(orderInsert["AppointmentDateTime"]);            
    Boolean isGroup = Convert.ToBoolean(dictionaryObject["IsGroup"]);

    //Call function to update resource
    update(id, startTime, isGroup);

    return Ok(appointmentModelList);
}

2

Về Cập nhật của bạn.

Khái niệm về CRUD tôi tin rằng đã gây ra một số nhầm lẫn về thiết kế API. CRUD là một khái niệm cấp thấp chung cho các hoạt động cơ bản để thực hiện trên dữ liệu và động từ HTTP chỉ là phương thức yêu cầu ( được tạo ra 21 năm trước ) có thể hoặc không thể ánh xạ tới hoạt động CRUD. Trong thực tế, hãy thử tìm sự hiện diện của từ viết tắt CRUD trong đặc tả HTTP 1.0 / 1.1.

Một hướng dẫn được giải thích rất rõ áp dụng quy ước thực dụng có thể được tìm thấy trong tài liệu API nền tảng đám mây của Google . Nó mô tả các khái niệm đằng sau việc tạo API dựa trên tài nguyên, một trong đó nhấn mạnh một lượng lớn tài nguyên qua các hoạt động và bao gồm các trường hợp sử dụng mà bạn đang mô tả. Mặc dù chỉ là một thiết kế hội nghị cho sản phẩm của họ, tôi nghĩ nó có rất nhiều ý nghĩa.

Khái niệm cơ sở ở đây (và một khái niệm tạo ra nhiều nhầm lẫn) là ánh xạ giữa "phương thức" và động từ HTTP. Một điều là xác định "hoạt động" (phương thức) API của bạn sẽ làm gì đối với loại tài nguyên nào (ví dụ: lấy danh sách khách hàng hoặc gửi email) và một loại khác là các động từ HTTP. Phải có định nghĩa về cả hai, phương thức và động từ mà bạn dự định sử dụng và ánh xạ giữa chúng .

Nó cũng nói rằng, khi một hoạt động không có bản đồ chính xác với một phương pháp tiêu chuẩn ( List, Get, Create, Update, Deletetrong trường hợp này), người ta có thể sử dụng "phương pháp Custom", giống như BatchGet, mà lấy một số đối tượng dựa trên một số đối tượng input id, hoặc SendEmail.


2

RFC 7394 : Bản vá hợp nhất JSON (được xuất bản bốn năm sau khi câu hỏi được đăng) mô tả các cách thực hành tốt nhất cho một BCH về mặt định dạng và quy tắc xử lý.

Tóm lại, bạn gửi HTTP PATCH đến tài nguyên đích với ứng dụng / merge-patch + json loại phương tiện MIME và phần thân chỉ đại diện cho các phần bạn muốn thay đổi / thêm / xóa và sau đó thực hiện theo các quy tắc xử lý bên dưới.

Quy tắc :

  • Nếu bản vá hợp nhất được cung cấp có chứa các thành viên không xuất hiện trong mục tiêu, các thành viên đó sẽ được thêm vào.

  • Nếu mục tiêu không chứa thành viên, giá trị được thay thế.

  • Các giá trị Null trong bản vá hợp nhất được cho ý nghĩa đặc biệt để chỉ ra việc loại bỏ các giá trị hiện có trong mục tiêu.

Các trường hợp thử nghiệm minh họa các quy tắc trên (như được thấy trong phần phụ lục của RFC đó):

 ORIGINAL         PATCH           RESULT
--------------------------------------------
{"a":"b"}       {"a":"c"}       {"a":"c"}

{"a":"b"}       {"b":"c"}       {"a":"b",
                                 "b":"c"}
{"a":"b"}       {"a":null}      {}

{"a":"b",       {"a":null}      {"b":"c"}
"b":"c"}

{"a":["b"]}     {"a":"c"}       {"a":"c"}

{"a":"c"}       {"a":["b"]}     {"a":["b"]}

{"a": {         {"a": {         {"a": {
  "b": "c"}       "b": "d",       "b": "d"
}                 "c": null}      }
                }               }

{"a": [         {"a": [1]}      {"a": [1]}
  {"b":"c"}
 ]
}

["a","b"]       ["c","d"]       ["c","d"]

{"a":"b"}       ["c"]           ["c"]

{"a":"foo"}     null            null

{"a":"foo"}     "bar"           "bar"

{"e":null}      {"a":1}         {"e":null,
                                 "a":1}

[1,2]           {"a":"b",       {"a":"b"}
                 "c":null}

{}              {"a":            {"a":
                 {"bb":           {"bb":
                  {"ccc":          {}}}
                   null}}}

1

Hãy xem http://www.odata.org/

Nó định nghĩa phương thức MERGE, vì vậy trong trường hợp của bạn, nó sẽ giống như thế này:

MERGE /customer/123

<customer>
   <status>DISABLED</status>
</customer>

Chỉ có statustài sản được cập nhật và các giá trị khác được bảo tồn.


MERGEmột động từ HTTP hợp lệ?
John Saunders

3
Hãy nhìn vào PATCH - đó là sắp có HTTP tiêu chuẩn và thực hiện điều tương tự.
Jan Algermissen

@ John Saunders Vâng, đó là một phương pháp mở rộng.
Max Toro

FYI MERGE đã bị xóa khỏi OData v4. MERGE was used to do PATCH before PATCH existed. Now that we have PATCH, we no longer need MERGE. Xem docs.oocation-open.org/odata/new-in-odata/v4.0/cn01/ Lời
tanguy_k

0

Nó không thành vấn đề. Về mặt REST, bạn không thể thực hiện NHẬN, vì nó không được lưu trong bộ nhớ cache, nhưng sẽ không có vấn đề gì nếu bạn sử dụng POST hoặc PATCH hoặc PUT hoặc bất cứ điều gì và không quan trọng URL trông như thế nào. Nếu bạn đang làm REST, điều quan trọng là khi bạn nhận được một đại diện tài nguyên của mình từ máy chủ, thì đại diện đó có thể cung cấp cho các tùy chọn chuyển trạng thái máy khách.

Nếu phản hồi GET của bạn có chuyển trạng thái, máy khách chỉ cần biết cách đọc chúng và máy chủ có thể thay đổi chúng nếu cần. Ở đây, một bản cập nhật được thực hiện bằng POST, nhưng nếu nó được thay đổi thành PATCH hoặc nếu URL thay đổi, khách hàng vẫn biết cách thực hiện cập nhật:

{
  "customer" :
  {
  },
  "operations":
  [
    "update" : 
    {
      "method": "POST",
      "href": "https://server/customer/123/"
    }]
}

Bạn có thể đi xa đến mức liệt kê các tham số bắt buộc / tùy chọn để khách hàng trả lại cho bạn. Nó phụ thuộc vào ứng dụng.

Theo như hoạt động kinh doanh, đó có thể là một tài nguyên khác được liên kết từ tài nguyên của khách hàng. Nếu bạn muốn gửi email cho khách hàng, có thể dịch vụ đó là tài nguyên của riêng bạn mà bạn có thể POST, vì vậy bạn có thể bao gồm các thao tác sau trong tài nguyên của khách hàng:

"email":
{
  "method": "POST",
  "href": "http://server/emailservice/send?customer=1234"
}

Một số video hay và ví dụ về kiến ​​trúc REST của người trình bày là những video này. Stormpath chỉ sử dụng GET / POST / DELETE, điều này rất tốt vì REST không liên quan gì đến những hoạt động bạn sử dụng hoặc giao diện của URL (ngoại trừ GET nên được lưu trong bộ nhớ cache):

https://www.youtube.com/watch?v=pspy1H6A3FM ,
https://www.youtube.com/watch?v=5WXYw4J4QOU ,
http://docs.stormpath.com/rest/quickstart/

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.