Các mẫu để xử lý các hoạt động hàng loạt trong các dịch vụ web REST?


170

Những mẫu thiết kế đã được chứng minh nào tồn tại cho các hoạt động hàng loạt trên các tài nguyên trong dịch vụ web kiểu REST?

Tôi đang cố gắng cân bằng giữa lý tưởng và thực tế về hiệu suất và sự ổn định. Chúng tôi đã có API ngay bây giờ khi tất cả các hoạt động truy xuất từ ​​tài nguyên danh sách (ví dụ: GET / user) hoặc trên một cá thể (PUT / user / 1, DELETE / user / 22, v.v.).

Có một số trường hợp bạn muốn cập nhật một trường duy nhất của toàn bộ các đối tượng. Có vẻ rất lãng phí khi gửi toàn bộ đại diện cho từng đối tượng qua lại để cập nhật một trường.

Trong API kiểu RPC, bạn có thể có một phương thức:

/mail.do?method=markAsRead&messageIds=1,2,3,4... etc. 

REST tương đương ở đây là gì? Hoặc là nó ổn để thỏa hiệp bây giờ và sau đó. Liệu nó có phá hỏng thiết kế để thêm vào một vài thao tác cụ thể trong đó nó thực sự cải thiện hiệu suất, v.v.? Ứng dụng khách trong mọi trường hợp ngay bây giờ là Trình duyệt web (ứng dụng javascript ở phía máy khách).

Câu trả lời:


77

Một mẫu RESTful đơn giản cho các lô là sử dụng tài nguyên bộ sưu tập. Ví dụ, để xóa một số tin nhắn cùng một lúc.

DELETE /mail?&id=0&id=1&id=2

Nó phức tạp hơn một chút để cập nhật hàng loạt tài nguyên một phần hoặc thuộc tính tài nguyên. Đó là, cập nhật từng thuộc tính markAsRead. Về cơ bản, thay vì coi thuộc tính là một phần của mỗi tài nguyên, bạn coi nó như một cái xô để đặt tài nguyên. Một ví dụ đã được đăng. Tôi điều chỉnh nó một chút.

POST /mail?markAsRead=true
POSTDATA: ids=[0,1,2]

Về cơ bản, bạn đang cập nhật danh sách thư được đánh dấu là đã đọc.

Bạn cũng có thể sử dụng điều này để gán một số mục cho cùng một danh mục.

POST /mail?category=junk
POSTDATA: ids=[0,1,2]

Rõ ràng là phức tạp hơn nhiều khi thực hiện cập nhật một phần theo kiểu iTunes (ví dụ: artist + albumTitle nhưng không phải trackTitle). Sự tương tự xô bắt đầu bị phá vỡ.

POST /mail?markAsRead=true&category=junk
POSTDATA: ids=[0,1,2]

Về lâu dài, việc cập nhật một tài nguyên một phần hoặc thuộc tính tài nguyên dễ dàng hơn nhiều. Chỉ cần sử dụng một nguồn con.

POST /mail/0/markAsRead
POSTDATA: true

Ngoài ra, bạn có thể sử dụng tài nguyên tham số. Điều này ít phổ biến hơn trong các mẫu REST, nhưng được cho phép trong thông số kỹ thuật URI và HTTP. Dấu chấm phẩy phân chia các tham số liên quan theo chiều ngang trong tài nguyên.

Cập nhật một số thuộc tính, một số tài nguyên:

POST /mail/0;1;2/markAsRead;category
POSTDATA: markAsRead=true,category=junk

Cập nhật một số tài nguyên, chỉ một thuộc tính:

POST /mail/0;1;2/markAsRead
POSTDATA: true

Cập nhật một số thuộc tính, chỉ một tài nguyên:

POST /mail/0/markAsRead;category
POSTDATA: markAsRead=true,category=junk

Sự sáng tạo RESTful đầy rẫy.


1
Người ta có thể lập luận rằng việc xóa của bạn thực sự phải là một bài đăng vì nó không thực sự phá hủy tài nguyên đó.
Chris Nicola

