Rails:使用 active_model_serializers 序列化深度嵌套关联

43
我正在使用Rails 4.2.1active_model_serializers 0.10.0.rc2 我对API很陌生,选择了active_model_serializers,因为它似乎正在成为Rails的标准(虽然我不排斥使用RABL或其他序列化程序)
我的问题是,我似乎无法在多层关系中包含各种属性。例如,我有: 项目
class ProjectSerializer < ActiveModel::Serializer
  attributes                      :id, 
                                  :name,
                                  :updated_at

  has_many                        :estimates, include_nested_associations: true

end

估计值

class EstimateSerializer < ActiveModel::Serializer
  attributes                      :id, 
                                  :name, 
                                  :release_version, 
                                  :exchange_rate, 
                                  :updated_at,

                                  :project_id, 
                                  :project_code_id, 
                                  :tax_type_id 

  belongs_to                      :project
  belongs_to                      :project_code
  belongs_to                      :tax_type

  has_many                        :proposals

end

提案

class ProposalSerializer < ActiveModel::Serializer
  attributes                      :id, 
                                  :name, 
                                  :updated_at,

                                  :estimate_id

  belongs_to                      :estimate
end

当我访问/projects/1时,上述内容会产生:
{
  "id": 1,
  "name": "123 Park Ave.",
  "updated_at": "2015-08-09T02:36:23.950Z",
  "estimates": [
    {
      "id": 1,
      "name": "E1",
      "release_version": "v1.0",
      "exchange_rate": "0.0",
      "updated_at": "2015-08-12T04:23:38.183Z",
      "project_id": 1,
      "project_code_id": 8,
      "tax_type_id": 1
    }
  ]
}

然而,我想要它生成的是:

{
  "id": 1,
  "name": "123 Park Ave.",
  "updated_at": "2015-08-09T02:36:23.950Z",
  "estimates": [
    {
      "id": 1,
      "name": "E1",
      "release_version": "v1.0",
      "exchange_rate": "0.0",
      "updated_at": "2015-08-12T04:23:38.183Z",
      "project": { 
        "id": 1,
        "name": "123 Park Ave."
      },
      "project_code": {
        "id": 8,
        "valuation": 30
      },
      "tax_type": {
        "id": 1,
        "name": "no-tax"
      },
      "proposals": [
        {
          "id": 1,
          "name": "P1",
          "updated_at": "2015-08-12T04:23:38.183Z"
        },
        {
          "id": 2,
          "name": "P2",
          "updated_at": "2015-10-12T04:23:38.183Z"
        }
      ]
    }
  ]
}

理想情况下,我还希望能够指定每个序列化器中包含哪些属性、关联以及这些关联的属性。我一直在查看AMS问题,似乎对于如何处理这个问题(或者是否真正支持这种功能)存在一些来回讨论,但我很难确定当前状态是什么。

提出的解决方案之一是通过重写属性并使用方法调用嵌套属性,但这似乎被认为是一种hack,所以如果可能的话,我想避免它。

无论如何,关于如何处理此问题的示例或一般API建议将不胜感激。


2
不是关于你的问题,但是你序列化文件中的空格太多了!它只会让代码变得更难读懂,你知道吗。 - Caleb Keene
就我个人而言,我觉得这样更易读,但我想每个人都有自己的偏好。 - Eric Norcross
7个回答

62
每次提交1426:https://github.com/rails-api/active_model_serializers/pull/1426 - 以及相关讨论,你可以看到jsonattributes序列化的默认嵌套级别为一级。
如果想要默认进行深层嵌套,可以在active_model_serializer初始化器中设置配置属性: ActiveModelSerializers.config.default_includes = '**' 有关详细参考,请查阅v0.10.6文档中的以下内容:https://github.com/rails-api/active_model_serializers/blob/v0.10.6/docs/general/adapters.md#include-option

4
如果您不希望默认使用深层嵌套,但是想要在一个特定的模型中使用它怎么办?(如果那个特定的模型需要2或3层嵌套呢?) - Mirror318
6
每个操作中您都可以传入'**'。请注意,这是一个安全问题。最好明确说明您想要包含的内容。 - BF4

18

如果你正在使用JSONAPI适配器,你可以按照以下方式处理嵌套关系的呈现:

