🐰

Dynamoid で where を実行したら Chain クラスが返るので ActiveModelSerializer は効かないらしい

2024/02/07に公開

Ruby on Rails で DynamoDB を ActiveRecord のように扱える Dynamoid を使用しています。
Controller から、とあるモデルの配列を返したかったのですが、うまく ActiveModelSerializer が働かずいろいろ調べたので備忘録としてまとめます。

2024/02/19 14:10 pluck メソッドに関する記述を追加

結論

  • where では Dynamoid::Criteria::Chain クラスが返るので、to_a などで変換する必要がある
  • project メソッドで列指定はできるけど、STI していると type 列も返ってくるので serializer との併用がおすすめ
  • pluck メソッドなら、列指定したうえで type 列を含まない配列を返せます

問題になったコード

モデル名などは架空です。
特定ユーザーの属性一覧を取得するクエリで、JSON の配列を取得する想定です。

./app/controllers/api/v2/user_controller.rb

class Api::V2::UserController < Api::V2::BaseController
  def index
    user_id = params[:id]
    r = User.where(
      pk: "U_##{user_id}",
      "sk.begins_with": "attributes_#"
    )

    render json: r,
           root: 'data',
           adapter: :json,
           each_serializer: UserSerializer,
           status: :ok,
           status_code: 200
    
  end
end

./app/serializers/user_serializer.rb

class UserSerializer < ActiveModel::Serializer
  attributes :attribute_id, :attribute
end

このコントローラーを起動すると、serializer が効かずに、以下のようなレスポンスが返ってきます。

[
    {
        "data: {
            "id": "xxxx",
            "attribute_id": "xxxx",
            "name": "xxxx",
            "attribute_name": "xxxx"
        }
    },
    {
        "data: {
            "id": "xxxx",
            "attribute_id": "xxxx",
            "name": "xxxx",
            "attribute_name": "xxxx"
        }
    } # 略....
]

理想形はこんな感じです。

{
    "data": [
        {
            "id": "xxxx",
            "attribute_id": "xxxx"
        },
        {
            "id": "xxxx",
            "attribute_id": "xxxx"
        } # 略....
    ]
}

原因

User.where の返り値が Dynamoid::Criteria::Chain であり、Array ではないため、コレクションに対して作用する each_serializer が効かなかったと思われる(Chain クラスの superclass が Object だった)。

https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/general/rendering.md#serializer

対策

array に変換してから render する。

./app/controllers/api/v2/user_controller.rb

class Api::V2::UserController < Api::V2::BaseController
  def index
    user_id = params[:id]
    r = User.where(
      pk: "U_##{user_id}",
      "sk.begins_with": "attributes_#"
    )

    render json: r.to_a,
    root: 'data',
    adapter: :json,
    each_serializer: UserSerializer,
    status: :ok,
    status_code: 200
    
  end
end

ちなみに、where のあとに、project メソッドでチェーンすれば指定列の抽出ができますが、STI(単一テーブル継承) していると type 列も返ってきてしまうので serializer も併用したいところです。

r = User.where(
  pk: "U_##{user_id}",
  "sk.begins_with": "attributes_#"
).project(:id)
# User がなんかのテーブルを継承していると、type 列も返ってくる
# 例) [#<User pk: nil, sk:nil, id: xxx, type: "User",....>, #<User ....>, ...]

また、pluck メソッドでチェーンすれば指定列の抽出ができるかつ、Array に変換できるので用途が決まっているのならこの方法がいいかもしれないです。

r = User.where(
  pk: "U_##{user_id}",
  "sk.begins_with": "attributes_#"
).pluck(:id)
# 例) [1, 2, 3 ...]

参考

https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/general/rendering.md#serializer
https://rubydoc.info/github/Dynamoid/dynamoid/Dynamoid/Criteria/Chain#pluck-instance_method

Discussion