6
Nó không cần thiết. POST là một phương thức mô hình nhà máy, nó ít rõ ràng và rõ ràng hơn PUT / DELETE / GET. Kỳ vọng duy nhất là máy chủ sẽ quyết định những gì sẽ làm như là kết quả của POST. POST chính xác như mọi khi, tôi gửi dữ liệu biểu mẫu và máy chủ thực hiện điều gì đó (hy vọng được mong đợi) và cung cấp cho tôi một số dấu hiệu về kết quả. Chúng tôi không bắt buộc phải tạo tài nguyên bằng POST, chúng tôi thường chọn. Tôi có thể dễ dàng tạo tài nguyên bằng PUT, tôi chỉ cần xác định URL tài nguyên là người gửi (không thường xuyên lý tưởng).
Chris Nicola

1
@ Vecant, trong trường hợp này, có lẽ bạn không cần tham chiếu nhiều tài nguyên trong URI, mà chỉ chuyển các bộ dữ liệu với các tham chiếu / giá trị trong phần thân của yêu cầu. ví dụ: POST / mail / markAsRead, BODY: i_0_id = 0 & i_0_value = true & i_1_id = 1 & i_1_value = false & i_2_id = 2 & i_2_value = true
Alex

3
dấu chấm phẩy được dành riêng cho mục đích này.
Alex

1
Ngạc nhiên rằng không ai chỉ ra rằng việc cập nhật một số thuộc tính trên một tài nguyên duy nhất được bảo vệ bởi PATCH- không cần sáng tạo trong trường hợp này.
LB2

25

Hoàn toàn không - Tôi nghĩ rằng tương đương REST là (hoặc ít nhất một giải pháp là) gần như chính xác - một giao diện chuyên dụng được thiết kế phù hợp với hoạt động theo yêu cầu của khách hàng.

Tôi đã nhắc về một mô hình được đề cập trong cuốn sách Ajax in Action của Crane và Prebello (một cuốn sách xuất sắc, rất được khuyến khích) trong đó họ minh họa việc thực hiện một loại đối tượng CommandQueue có nhiệm vụ xếp hàng các yêu cầu thành các đợt và sau đó gửi chúng đến máy chủ định kỳ.

Đối tượng, nếu tôi nhớ chính xác, về cơ bản chỉ cần giữ một mảng "lệnh" - ví dụ, để mở rộng ví dụ của bạn, mỗi bản ghi chứa lệnh "markAsRead", "messageId" và có thể là tham chiếu đến hàm gọi lại / xử lý chức năng - và sau đó theo một lịch trình hoặc trên một số hành động của người dùng, đối tượng lệnh sẽ được tuần tự hóa và đăng lên máy chủ, và máy khách sẽ xử lý hậu xử lý hậu quả.

Tôi không có các chi tiết tiện dụng, nhưng có vẻ như một hàng lệnh của loại này sẽ là một cách để xử lý vấn đề của bạn; nó sẽ làm giảm đáng kể độ chói tổng thể và nó trừu tượng hóa giao diện phía máy chủ theo cách bạn có thể thấy linh hoạt hơn trên đường.


Cập nhật : Aha! Tôi đã tìm thấy một đoạn trích từ cuốn sách trực tuyến đó, hoàn chỉnh với các mẫu mã (mặc dù tôi vẫn khuyên bạn nên chọn cuốn sách thực tế!). Hãy xem tại đây , bắt đầu với phần 5.5.3:

Điều này dễ mã hóa nhưng có thể dẫn đến rất nhiều lưu lượng rất nhỏ đến máy chủ, không hiệu quả và có khả năng gây nhầm lẫn. Nếu chúng tôi muốn kiểm soát lưu lượng của mình, chúng tôi có thể nắm bắt các cập nhật này và xếp hàng chúng cục bộ và sau đó gửi chúng đến máy chủ theo từng đợt một cách thoải mái. Một hàng đợi cập nhật đơn giản được triển khai trong JavaScript được hiển thị trong danh sách 5.13. [...]

