Rails APIでJbuilderを使わずControllersのrender jsonを親・子・孫・ひ孫で出力

RailsのAPIモードで、Controllersのrender jsonを親・子・孫・ひ孫という、深いリレーションをJSON形式でそれぞれネストし出力する方法のメモ。

環境

  • Rails6のAPIモードでJbuilderは不使用のためviewはなくController完結
  • ControllerでJSONを標準で利用(render jsonで出力)
  • 親・子・孫・ひ孫のアソシエーションを設定。きれいな4階層の世代関係ではなく2つの塊でリレーション
    • 1つ目の塊:親ー子
    • 2つ目の塊:子ー孫ーひ孫

設定

(親モデル)api/app/models/document_group.rb

class DocumentGroup < ApplicationRecord
  belongs_to :user
  has_many :document_agenda_items, -> { order('order_number') }, dependent: :destroy

  scope :ordered, -> {
    order('name')
  }
end

(子モデル)api/app/models/document_agenda_item.rb

class DocumentAgendaItem < ApplicationRecord
  belongs_to :document_group
  belongs_to :agenda_item
  has_one :agenda_details, through: :agenda_item

  scope :ordered, -> {
    order('order_number')
  }
end

(孫モデル)api/app/models/agenda_item.rb

class AgendaItem < ApplicationRecord
  belongs_to :agenda_group
  has_many :agenda_details, dependent: :destroy
  has_one :document_agenda_item, dependent: :destroy

  scope :ordered, -> {
    order('order_number')
  }
end

(ひ孫モデル)api/app/models/agenda_detail.rb

class AgendaDetail < ApplicationRecord
  belongs_to :agenda_item
end

api/app/controllers/v1/document_groups_controller.rb

class V1::DocumentGroupsController < ApplicationController

  def show_relations
    @document_groups = DocumentGroup.ordered

    render json:
      @document_groups, # 親モデル
      include: {
        :document_agenda_items => { # 子モデル
          :include => {
            :agenda_item => { # 孫モデル
              include: :agenda_details # ひ孫モデル
            }
          }
        }
      }
  end

end

アソシエーションを設定して「include」を利用すると取得できる。

出力結果

localhost:3000/v1/document_groups/{{DOCUMENT_GROUP_ID}}/show_relations

