Pola untuk menangani operasi batch dalam layanan web REST?

170

Pola desain apa yang terbukti ada untuk operasi batch pada sumber daya dalam layanan web gaya REST?

Saya mencoba untuk menjadi keseimbangan antara cita-cita dan kenyataan dalam hal kinerja dan stabilitas. Kami memiliki API sekarang di mana semua operasi diambil dari sumber daya daftar (yaitu: GET / pengguna) atau dalam satu contoh (PUT / pengguna / 1, DELETE / pengguna / 22, dll).

Ada beberapa kasus di mana Anda ingin memperbarui satu bidang dari seluruh rangkaian objek. Tampaknya sangat boros untuk mengirim seluruh representasi untuk setiap objek bolak-balik untuk memperbarui satu bidang.

Dalam API gaya RPC, Anda dapat memiliki metode:

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

Apa persamaan REST di sini? Atau tidak masalah untuk berkompromi sesekali. Apakah itu merusak desain untuk menambah beberapa operasi tertentu di mana itu benar-benar meningkatkan kinerja, dll? Klien dalam semua kasus sekarang adalah Web Browser (aplikasi javascript di sisi klien).

Mark Renouf
sumber

Jawaban:

77

Pola RESTful sederhana untuk batch adalah untuk menggunakan sumber daya koleksi. Misalnya, untuk menghapus beberapa pesan sekaligus.

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

Ini sedikit lebih rumit untuk memperbarui sumber daya parsial, atau atribut sumber daya. Yaitu, perbarui setiap atribut yang ditandaiAsRead. Pada dasarnya, alih-alih memperlakukan atribut sebagai bagian dari masing-masing sumber daya, Anda memperlakukannya sebagai ember untuk menempatkan sumber daya. Salah satu contoh sudah diposting. Saya sedikit menyesuaikannya.

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

Pada dasarnya, Anda memperbarui daftar surat yang ditandai sebagai sudah dibaca.

Anda juga dapat menggunakan ini untuk menetapkan beberapa item ke kategori yang sama.

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

Jelas jauh lebih rumit untuk melakukan pembaruan parsial gaya iTunes (misalnya, artis + albumTitle tetapi tidak melacakTitle). Analogi ember mulai terurai.

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

Dalam jangka panjang, jauh lebih mudah untuk memperbarui sumber daya parsial tunggal, atau atribut sumber daya. Manfaatkan saja sub-sumber daya.

POST /mail/0/markAsRead
POSTDATA: true

Atau, Anda dapat menggunakan sumber daya yang diparameterisasi. Ini kurang umum dalam pola REST, tetapi diizinkan dalam spesifikasi URI dan HTTP. Tanda titik koma membagi parameter yang terkait secara horizontal dalam suatu sumber.

Perbarui beberapa atribut, beberapa sumber daya:

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

Perbarui beberapa sumber daya, hanya satu atribut:

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

Perbarui beberapa atribut, hanya satu sumber daya:

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

Kreativitas yang tenang berlimpah.

Alex
sumber
1
Orang dapat berargumen bahwa penghapusan Anda seharusnya adalah sebuah postingan karena itu tidak benar-benar menghancurkan sumber daya itu.
Chris Nicola
6
Itu tidak perlu. POST adalah metode pola pabrik, kurang eksplisit dan jelas daripada PUT / DELETE / GET. Satu-satunya harapan adalah bahwa server akan memutuskan apa yang harus dilakukan sebagai akibat dari POST. POST persis seperti biasanya, saya mengirimkan data formulir dan server melakukan sesuatu (semoga diharapkan) dan memberi saya beberapa indikasi mengenai hasilnya. Kami tidak diharuskan untuk membuat sumber daya dengan POST, kami hanya sering memilih untuk melakukannya. Saya dapat dengan mudah membuat sumber daya dengan PUT, saya hanya perlu mendefinisikan URL sumber daya sebagai pengirim (tidak sering ideal).
Chris Nicola
1
@nishant, dalam hal ini, Anda mungkin tidak perlu referensi beberapa sumber daya di URI, tetapi hanya mengirimkan tupel dengan referensi / nilai dalam tubuh permintaan. mis. 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
titik koma dicadangkan untuk tujuan ini.
Alex
1
Terkejut bahwa tidak ada yang menunjukkan bahwa memperbarui beberapa atribut pada satu sumber daya tercakup dengan baik PATCH- tidak perlu kreativitas dalam hal ini.
LB2
25

