REST 웹 서비스에서 일괄 작업을 처리하기위한 패턴?


170

REST 스타일 웹 서비스 내의 리소스에 대한 배치 작업에 대해 어떤 입증 된 디자인 패턴이 있습니까?

성능과 안정성 측면에서 이상과 현실 사이의 균형을 유지하려고합니다. 현재 모든 작업이 목록 리소스 (예 : GET / user) 또는 단일 인스턴스 (PUT / user / 1, DELETE / user / 22 등)에서 검색하는 API가 있습니다.

전체 개체 집합의 단일 필드를 업데이트하려는 경우가 있습니다. 하나의 필드를 업데이트하기 위해 각 객체에 대한 전체 표현을주고받는 것이 매우 낭비적인 것처럼 보입니다.

RPC 스타일 API에서는 다음과 같은 방법을 사용할 수 있습니다.

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

여기서 REST와 동등한 것은 무엇입니까? 아니면 타협해도 괜찮습니다. 실제로 성능 등을 향상시키는 몇 가지 특정 작업을 추가하도록 디자인을 망치게됩니까? 모든 경우에있어서 클라이언트는 현재 웹 브라우저 (클라이언트 측의 자바 스크립트 애플리케이션)입니다.

답변:


77

배치에 대한 간단한 RESTful 패턴은 콜렉션 자원을 사용하는 것입니다. 예를 들어 한 번에 여러 메시지를 삭제합니다.

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

부분 리소스 또는 리소스 속성을 일괄 업데이트하는 것이 조금 더 복잡합니다. 즉, 각 표시된 AsRead 속성을 업데이트하십시오. 기본적으로 속성을 각 리소스의 일부로 취급하는 대신 리소스를 넣을 버킷으로 취급합니다. 하나의 예가 이미 게시되었습니다. 조금 조정했습니다.

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

하나의 속성만으로 여러 리소스를 업데이트하십시오.

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

하나의 자원만으로 여러 속성을 업데이트하십시오.

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

RESTful 한 창의력이 풍부합니다.


1
삭제는 실제로 해당 리소스를 파괴하지 않기 때문에 실제로 게시물이어야한다고 주장 할 수 있습니다.
Chris Nicola

6
필요하지 않습니다. POST는 팩토리 패턴 방법이며 PUT / DELETE / GET보다 덜 명확하고 명백합니다. 서버가 POST의 결과로 수행 할 작업을 결정할 것입니다. POST는 항상 항상 그렇습니다. 양식 데이터를 제출하고 서버는 무언가를 (희망적으로 기대) 수행하고 결과에 대한 표시를 제공합니다. POST로 리소스를 만들 필요는 없으며 종종 선택합니다. PUT을 사용하여 쉽게 리소스를 만들 수 있으며 리소스 URL을 보낸 사람으로 정의하면됩니다 (종종 이상적이지 않음).
Chris Nicola

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
세미콜론은이 목적으로 예약되어 있습니다.
Alex

1
단일 리소스에서 여러 속성을 업데이트하는 PATCH것은이 경우 창의성을 필요로하지 않는다고 지적한 사람이 아무도 없습니다 .
LB2

25

전혀 그렇지 않다. 나는 REST에 상응하는 것이 (또는 적어도 하나의 솔루션이다) 거의 정확하게 그와 같다고 생각한다. 클라이언트가 요구하는 작업을 수용하도록 설계된 특수 인터페이스.

나는 Crane and Pascarello의 책 Ajax in Action (훌륭한 책, 강력히 권장되는 책) 에서 언급 한 패턴을 상기시켜 줍니다.이 명령은 CommandQueue 종류의 객체를 요청으로 일괄 처리하는 작업을 구현하는 것을 보여줍니다. 그런 다음 정기적으로 서버에 게시하십시오.

내가 정확하게 기억한다면 객체는 본질적으로 "명령"의 배열을 가졌다. 예를 들어, 각각의 "markAsRead"명령을 포함하는 레코드, "messageId"및 콜백 / 핸들러에 대한 참조 기능-일정에 따라 또는 일부 사용자 작업에 따라 명령 개체가 직렬화되어 서버에 게시되고 클라이언트는 후속 사후 처리를 처리합니다.

자세한 내용은 알 수 없지만 이런 종류의 명령 대기열이 문제를 처리하는 한 가지 방법 인 것 같습니다. 전체적인 대화 성을 크게 줄이며,보다 유연한 방법으로 서버 측 인터페이스를 추상화합니다.


