Praktik terbaik untuk pembaruan parsial dalam layanan yang tenang


208

Saya menulis layanan tenang untuk sistem manajemen pelanggan dan saya mencoba untuk menemukan praktik terbaik untuk memperbarui catatan secara parsial. Misalnya, saya ingin penelepon dapat membaca catatan lengkap dengan permintaan GET. Tetapi untuk memperbaruinya hanya operasi tertentu pada catatan yang diizinkan, seperti mengubah status dari ENABLED ke DISABLED. (Saya punya skenario yang lebih kompleks dari ini)

Saya tidak ingin penelepon menyerahkan seluruh catatan hanya dengan bidang yang diperbarui untuk alasan keamanan (rasanya juga berlebihan).

Apakah ada cara yang disarankan untuk membangun URI? Saat membaca buku REST, panggilan gaya RPC tampaknya disukai.

Jika panggilan berikut mengembalikan catatan pelanggan lengkap untuk pelanggan dengan id 123

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

bagaimana saya harus memperbarui status?

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

POST /customer/123/changeStatus
DISABLED

...

Pembaruan : Untuk menambah pertanyaan. Bagaimana cara memasukkan 'panggilan logika bisnis' ke api REST? Apakah ada cara yang disepakati untuk melakukan ini? Tidak semua metode CRUD secara alami. Beberapa lebih kompleks, seperti ' sendEmailToCustomer (123) ', ' mergeCustomers (123, 456) ', ' countCustomers () '

POST /customer/123?cmd=sendEmail

POST /cmd/sendEmail?customerId=123

GET /customer/count 

3
Untuk menjawab pertanyaan Anda tentang "panggilan logika bisnis" di sini adalah posting tentang POSTdari Roy Fielding sendiri: roy.gbiv.com/untangled/2009/it-is-okay-to-use-post di mana ide dasarnya adalah: jika tidak ada metode (seperti GETatau PUT) cocok untuk penggunaan operasi Anda POST.
rojoca

Inilah yang akhirnya saya lakukan. Buat panggilan REST untuk mengambil dan memperbarui sumber daya yang diketahui menggunakan GET, PUT, DELETE. POST untuk menambahkan sumber daya baru dan POST dengan beberapa URL deskriptif untuk panggilan logika bisnis.
magiconair

Apa pun yang Anda putuskan, jika operasi itu bukan bagian dari respons GET, Anda tidak memiliki layanan RESTful. Saya tidak melihat itu di sini
MStodd

Jawaban:


69

Anda pada dasarnya memiliki dua opsi:

  1. Gunakan PATCH(tetapi perhatikan bahwa Anda harus menentukan jenis media Anda sendiri yang menentukan apa yang akan terjadi dengan tepat)

  2. Gunakan POSTuntuk sub sumber daya dan kembalikan 303 Lihat Lainnya dengan tajuk Lokasi yang menunjuk ke sumber daya utama. Maksud dari 303 adalah untuk memberi tahu klien: "Saya telah melakukan POST Anda dan efeknya adalah bahwa beberapa sumber daya lain telah diperbarui. Lihat header Lokasi untuk sumber daya yang mana." POST / 303 dimaksudkan untuk penambahan berulang ke sumber daya untuk membangun keadaan beberapa sumber daya utama dan itu sangat cocok untuk pembaruan parsial.


OK, POST / 303 masuk akal bagi saya. PATCH dan MERGE Saya tidak dapat menemukan dalam daftar kata kerja HTTP yang valid sehingga akan membutuhkan pengujian lebih lanjut. Bagaimana saya membangun URI jika saya ingin sistem mengirim email ke pelanggan 123? Sesuatu seperti pemanggilan metode RPC murni yang tidak mengubah keadaan objek sama sekali. Apa cara tenang untuk melakukan ini?
magiconair

Saya tidak mengerti pertanyaan URI email. Apakah Anda ingin menerapkan gateway yang dapat Anda POST untuk membuatnya mengirim email atau Anda mencari mailto: customer.123@service.org?
Jan Algermissen

15
Baik REST atau HTTP tidak ada hubungannya dengan CRUD selain dari beberapa orang menyamakan metode HTTP dengan CRUD. REST adalah tentang memanipulasi keadaan sumber daya dengan mentransfer representasi. Apa pun yang ingin Anda capai, Anda lakukan dengan mentransfer representasi ke sumber daya dengan semantik yang sesuai. Waspadalah terhadap istilah 'panggilan metode murni' atau 'logika bisnis' karena terlalu mudah menyiratkan 'HTTP untuk transportasi'. Jika Anda perlu mengirim email, POST ke sumber daya gateway, jika Anda perlu bergabung ke akun, buat yang baru dan representasi POST dari dua lainnya, dll.
Jan Algermissen

