Rails:使用 active_model_serializers 序列化深度嵌套关联
Rails: Serializing deeply nested associations with active_model_serializers
我正在使用 Rails 4.2.1
和 active_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 问题,关于如何处理这个问题(或者是否实际上支持这种功能)似乎确实存在一些来回的问题,但我很难弄清楚确切地知道当前状态是什么。
- https://github.com/rails-api/active_model_serializers/issues/835
- https://github.com/rails-api/active_model_serializers/issues/968
- https://github.com/rails-api/active_model_serializers/issues/414
- https://github.com/rails-api/active_model_serializers/issues/444
建议的解决方案之一是使用调用嵌套属性的方法覆盖属性,但这似乎被视为 hack,所以我想尽可能避免它。
无论如何,我们将不胜感激如何处理此问题或一般性 API 建议的示例。
这应该能满足您的需求。
@project.to_json( include: { estimates: {
include: {:project, :project_code, :tax_type, :proposals } } } )
顶级嵌套将被自动包含,但任何比这更深的嵌套都需要包含在您的 show 动作中或您调用它的任何地方。
所以这不是最好的答案,甚至不是一个好的答案,但这是我需要的。
虽然在将 json_api
适配器与 AMS 一起使用时似乎支持包含嵌套和侧载属性,但我需要对平面 json 的支持。此外,这种方法效果很好,因为每个序列化程序都专门生成我需要它独立于任何其他序列化程序的内容,而无需在控制器中执行任何操作。
随时欢迎评论/替代方法。
项目模型
class Project < ActiveRecord::Base
has_many :estimates, autosave: true, dependent: :destroy
end
项目控制器
def index
@projects = Project.all
render json: @projects
end
ProjectSerializer
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_many
或 belongs_to
关联,只是自定义了行为。我使用 slice
到 select 特定属性。希望能有更优雅的解决方案。
如果您使用的是 JSONAPI 适配器,则可以执行以下操作来呈现嵌套关系:
render json: @project, include: ['estimates', 'estimates.project_code', 'estimates.tax_type', 'estimates.proposals']
您可以从 jsonapi 文档中阅读更多内容:http://jsonapi.org/format/#fetching-includes
Per commit 1426: https://github.com/rails-api/active_model_serializers/pull/1426 - 以及相关讨论,您可以看到 json
和 attributes
序列化的默认嵌套是一层。
如果你想要默认深度嵌套,你可以在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
您可以将 default_includes
更改为 ActiveModel::Serializer
:
# 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"
}
]
}
]
}
]
}
}
在我的例子中,我创建了一个名为 'active_model_serializer.rb' 的文件,该文件位于 'MyApp/config/initializers',其内容如下:
ActiveModelSerializers.config.default_includes = '**'
别忘了重启服务器:
$ rails s
为了支持 ,我添加了以下答案。
我在 rails 应用程序中使用 jsonapi-serializer gem 进行序列化。我没有发现包括嵌套在控制器中和侧面加载的属性对我来说很方便。我只是想更好地分离关注点。所以任何与序列化有关的东西都应该只在序列化文件中,它们应该与控制器文件无关。
所以就我而言,我有以下联想:
学校模型
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
就这些了。
希望对您有所帮助
我正在使用 Rails 4.2.1
和 active_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 问题,关于如何处理这个问题(或者是否实际上支持这种功能)似乎确实存在一些来回的问题,但我很难弄清楚确切地知道当前状态是什么。
- https://github.com/rails-api/active_model_serializers/issues/835
- https://github.com/rails-api/active_model_serializers/issues/968
- https://github.com/rails-api/active_model_serializers/issues/414
- https://github.com/rails-api/active_model_serializers/issues/444
建议的解决方案之一是使用调用嵌套属性的方法覆盖属性,但这似乎被视为 hack,所以我想尽可能避免它。
无论如何,我们将不胜感激如何处理此问题或一般性 API 建议的示例。
这应该能满足您的需求。
@project.to_json( include: { estimates: {
include: {:project, :project_code, :tax_type, :proposals } } } )
顶级嵌套将被自动包含,但任何比这更深的嵌套都需要包含在您的 show 动作中或您调用它的任何地方。
所以这不是最好的答案,甚至不是一个好的答案,但这是我需要的。
虽然在将 json_api
适配器与 AMS 一起使用时似乎支持包含嵌套和侧载属性,但我需要对平面 json 的支持。此外,这种方法效果很好,因为每个序列化程序都专门生成我需要它独立于任何其他序列化程序的内容,而无需在控制器中执行任何操作。
随时欢迎评论/替代方法。
项目模型
class Project < ActiveRecord::Base
has_many :estimates, autosave: true, dependent: :destroy
end
项目控制器
def index
@projects = Project.all
render json: @projects
end
ProjectSerializer
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_many
或 belongs_to
关联,只是自定义了行为。我使用 slice
到 select 特定属性。希望能有更优雅的解决方案。
如果您使用的是 JSONAPI 适配器,则可以执行以下操作来呈现嵌套关系:
render json: @project, include: ['estimates', 'estimates.project_code', 'estimates.tax_type', 'estimates.proposals']
您可以从 jsonapi 文档中阅读更多内容:http://jsonapi.org/format/#fetching-includes
Per commit 1426: https://github.com/rails-api/active_model_serializers/pull/1426 - 以及相关讨论,您可以看到 json
和 attributes
序列化的默认嵌套是一层。
如果你想要默认深度嵌套,你可以在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
您可以将 default_includes
更改为 ActiveModel::Serializer
:
# 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"
}
]
}
]
}
]
}
}
在我的例子中,我创建了一个名为 'active_model_serializer.rb' 的文件,该文件位于 'MyApp/config/initializers',其内容如下:
ActiveModelSerializers.config.default_includes = '**'
别忘了重启服务器:
$ rails s
为了支持
我在 rails 应用程序中使用 jsonapi-serializer gem 进行序列化。我没有发现包括嵌套在控制器中和侧面加载的属性对我来说很方便。我只是想更好地分离关注点。所以任何与序列化有关的东西都应该只在序列化文件中,它们应该与控制器文件无关。
所以就我而言,我有以下联想:
学校模型
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
就这些了。
希望对您有所帮助