Phương thức RESTful tốt nhất để trả về tổng số mục trong một đối tượng là gì?


139

Tôi đang phát triển dịch vụ API REST cho một trang web mạng xã hội lớn mà tôi tham gia. Cho đến nay, nó hoạt động rất tốt. Tôi có thể phát hành GET, POST, PUTDELETEyêu cầu URL đối tượng và ảnh hưởng đến dữ liệu của tôi. Tuy nhiên, dữ liệu này được phân trang (giới hạn ở 30 kết quả cùng một lúc).

Tuy nhiên, điều gì sẽ là cách RESTful tốt nhất để có được tổng số người nói, thành viên, thông qua API của tôi?

Hiện tại, tôi đưa ra yêu cầu cho cấu trúc URL như sau:

  • / api / thành viên LỚN Trả lại danh sách các thành viên (30 tại một thời điểm như đã đề cập ở trên)
  • / api / thành viên / 1 Đợi Có một thành viên, tùy thuộc vào phương thức yêu cầu được sử dụng

Câu hỏi của tôi là: làm thế nào tôi có thể sử dụng cấu trúc URL tương tự để có được tổng số thành viên trong ứng dụng của mình? Rõ ràng chỉ yêu cầu idtrường (tương tự API đồ thị của Facebook) và việc đếm kết quả sẽ không hiệu quả nếu chỉ có một lát 30 kết quả sẽ chỉ được trả về.


Câu trả lời:


84

Mặc dù phản hồi cho / API / người dùng được phân trang và chỉ trả về 30 bản ghi, nhưng không có gì ngăn bạn đưa vào phản hồi cả tổng số bản ghi và thông tin liên quan khác, như kích thước trang, số trang / phần bù, v.v. .

API StackOverflow là một ví dụ tốt về thiết kế tương tự. Đây là tài liệu cho phương thức Người dùng - https://api.stackexchange.com/docs/users


3
+1: Chắc chắn là điều RESTful nhất phải làm nếu giới hạn tìm nạp sẽ được áp đặt ở tất cả.
Donal Fellows

2
@bzim Bạn sẽ biết có một trang tiếp theo để tìm nạp bởi vì có một liên kết với rel = "next".
Darrel Miller

4
@Donal rel "tiếp theo" được đăng ký với IANA iana.org/assignments/link-relations/link-relations.txt
Darrel Miller

1
@Darrel - vâng, nó có thể được thực hiện với bất kỳ loại cờ "tiếp theo" nào trong tải trọng. Tôi chỉ cảm thấy rằng có tổng số mục của bộ sưu tập trong phản hồi là có giá trị và nó hoạt động như một cờ "tiếp theo" giống nhau.
Franci Penov

5
Để trả về một đối tượng không phải là danh sách các mục không phải là một triển khai API REST đúng cách nhưng REST không cung cấp bất kỳ cách nào để có được danh sách một phần kết quả. Vì vậy, để tôn trọng điều đó, tôi nghĩ rằng chúng ta nên sử dụng các tiêu đề để truyền các thông tin khác như tổng số, mã thông báo trang tiếp theo và mã thông báo trang trước đó. Tôi chưa bao giờ thử nó và tôi cần lời khuyên từ các nhà phát triển khác.
Loenix

74

Tôi thích sử dụng Tiêu đề HTTP cho loại thông tin theo ngữ cảnh này.

Đối với tổng số phần tử tôi sử dụng X-total-counttiêu đề.
Đối với các liên kết đến trang tiếp theo, v.v. Tôi sử dụng httpLink tiêu đề :
http://www.w3.org/wiki/LinkHeader

Github thực hiện theo cách tương tự: https://developer.github.com/v3/#pagination

Theo tôi, nó sạch hơn vì nó cũng có thể được sử dụng khi bạn trả lại nội dung không hỗ trợ siêu liên kết (ví dụ: nhị phân, hình ảnh).


5
RFC6648 không chấp nhận quy ước tiền tố tên của các tham số không đạt tiêu chuẩn với chuỗi X-.
JDawg

70

Gần đây tôi đã thực hiện một số nghiên cứu sâu rộng về vấn đề này và các câu hỏi liên quan đến phân trang REST khác và nghĩ rằng nó mang tính xây dựng để thêm một số phát hiện của tôi ở đây. Tôi đang mở rộng câu hỏi một chút để bao gồm những suy nghĩ về phân trang cũng như số lượng vì chúng có liên quan mật thiết với nhau.

Tiêu đề