9
Lihat juga bagaimana Google melakukannya: googlecode.blogspot.com/2010/03/...
Marius

4
williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot PATCH [{"op": "test", "path": "/ a / b / c", "value" : "foo"}, {"op": "hapus", "path": "/ a / b / c"}, {"op": "add", "path": "/ a / b / c" , "value": ["foo", "bar"]}, {"op": "replace", "path": "/ a / b / c", "value": 42}, {"op": "pindah", "dari": "/ a / b / c", "path": "/ a / b / d"}, {"op": "copy", "from": "/ a / b / d "," path ":" / a / b / e "}]
intotecho

48

Anda harus menggunakan POST untuk pembaruan sebagian.

Untuk memperbarui bidang untuk pelanggan 123, buat POST ke / pelanggan / 123.

Jika Anda ingin memperbarui hanya statusnya, Anda juga dapat PUT ke / pelanggan / 123 / status.

Umumnya, permintaan GET tidak boleh memiliki efek samping, dan PUT adalah untuk menulis / mengganti seluruh sumber daya.

Ini mengikuti langsung dari HTTP, seperti yang terlihat di sini: http://en.wikipedia.org/wiki/HTTP_PUT#Request_methods


1
@John Saunders POST tidak harus selalu membuat sumber daya baru yang dapat diakses dari URI: tools.ietf.org/html/rfc2616#section-9.5
wsorenson

10
@wsorensen: Saya tahu itu tidak perlu menghasilkan URL baru, tetapi masih berpikir POST /customer/123harus membuat hal yang jelas yang secara logis di bawah pelanggan 123. Mungkin pesanan? TETAPI /customer/123/statustampaknya lebih masuk akal, dengan asumsi POST /customerssecara implisit menciptakan status(dan menganggap itu sah REST).
John Saunders

1
@John Saunders: secara praktis, jika kita ingin memperbarui bidang pada sumber daya yang terletak di URI tertentu, POST lebih masuk akal daripada PUT, dan kurang UPDATE, saya percaya ini sering digunakan dalam layanan REST. POST ke / pelanggan dapat membuat pelanggan baru, dan status PUT ke / pelanggan / 123 / dapat menyelaraskan lebih baik dengan kata spesifikasi, tetapi untuk praktik terbaik, saya tidak berpikir ada alasan untuk tidak POST ke / pelanggan / 123 untuk memperbarui bidang - itu singkat, masuk akal, dan tidak sepenuhnya bertentangan dengan apa pun dalam spesifikasi.
wsorenson

8
Bukankah seharusnya permintaan POST tidak idempoten? Tentunya memperbarui entri adalah idempoten dan karenanya harus menjadi PUT?
Martin Andersson

1
@MartinAndersson POST-pertanyaan tidak perlu non-idempoten. Dan seperti yang disebutkan, PUTharus mengganti seluruh sumber daya.
Halle Knast

10

