REST web hizmetlerinde toplu işlemleri gerçekleştirme kalıpları?


170

REST tarzı web hizmetindeki kaynaklar üzerinde toplu işlemler için kanıtlanmış tasarım modelleri nelerdir?

Performans ve istikrar açısından idealler ve gerçeklik arasında bir denge kurmaya çalışıyorum. Şu anda tüm işlemlerin bir liste kaynağından (yani: GET / kullanıcı) veya tek bir örnekte (PUT / kullanıcı / 1, DELETE / kullanıcı / 22 vb.) Aldığı bir API'miz var.

Tüm nesne kümesinin tek bir alanını güncellemek istediğiniz bazı durumlar vardır. Bir alanı güncellemek için her nesne için tüm temsili ileri ve geri göndermek çok israf gibi görünüyor.

RPC stili API'da bir yönteminiz olabilir:

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

Burada REST eşdeğeri nedir? Yoksa arada sırada uzlaşmak uygun mudur? Performansı gerçekten geliştirdiği birkaç özel işlem eklemek için tasarımı mahvediyor mu? İstemci şu anda her durumda bir Web Tarayıcısıdır (istemci tarafında javascript uygulaması).

Yanıtlar:


77

Toplu iş için basit bir RESTful kalıbı bir toplama kaynağı kullanmaktır. Örneğin, aynı anda birden fazla mesajı silmek için.

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

Kısmi kaynakları veya kaynak özniteliklerini toplu olarak güncellemek biraz daha karmaşıktır. Yani, işaretlenen her AsRead özniteliğini güncelleyin. Temel olarak, özniteliği her kaynağın bir parçası olarak ele almak yerine, onu kaynakların koyacağı bir grup olarak ele alırsınız. Bir örnek zaten gönderildi. Biraz ayarladım.

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

Temel olarak, okundu olarak işaretlenen postaların listesini güncelleştiriyorsunuz.

Bunu aynı kategoriye birkaç öğe atamak için de kullanabilirsiniz.

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

İTunes tarzı toplu kısmi güncellemeler yapmak çok daha karmaşıktır (örn. Sanatçı + albumTitle ancak trackTitle değil). Kova benzeşmesi bozulmaya başlar.

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

Uzun vadede, tek bir kısmi kaynağı veya kaynak özelliklerini güncellemek çok daha kolaydır. Sadece bir alt kaynak kullanın.

POST /mail/0/markAsRead
POSTDATA: true

Alternatif olarak, parametreli kaynakları kullanabilirsiniz. Bu, REST kalıplarında daha az görülür, ancak URI ve HTTP özelliklerinde izin verilir. Noktalı virgül, bir kaynak içindeki yatay olarak ilişkili parametreleri böler.

Birkaç özelliği, birkaç kaynağı güncelleyin:

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

Birkaç kaynağı güncelleyin, yalnızca bir özellik:

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

Birkaç özelliği güncelleyin, yalnızca bir kaynak:

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

RESTful yaratıcılığı bol.


1
Biri, silmenizin aslında bir kaynak olması gerektiğini iddia edebilir, çünkü bu kaynağı gerçekten yok etmez.
Chris Nicola

6
Gerekli değil. POST bir fabrika modeli yöntemidir, PUT / DELETE / GET'ten daha az açık ve belirgindir. Tek beklenti, sunucunun POST sonucunda ne yapacağına karar vermesidir. POST tam olarak her zaman olduğu gibi, form verileri gönderiyorum ve sunucu (umarım beklenen) bir şey yapar ve bana sonuçla ilgili bazı göstergeler verir. POST ile kaynak yaratmamız gerekmiyor, sadece sık sık seçiyoruz. PUT ile kolayca bir kaynak oluşturabilirim, sadece kaynak URL'yi gönderen olarak tanımlamak zorundayım (genellikle ideal değil).
Chris Nicola

1
@nishant, bu durumda, muhtemelen URI'daki birden fazla kaynağa başvurmanız gerekmez, ancak yalnızca isteklerin gövdesindeki referanslar / değerlerle tuples geçirin. ör. POST / posta / 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
noktalı virgül bu amaç için ayrılmıştır.
Alex

1
Hiç kimsenin, tek bir kaynak üzerindeki birkaç özniteliği güncellemenin güzel bir şekilde kapsandığına dikkat PATCHçekmemesi şaşırtıcıydı - bu durumda yaratıcılığa gerek yok.
LB2

25