업데이트 : 아하! 나는 그 책에서 코드 샘플로 완성 된 코드를 발견했습니다 (실제로 책을 집어 올리는 것이 좋습니다!). 섹션 5.5.3으로 시작하는 여기를 살펴보십시오 .

이것은 코딩하기 쉽지만 서버에 대한 아주 작은 비트의 트래픽을 초래할 수 있으며, 이는 비효율적이고 혼란을 줄 수 있습니다. 트래픽을 제어하려면 이러한 업데이트를 캡처 하여 로컬로 큐에 넣은 다음 여가 시간에 일괄 적으로 서버로 보낼 수 있습니다. JavaScript로 구현 된 간단한 업데이트 큐가 목록 5.13에 나와 있습니다. [...]

대기열은 두 개의 배열을 유지합니다. queued 새로운 업데이트가 추가되는 숫자 인덱스 배열입니다. sent 서버로 전송되었지만 응답을 기다리는 업데이트가 포함 된 연관 배열입니다.

다음은 두 가지 관련 기능입니다. 하나는 명령을 대기열에 추가하고 ( addCommand) 직렬화 한 다음 서버로 전송하는 역할을합니다 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

흠, 괜찮아요-가벼운 요청을 통해 많은 수의 서버 (서버)에서 작업을 수행하고 싶다고 생각했습니다. 내가 오해 했습니까?
Christian Nunciato

예, 그러나 해당 코드 샘플이 어떻게 더 효율적으로 작업을 수행하는지 알 수 없습니다. 요청을 일괄 처리하지만 한 번에 하나씩 서버로 보냅니다. 내가 잘못 해석하고 있습니까?
Mark Renouf

실제로는 일괄 처리 한 다음 한 번에 모두 전송합니다. fireRequest ()의 for 루프는 기본적으로 모든 미해결 명령을 수집하고 .toRequestString ()을 사용하여 문자열로 직렬화합니다 (예 : "method = markAsRead & messageIds = 1,2,3). , 4 "), 해당 문자열을"data "에 할당하고 데이터를 서버에 POST합니다.
Christian Nunciato

20

@Alex가 올바른 길을 가고 있다고 생각하지만 개념적으로 제안 된 것과 반대이어야한다고 생각합니다.

URL은 사실상 "우리가 타겟팅하는 리소스"입니다.

    [GET] mail/1

ID가 1 인 메일에서 레코드를 가져오고

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

메일 레코드에 id 1을 패치하는 것을 의미합니다. querystring은 "필터"이며 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]})? 이 대체 접근 방식의 또 다른 이점은 컬렉션에서 수백 / 수천 개의 리소스를 업데이트하는 경우 "414 Request URI too long"오류가 발생하지 않는다는 것입니다.
rinogo

@ rinogo-실제로 아니요. 이것이 제가하고있는 요점입니다. querystring은 우리가 처리하고자하는 레코드에 대한 필터입니다 (예 : [GET] mail / 1은 id가 1 인 메일 레코드를 가져 오는 반면, [GET] mail? markasRead = true는 markAsRead가 이미 true 인 메일을 반환합니다). 실제로 필드 markAsRead의 현재 상태 ID가 1,2,3, REGARDLESS 인 특정 레코드를 패치하려고 할 때 동일한 URL (예 : "markAsRead = true 인 레코드 패치")에 패치하는 것은 의미가 없습니다. 따라서 내가 설명한 방법. 많은 레코드를 업데이트하는 데 문제가 있음에 동의하십시오. 덜 밀접하게 연결된 끝점을 작성합니다.
fezfox

11

귀하의 언어 인 " 매우 낭비스러운 것 같습니다 ..."는 나에게 조기 최적화 시도를 나타냅니다. 객체의 전체 표현을 전송하는 것이 주요 성능 적중이라는 것을 알 수 없다면 (우리는> 150ms로 사용자에게 용납 할 수 없다고 말하고 있습니다) 새로운 비표준 API 동작을 만들려는 시도는 없습니다. API가 단순할수록 사용하기 쉬워집니다.

삭제는 서버가 삭제가 발생하기 전에 객체의 상태에 대해 알 필요가 없으므로 다음을 전송하십시오.

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

다음 생각은 응용 프로그램이 객체의 대량 업데이트와 관련하여 성능 문제가 발생하면 각 객체를 여러 객체로 나눌 때 고려해야 할 사항입니다. 그런 식으로 JSON 페이로드는 크기의 일부입니다.

