web-dev-qa-db-ja.com

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

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アプリケーション)です。

165
Mark Renouf

バッチの単純な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な創造性はたくさんあります。

74
Alex

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

Crane and Pascarelloの本 Ajax in Action (素晴らしい本、ところで-推奨)で言及されているパターンを思い出します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
        }
    }
}

それはあなたを軌道に乗せるはずです。幸運を!

25

@Alexは正しい道を進んでいると思いますが、概念的には提案されたものの逆であるべきだと思います。

そのため、URLは実質的に「対象のリソース」です。

    [GET] mail/1

は、ID 1のメールからレコードを取得することを意味し、

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

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

    [GET] mail?markAsRead=true

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

したがって、この考え方に従うバッチ方式は次のとおりです。

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

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

19
fezfox

あなたの言語「それseems非常に無駄が多い...」は、私にとっては時期尚早な最適化の試みを示しています。オブジェクトの表現全体を送信することがパフォーマンスに大きな影響を与えることが示されない限り(150ミリ秒を超えるとユーザーに受け入れられません)、新しい非標準APIの動作を作成しようとしても意味がありません。 APIが単純であるほど、使いやすくなることを忘れないでください。

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

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

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

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

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

電子メールの変更可能なコンポーネント(読み取り、アーカイブ、重要度、ラベル)を別のオブジェクトに分割し、他のオブジェクト(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の使用を活用することです。更新する予定のプロパティと、他のすべてのプロパティを無視する必要があることを明示的に示すため。

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

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

まとめると、バッチアクションを使用してRESTful原則を遵守し、許容可能なパフォーマンスを維持することが可能です。

11
justin.hughey

GoogleドライブAPIには、この問題を解決するための非常に興味深いシステムがあります( こちらを参照 )。

基本的に、異なるリクエストを1つのContent-Type: multipart/mixedリクエストにグループ化し、個々の完全なリクエストを定義済みの区切り文字で区切ります。バッチリクエストのヘッダーとクエリパラメータは、個々のリクエストでオーバーライドされない限り、個々のリクエスト(つまりAuthorization: Bearer some_token)に継承されます。


:(彼らの docs から取られた)

リクエスト:

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

応答:

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

私はあなたの例のような範囲パーサーを書くような操作に誘惑されます。

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

1
One Monkey

素晴らしい投稿。私は数日間解決策を探していました。コンマで区切られたバンチIDを含むクエリ文字列を渡すことで、次のような解決策を思い付きました。

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

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

1
Roberto

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

バッチパラメータとトークン用に1つのHTTP要求が作成されます。

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

詳細: FacebookバッチAPI

0