RailsにバッチAPI導入してみた

# バッチAPI

---

## バッチAPIの目的

- システムによって、バッチAPIの目的が違います
    - 非同期の処理のエンドポイント
        - Gitlabバックアップ
        - Twitter API
    - 複数リクエスをまとめるエンドポイント
        - [Facebook API](https://developers.facebook.com/docs/graph-api/making-multiple-requests/)
        - [Microsoft API](https://docs.microsoft.com/en-us/graph/json-batching)
        - [Google API](https://developers.google.com/gmail/api/guides/batch)

---

## 非同期の処理のエンドポイント

- リクエストして、返すのはデーターじゃなくJobID
- その後に、Jobの状態を収得して、成功であれば、データーが取れます。
- [Twitter API](https://developer.twitter.com/en/docs/ads/analytics/api-reference/asynchronous)

---

## 複数リクエスをまとめるエンドポイント

- 多くリクエストをしたい場合　→　一つのリクエストにまとめる
    - 100リクエストを送りたい
    - 同じEndpointですが、Paramsが違います
- 複数リクエストと関係があり　→　一つのリクエストにまとめる
    - 同時に成功した・同時に失敗したい
    - 前のリクエストの結果は次のリクエストのParams
- [Google API](https://developers.google.com/gmail/api/guides/batch)

---

## バッチAPIが必要

- フロントエンドは良く変えましたが、バックエンド修正したくない
- 同時に複数POSTリクエストを送りたい
- 複数POSTリクエストの中に一つ失敗したら、戻したい

---

## RailsにバッチAPI簡単に入れられます

- Rails middleware処理
- リクエスト　→　``MiddleWare`` →　Router →　Controller
- [Rack Middleware 入門](https://qiita.com/nishio-dens/items/8011842f50995f46eafe)

---

## バッチ導入しました

- Batch middleware追加 →　複数リクエスト対応
- DB Transactions追加　→　リクエスト失敗時に、戻せる

---

## RailsにバッチAPI簡単に入れられますが 
- Auth処理 欠点
- DB Transactions 欠点

## Rails6.0のバッチAPIのコード

```ruby

class BatchApi
  def initialize(app)
    @app = app
  end

  def call(env)
    if env['PATH_INFO'] == '/api/batch'
      process_batch(env)
    else
      @app.call(env)
    end
  end

  def process_batch(env)
    responses = []
    request = ActionDispatch::Request.new(env.deep_dup)
    ActiveRecord::Base.transaction do
      request.parameters[:requests].each do |override|
        # 子供は別のtransationを使いたいですので、requires_new: trueをつけます。
        ActiveRecord::Base.transaction(requires_new: true) do
          response = process_request(env.deep_dup, override)
          responses.push(response)

          # 子供は何かエラーは発生した時に、子供のデータをロルバックする
          raise ActiveRecord::Rollback unless success?([response])
        end
      end

      # レスポンスが失敗の場合、全部ロールバックします。
      raise ActiveRecord::Rollback unless success?(responses)
    end

    render_response(responses)
  end

  def process_request(env, override)
    path, query = override['url'].split('?')
    env['REQUEST_METHOD'] = override['method']
    env['PATH_INFO'] = path
    env['QUERY_STRING'] = query
    env['FROM_BATCH_API'] = true
    # env['rack.input'] = StringIO.new(override['body'].to_json)
    # 2020/11/12アップデート: to_jsonは&がある場合、変気宇されちゃったのでたまにエラー発生した、代わりにJSON::generate または JSON::dump　使うべき
    env['rack.input'] = StringIO.new(JSON::generate(override['body']))


    status, headers, body = @app.call(env)
    { status: status, headers: headers, body: status == 200 ? JSON.parse(body&.body) : body }
  end

  def render_response(responses)
    hash = {}
    hash[:l] = responses

    success_response = responses&.find { |response| response[:status] == 200 }
    headers = success_response ? success_response[:headers] : responses[0][:headers]

    hash_json = hash.to_json

    # レスポンスの長さを更新します。
    headers = headers.merge('Content-Length' => hash_json.bytesize)

    [200, headers, [hash_json]]
  end

  def success?(responses)
    responses&.each do |response|
      return false if response[:status] != 200 
    end
    true
  end
end

```

---

## まとめる

- バッチAPIが作れました
- メリット/デメリット出来ました