Anda harus menggunakan PATCH untuk pembaruan sebagian - baik menggunakan dokumen json-patch (lihat http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-08 atau http://www.mnot.net/ blog / 2012/09/05 / patch ) atau framework patch XML (lihat http://tools.ietf.org/html/rfc5261 ). Namun menurut saya, json-patch adalah yang paling cocok untuk jenis data bisnis Anda.

PATCH dengan dokumen patch JSON / XML memiliki semantik maju sangat selat untuk pembaruan parsial. Jika Anda mulai menggunakan POST, dengan salinan yang diubah dari dokumen asli, untuk pembaruan parsial Anda segera mengalami masalah di mana Anda ingin nilai yang hilang (atau, lebih tepatnya, nilai nol) untuk mewakili "abaikan properti ini" atau "setel properti ini ke nilai kosong "- dan itu mengarah ke lubang kelinci dari solusi yang diretas yang pada akhirnya akan menghasilkan jenis format tambalan Anda sendiri.

Anda dapat menemukan jawaban yang lebih mendalam di sini: http://soabits.blogspot.dk/2013/01/http-put-patch-or-post-partial-updates.html .


Harap dicatat bahwa sementara itu RFC untuk patch json dan xml-patch telah selesai.
botchniaque

8

Saya mengalami masalah yang sama. PUT pada sub-sumber daya tampaknya berfungsi ketika Anda hanya ingin memperbarui satu bidang. Namun, kadang-kadang Anda ingin memperbarui banyak hal: Pikirkan formulir web yang mewakili sumber daya dengan opsi untuk mengubah beberapa entri. Pengiriman formulir oleh pengguna tidak boleh menghasilkan beberapa PUT.

Berikut adalah dua solusi yang dapat saya pikirkan:

  1. lakukan PUT dengan seluruh sumber daya. Di sisi server, tentukan semantik bahwa PUT dengan seluruh sumber daya mengabaikan semua nilai yang belum berubah.

  2. lakukan PUT dengan sumber daya parsial. Di sisi server, tentukan semantik ini menjadi gabungan.

2 hanyalah optimasi bandwidth 1. Kadang-kadang 1 adalah satu-satunya pilihan jika sumber daya mendefinisikan beberapa bidang isian wajib (pikirkan buffer proto).

Masalah dengan kedua pendekatan ini adalah cara membersihkan bidang. Anda harus menetapkan nilai null khusus (terutama untuk buffer proto karena nilai null tidak didefinisikan untuk buffer proto) yang akan menyebabkan pembersihan bidang.

Komentar?


2
Ini akan lebih berguna jika diposting sebagai pertanyaan terpisah.
intotecho

6

Untuk memodifikasi status, saya pikir pendekatan RESTful adalah dengan menggunakan sub-sumber daya logis yang menggambarkan status sumber daya. IMO ini sangat berguna dan bersih ketika Anda memiliki set status yang berkurang. Itu membuat API Anda lebih ekspresif tanpa memaksakan operasi yang ada untuk sumber daya pelanggan Anda.

Contoh:

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

Layanan POST harus mengembalikan pelanggan yang baru dibuat dengan id:

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

GET untuk sumber daya yang dibuat akan menggunakan lokasi sumber daya:

GET /customer/123/active

GET / pelanggan / 123 / tidak aktif harus mengembalikan 404

Untuk operasi PUT, tanpa menyediakan entitas Json, ia hanya akan memperbarui status

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

Memberikan entitas akan memungkinkan Anda untuk memperbarui konten pelanggan dan memperbarui status pada saat bersamaan.

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

Anda membuat sub-sumber daya konseptual untuk sumber daya pelanggan Anda. Ini juga konsisten dengan definisi Roy Fielding tentang sumber daya: "... Sumber daya adalah pemetaan konseptual untuk satu set entitas, bukan entitas yang sesuai dengan pemetaan pada titik waktu tertentu ..." Dalam hal ini pemetaan konseptual adalah pelanggan aktif ke pelanggan dengan status = AKTIF.

Baca operasi:

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

Jika Anda melakukan panggilan-panggilan itu satu per satu setelah yang lainnya harus mengembalikan status 404, output yang berhasil mungkin tidak termasuk status karena itu implisit. Tentu saja Anda masih dapat menggunakan GET / pelanggan / 123? Status = ACTIVE | INACTIVE untuk meminta sumber daya pelanggan secara langsung.

Operasi DELETE menarik karena semantiknya dapat membingungkan. Tetapi Anda memiliki pilihan untuk tidak menerbitkan operasi itu untuk sumber daya konseptual ini, atau menggunakannya sesuai dengan logika bisnis Anda.

DELETE /customer/123/active

Yang satu dapat membawa pelanggan Anda ke status DELETED / DISABLED atau ke status yang berlawanan (AKTIF / TIDAK AKTIF).


Bagaimana Anda sampai ke sub-sumber daya?
MStodd

Saya refactored jawaban yang berusaha membuatnya lebih jelas
raspacorp

5

Hal-hal untuk ditambahkan ke pertanyaan Anda yang ditambah. Saya pikir Anda sering dapat merancang aksi bisnis yang lebih rumit dengan sempurna Tetapi Anda harus memberikan metode / prosedur gaya berpikir dan berpikir lebih dalam sumber daya dan kata kerja.

pengiriman surat


POST /customers/123/mails

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

Implementasi sumber daya ini + POST kemudian akan mengirimkan surat. jika perlu Anda bisa menawarkan sesuatu seperti / pelanggan / 123 / kotak keluar dan kemudian menawarkan tautan sumber daya ke / pelanggan / mails / {mailId}.

jumlah pelanggan

Anda bisa menanganinya seperti sumber pencarian (termasuk metadata pencarian dengan informasi paging dan num-found, yang memberi Anda jumlah pelanggan).


GET /customers

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


Saya suka cara pengelompokan logis bidang dalam sub-sumber daya POST.
gertas

3

Gunakan PUT untuk memperbarui sumber daya tidak lengkap / parsial.

Anda dapat menerima jObject sebagai parameter dan menguraikan nilainya untuk memperbarui sumber daya.

Di bawah ini adalah fungsi yang dapat Anda gunakan sebagai referensi:

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

Mengenai Pembaruan Anda.

Konsep CRUD saya percaya telah menyebabkan beberapa kebingungan mengenai desain API. CRUD adalah konsep umum tingkat rendah untuk operasi dasar untuk dilakukan pada data, dan kata kerja HTTP hanyalah metode permintaan ( dibuat 21 tahun yang lalu ) yang mungkin atau mungkin tidak memetakan ke operasi CRUD. Bahkan, coba cari keberadaan akronim CRUD dalam spesifikasi HTTP 1.0 / 1.1.

Panduan yang dijelaskan dengan sangat baik yang menerapkan konvensi pragmatis dapat ditemukan dalam dokumentasi API platform Google cloud . Ini menjelaskan konsep di balik pembuatan API berbasis sumber daya, yang menekankan sejumlah besar sumber daya dibandingkan operasi, dan termasuk kasus penggunaan yang Anda gambarkan. Meskipun hanya desain konvensi untuk produk mereka, saya pikir itu masuk akal.

Konsep dasar di sini (dan yang menghasilkan banyak kebingungan) adalah pemetaan antara "metode" dan kata kerja HTTP. Satu hal adalah menentukan "operasi" (metode) apa yang akan dilakukan API Anda atas jenis sumber daya apa (misalnya, mendapatkan daftar pelanggan, atau mengirim email), dan yang lainnya adalah kata kerja HTTP. Harus ada definisi dari keduanya, metode dan kata kerja yang Anda rencanakan untuk digunakan dan pemetaan di antara keduanya .

Hal ini juga mengatakan bahwa, ketika operasi tidak peta persis dengan metode standar ( List, Get, Create, Update, Deletedalam hal ini), kita bisa menggunakan "metode Custom", seperti BatchGet, yang mengambil beberapa objek berdasarkan beberapa masukan objek id, atau SendEmail.


2

RFC 7396 : JSON Merge Patch (diterbitkan empat tahun setelah pertanyaan diposting) menjelaskan praktik terbaik untuk PATCH dalam hal format dan aturan pemrosesan.

Singkatnya, Anda mengirimkan HTTP PATCH ke sumber daya target dengan jenis media aplikasi / merge-patch + json MIME dan sebuah badan yang hanya mewakili bagian-bagian yang ingin Anda ubah / tambahkan / hapus lalu ikuti aturan pemrosesan di bawah ini.

Aturan :

  • Jika patch gabungan yang disediakan berisi anggota yang tidak muncul dalam target, anggota tersebut ditambahkan.

  • Jika target mengandung anggota, nilainya diganti.

  • Nilai kosong di patch gabungan diberi arti khusus untuk menunjukkan penghapusan nilai yang ada di target.

Contoh kasus uji yang menggambarkan aturan di atas (seperti yang terlihat dalam lampiran RFC itu):

 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

Lihat http://www.odata.org/

Itu mendefinisikan metode MERGE, jadi dalam kasus Anda itu akan menjadi seperti ini:

MERGE /customer/123

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

Hanya statusproperti yang diperbarui dan nilai-nilai lainnya dipertahankan.


Apakah MERGEkata kerja HTTP yang valid?
John Saunders

3
Lihatlah PATCH - yang merupakan HTTP standar segera dan melakukan hal yang sama.
Jan Algermissen

@ John Saunders Ya, ini merupakan metode ekstensi.
Max Toro

FYI MERGE telah dihapus dari OData v4. MERGE was used to do PATCH before PATCH existed. Now that we have PATCH, we no longer need MERGE. Lihat docs.oasis-open.org/odata/new-in-odata/v4.0/cn01/…
tanguy_k

0

Itu tidak masalah. Dalam hal REST, Anda tidak dapat melakukan GET, karena itu tidak dapat di-cache, tetapi tidak masalah jika Anda menggunakan POST atau PATCH atau PUT atau apa pun, dan tidak masalah seperti apa bentuk URL itu. Jika Anda melakukan REST, yang penting adalah ketika Anda mendapatkan representasi sumber daya Anda dari server, representasi itu dapat memberikan opsi transisi keadaan klien.

Jika respons GET Anda memiliki transisi status, klien hanya perlu tahu cara membacanya, dan server dapat mengubahnya jika perlu. Di sini pembaruan dilakukan menggunakan POST, tetapi jika itu diubah menjadi PATCH, atau jika URL berubah, klien masih tahu cara membuat pembaruan:

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

Anda dapat memberikan daftar parameter yang diperlukan / opsional untuk diberikan klien kepada Anda. Tergantung aplikasinya.

Sejauh operasi bisnis, itu mungkin sumber daya yang berbeda terkait dengan sumber daya pelanggan. Jika Anda ingin mengirim email ke pelanggan, mungkin layanan itu adalah sumber dayanya sendiri yang dapat Anda POST, sehingga Anda dapat menyertakan operasi berikut dalam sumber daya pelanggan:

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

Beberapa video bagus, dan contoh arsitektur REST presenter adalah ini. Stormpath hanya menggunakan GET / POST / DELETE, yang baik-baik saja karena REST tidak ada hubungannya dengan operasi apa yang Anda gunakan atau bagaimana tampilan URL (kecuali GET harus dapat di-cache):

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

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.