Siêu dữ liệu phân trang được bao gồm trong phản hồi dưới dạng tiêu đề phản hồi. Lợi ích lớn của phương pháp này là bản thân tải trọng phản hồi chỉ là người yêu cầu dữ liệu thực tế đã yêu cầu. Làm cho việc xử lý phản hồi dễ dàng hơn đối với các khách hàng không quan tâm đến thông tin phân trang.

Có một loạt các tiêu đề (tiêu chuẩn và tùy chỉnh) được sử dụng trong tự nhiên để trả về thông tin liên quan đến phân trang, bao gồm tổng số.

Tổng số X

X-Total-Count: 234

Điều này được sử dụng trong một số API tôi tìm thấy trong tự nhiên. Ngoài ra còn có các gói NPM để thêm hỗ trợ cho tiêu đề này, ví dụ Loopback. Một số bài viết khuyên bạn nên thiết lập tiêu đề này là tốt.

Nó thường được sử dụng kết hợp với Linktiêu đề, đây là một giải pháp khá tốt để phân trang, nhưng thiếu thông tin tổng số.

Liên kết

Link: </TheBook/chapter2>;
      rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
      </TheBook/chapter4>;
      rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel

Tôi cảm thấy, từ việc đọc rất nhiều về chủ đề này, rằng sự đồng thuận chung là sử dụng Linktiêu đề để cung cấp liên kết phân trang cho khách hàng sử dụng rel=next, rel=previousv.v. Vấn đề với điều này là nó thiếu thông tin về tổng số hồ sơ có, đó là tại sao nhiều API kết hợp điều này với X-Total-Counttiêu đề.

Ngoài ra, một số API và ví dụ: tiêu chuẩn JsonApi , sử dụng Linkđịnh dạng, nhưng thêm thông tin vào phong bì phản hồi thay vì tiêu đề. Điều này giúp đơn giản hóa việc truy cập vào siêu dữ liệu (và tạo một nơi để thêm tổng số thông tin) với chi phí tăng độ phức tạp của việc truy cập dữ liệu thực tế (bằng cách thêm một phong bì).

Phạm vi nội dung

Content-Range: items 0-49/234

Được quảng bá bởi một bài viết trên blog có tên Range title, tôi chọn bạn (để phân trang)! . Tác giả làm cho một trường hợp mạnh mẽ để sử dụng RangeContent-Rangetiêu đề cho phân trang. Khi chúng tôi cẩn thận đọc các RFC về các tiêu đề, chúng ta thấy rằng việc mở rộng ý nghĩa của chúng vượt quá phạm vi của các byte đã thực sự dự đoán của RFC và được phép một cách rõ ràng. Khi được sử dụng trong bối cảnh itemsthay vìbytes , tiêu đề Phạm vi thực sự cung cấp cho chúng ta một cách để cả hai yêu cầu một phạm vi mục nhất định và cho biết phạm vi của tổng kết quả mà các mục phản hồi liên quan đến. Tiêu đề này cũng cung cấp một cách tuyệt vời để hiển thị tổng số. Và nó là một tiêu chuẩn thực sự mà chủ yếu là ánh xạ một-một để phân trang. Nó cũng được sử dụng trong tự nhiên .

Phong bì

Nhiều API, bao gồm một API từ trang web Hỏi & Đáp yêu thích của chúng tôi sử dụng một phong bì , một trình bao bọc xung quanh dữ liệu được sử dụng để thêm thông tin meta về dữ liệu. Ngoài ra, ODataJsonApi các tiêu chuẩn đều sử dụng phong bì phản hồi.

Nhược điểm lớn của điều này (imho) là việc xử lý dữ liệu phản hồi trở nên phức tạp hơn vì dữ liệu thực tế phải được tìm thấy ở đâu đó trong phong bì. Ngoài ra có nhiều định dạng khác nhau cho phong bì đó và bạn phải sử dụng đúng. Người ta nói rằng các phong bì phản hồi từ OData và JsonApi rất khác nhau, với OData trộn trong siêu dữ liệu tại nhiều điểm trong phản hồi.

Điểm cuối riêng biệt

Tôi nghĩ rằng điều này đã được bao phủ đủ trong các câu trả lời khác. Tôi đã không điều tra điều này nhiều vì tôi đồng ý với các ý kiến ​​rằng điều này gây nhầm lẫn vì hiện tại bạn có nhiều loại điểm cuối. Tôi nghĩ rằng nó là tốt nhất nếu mỗi điểm cuối đại diện cho một (bộ sưu tập) tài nguyên.

Suy nghĩ xa hơn