예를 들어 두 개의 개별 전자 메일의 "읽기"및 "보관 된"상태를 업데이트하기 위해 응답을 보낼 때 다음을 보내야합니다.

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

전자 메일의 가변 구성 요소 (읽기, 보관, 중요도, 레이블)를 다른 개체 (대상, 텍스트, 텍스트)가 업데이트되지 않으므로 별도의 개체로 분리합니다.

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 API 전체를 살펴보면 직관적이지 않은 일회성입니다. 또한 위의 구현은 GitHub가 PATCH를 구현 한 방법 입니다.

요약하면 배치 작업으로 RESTful 원칙을 준수하면서도 여전히 수용 가능한 성능을 유지할 수 있습니다.


PATCH가 가장 의미가 있음에 동의합니다. 문제는 해당 속성이 변경 될 때 실행해야하는 다른 상태 전이 코드가 있으면 간단한 PATCH로 구현하기가 더 어렵다는 것입니다. REST는 실제로 상태가없는 것으로 가정 할 때 어떤 종류의 상태 전환도 수용하지 않는다고 생각합니다. 전환은 현재 상태가 무엇인지 상관하지 않으며 현재 상태 만 신경 쓰지 않습니다.
BeniRose

이봐 BeniRose, 의견을 추가 주셔서 감사합니다, 나는 종종 사람들이 이러한 게시물 중 일부를 볼 수 궁금합니다. 사람들이하는 것을 보게되어 기쁩니다. REST의 "상태 비 저장"특성과 관련된 리소스는 요청 전체에서 상태를 유지 관리 할 필요가없는 서버의 문제로 정의합니다. 따라서 어떤 문제를 설명하고 있는지 명확하지 않습니다. 예를 들어 설명 할 수 있습니까?
justin.hughey

8

Google 드라이브 API에는이 문제를 해결하기위한 정말 흥미로운 시스템이 있습니다 ( 여기 참조 ).

그들이하는 일은 기본적으로 하나의 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

안녕하세요, 이해합니다. 최적화는 개념을 좀 더 편안하게 만들고 주제에서 작은 길을 방황했기 때문에 조언을 남기고 싶지 않았습니다.

1

좋은 포스트. 며칠 동안 해결책을 찾고있었습니다. 다음과 같이 쉼표로 구분 된 묶음 ID로 쿼리 문자열을 전달하는 솔루션을 생각해 냈습니다.

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

... 그런 다음 WHERE IN내 SQL 의 절로 전달합니다 . 그것은 잘 작동하지만 다른 사람들 이이 접근법에 대해 어떻게 생각하는지 궁금합니다.


1
나는 그것이 새로운 유형, 당신이 어디에서 목록으로 사용하는 문자열을 소개하기 때문에 그것을 좋아하지 않습니다. 대신 언어 특정 유형으로 구문 분석 한 다음 동일한 방법을 사용할 수 있습니다. 시스템의 여러 다른 부분에서 동일한 방식으로.
softarn September

4
이 방법을 사용할 때는 SQL 주입 공격에주의하고 항상 데이터를 정리하고 바인드 매개 변수를 사용하십시오.
justin.hughey

2
DELETE /books/delete?id=1,2,3책 # 3이 존재하지 않을 때 의 원하는 동작에 따라 - WHERE IN레코드가 자동으로 무시되는 반면 DELETE /books/delete?id=33이 없으면 404를 기대 합니다.
chbrown

3
이 솔루션을 사용할 때 발생할 수있는 다른 문제는 URL 문자열에 허용되는 문자 제한입니다. 누군가 5,000 레코드를 대량 삭제하기로 결정한 경우 브라우저가 URL을 거부하거나 HTTP 서버 (예 : Apache)가이를 거부 할 수 있습니다. 더 나은 서버와 소프트웨어로 바뀔 수있는 일반적인 규칙은 최대 2KB 크기입니다. POST 본문으로 최대 10MB까지 갈 수 있습니다. stackoverflow.com/questions/2364840/…
justin.hughey

0

내 관점에서 볼 때 Facebook이 최상의 구현이라고 생각합니다.

일괄 처리 매개 변수와 토큰에 대한 단일 HTTP 요청이 작성됩니다.

일괄 처리에서 json이 전송됩니다. 여기에는 "요청"모음이 포함됩니다. 각 요청에는 메소드 특성 (get / post / put / delete / etc ...)과 relative_url 특성 (끝점의 URI)이 있으며, post 및 put 메소드는 필드가 업데이트되는 "body"특성을 허용합니다. 전송됩니다.

자세한 정보 : Facebook batch API

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.