[
    {
        "id": 1,
        "user_id": 1,
        "name": "SFA PJ",
        "created_at": "2021-08-14T05:56:07.534Z",
        "updated_at": "2021-08-14T05:56:07.534Z",
        "document_agenda_items": [
            {
                "id": 3,
                "document_group_id": 1,
                "agenda_item_id": 10,
                "order_number": 1,
                "created_at": "2021-09-18T09:06:53.429Z",
                "updated_at": "2021-09-18T09:06:53.429Z",
                "indent_level": 1,
                "agenda_item": {
                    "id": 10,
                    "agenda_group_id": 1,
                    "name": "Project Phases",
                    "order_number": 2,
                    "created_at": "2021-09-05T07:21:39.276Z",
                    "updated_at": "2021-09-05T07:21:39.276Z",
                    "indent_level": null,
                    "agenda_details": []
                }
            },
            {
                "id": 1,
                "document_group_id": 1,
                "agenda_item_id": 1,
                "order_number": 2,
                "created_at": "2021-08-14T05:57:13.772Z",
                "updated_at": "2021-09-15T11:00:40.122Z",
                "indent_level": 2,
                "agenda_item": {
                    "id": 1,
                    "agenda_group_id": 1,
                    "name": "Overview",
                    "order_number": 1,
                    "created_at": "2021-08-14T05:42:50.929Z",
                    "updated_at": "2021-08-14T05:42:50.929Z",
                    "indent_level": null,
                    "agenda_details": [
                        {
                            "id": 1,
                            "agenda_item_id": 1,
                            "body": "This document aims to give a detailed description on how the applications that will be implemented are designed. This is done by using high-level, component and more detailed class charts.",
                            "created_at": "2021-08-14T05:47:56.855Z",
                            "updated_at": "2021-08-14T05:47:56.855Z"
                        },
                        {
                            "id": 12,
                            "agenda_item_id": 1,
                            "body": "The Project will be divided into 6 phases.",
                            "created_at": "2021-09-14T15:06:34.994Z",
                            "updated_at": "2021-09-14T15:06:34.994Z"
                        },
                        {
                            "id": 5,
                            "agenda_item_id": 1,
                            "body": "コレクションをインスタンスから取得できます。",
                            "created_at": "2021-09-05T07:27:47.658Z",
                            "updated_at": "2021-09-15T10:54:58.337Z"
                        }
                    ]
                }
            },
            {
                "id": 2,
                "document_group_id": 1,
                "agenda_item_id": 8,
                "order_number": 3,
                "created_at": "2021-09-18T09:06:16.287Z",
                "updated_at": "2021-09-18T09:06:16.287Z",
                "indent_level": 2,
                "agenda_item": {
                    "id": 8,
                    "agenda_group_id": 9,
                    "name": "Sample Agenda Item",
                    "order_number": 1,
                    "created_at": "2021-08-15T14:56:33.134Z",
                    "updated_at": "2021-08-15T14:56:33.134Z",
                    "indent_level": null,
                    "agenda_details": [
                        {
                            "id": 3,
                            "agenda_item_id": 8,
                            "body": "9つの知識エリアをもとにプロジェクト管理を推進します。",
                            "created_at": "2021-08-15T15:08:04.410Z",
                            "updated_at": "2021-08-15T15:08:22.285Z"
                        }
                    ]
                }
            }
        ]
    },
    {
        "id": 4,
        "user_id": 1,
        "name": "SFA PJ Requirements Definition Document",
        "created_at": "2021-09-05T07:28:04.673Z",
        "updated_at": "2021-09-05T07:28:04.673Z",
        "document_agenda_items": []
    },
    {
        "id": 2,
        "user_id": 1,
        "name": "Webシステム開発RFP",
        "created_at": "2021-08-15T15:09:16.221Z",
        "updated_at": "2021-08-15T15:09:28.699Z",
        "document_agenda_items": [
            {
                "id": 8,
                "document_group_id": 2,
                "agenda_item_id": 9,
                "order_number": 1,
                "created_at": "2021-09-18T10:28:38.363Z",
                "updated_at": "2021-09-18T10:28:38.363Z",
                "indent_level": 1,
                "agenda_item": {
                    "id": 9,
                    "agenda_group_id": 1,
                    "name": "Loadmap",
                    "order_number": 2,
                    "created_at": "2021-09-05T07:21:23.096Z",
                    "updated_at": "2021-09-15T10:50:29.379Z",
                    "indent_level": 2,
                    "agenda_details": [
                        {
                            "id": 4,
                            "agenda_item_id": 9,
                            "body": "The Project will be divided into 6 phases.",
                            "created_at": "2021-09-05T07:22:30.368Z",
                            "updated_at": "2021-09-05T07:22:30.368Z"
                        }
                    ]
                }
            }
        ]
    }
]

その他

親・子・孫の場合

3世代のリレーションにしたい場合

  def show_relations
    @document_groups = DocumentGroup.ordered

    render json:
      @document_groups,
      include: {
        :document_agenda_items => {
          include: :agenda_item
        }
      }
  end

根本的な解決

本件はあくまで暫定策になるため、プロトタイピングなどを除き、利用は非推奨(個人的な意見)。根本的な解決は以下が必要。

  • リレーションが深すぎる場合はDB設計の見直し
  • Fat Controllerになってしまい汎用性もなくなりバグ誘発の可能性あり。Fat Modelにするため、Modle側にアソシエーションとメソッドを定義して、それをJbuilderのViews側で出力する方法を推奨(おそらく一般的にはこの方法)

参考情報

Stack Overflow - Ruby on Rails 5.0.2 - Multiple Nested Json Render