Tidak sama sekali - saya pikir REST yang setara adalah (atau setidaknya satu solusi) hampir persis seperti itu - antarmuka khusus yang dirancang mengakomodasi operasi yang diperlukan oleh klien.

Saya teringat akan pola yang disebutkan dalam buku Crane dan Pascarello, Ajax in Action (buku yang luar biasa, sangat direkomendasikan) di mana mereka menggambarkan penerapan CommandQueue jenis objek yang tugasnya adalah mengantri permintaan ke dalam kumpulan dan kemudian kirimkan ke server secara berkala.

Objek, jika saya ingat dengan benar, pada dasarnya hanya memegang array "perintah" - misalnya, untuk memperluas contoh Anda, masing-masing catatan berisi perintah "markAsRead", "messageId" dan mungkin referensi ke callback / handler fungsi - dan kemudian sesuai dengan beberapa jadwal, atau pada beberapa tindakan pengguna, objek perintah akan diserialisasi dan diposting ke server, dan klien akan menangani pasca pemrosesan.

Saya tidak kebetulan memiliki detailnya, tetapi sepertinya antrian perintah semacam ini akan menjadi salah satu cara untuk menangani masalah Anda; itu akan mengurangi chattiness keseluruhan secara substansial, dan itu akan abstrak antarmuka sisi server dengan cara Anda mungkin menemukan lebih fleksibel di jalan.


Perbarui : Aha! Saya telah menemukan potongan dari buku online itu, lengkap dengan contoh kode (meskipun saya masih menyarankan mengambil buku yang sebenarnya!). Lihat di sini , dimulai dengan bagian 5.5.3:

Ini mudah dikodekan tetapi dapat menghasilkan banyak lalu lintas sangat kecil ke server, yang tidak efisien dan berpotensi membingungkan. Jika kami ingin mengontrol lalu lintas kami, kami dapat menangkap pembaruan ini dan mengantre secara lokal dan kemudian mengirimkannya ke server dalam batch di waktu luang kami. Antrean pembaruan sederhana yang diterapkan dalam JavaScript ditunjukkan pada daftar 5.13. [...]

Antrian mempertahankan dua array. queued adalah array yang diindeks secara numerik, tempat pembaruan baru ditambahkan. sent adalah array asosiatif, yang berisi pembaruan yang telah dikirim ke server tetapi sedang menunggu balasan.

Berikut adalah dua fungsi terkait - satu bertanggung jawab untuk menambahkan perintah ke antrian ( addCommand), dan satu bertanggung jawab untuk membuat serial dan kemudian mengirimkannya ke server ( 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
        }
    }
}

Itu seharusnya membuat Anda pergi. Semoga berhasil!

Christian Nunciato
sumber
Terima kasih. Itu sangat mirip dengan ide-ide saya tentang bagaimana saya akan maju jika kami terus menjalankan operasi batch pada klien. Masalahnya adalah waktu pulang-pergi untuk melakukan operasi pada sejumlah besar objek.
Mark Renouf
Hm, ok - Saya pikir Anda ingin melakukan operasi pada sejumlah besar objek (di server) dengan permintaan ringan. Apakah saya salah paham?
Christian Nunciato
Ya, tapi saya tidak melihat bagaimana contoh kode itu akan melakukan operasi lebih efisien. Itu mengumpulkan permintaan tetapi masih mengirimkannya ke server satu per satu. Apakah saya salah menafsirkan?
Mark Renouf
Sebenarnya ia mengumpulkan mereka dan kemudian mengirimkannya sekaligus: bahwa untuk loop in fireRequest () pada dasarnya mengumpulkan semua perintah yang luar biasa, membuat serial mereka sebagai string (dengan .toRequestString (), misalnya, "method = markAsRead & messageIds = 1,2,3 , 4 "), menetapkan string itu ke" data ", dan data POST ke server.
Christian Nunciato
20

Sementara saya pikir @Alex berada di jalur yang benar, secara konseptual saya pikir itu harus menjadi kebalikan dari apa yang disarankan.

URL berlaku "sumber daya yang kami targetkan" karenanya:

    [GET] mail/1

berarti mendapatkan catatan dari email dengan id 1 dan

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

berarti menambal catatan surat dengan id 1. Pertanyaannya adalah "filter", memfilter data yang dikembalikan dari URL.

    [GET] mail?markAsRead=true