Hiç de değil - REST eşdeğeri (ya da en azından bir çözüm) neredeyse tam olarak bu - müşteri tarafından gerekli bir operasyonu tasarlanmış özel bir arayüz olduğunu düşünüyorum.

Crane ve Pascarello'nun Ajax in Action kitabında (bu arada mükemmel bir kitap - şiddetle tavsiye edilir) belirtilen , işlerini istekleri toplu olarak sıralamak olan CommandQueue türünde bir nesnenin uygulanmasını gösteren bir model hatırlatıyorum . daha sonra bunları düzenli olarak sunucuya gönderin.

Nesne, eğer doğru hatırlıyorsam, aslında sadece bir "komutlar" dizisi tuttum - örneğin, örneğin her biri bir "markAsRead" komutu, bir "messageId" ve belki de bir geri arama / işleyici referans içeren bir kayıt işlev - ve sonra bir zamanlamaya veya bazı kullanıcı eylemlerine göre, komut nesnesi serileştirilir ve sunucuya gönderilir ve istemci sonuçta sonradan işlenir.

Detaylara sahip değilim, ama bu tür bir komut kuyruğu sorununuzu ele almanın bir yolu gibi görünüyor; genel sohbeti önemli ölçüde azaltacak ve sunucu tarafı arayüzünü yolda daha esnek bulabileceğiniz bir şekilde soyutlayacaktır.


Güncelleme : Aha! Kod kitaplarıyla tam olarak bu çok kitaptan bir ipucu buldum (yine de gerçek kitabı almayı öneririm!). Bölüm 5.5.3'ten başlayarak buraya bir göz atın :

Bu kodlamak kolaydır, ancak sunucuya çok küçük trafik bitleri ile sonuçlanabilir, bu da verimsiz ve potansiyel olarak kafa karıştırıcıdır. Trafiğimizi kontrol etmek istiyorsak, bu güncellemeleri yakalayabilir ve yerel olarak sıralayabilir ve daha sonra boş zamanlarımızda toplu olarak sunucuya gönderebiliriz. JavaScript'te uygulanan basit bir güncelleme kuyruğu 5.13 listesinde gösterilmiştir. [...]

Kuyruk iki diziyi korur. queued yeni güncelleştirmelerin eklendiği sayısal olarak dizine alınmış bir dizidir. sent sunucuya gönderilen ancak yanıt bekleyen güncellemeleri içeren ilişkilendirilebilir bir dizidir.

İşte iki ilgili işlev: biri kuyruğa ( addCommand) komut eklemekten sorumlu , diğeri serileştirmeden ve bunları sunucuya göndermekten sorumludur ( 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
        }
    }
}

Bu seni harekete geçirmeli. İyi şanslar!


Teşekkürler. Bu, toplu işlemleri istemcide tutsaydık nasıl ilerleyeceğime dair fikirlerime çok benziyor. Sorun, çok sayıda nesne üzerinde işlem yapmak için gidiş-dönüş süresidir.
Mark Renouf

Hm, tamam - Hafif bir istekle çok sayıda nesne (sunucuda) üzerinde işlem yapmak istediğinizi düşündüm. Yanlış anladım mı?
Christian Nunciato

Evet, ancak bu kod örneğinin işlemi daha verimli bir şekilde nasıl gerçekleştireceğini görmüyorum. İstekleri toplu olarak işler, ancak yine de birer birer sunucuya gönderir. Yanlış mı yorumluyorum?
Mark Renouf

Aslında bunları toplu olarak gönderir ve hepsini bir kerede gönderir: fireRequest () içindeki loop için esasen tüm bekleyen komutları toplar, bunları bir dize (.toRequestString () ile serileştirir, örneğin, "method = markAsRead & messageIds = 1,2,3 , 4 "), bu dizeyi" data "a ve POSTs verilerini sunucuya atar.
Christian Nunciato

20

@Alex'in doğru yolda olduğunu düşünürken, kavramsal olarak önerilenin tersi olması gerektiğini düşünüyorum.

URL aslında "hedeflediğimiz kaynaklar" olduğundan:

    [GET] mail/1

1 numaralı postadan kayıt almak anlamına gelir ve

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

posta kaydını id 1 ile düzeltme anlamına gelir. querystring, URL'den döndürülen verileri filtreleyen bir "filtre" dir.

    [GET] mail?markAsRead=true

Burada zaten okundu olarak işaretlenmiş tüm postaları istiyoruz. Yani [PATCH] bu yola " zaten doğru olarak işaretlenmiş kayıtları yama" derdi ... bu bizim elde etmeye çalıştığımız şey değil.