Hàng đợi duy trì hai mảng. queued là một mảng được lập chỉ mục bằng số, mà các bản cập nhật mới được thêm vào. sent là một mảng kết hợp, chứa các bản cập nhật đã được gửi đến máy chủ nhưng đang chờ trả lời.

Dưới đây là hai hàm thích hợp - một hàm chịu trách nhiệm thêm lệnh vào hàng đợi ( addCommand) và một hàm chịu trách nhiệm tuần tự hóa và sau đó gửi chúng đến máy chủ ( fireRequest):

CommandQueue.prototype.addCommand = function(command)
{ 
    if (this.isCommand(command))
    {
        this.queue.append(command,true);
    }
}

CommandQueue.prototype.fireRequest = function()
{
    if (this.queued.length == 0)
    { 
        return; 
    }

    var data="data=";

    for (var i = 0; i < this.queued.length; i++)
    { 
        var cmd = this.queued[i]; 
        if (this.isCommand(cmd))
        {
            data += cmd.toRequestString(); 
            this.sent[cmd.id] = cmd;

            // ... and then send the contents of data in a POST request
        }
    }
}

Điều đó nên đưa bạn đi. Chúc may mắn!


Cảm ơn. Điều đó rất giống với ý tưởng của tôi về cách tôi sẽ tiếp tục nếu chúng tôi duy trì các hoạt động hàng loạt trên máy khách. Vấn đề là thời gian khứ hồi để thực hiện một thao tác trên một số lượng lớn các đối tượng.
Đánh dấu Renouf

Hừm, ok - Tôi nghĩ bạn muốn thực hiện thao tác trên một số lượng lớn đối tượng (trên máy chủ) bằng một yêu cầu nhẹ. Có phải tôi đã hiểu lầm?
Christian Nunciato

Có, nhưng tôi không thấy mẫu mã đó sẽ thực hiện thao tác hiệu quả hơn thế nào. Nó xử lý hàng loạt yêu cầu nhưng vẫn gửi chúng đến máy chủ cùng một lúc. Có phải tôi đang hiểu sai?
Đánh dấu Renouf

Trên thực tế, nó bó chúng lại và sau đó gửi tất cả chúng cùng một lúc: vòng lặp đó trong fireRequest () về cơ bản tập hợp tất cả các lệnh đang tồn tại, tuần tự hóa chúng thành một chuỗi (với .toRequestString (), ví dụ: "method = markAsRead & messageIds = 1,2,3 , 4 "), gán chuỗi đó cho" dữ liệu "và POST dữ liệu cho máy chủ.
Christian Nunciato

20

Mặc dù tôi nghĩ rằng @Alex đang đi đúng hướng, nhưng về mặt khái niệm tôi nghĩ nó nên ngược lại với những gì được đề xuất.

Do đó, URL có hiệu lực "tài nguyên chúng tôi đang nhắm mục tiêu":

    [GET] mail/1

có nghĩa là lấy bản ghi từ thư có id 1 và

    [PATCH] mail/1 data: mail[markAsRead]=true

nghĩa là vá bản ghi thư bằng id 1. Chuỗi truy vấn là "bộ lọc", lọc dữ liệu được trả về từ URL.

    [GET] mail?markAsRead=true

Vì vậy, ở đây chúng tôi yêu cầu tất cả các thư đã được đánh dấu là đã đọc. Vì vậy, với [VÁCH] cho đường dẫn này sẽ nói "vá các bản ghi đã được đánh dấu là đúng" ... đó không phải là những gì chúng tôi đang cố gắng đạt được.

Vì vậy, một phương pháp hàng loạt, theo suy nghĩ này nên là:

    [PATCH] mail/?id=1,2,3 <the records we are targeting> data: mail[markAsRead]=true

tất nhiên tôi không nói đây là REST đúng (không cho phép thao tác bản ghi hàng loạt), thay vào đó nó tuân theo logic đã có và được sử dụng bởi REST.