Chúng tôi không chỉ phải truyền đạt thông tin meta phân trang liên quan đến phản hồi mà còn cho phép khách hàng yêu cầu các trang / phạm vi cụ thể. Thật thú vị khi nhìn vào khía cạnh này để kết thúc với một giải pháp mạch lạc. Ở đây chúng ta cũng có thể sử dụng các tiêu đề ( Rangetiêu đề có vẻ rất phù hợp) hoặc các cơ chế khác như tham số truy vấn. Một số người chủ trương xử lý các trang kết quả như nguồn lực riêng biệt, trong đó có thể có ý nghĩa trong một số trường hợp sử dụng (ví dụ /books/231/pages/52. Tôi đã kết thúc việc lựa chọn một phạm vi hoang dã của các thông số yêu cầu thường xuyên được sử dụng như pagesize, page[size]limitvân vân ngoài việc hỗ trợ các Rangetiêu đề (và như request parameter cũng).


Tôi đặc biệt quan tâm đến Rangetiêu đề, tuy nhiên tôi không thể tìm thấy đủ bằng chứng cho thấy việc sử dụng bất kỳ thứ gì ngoài bytesloại phạm vi, là hợp lệ.
VisioN

2
Tôi nghĩ rằng bằng chứng rõ ràng nhất có thể được tìm thấy trong phần 14,5 của RFC : acceptable-ranges = 1#range-unit | "none"Tôi nghĩ rằng công thức này rõ ràng để lại chỗ cho các đơn vị phạm vi khác hơn bytes, mặc dù bản thân thông số kỹ thuật chỉ xác định bytes.
Stijn de Witt

24

Thay thế khi bạn không cần các mặt hàng thực tế

Câu trả lời của Franci Penov chắc chắn là cách tốt nhất để bạn luôn trả lại các mục cùng với tất cả các siêu dữ liệu bổ sung về các thực thể của bạn được yêu cầu. Đó là cách nó nên được thực hiện.

nhưng đôi khi trả lại tất cả dữ liệu không có ý nghĩa, bởi vì bạn có thể không cần chúng. Có lẽ tất cả những gì bạn cần là siêu dữ liệu về tài nguyên được yêu cầu của bạn. Giống như tổng số hoặc số lượng trang hoặc cái gì đó khác. Trong trường hợp như vậy, bạn luôn có thể có truy vấn URL yêu cầu dịch vụ của bạn không trả lại các mục mà chỉ là siêu dữ liệu như:

/api/members?metaonly=true
/api/members?includeitems=0

hoặc một cái gì đó tương tự ...


10
Việc nhúng thông tin này vào các tiêu đề có lợi thế là bạn có thể thực hiện một yêu cầu CHÍNH để chỉ nhận được số đếm.
felixfbecker

1
@felixfbecker chính xác, cảm ơn vì đã phát minh lại bánh xe và làm lộn xộn các API với tất cả các loại cơ chế khác nhau :)
EralpB

1
@EralpB Cảm ơn bạn đã phát minh lại bánh xe và làm lộn xộn các API!? Đầu được chỉ định trong HTTP. metaonlyhoặc includeitemslà không.
felixfbecker

2
@felixfbecker chỉ "chính xác" là dành cho bạn, phần còn lại dành cho OP. Xin lỗi vì sự nhầm lẫn.
EralpB

REST là tất cả về việc tận dụng HTTP và sử dụng nó cho những gì nó được dự định càng nhiều càng tốt. Phạm vi nội dung (RFC7233) nên được sử dụng trong trường hợp này. Các giải pháp trong cơ thể là không tốt, đặc biệt là vì nó sẽ không hoạt động với HEAD. tạo tiêu đề mới như đề xuất ở đây là không cần thiết và sai.
Vance Shipley

23

Bạn có thể trả về số lượng dưới dạng tiêu đề HTTP tùy chỉnh để đáp ứng yêu cầu CHÍNH. Bằng cách này, nếu khách hàng chỉ muốn đếm, bạn không cần phải trả về danh sách thực tế và không cần thêm URL.

(Hoặc, nếu bạn đang ở trong môi trường được kiểm soát từ điểm cuối đến điểm cuối, bạn có thể sử dụng động từ HTTP tùy chỉnh, chẳng hạn như COUNT.)


4
Tiêu đề HTTP tùy chỉnh HTTP? Điều đó sẽ xuất hiện dưới tiêu đề hơi ngạc nhiên, điều này trái ngược với những gì tôi nghĩ rằng một API RESTful nên có. Cuối cùng, nó không đáng ngạc nhiên.
Donal Fellows