Jadi di sini kami meminta semua surat yang sudah ditandai sebagai sudah dibaca. Jadi untuk [PATCH] ke jalur ini akan mengatakan "tambalan catatan sudah ditandai sebagai benar" ... yang bukan apa yang kita coba capai.

Jadi metode batch, mengikuti pemikiran ini harus:

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

tentu saja saya tidak mengatakan ini benar REST (yang tidak mengizinkan manipulasi catatan batch), melainkan mengikuti logika yang sudah ada dan digunakan oleh REST.

fezfox
sumber
Jawaban yang menarik! Sebagai contoh terakhir Anda, bukankah akan lebih konsisten dengan [GET]format yang harus dilakukan [PATCH] mail?markAsRead=true data: [{"id": 1}, {"id": 2}, {"id": 3}](atau bahkan adil data: {"ids": [1,2,3]})? Manfaat lain dari pendekatan alternatif ini adalah Anda tidak akan mengalami kesalahan "414 URI Request terlalu lama" jika Anda memperbarui ratusan / ribuan sumber daya dalam koleksi.
rinogo
@rinogo - sebenarnya tidak. Inilah poin yang saya buat. Querystring adalah filter untuk catatan yang ingin kami tangani (mis. [GET] mail / 1 mendapat record mail dengan id 1, sedangkan email [GET]? MarkasRead = true mengembalikan email di mana markAsRead sudah benar). Tidak masuk akal untuk menambal ke URL yang sama (mis. "Menambal catatan di mana markAsRead = true") padahal sebenarnya kami ingin menambal catatan tertentu dengan id 1,2,3, TERKAIT DENGAN status saat ini dari tanda bidangAsAsead. Maka metode yang saya jelaskan. Setuju ada masalah dengan memperbarui banyak catatan. Saya akan membangun titik akhir yang tidak terlalu erat.
fezfox
11

Bahasa Anda, " Sepertinya sangat boros ...", bagi saya menunjukkan upaya optimasi prematur. Kecuali dapat ditunjukkan bahwa mengirimkan seluruh representasi objek adalah hit kinerja utama (kita berbicara tidak dapat diterima pengguna sebagai> 150 ms) maka tidak ada gunanya mencoba membuat perilaku API non-standar yang baru. Ingat, semakin sederhana API, semakin mudah digunakan.

Untuk menghapus, kirim yang berikut karena server tidak perlu tahu apa-apa tentang keadaan objek sebelum penghapusan terjadi.

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

Pikiran berikutnya adalah bahwa jika suatu aplikasi mengalami masalah kinerja mengenai pembaruan massal objek maka pertimbangan memecah setiap objek menjadi beberapa objek harus diberikan. Dengan begitu muatan JSON adalah sebagian kecil dari ukuran.

Sebagai contoh ketika mengirim respons untuk memperbarui status "baca" dan "diarsipkan" dari dua email terpisah, Anda harus mengirim yang berikut:

PUT /emails
POSTDATA: [
            {
              id:1,
              to:"[email protected]",
              from:"[email protected]",
              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:"[email protected]",
              from:"[email protected]",
              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"]
            }
            ]

Saya akan membagi komponen email yang dapat diubah (baca, diarsipkan, penting, label) menjadi objek terpisah karena yang lain (agar, dari, subjek, teks) tidak akan pernah diperbarui.

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"]}
          ]

Pendekatan lain yang harus diambil adalah memanfaatkan penggunaan PATCH. Untuk secara eksplisit menunjukkan properti yang ingin Anda perbarui dan yang lainnya harus diabaikan.

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

Orang-orang menyatakan bahwa PATCH harus diimplementasikan dengan memberikan berbagai perubahan yang mengandung: action (CRUD), path (URL), dan perubahan nilai. Ini dapat dianggap sebagai implementasi standar tetapi jika Anda melihat keseluruhan REST API, ini adalah non-intuitif sekali saja. Juga, implementasi di atas adalah bagaimana GitHub telah mengimplementasikan PATCH .

Singkatnya, dimungkinkan untuk mematuhi prinsip RESTful dengan aksi batch dan masih memiliki kinerja yang dapat diterima.