Câu trả lời thú vị! Ví dụ cuối cùng của bạn, nó sẽ không phù hợp hơn với [GET]định dạng để làm [PATCH] mail?markAsRead=true data: [{"id": 1}, {"id": 2}, {"id": 3}](hoặc thậm chí chỉ data: {"ids": [1,2,3]})? Một lợi ích khác cho cách tiếp cận thay thế này là bạn sẽ không gặp phải lỗi "414 Yêu cầu URI quá lâu" nếu bạn đang cập nhật hàng trăm / nghìn tài nguyên trong bộ sưu tập.
rinogo

@rinogo - thực sự là không Đây là điểm tôi đang làm. Chuỗi truy vấn là một bộ lọc cho các bản ghi mà chúng tôi muốn thực hiện (ví dụ: [GET] mail / 1 nhận bản ghi thư với id là 1, trong khi [GET] mail? MarkasRead = true trả về thư trong đó markAsRead đã đúng). Sẽ không có ý nghĩa gì khi vá vào cùng một URL đó (nghĩa là "vá các bản ghi trong đó markAsRead = true") trong khi thực tế chúng tôi muốn vá các bản ghi cụ thể với id 1,2,3, ĐĂNG KÝ trạng thái hiện tại của trường markAsRead. Do đó phương pháp tôi mô tả. Đồng ý có một vấn đề với việc cập nhật nhiều hồ sơ. Tôi sẽ xây dựng một điểm cuối kết hợp ít chặt chẽ hơn.
fezfox

11

Ngôn ngữ của bạn, "Có vẻ rất lãng phí ...", với tôi cho thấy nỗ lực tối ưu hóa sớm. Trừ khi có thể chứng minh rằng việc gửi toàn bộ đại diện của các đối tượng là một điểm nhấn hiệu suất lớn (chúng tôi đang nói không thể chấp nhận được với người dùng là> 150ms) thì không có ý định tạo hành vi API không chuẩn mới. Hãy nhớ rằng, API càng đơn giản thì càng dễ sử dụng.

Để xóa, hãy gửi như sau vì máy chủ không cần biết gì về trạng thái của đối tượng trước khi xóa.

DELETE /emails
POSTDATA: [{id:1},{id:2}]

Ý nghĩ tiếp theo là nếu một ứng dụng đang gặp vấn đề về hiệu năng liên quan đến việc cập nhật hàng loạt đối tượng thì nên xem xét việc chia từng đối tượng thành nhiều đối tượng. Bằng cách đó, tải trọng JSON là một phần nhỏ của kích thước.

Ví dụ khi gửi phản hồi để cập nhật trạng thái "đã đọc" và "đã lưu trữ" của hai email riêng biệt, bạn sẽ phải gửi như sau:

PUT /emails
POSTDATA: [
            {
              id:1,
              to:"someone@bratwurst.com",
              from:"someguy@frommyville.com",
              subject:"Try this recipe!",
              text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1t Mustard Powder",
              read:true,
              archived:true,
              importance:2,
              labels:["Someone","Mustard"]
            },
            {
              id:2,
              to:"someone@bratwurst.com",
              from:"someguy@frommyville.com",
              subject:"Try this recipe (With Fix)",
              text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1T Mustard Powder, 1t Garlic Powder",
              read:true,
              archived:false,
              importance:1,
              labels:["Someone","Mustard"]
            }
            ]

Tôi sẽ tách các thành phần có thể thay đổi của email (đọc, lưu trữ, tầm quan trọng, nhãn) thành một đối tượng riêng biệt vì các đối tượng khác (đến, từ, chủ đề, văn bản) sẽ không bao giờ được cập nhật.

PUT /email-statuses
POSTDATA: [
            {id:15,read:true,archived:true,importance:2,labels:["Someone","Mustard"]},
            {id:27,read:true,archived:false,importance:1,labels:["Someone","Mustard"]}
          ]

Một cách tiếp cận khác là sử dụng đòn bẩy. Để chỉ rõ ràng những thuộc tính nào bạn định cập nhật và tất cả những thuộc tính khác nên được bỏ qua.