render json: @project, include: ['estimates', 'estimates.project_code', 'estimates.tax_type', 'estimates.proposals']

您可以从jsonapi文档中阅读更多信息:http://jsonapi.org/format/#fetching-includes


1
我想,我对这种方法最大的问题在于它需要管理两个不同文件中的代码:控制器和序列化器。这并不是说这不是正确的做法;只是我觉得这更加不方便。 - Eric Norcross
这是获取“嵌套”最好的方法..尽管 JSON:API 实际上将其展开。 - BF4

17

您可以更改 ActiveModel::Serializerdefault_includes

# config/initializers/active_model_serializer.rb
ActiveModel::Serializer.config.default_includes = '**' # (default '*')

另外,为了避免无限递归,您可以控制嵌套序列化的方式如下:

class UserSerializer < ActiveModel::Serializer
  include Rails.application.routes.url_helpers

  attributes :id, :phone_number, :links, :current_team_id

  # Using serializer from app/serializers/profile_serializer.rb
  has_one :profile
  # Using serializer described below:
  # UserSerializer::TeamSerializer
  has_many :teams

  def links
    {
      self: user_path(object.id),
      api: api_v1_user_path(id: object.id, format: :json)
    }
  end

  def current_team_id
    object.teams&.first&.id
  end

  class TeamSerializer < ActiveModel::Serializer
    attributes :id, :name, :image_url, :user_id

    # Using serializer described below:
    # UserSerializer::TeamSerializer::GameSerializer
    has_many :games

    class GameSerializer < ActiveModel::Serializer
      attributes :id, :kind, :address, :date_at

      # Using serializer from app/serializers/gamers_serializer.rb
      has_many :gamers
    end
  end
end

结果:

{
   "user":{
      "id":1,
      "phone_number":"79202700000",
      "links":{
         "self":"/users/1",
         "api":"/api/v1/users/1.json"
      },
      "current_team_id":1,
      "profile":{
         "id":1,
         "name":"Alexander Kalinichev",
         "username":"Blackchestnut",
         "birthday_on":"1982-11-19",
         "avatar_url":null
      },
      "teams":[
         {
            "id":1,
            "name":"Agile Season",
            "image_url":null,
            "user_id":1,
            "games":[
               {
                  "id":13,
                  "kind":"training",
                  "address":"",
                  "date_at":"2016-12-21T10:05:00.000Z",
                  "gamers":[
                     {
                        "id":17,
                        "user_id":1,
                        "game_id":13,
                        "line":1,
                        "created_at":"2016-11-21T10:05:54.653Z",
                        "updated_at":"2016-11-21T10:05:54.653Z"
                     }
                  ]
               }
            ]
         }
      ]
   }
}

16

这可能不是最好的或者说不是个好答案,但它能够按照我的需求正常工作。

当使用AMS和json_api适配器时,似乎可以支持嵌套和侧向加载属性,但我需要支持平面JSON。此外,这种方法非常有效,因为每个序列化程序都会根据我需要的精确生成内容,而不需要在控制器中执行任何其他序列化程序。

欢迎提供意见/替代方案。

项目模型

class Project < ActiveRecord::Base      
  has_many  :estimates, autosave: true, dependent: :destroy
end

项目控制器

def index
  @projects = Project.all
  render json: @projects
end

项目序列化器

class ProjectSerializer < ActiveModel::Serializer
  attributes  :id, 
              :name,
              :updated_at,

              # has_many
              :estimates



  def estimates
    customized_estimates = []

    object.estimates.each do |estimate|
      # Assign object attributes (returns a hash)
      # ===========================================================
      custom_estimate = estimate.attributes


      # Custom nested and side-loaded attributes
      # ===========================================================
      # belongs_to
      custom_estimate[:project] = estimate.project.slice(:id, :name) # get only :id and :name for the project
      custom_estimate[:project_code] = estimate.project_code
      custom_estimate[:tax_type] = estimate.tax_type

      # has_many w/only specified attributes
      custom_estimate[:proposals] = estimate.proposals.collect{|proposal| proposal.slice(:id, :name, :updated_at)}

      # ===========================================================
      customized_estimates.push(custom_estimate)
    end

    return customized_estimates
  end
end

结果