justin.hughey
sumber
Saya setuju bahwa PATCH paling masuk akal, masalahnya adalah jika Anda memiliki kode transisi negara lain yang perlu dijalankan ketika properti tersebut berubah, menjadi lebih sulit untuk diterapkan sebagai PATCH sederhana. Saya tidak berpikir REST benar-benar mengakomodasi segala bentuk transisi negara, mengingat itu seharusnya tanpa kewarganegaraan, tidak peduli apa itu transisi dari dan ke, hanya apa itu keadaan saat ini.
BeniRose
Hai BeniRose, terima kasih telah menambahkan komentar, saya sering bertanya-tanya apakah orang melihat beberapa posting ini. Itu membuat saya senang melihat orang melakukannya. Sumber daya mengenai sifat "stateless" dari REST mendefinisikannya sebagai masalah dengan server tidak harus mempertahankan status di seluruh permintaan. Karena itu, tidak jelas bagi saya masalah apa yang Anda gambarkan, dapatkah Anda menguraikan dengan contoh?
justin.hughey
8

Google drive API memiliki sistem yang sangat menarik untuk menyelesaikan masalah ini ( lihat di sini ).

Apa yang mereka lakukan pada dasarnya adalah mengelompokkan permintaan yang berbeda dalam satu Content-Type: multipart/mixedpermintaan, dengan masing-masing permintaan lengkap dipisahkan oleh beberapa pembatas yang ditentukan. Header dan parameter kueri dari permintaan batch diwarisi untuk permintaan individu (yaitu Authorization: Bearer some_token) kecuali mereka ditimpa dalam permintaan individu.


Contoh : (diambil dari dokumen mereka )

Permintaan:

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":"[email protected]",
  "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--

Tanggapan:

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--
Pembantu
sumber
1

Saya akan tergoda dalam operasi seperti yang ada dalam contoh Anda untuk menulis parser rentang.

Tidak sulit untuk membuat parser yang bisa membaca "messageIds = 1-3,7-9,11,12-15". Ini tentu akan meningkatkan efisiensi untuk operasi selimut yang mencakup semua pesan dan lebih terukur.


sumber
Pengamatan yang baik dan optimasi yang baik, tetapi pertanyaannya adalah apakah gaya permintaan ini bisa "kompatibel" dengan konsep REST.
Mark Renouf
Hai, ya saya mengerti. Optimalisasi memang membuat konsep itu lebih tenang dan saya tidak ingin mengabaikan saran saya hanya karena itu mengembara jauh dari topik.
1

Pos yang bagus. Saya telah mencari solusi selama beberapa hari. Saya datang dengan solusi menggunakan melewatkan string kueri dengan banyak ID yang dipisahkan oleh koma, seperti:

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

... lalu meneruskannya ke WHERE INklausa di SQL saya. Ini bekerja dengan baik, tetapi pikirkan apa yang orang lain pikirkan tentang pendekatan ini.

Roberto
sumber
1
Saya tidak benar-benar menyukainya karena ini memperkenalkan jenis baru, string yang Anda gunakan sebagai daftar di mana. Saya lebih suka menguraikannya ke jenis bahasa tertentu dan kemudian saya dapat menggunakan metode yang sama di cara yang sama di berbagai bagian sistem.
softarn
4
Pengingat untuk berhati-hati terhadap serangan injeksi SQL dan selalu membersihkan data Anda dan menggunakan parameter bind saat mengambil pendekatan ini.
justin.hughey
2
Tergantung pada perilaku yang diinginkan DELETE /books/delete?id=1,2,3ketika buku # 3 tidak ada - WHERE INkehendak diam-diam mengabaikan catatan, sedangkan saya biasanya berharap DELETE /books/delete?id=3untuk 404 jika 3 tidak ada.
chbrown
3
Masalah lain yang mungkin Anda temui dalam menggunakan solusi ini adalah batas karakter yang diizinkan dalam string URL. Jika seseorang memutuskan untuk menghapus 5.000 data secara massal, browser mungkin menolak URL atau Server HTTP (Apache misalnya) dapat menolaknya. Aturan umum (yang diharapkan berubah dengan server dan perangkat lunak yang lebih baik) telah berjalan dengan ukuran maksimum 2KB. Di mana dengan tubuh POST Anda bisa mencapai 10MB. stackoverflow.com/questions/2364840/…
justin.hughey
0

Dari sudut pandang saya, saya pikir Facebook memiliki implementasi terbaik.

Permintaan HTTP tunggal dibuat dengan parameter batch dan satu untuk token.

Dalam batch json dikirim. yang berisi koleksi "permintaan". Setiap permintaan memiliki properti metode (get / post / put / delete / etc ...), dan properti relative_url (uri dari titik akhir), selain itu metode post dan put memungkinkan properti "tubuh" tempat bidang diperbarui terkirim .

info lebih lanjut di: Facebook batch API

Leonardo Jauregui
sumber