PATCH /emails
POSTDATA: [
            {
              id:1,
              read:true,
              archived:true
            },
            {
              id:2,
              read:true,
              archived:false
            }
          ]

Mọi người tuyên bố rằng nên thực hiện PATCH bằng cách cung cấp một loạt các thay đổi có chứa: hành động (CRUD), đường dẫn (URL) và thay đổi giá trị. Đây có thể được coi là một triển khai tiêu chuẩn nhưng nếu bạn xem xét toàn bộ API REST thì đó là một lần không trực quan. Ngoài ra, cách thực hiện ở trên là cách GitHub đã triển khai PATCH .

Tóm lại, có thể tuân thủ các nguyên tắc RESTful với các hành động hàng loạt và vẫn có hiệu suất chấp nhận được.


Tôi đồng ý rằng PATCH có ý nghĩa nhất, vấn đề là nếu bạn có mã chuyển đổi trạng thái khác cần chạy khi các thuộc tính đó thay đổi, việc thực hiện như một BẠCH đơn giản sẽ trở nên khó khăn hơn. Tôi không nghĩ REST thực sự thích ứng với bất kỳ loại chuyển đổi trạng thái nào, vì nó được coi là không trạng thái, nó không quan tâm đến việc chuyển đổi từ và sang, chỉ là trạng thái hiện tại là gì.
BeniRose

Này BeniRose, cảm ơn vì đã thêm một bình luận, tôi thường tự hỏi nếu mọi người thấy một số bài viết này. Nó làm cho tôi hạnh phúc khi thấy rằng mọi người làm. Các tài nguyên liên quan đến bản chất "không trạng thái" của REST định nghĩa nó là mối quan tâm với việc máy chủ không phải duy trì trạng thái qua các yêu cầu. Như vậy, tôi không rõ vấn đề gì mà bạn đang mô tả, bạn có thể giải thích bằng một ví dụ không?
justin.hughey

8

API Google drive có một hệ thống thực sự thú vị để giải quyết vấn đề này ( xem tại đây ).

Những gì họ làm về cơ bản là nhóm các yêu cầu khác nhau trong một Content-Type: multipart/mixedyêu cầu, với mỗi yêu cầu hoàn thành riêng lẻ được phân tách bằng một số dấu phân cách được xác định. Các tiêu đề và tham số truy vấn của yêu cầu lô được kế thừa cho các yêu cầu riêng lẻ (nghĩa là Authorization: Bearer some_token) trừ khi chúng bị ghi đè trong yêu cầu riêng lẻ.


Ví dụ : (lấy từ tài liệu của họ )

Yêu cầu:

POST https://www.googleapis.com/batch

Accept-Encoding: gzip
User-Agent: Google-HTTP-Java-Client/1.20.0 (gzip)
Content-Type: multipart/mixed; boundary=END_OF_PART
Content-Length: 963

--END_OF_PART
Content-Length: 337
Content-Type: application/http
content-id: 1
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id
Authorization: Bearer authorization_token
Content-Length: 70
Content-Type: application/json; charset=UTF-8


{
  "emailAddress":"example@appsrocks.com",
  "role":"writer",
  "type":"user"
}
--END_OF_PART
Content-Length: 353
Content-Type: application/http
content-id: 2
content-transfer-encoding: binary


POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id&sendNotificationEmail=false
Authorization: Bearer authorization_token
Content-Length: 58
Content-Type: application/json; charset=UTF-8


{
  "domain":"appsrocks.com",
   "role":"reader",
   "type":"domain"
}
--END_OF_PART--

Phản ứng:

HTTP/1.1 200 OK
Alt-Svc: quic=":443"; p="1"; ma=604800
Server: GSE
Alternate-Protocol: 443:quic,p=1
X-Frame-Options: SAMEORIGIN
Content-Encoding: gzip
X-XSS-Protection: 1; mode=block
Content-Type: multipart/mixed; boundary=batch_6VIxXCQbJoQ_AATxy_GgFUk
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
Date: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Vary: X-Origin
Vary: Origin
Expires: Fri, 13 Nov 2015 19:28:59 GMT