Dolayısıyla, bu düşünceyi takip eden bir toplu yöntem şöyle olmalıdır:

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

Tabii ki bunun gerçek REST (toplu kayıt manipülasyonuna izin vermez) olduğunu söylemiyorum, daha ziyade zaten var olan ve REST tarafından kullanılan mantığı izliyor.


İlginç bir cevap! Son örneğiniz için, yapılacak [GET]biçimle [PATCH] mail?markAsRead=true data: [{"id": 1}, {"id": 2}, {"id": 3}](hatta yalnızca data: {"ids": [1,2,3]}) daha tutarlı olmaz mıydı ? Bu alternatif yaklaşımın bir diğer yararı da, koleksiyondaki yüzlerce / binlerce kaynağı güncelliyorsanız "414 URI'yi çok uzun süre" hatalarına karşı çalıştırmayacağınızdır.
rinogo

@rinogo - aslında hayır. İşte bu noktaya geldim. Sorgu dizesi, üzerinde işlem yapmak istediğimiz kayıtlar için bir filtredir (örn. [GET] mail / 1, 1 kaydını içeren posta kaydını alırken [GET] mail? MarkasRead = true, markAsRead'in zaten doğru olduğu postaları döndürür). Aslında, işareti markAsRead'in geçerli durumunun REGARDLESS kimliğiyle 1,2,3 kimliğine sahip belirli kayıtları yamalamak istediğimizde aynı URL'yi yamalamak (yani, "markAsRead = true olduğu yerlerde kayıtları yamalamak" anlamsızdır). Dolayısıyla tarif ettiğim yöntem. Birçok kayıt güncellenirken bir sorun olduğunu kabul edin. Daha az sıkı bir bağlantı noktası oluştururdum.
fezfox

11

" Çok israflı görünüyor ..." diliniz, zamanından önce optimizasyon girişimi olduğunu gösteriyor. Nesnelerin tüm temsilini göndermenin önemli bir performans isabeti olduğu gösterilemediği sürece (kullanıcılara> 150ms olarak kabul edilemez konuşuyoruz), yeni bir standart dışı API davranışı oluşturmaya çalışmanın bir anlamı yoktur. Unutmayın, API ne kadar basit olursa o kadar kolay kullanılır.

Silme işlemleri için, sunucunun silme gerçekleşmeden önce nesnenin durumu hakkında hiçbir şey bilmesine gerek olmadığı için aşağıdakileri gönderin.

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

Bir sonraki düşünce, bir uygulama nesnelerin toplu olarak güncellenmesi ile ilgili performans sorunlarıyla karşılaşıyorsa, her bir nesneyi birden çok nesneye bölmeyi düşünmelisiniz. Bu şekilde JSON yükü, boyutun bir kısmıdır.

Örnek olarak, iki ayrı e-postanın "okuma" ve "arşivlenmiş" durumlarını güncellemek için bir yanıt gönderirken aşağıdakileri göndermeniz gerekir:

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

E-postanın değişebilir bileşenlerini (okuma, arşivleme, önem, etiketler) diğerlerine (konudan metne) asla güncellenmeyeceği için ayrı bir nesneye bölerdim.

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

Alınacak başka bir yaklaşım, bir PATCH kullanımından faydalanmaktır. Hangi özellikleri güncellemek istediğinizi ve diğerlerinin yoksayılması gerektiğini açıkça belirtmek için.

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

İnsanlar PATCH işleminin aşağıdakileri içeren bir dizi değişiklik sağlayarak uygulanması gerektiğini belirtir: eylem (CRUD), yol (URL) ve değer değişikliği. Bu standart bir uygulama olarak kabul edilebilir, ancak bir REST API'sinin tamamına bakarsanız, sezgisel olmayan bir kereliktir. Ayrıca, yukarıdaki uygulama GitHub'ın PATCH uygulamasını nasıl gerçekleştirdiğidir .

Özetlemek gerekirse, toplu eylemlerle RESTful ilkelerine uymak ve hala kabul edilebilir bir performansa sahip olmak mümkündür.


PATCH'ın en mantıklı olduğunu kabul ediyorum, sorun şu ki, bu özellikler değiştiğinde çalışması gereken başka bir durum geçiş kodunuz varsa, basit bir PATCH olarak uygulanması daha zor hale gelir. Ben vatansız olması gerekiyorsa, REST'in herhangi bir devlet geçişini gerçekten barındırdığını düşünmüyorum, ne olduğunu ve ne olduğunu, sadece mevcut durumun ne olduğunu umursamıyor.
BeniRose

Hey BeniRose, yorum eklediğiniz için teşekkürler, genellikle insanların bu yayınlardan bazılarını görüp görmediğini merak ediyorum. İnsanların yaptığını görmek beni mutlu ediyor. REST'in "vatansız" niteliğine ilişkin kaynaklar, sunucuyu isteklerde durumu korumak zorunda kalmama sorunu olarak tanımlar. Bu nedenle, hangi meseleyi tanımladığınızı net değil, bir örnekle açıklayabilir misiniz?
justin.hughey

8

Google Drive API'sı bu sorunu çözmek için gerçekten ilginç bir sisteme sahiptir ( buraya bakın ).

Yaptıkları temelde farklı istekleri tek bir Content-Type: multipart/mixedtalepte gruplamaktır ve her bir tam talep belirli bir sınırlayıcı ile ayrılmıştır. Toplu isteğin üstbilgileri ve sorgu parametresi, ayrı ayrı istekte Authorization: Bearer some_tokengeçersiz kılınmadıkça ayrı isteklere (yani ) devralınır .


Örnek : ( dokümanlarından alınmıştır )

İstek:

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--

Tepki:

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

Bir aralık ayrıştırıcısı yazmak için örneğinizdeki gibi bir işlemde cazip olurdum.

"MessageIds = 1-3,7-9,11,12-15" okuyabilen bir ayrıştırıcı yapmak çok zahmetli değil. Kesinlikle tüm mesajları kapsayan battaniye operasyonları için verimliliği artıracak ve daha ölçeklenebilir.


İyi gözlem ve iyi bir optimizasyon, ancak soru, bu talep tarzının REST konseptiyle hiç "uyumlu" olup olmadığıydı.
Mark Renouf

Merhaba, evet anladım. Optimizasyon, konsepti daha RESTful yapıyor ve sadece konudan küçük bir şekilde dolaştığı için tavsiyemi bırakmak istemedim.

1

Harika gönderi. Birkaç gündür çözüm arıyordum. Gibi virgülle ayrılmış bir grup kimlikleri ile bir sorgu dizesi geçen kullanarak bir çözüm ile geldi:

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

... sonra bunu SQL'imdeki bir WHERE INmaddeye aktarıyorum. Harika çalışıyor, ancak başkalarının bu yaklaşım hakkında ne düşündüğünü merak ediyorum.


1
Gerçekten hoşlanmıyorum çünkü yeni bir tür tanıttı, burada bir liste olarak kullandığınız dize. Bunun yerine dile özgü bir türe ayrıştırmak ve sonra aynı yöntemi kullanabilirsiniz sistemin farklı bölümlerinde de aynı şekilde.
softarn

4
SQL enjeksiyon saldırılarına karşı dikkatli olun ve bu yaklaşımı uygularken verilerinizi her zaman temizleyin ve bağlama parametrelerini kullanın.
justin.hughey

2
DELETE /books/delete?id=1,2,33 numaralı kitabın olmadığı zaman istenen davranışa bağlıdır - WHERE INsessizce kayıtları görmezden gelirken DELETE /books/delete?id=33 olmazsa genellikle 404 beklerim.
chbrown

3
Bu çözümü kullanırken karşılaşabileceğiniz farklı bir sorun, bir URL dizesinde izin verilen karakter sınırlamasıdır. Birisi 5.000 kaydı toplu olarak silmeye karar verirse tarayıcı URL'yi reddedebilir veya HTTP Sunucusu (örneğin Apache) reddedebilir. Genel kural (umarım daha iyi sunucular ve yazılımlarla değişir) maksimum 2 KB boyutundadır. Bir POST gövdesi ile 10MB'a kadar gidebilirsiniz. stackoverflow.com/questions/2364840/…
justin.hughey

0

Benim açımdan, Facebook'un en iyi uygulamaya sahip olduğunu düşünüyorum.

Bir toplu iş parametresi ve bir belirteç için tek bir HTTP isteği yapılır.

Toplu olarak bir json gönderilir. "isteklerin" bir koleksiyonunu içerir. Her istek bir method özelliğine (get / post / put / delete / etc ...) ve relative_url özelliğine (bitiş noktasının uri) sahiptir, ayrıca post ve put yöntemleri alanların güncelleneceği bir "body" özelliğine izin verir gönderilir.

daha fazla bilgi için: Facebook batch API

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.