[
  {
    "id": 1,
    "name": "123 Park Ave.",
    "updated_at": "2015-08-09T02:36:23.950Z",
    "estimates": [
      {
        "id": 1,
        "name": "E1",
        "release_version": "v1.0",
        "exchange_rate": "0.0",
        "created_at": "2015-08-12T04:23:38.183Z",
        "updated_at": "2015-08-12T04:23:38.183Z",
        "project": {
          "id": 1,
          "name": "123 Park Ave."
        },
        "project_code": {
          "id": 8,
          "valuation": 30,
          "created_at": "2015-08-09T18:02:42.079Z",
          "updated_at": "2015-08-09T18:02:42.079Z"
        },
        "tax_type": {
          "id": 1,
          "name": "No Tax",
          "created_at": "2015-08-09T18:02:42.079Z",
          "updated_at": "2015-08-09T18:02:42.079Z"
        },
        "proposals": [
          {
            "id": 1,
            "name": "P1",
            "updated_at": "2015-08-12T04:23:38.183Z"
          },
          {
            "id": 2,
            "name": "P2",
            "updated_at": "2015-10-12T04:23:38.183Z"
          }
        ]
      }
    ]
  }
]

我基本上没有尝试在序列化器中实现任何has_manybelongs_to关联,而是自定义了行为。我使用slice选择特定的属性。希望会有更优雅的解决方案。


1
虽然这个问题的最受赞同的答案肯定是正确的传统方法,但是遵循AMS约定时很难得到你想要的结果。我也只是编写自己的回调来显示嵌套关联,并发现AMS约定太笨重了。我发现这在几乎所有涉及Rails和嵌套关联的事情上都是如此 - 约定笨重,退而求其次使用普通的Ruby。 - Todd

11
在我的情况下,我创建了一个名为 'active_model_serializer.rb' 的文件,放置在'MyApp/config/initializers'目录下,并具有以下内容:
ActiveModelSerializers.config.default_includes = '**'

图片描述

别忘记重新启动服务器:

$ rails s

4

这应该能够满足你的需求。

@project.to_json(include: { estimates: { include: {:project, :project_code, :tax_type, :proposals } } })

顶层嵌套将被自动包含,但比这更深层次的内容需要在您的 show action 或者调用此方法的地方进行包含。


5
这种方法实际上是利用了 active_model_serializers 还是只是 Rails 默认的输出 JSON 的方式? - Eric Norcross
@greetification 不是,它使用了在Rails中被还原时留下的AMS遗迹。请参见Rails ActiveModel :: Serializers :: JSON和ActiveModel :: Serialization。 - BF4

1

为了支持Eric Norcross的答案,我添加了以下答案。

我在Rails应用程序中使用jsonapi-serializer gem进行序列化。我发现在控制器和侧加载属性中包含嵌套内容对我来说不太方便。我只想要更好的关注点分离。因此,与序列化有关的任何内容都应该仅存在于序列化文件中,它们不应该与控制器文件有任何关系。

所以在我的情况下,我有以下关联:

School Model

module Baserecord
  class School < ApplicationRecord
    has_many :programs, class_name: Baserecord.program_class, dependent: :destroy
    has_many :faculties, class_name: Baserecord.faculty_class, through: :programs, dependent: :destroy
end

程序模型
module Baserecord
  class Faculty < ApplicationRecord
    belongs_to :program, class_name: Baserecord.program_class
    has_many :departments, class_name: Baserecord.department_class, dependent: :destroy
    has_many :program_of_studies, class_name: Baserecord.program_of_study_class, through: :departments,
                                  dependent: :destroy
  end
end

以下是我如何构建序列化器文件的方式:
学校序列化器
module Baserecord
  class SchoolSerializer
    include JSONAPI::Serializer
    attributes :id, :name, :code, :description, :school_logo, :motto, :address

    attribute :programs do |object|

      # Create an empty array
      customized_programs = []

      object.programs.each do |program|

        # Assign object attributes (returns a hash)
        custom_program = program.attributes

        # Create custom nested and side-loaded attributes
        custom_program[:faculties] = program.faculties

        # Push the created custom nested and side-loaded attributes into the empty array
        customized_programs.push(custom_program)
      end

      # Return the new array
      customized_programs
    end

    cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
  end
end

那就这样。

希望这能有所帮助


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接