REST Webサービスでバッチ操作を処理するためのパターン?


170

RESTスタイルのWebサービス内のリソースに対するバッチ操作には、実績のある設計パターンが存在しますか?

パフォーマンスと安定性の観点から理想と現実のバランスをとろうとしています。現在、すべての操作がリストリソース(つまり、GET / user)または単一のインスタンス(PUT / user / 1、DELETE / user / 22など)から取得するAPIがあります。

オブジェクトのセット全体の単一のフィールドを更新したい場合があります。1つのフィールドを更新するために、各オブジェクトの表現全体を前後に送信するのは非常に無駄です。

RPCスタイルのAPIでは、次のメソッドを使用できます。

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

ここで同等のRESTとは何ですか?それとも妥協しても大丈夫ですか。デザインを台無しにして、パフォーマンスなどを実際に向上させるいくつかの特定の操作を追加しますか?現在のところ、すべてのクライアントはWebブラウザ(クライアント側のJavaScriptアプリケーション)です。

回答:


77

バッチの単純なRESTfulパターンは、コレクションリソースを利用することです。たとえば、一度に複数のメッセージを削除します。

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

部分的なリソースまたはリソース属性をバッチ更新するのは少し複雑です。つまり、各markedAsRead属性を更新します。基本的に、属性を各リソースの一部として扱うのではなく、リソースを入れるバケットとして扱います。1つの例はすでに投稿されています。少し調整しました。

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

基本的に、あなたは既読としてマークされたメールのリストを更新しています。

これを使用して、同じカテゴリに複数のアイテムを割り当てることもできます。

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

iTunesスタイルのバッチ部分更新(たとえば、artist + albumTitleは実行するが、trackTitleは実行しない)を行うと、明らかにはるかに複雑になります。バケットのアナロジーは崩壊し始めます。

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

長期的には、単一の部分リソースまたはリソース属性を更新する方がはるかに簡単です。サブリソースを利用するだけです。

POST /mail/0/markAsRead
POSTDATA: true

または、パラメーター化されたリソースを使用することもできます。これはRESTパターンではあまり一般的ではありませんが、URIおよびHTTP仕様では許可されています。セミコロンは、リソース内の水平方向に関連するパラメーターを分割します。

複数の属性、複数のリソースを更新します。

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

いくつかのリソースを更新し、1つの属性のみ:

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

複数の属性を更新しますが、リソースは1つだけです。

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

RESTfulな創造性はたくさんあります。


1
削除は実際にはそのリソースを破壊するものではないので、削除は実際には投稿であると主張することができます。
クリスニコラ

6
必要ありません。POSTはファクトリパターンのメソッドであり、PUT / DELETE / GETよりも明確で明確ではありません。唯一の期待は、サーバーがPOSTの結果として何をすべきかを決定することです。POSTは以前とまったく同じです。フォームデータを送信すると、サーバーが何かを実行し(うまくいけば期待どおり)、結果を表示します。POSTでリソースを作成する必要はありません。多くの場合、選択するだけです。PUTを使用してリソースを簡単に作成できます。リソースURLを送信者として定義する必要があります(多くの場合、理想的ではありません)。
クリスニコラ

1
@nishant(この場合、URIで複数のリソースを参照する必要はないでしょう)が、リクエストの本文で参照/値を持つタプルを渡すだけです。例: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
セミコロンはこの目的のために予約されています。
アレックス

1
単一のリソースのいくつかの属性の更新がうまくカバーされていることを誰も指摘しなかったことに驚きましたPATCH-この場合、創造性の必要はありません。
LB2

25

まったくそうではありません-RESTと同等のもの(または少なくとも1つのソリューションはそうです)とほぼ同じだと思います-クライアントが必要とする操作に対応するように設計された特殊なインターフェース

私は、クレーンで述べたとPascarelloの本パターンのことを思い出したよアクションでアヤックス -彼らは実装を示している(方法によって優れた本が、強く推奨)CommandQueueでの仕事、それがバッチに要求をキューすることで、オブジェクトの並べ替えをし、その後、定期的にサーバーに投稿します。

オブジェクトは、私が正しく覚えていれば、基本的には「コマンド」の配列を保持しているだけです。たとえば、例を拡張するために、それぞれが「markAsRead」コマンド、「messageId」、そしておそらくコールバック/ハンドラーへの参照を含むレコード関数-そして、いくつかのスケジュールに従って、またはいくつかのユーザーアクションに従って、コマンドオブジェクトがシリアル化されてサーバーにポストされ、クライアントがその後のポストプロセスを処理します。

詳細はわかりませんが、この種のコマンドキューは問題を処理する1つの方法のように思えます。それは全体的な雑談を大幅に減らし、将来あなたがより柔軟に感じるかもしれない方法でサーバー側インターフェースを抽象化します。


