web-dev-qa-db-fra.com

Sérialisation d'associations profondément imbriquées avec active_model_serializers

J'utilise Rails 4.2.1 et active_model_serializers 0.10.0.rc2

Je suis nouveau dans les API et j'ai choisi active_model_serializers car il semble devenir la norme pour Rails (même si je ne suis pas opposé à l'utilisation de RABL ou d'un autre sérialiseur)

Le problème que j'ai est que je n'arrive pas à inclure divers attributs dans les relations à plusieurs niveaux. Par exemple, j'ai:

Projets

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

  has_many                        :estimates, include_nested_associations: true

end

et Estimations

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

Propositions

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

                                  :estimate_id

  belongs_to                      :estimate
end

Quand j'ai frappé le /projects/1 ce qui précède produit:

{
  "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
    }
  ]
}

Cependant, ce que j'aimerais qu'il produise, c'est:

{
  "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"
        }
      ]
    }
  ]
}

Idéalement, j'aimerais également pouvoir spécifier quels attributs, associations et attributs de ces associations qui sont inclus dans chaque sérialiseur.

J'ai examiné les problèmes AMS, et il semble y avoir des allers-retours sur la façon dont cela devrait être géré (ou si ce type de fonctionnalité est même réellement pris en charge), mais j'ai du mal à déterminer exactement ce que le courant l'état est.

L'une des solutions proposées était de remplacer l'attribut par une méthode pour appeler les attributs imbriqués, mais cela semble être considéré comme un hack donc je voulais l'éviter si possible.

Quoi qu'il en soit, un exemple de la façon de procéder ou des conseils généraux sur l'API seraient très appréciés.

31
Eric Norcross

Donc, ce n'est pas la meilleure ou même une bonne réponse, mais cela fonctionne comme j'en ai besoin.

Tout en incluant les attributs imbriqués et chargés latéralement semble être pris en charge lors de l'utilisation de json_api adaptateur avec AMS, j'avais besoin de support pour json plat. De plus, cette méthode a bien fonctionné car chaque sérialiseur génère spécifiquement exactement ce dont j'ai besoin pour être indépendant de tout autre sérialiseur et sans avoir à faire quoi que ce soit dans le contrôleur.

Les commentaires/méthodes alternatives sont toujours les bienvenus.

Modèle de projet

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

ProjectsController

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

Résultat

[
  {
    "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"
          }
        ]
      }
    ]
  }
]

J'ai essentiellement ignoré toute tentative d'implémenter has_many ou belongs_to associations dans les sérialiseurs et vient de personnaliser le comportement. J'ai utilisé slice pour sélectionner des attributs spécifiques. Espérons qu'une solution plus élégante sera proposée.

12
Eric Norcross

Par commit 1426: https://github.com/Rails-api/active_model_serializers/pull/1426 - et discussion connexe, vous pouvez voir que l'imbrication par défaut pour json et attributes la sérialisation est un niveau.

Si vous souhaitez une imbrication approfondie par défaut, vous pouvez définir une propriété de configuration dans un initialiseur active_model_serializer:

ActiveModelSerializers.config.default_includes = '**'

Pour une référence détaillée de v0.10.6 : https://github.com/Rails-api/active_model_serializers/blob/v0.10.6/ docs/general/adapters.md # include-option

48
Daniel D

Si vous utilisez l'adaptateur JSONAPI, vous pouvez effectuer les opérations suivantes pour rendre les relations imbriquées:

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

Vous pouvez lire plus de la documentation jsonapi: http://jsonapi.org/format/#fetching-includes

11
Brandon Patram

Dans mon cas, j'ai créé un fichier appelé 'active_model_serializer.rb' placé dans 'MyApp/config/initializers' avec le contenu suivant:

ActiveModelSerializers.config.default_includes = '**'

enter image description here

N'oubliez pas de redémarrer le serveur:

$ Rails s

Vous pouvez changer default_includes pour le ActiveModel::Serializer:

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

De plus, afin d'éviter une récursion infinie, vous pouvez contrôler la sérialisation imbriquée comme suit:

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

Résultat:

{
   "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"
                     }
                  ]
               }
            ]
         }
      ]
   }
}
8
blackchestnut

Cela devrait faire ce que vous cherchez.

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

L'imbrication de niveau supérieur sera automatiquement incluse, mais tout ce qui est plus profond devra être inclus dans votre action de show ou où que vous l'appeliez.

2
Colton Fent