21
@Donal tôi biết. Nhưng tất cả các câu trả lời tốt đã được thực hiện. :(
bzlm

1
Tôi cũng biết, nhưng đôi khi bạn phải để người khác trả lời. Hoặc làm cho sự đóng góp của bạn tốt hơn theo những cách khác, chẳng hạn như một lời giải thích chi tiết về lý do tại sao nó nên được thực hiện theo cách tốt nhất hơn là những cách khác.
Donal Fellows

4
Trong một môi trường được kiểm soát, điều này có thể không gây ngạc nhiên, vì nó có thể được sử dụng nội bộ & dựa trên chính sách API của nhà phát triển của bạn. Tôi muốn nói rằng đây là một giải pháp tốt trong một số trường hợp và đáng để lưu ý ở đây như một lưu ý về một giải pháp bất thường có thể có.
James Billingham

1
Tôi rất thích sử dụng các tiêu đề HTTP cho loại điều này (nó thực sự là nơi nó thuộc về). Tiêu đề Liên kết chuẩn có thể phù hợp trong trường hợp này (API Github sử dụng điều này).
Mike Marcacci

11

Tôi sẽ khuyên bạn nên thêm các tiêu đề cho cùng, như:

HTTP/1.1 200

Pagination-Count: 100
Pagination-Page: 5
Pagination-Limit: 20
Content-Type: application/json

[
  {
    "id": 10,
    "name": "shirt",
    "color": "red",
    "price": "$23"
  },
  {
    "id": 11,
    "name": "shirt",
    "color": "blue",
    "price": "$25"
  }
]

Để biết chi tiết tham khảo:

https://github.com/adnan-kamili/rest-api-response-format

Đối với tập tin vênh vang:

https://github.com/adnan-kamili/swagger-response-template


7

Kể từ "X -" - Tiền tố không được dùng nữa. (xem: https://tools.ietf.org/html/rfc6648 )

Chúng tôi đã tìm thấy "Phạm vi chấp nhận" là đặt cược tốt nhất để ánh xạ phân trang: https://tools.ietf.org/html/rfc7233#section-2.3 Vì "Đơn vị phạm vi" có thể là "byte" hoặc " mã thông báo". Cả hai không đại diện cho một loại dữ liệu tùy chỉnh. (xem: https://tools.ietf.org/html/rfc7233#section-4.2 ) Tuy nhiên, vẫn có tuyên bố rằng

Việc triển khai HTTP / 1.1 CÓ THỂ bỏ qua các phạm vi được chỉ định bằng cách sử dụng các đơn vị khác.

Điều này cho biết: sử dụng Đơn vị Phạm vi tùy chỉnh không chống lại giao thức, nhưng nó có thể bị bỏ qua.

Theo cách này, chúng tôi sẽ phải đặt Phạm vi chấp nhận thành "thành viên" hoặc bất kỳ loại đơn vị tầm xa nào, chúng tôi mong đợi. Và ngoài ra, cũng đặt Phạm vi Nội dung thành phạm vi hiện tại. (xem: https://www.w3.org/Prot Protocol / rfc2616 / rfc2616-sec3.html # sec3.12 )

Dù bằng cách nào, tôi sẽ tuân theo khuyến nghị của RFC7233 ( https://tools.ietf.org/html/rfc7233#page-8 ) để gửi 206 thay vì 200:

Nếu tất cả các điều kiện tiên quyết là đúng, máy chủ hỗ trợ trường
tiêu đề Phạm vi cho tài nguyên đích và (các) phạm vi được chỉ định là
hợp lệ và thỏa đáng (như được định nghĩa trong Mục 2.1), máy chủ NÊN
gửi phản hồi 206 (Nội dung một phần) với một trọng tải chứa một
hoặc nhiều biểu diễn một phần tương ứng với các
phạm vi thỏa đáng được yêu cầu, như được định nghĩa trong Phần 4.

Do đó, do đó, chúng tôi sẽ có các trường tiêu đề HTTP sau:

Đối với nội dung một phần:

206 Partial Content
Accept-Ranges: members
Content-Range: members 0-20/100

Đối với nội dung đầy đủ:

200 OK
Accept-Ranges: members
Content-Range: members 0-20/20

3

Có vẻ dễ nhất chỉ cần thêm một

GET
/api/members/count

và trả lại tổng số thành viên


11
Không phải là một ý tưởng tốt. Bạn bắt buộc khách hàng thực hiện 2 yêu cầu xây dựng phân trang trên trang của họ. Yêu cầu đầu tiên để có được danh sách các tài nguyên và thứ hai để đếm tổng số.
Jekis

Tôi nghĩ rằng đó là cách tiếp cận tốt ... bạn cũng có thể trả về danh sách kết quả là json và ở phía khách hàng kiểm tra kích thước của bộ sưu tập để trường hợp đó là ví dụ ngu ngốc ... hơn nữa bạn có thể có / api / thành viên / đếm và sau đó / api / thành viên? offset = 10 & giới hạn = 20
Michał Ziobro

1
Ngoài ra, hãy nhớ rằng rất nhiều kiểu phân trang không yêu cầu số đếm (Chẳng hạn như cuộn vô hạn) - Tại sao phải tính toán điều này khi khách hàng có thể không cần nó
tofarr

2

Điều gì về một điểm kết thúc mới> / api / thành viên / số mà chỉ cần gọi Member.Count () và trả về kết quả


27
Việc đếm số điểm cuối rõ ràng làm cho nó trở thành một tài nguyên địa chỉ độc lập. Nó sẽ hoạt động, nhưng sẽ đưa ra những câu hỏi thú vị cho bất kỳ ai mới biết về API của bạn - Số lượng thành viên của bộ sưu tập có phải là tài nguyên riêng biệt từ bộ sưu tập không? Tôi có thể cập nhật nó với yêu cầu PUT không? Nó tồn tại cho một bộ sưu tập trống hay chỉ khi có vật phẩm trong đó? Nếu membersbộ sưu tập có thể được tạo bởi một yêu cầu POST /api, cũng sẽ /api/members/countđược tạo như một hiệu ứng phụ, hoặc tôi phải thực hiện một yêu cầu POST rõ ràng để tạo nó trước khi yêu cầu? :-)
Franci Penov

2

Đôi khi các khung (như $ resource / AngularJS) yêu cầu một mảng làm kết quả truy vấn và bạn thực sự không thể có câu trả lời như {count:10,items:[...]} trong trường hợp này tôi lưu trữ "tính" trong answerHeaders.

PS Thực tế bạn có thể làm điều đó với $ resource / AngularJS, nhưng nó cần một số điều chỉnh.


Những điều chỉnh đó là gì? Chúng sẽ hữu ích cho các câu hỏi như thế này: stackoverflow.com/questions/19140017/ trên
JBCP

Angular không YÊU CẦU một mảng làm kết quả truy vấn, bạn chỉ cần định cấu hình tài nguyên của mình với thuộc tính đối tượng tùy chọn:isArray: false|true
Rémi Becheras

0

Bạn có thể coi countsnhư là một tài nguyên. URL sau đó sẽ là:

/api/counts/member

-1

Khi yêu cầu dữ liệu phân trang, bạn biết (theo giá trị tham số kích thước trang rõ ràng hoặc giá trị kích thước trang mặc định) kích thước trang, để bạn biết liệu bạn có phản hồi tất cả dữ liệu hay không. Khi có ít dữ liệu phản hồi hơn kích thước trang, thì bạn đã có toàn bộ dữ liệu. Khi một trang đầy đủ được trả về, bạn phải hỏi lại cho một trang khác.

Tôi thích có điểm cuối riêng cho số đếm (hoặc cùng điểm cuối với tham số CountOnly). Bởi vì bạn có thể chuẩn bị người dùng cuối cho quá trình tiêu tốn thời gian / thời gian dài bằng cách hiển thị thanh tiến trình được khởi tạo đúng cách.

Nếu bạn muốn trả về dữ liệu hóa trong mỗi phản hồi, thì cũng nên có pageSize, offset cũng được đề cập. Thành thật mà nói cách tốt nhất là lặp lại một bộ lọc yêu cầu. Nhưng phản ứng trở nên rất phức tạp. Vì vậy, tôi thích điểm cuối dành riêng để trả về số lượng.

<data>
  <originalRequest>
    <filter/>
    <filter/>
  </originalReqeust>
  <totalRecordCount/>
  <pageSize/>
  <offset/>
  <list>
     <item/>
     <item/>
  </list>
</data>

Couleage của tôi, thích tham số CountOnly với điểm cuối hiện có. Vì vậy, khi được chỉ định, phản hồi chỉ chứa siêu dữ liệu.

điểm cuối? bộ lọc = giá trị

<data>
  <count/>
  <list>
    <item/>
    ...
  </list>
</data>

điểm cuối? bộ lọc = giá trị & CountOnly = true

<data>
  <count/>
  <!-- empty list -->
  <list/>
</data>
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.