アップデート:あはは!その本からオンラインでコードサンプルを含む断片を見つけました(ただし、実際の本を入手することをお勧めします!)。 セクション5.5.3から始めて、こちらをご覧ください

これは簡単にコーディングできますが、サーバーへのトラフィックが非常に少量になる可能性があり、非効率的で混乱を招く可能性があります。トラフィックを制御したい場合は、これらの更新をキャプチャしてローカルキューに入れてから、自由にバッチでサーバーに送信できます。JavaScriptで実装された簡単な更新キューをリスト5.13に示します。[...]

キューは2つの配列を保持します。queued 新しい更新が追加される、数値でインデックス付けされた配列です。sent サーバーに送信されたが、応答を待っている更新を含む連想配列です。

次に、2つの関連する関数を示します。1つはコマンドをキューに追加する役割(addCommand)、もう1つはシリアル化してサーバーに送信する役割(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
        }
    }
}

それであなたはうまくいくはずです。幸運を!


ありがとう。これは、クライアントでバッチ操作を続けた場合にどうするかという私の考えと非常によく似ています。問題は、多数のオブジェクトに対して操作を実行するための往復時間です。
Mark Renouf、

うーん、わかりました-軽量のリクエストを介して(サーバー上の)多数のオブジェクトに対して操作を実行したいと思っていました。私は誤解しましたか?
クリスチャンヌンチャアート09

はい、しかし、そのコードサンプルが操作をより効率的に実行する方法がわかりません。リクエストをバッチ処理しますが、一度に1つずつサーバーに送信します。私は誤解していますか?
マークルヌーフ、

実際には、それらをまとめて一度に送信します。fireRequest()のforループは、基本的にすべての未処理のコマンドを収集し、それらを文字列としてシリアル化します(.toRequestString()を使用して、たとえば "method = markAsRead&messageIds = 1,2,3 、4 ")は、その文字列を" data "に割り当て、データをサーバーにPOSTします。
クリスチャンヌンチャアート09

20

@Alexは正しい方向に進んでいると思いますが、概念的には、示唆されていることの逆になるはずです。

このURLは、実際には「対象とするリソース」であるため、次のようになります。

    [GET] mail/1

ID 1のメールからレコードを取得し、

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

は、メールレコードにID 1をパッチすることを意味します。クエリ文字列は「フィルタ」であり、URLから返されたデータをフィルタリングします。

    [GET] mail?markAsRead=true

したがって、ここではすでに既読としてマークされているすべてのメールをリクエストしています。したがって、このパスに[PATCH]するには、「すでに trueとマークされているレコードにパッチを適用する」ということになります...これは、達成しようとしていることではありません。

したがって、この考え方に従うバッチメソッドは次のようになります。

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

もちろん、これが本当のREST(バッチレコード操作を許可しない)だと言っているのではなく、RESTによってすでに存在し、使用されているロジックに従います。


興味深い答えです!あなたの最後の例については、[GET]行うべきフォーマットとの整合性がより高いと思いませんか[PATCH] mail?markAsRead=true data: [{"id": 1}, {"id": 2}, {"id": 3}](あるいは単にdata: {"ids": [1,2,3]})この代替アプローチのもう1つの利点は、コレクション内の数百/数千のリソースを更新している場合に、「414 Request URI too long」エラーが発生しないことです。
rinogo

@rinogo-実際にはありません。これが私が言っていた点です。クエリ文字列は、対象となるレコードのフィルターです(たとえば、[GET] mail / 1は、IDが1のメールレコードを取得しますが、[GET] mail?markasRead = trueは、markAsReadがすでにtrueであるメールを返します)。実際に、markAsReadフィールドの現在のステータスのID 1、2、3、REGARDLESSを持つ特定のレコードにパッチを適用する場合、同じURLにパッチを適用しても意味がありません(つまり、「markAsRead = trueの場合はレコードをパッチ」)。したがって、私が説明した方法。多くのレコードの更新に問題があることに同意します。私はそれほど強く結合されていないエンドポイントを構築します。
fezfox 2016

11

「非常に無駄に見えます...」というあなたの言葉は、時期尚早の最適化の試みを示しています。オブジェクトの表現全体を送信するとパフォーマンスが大幅に低下することが示されている場合を除き(150 msを超えるとユーザーには受け入れられないことです)、新しい非標準のAPI動作を作成しようとしても意味がありません。覚えておいてください、APIが単純であるほど、それは使いやすくなります。

削除が発生する前に、サーバーはオブジェクトの状態について何も知る必要がないため、削除の場合は以下を送信します。

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

次の考えは、アプリケーションがオブジェクトの一括更新に関するパフォーマンスの問題に直面している場合、各オブジェクトを複数のオブジェクトに分割することを検討する必要があるということです。そうすることで、JSONペイロードはサイズの一部になります。

例として、2つの個別の電子メールの「既読」および「アーカイブ済み」ステータスを更新するための応答を送信する場合、以下を送信する必要があります。

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