--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-1


HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35


{
 "id": "12218244892818058021i"
}


--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-2


HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35


{
 "id": "04109509152946699072k"
}


--batch_6VIxXCQbJoQ_AATxy_GgFUk--

1

Tôi sẽ bị cám dỗ trong một hoạt động như trong ví dụ của bạn để viết một trình phân tích cú pháp phạm vi.

Không có quá nhiều phiền phức để tạo một trình phân tích cú pháp có thể đọc "messageIds = 1-3,7-9,11,12-15". Nó chắc chắn sẽ tăng hiệu quả cho các hoạt động chăn bao gồm tất cả các thông điệp và có khả năng mở rộng hơn.


Quan sát tốt và tối ưu hóa tốt, nhưng câu hỏi đặt ra là liệu phong cách yêu cầu này có thể "tương thích" với khái niệm REST hay không.
Đánh dấu Renouf

Xin chào, tôi hiểu rồi. Việc tối ưu hóa làm cho khái niệm trở nên RESTful hơn và tôi không muốn bỏ qua lời khuyên của mình chỉ vì nó đi lang thang một cách nhỏ từ chủ đề.

1

Bài đăng tuyệt vời. Tôi đã tìm kiếm một giải pháp trong vài ngày. Tôi đã đưa ra một giải pháp sử dụng truyền chuỗi truy vấn với một ID ID được phân tách bằng dấu phẩy, như:

DELETE /my/uri/to/delete?id=1,2,3,4,5

... sau đó chuyển nó đến một WHERE INmệnh đề trong SQL của tôi. Nó hoạt động rất tốt, nhưng tự hỏi những gì người khác nghĩ về phương pháp này.


1
Tôi thực sự không thích nó bởi vì nó giới thiệu một loại mới, chuỗi mà bạn sử dụng làm danh sách trong đó. Tôi muốn phân tích nó thành một loại ngôn ngữ cụ thể thay vào đó và sau đó tôi có thể sử dụng cùng một phương thức trong cùng một cách trong nhiều phần khác nhau của hệ thống.
softarn

4
Một lời nhắc nhở phải thận trọng với các cuộc tấn công tiêm nhiễm SQL và luôn xóa dữ liệu của bạn và sử dụng các tham số liên kết khi thực hiện phương pháp này.
justin.hughey

2
Phụ thuộc vào hành vi mong muốn DELETE /books/delete?id=1,2,3khi cuốn sách số 3 không tồn tại - ý WHERE INchí sẽ âm thầm bỏ qua các hồ sơ, trong khi tôi thường mong đợi DELETE /books/delete?id=3404 nếu 3 không tồn tại.
chbrown

3
Một vấn đề khác bạn có thể gặp phải khi sử dụng giải pháp này là giới hạn đối với các ký tự được phép trong chuỗi URL. Nếu ai đó quyết định xóa hàng loạt 5.000 bản ghi, trình duyệt có thể từ chối URL hoặc Máy chủ HTTP (ví dụ Apache) có thể từ chối nó. Quy tắc chung (hy vọng sẽ thay đổi với các máy chủ và phần mềm tốt hơn) đã được áp dụng với kích thước tối đa là 2KB. Trường hợp với phần thân của POST bạn có thể lên tới 10MB. stackoverflow.com/questions/2364840/ từ
justin.hughey

0

Từ quan điểm của tôi, tôi nghĩ rằng Facebook có triển khai tốt nhất.

Một yêu cầu HTTP được thực hiện với một tham số bó và một cho mã thông báo.

Trong đợt một json được gửi. trong đó có một bộ "yêu cầu". Mỗi yêu cầu có một thuộc tính phương thức (get / post / put / xóa / etc ...) và một thuộc tính Rel_url (uri của điểm cuối), ngoài ra các phương thức post và put cho phép một thuộc tính "body" trong đó các trường được cập nhật được gửi .

Thêm thông tin tại: API lô Facebook

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.