他の(to、from、subject、text)は更新されないので、メールの変更可能なコンポーネント(読み取り、アーカイブ、重要度、ラベル)を個別のオブジェクトに分割します。

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

取る別のアプローチは、パッチの使用を活用することです。更新する予定のプロパティを明示し、その他のプロパティはすべて無視する必要があることを明示するため。

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

人々は、アクション(CRUD)、パス(URL)、および値の変更を含む一連の変更を提供することにより、PATCHを実装する必要があると述べています。これは標準の実装と考えることができますが、REST API全体を見ると、直感的ではない1回限りのものです。また、上記の実装は、GitHubがPATCHを実装した方法です。

要約すると、バッチアクションでRESTfulの原則を順守し、許容できるパフォーマンスを維持できます。


PATCHが最も理にかなっていることに同意します。問題は、これらのプロパティが変更されたときに実行する必要がある他の状態遷移コードがある場合、単純なPATCHとして実装することがより困難になることです。RESTは、ステートレスであると想定されているため、実際にはあらゆる種類の状態遷移に対応しているとは思いません。現在の状態が何であるかを問わず、RESTは何からどのように遷移するかは関係ありません。
BeniRose 2017年

こんにちはBeniRose、コメントを追加してくれてありがとう、私はしばしば人々がこれらの投稿のいくつかを見ているのかと思います。人々がそうするのを見て私は幸せです。RESTの「ステートレス」の性質に関するリソースは、RESTを、リクエスト間でサーバーが状態を維持する必要がないという懸念として定義しています。そのため、どの問題について説明していたのかはわかりませんが、例を挙げて詳しく説明していただけますか?
justin.hughey 2017年

8

google drive APIには、この問題を解決するための非常に興味深いシステムがあります(ここを参照))。

彼らが行うことは、基本的に、1つのContent-Type: multipart/mixedリクエストにさまざまなリクエストをグループ化することです。個々の完全なリクエストは、定義された区切り文字で区切られます。バッチリクエストのヘッダーとクエリパラメータは、個々のリクエストでAuthorization: Bearer some_token上書きされない限り、個々のリクエストに継承されます(つまり)。


:(ドキュメントから取得)

リクエスト:

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

応答:

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

あなたの例のような操作で範囲パーサーを作成したくなります。

「messageIds = 1-3,7-9,11,12-15」を読み取ることができるパーサーを作成することはそれほど面倒ではありません。それは確かにすべてのメッセージをカバーするブランケット操作の効率を高め、よりスケーラブルです。


良い観察と優れた最適化ですが、問題は、このスタイルのリクエストがRESTの概念と「互換性がある」かどうかです。
Mark Renouf、

こんにちは、そうです。最適化によってコンセプトがよりRESTfulになり、トピックから少し離れただけでアドバイスを省略したくありませんでした。

1

素晴らしいポスト。私は数日間解決策を探していました。私は、次のように、カンマで区切られた一連のIDを含むクエリ文字列を渡すソリューションを考え出しました。

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

...それをWHERE INSQLの句に渡します。それはうまく機能しますが、他の人がこのアプローチをどう思うか疑問に思います。


1
新しいタイプ、つまりリストのどこかで使用する文字列が導入されるため、私はあまり好きではありません。代わりに、言語固有のタイプに解析して、同じメソッドをシステムの複数の異なる部分で同じように。
softarn、2014

4
SQLインジェクション攻撃に注意し、常にデータをクレンジングし、このアプローチを採用する場合はバインドパラメータを使用することを忘れないでください。
justin.hughey 2014年

2
DELETE /books/delete?id=1,2,3本#3が存在しない場合の望ましい動作に依存します- WHERE INは黙ってレコードを無視しますが、通常DELETE /books/delete?id=33が存在しない場合は404を期待します。
chbrown 2014年

3
このソリューションを使用して発生する可能性のある別の問題は、URL文字列で許可される文字数の制限です。誰かが5,000レコードを一括削除することを決定した場合、ブラウザはURLを拒否するか、HTTPサーバー(Apacheなど)がそれを拒否する可能性があります。一般的な規則(うまくいけば、より良いサーバーとソフトウェアで変更されるでしょう)は、最大サイズが2KBになるようになっています。POSTの本文では、最大10MBまで移動できます。 stackoverflow.com/questions/2364840/…–
justin.hughey

0

私の観点からは、Facebookが最良の実装だと思います。

単一のHTTPリクエストは、バッチパラメータとトークン用に1つ作成されます。

バッチでjsonが送信されます。「リクエスト」のコレクションが含まれています。各リクエストには、メソッドプロパティ(get / post / put / delete / etc ...)とrelative_urlプロパティ(エンドポイントのURI)があり、さらにpostメソッドとputメソッドでは、フィールドを更新する「body」プロパティを使用できます送られた 。

詳細:FacebookバッチAPI

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.