I'm trying to build a Rails API with the following JSON structure:
{ team: "team_name",
players: players: [
{
name: "player_one",
contracts: [
{
start_date: '7/1/2017',
seasons: [
{
year: 2017,
salary: 1000000
}
]
}
]
},
{
name: "player_two"
}
]
}
My models are set up as follows:
class Team < ApplicationRecord
has_many :players
end
class Player < ApplicationRecord
belongs_to :team
has_many :contracts
end
class Contract < ApplicationRecord
belongs_to :player
has_many :seasons
end
class Season < ApplicationRecord
belongs_to :contract
end
I'm currently using the following code, but I'd like to clean this up using ActiveModel Serializers (Or other clean solutions)
def show
#team = Team.find(params[:id])
render json: #team.as_json(
except: [:id, :created_at, :updated_at],
include: {
players: {
except: [:created_at, :updated_at, :team_id],
include: {
contracts: {
except: [:id, :created_at, :updated_at, :player_id],
include: {
seasons: {
except: [:id, :contract_id, :created_at, :updated_at]
}
}
}
}
}
}
)
end
Additionally, the array items are showing in descending order by ID, I'm hoping to reverse that. I'm also hoping to order the players by the first contract, first season, salary.
Active Model Serializers is a nice gem to fit into your needs. Assuming you would use 0-10-stable branch (latest: 0.10.7), you can go in this way:
Serializers (Doc)
# app/serializers/team_serializer.rb or app/serializers/api/v1/team_serializer.rb (if your controllers are in app/controllers/api/v1/ subdirectory)
class TeamSerializer < ActiveModel::Serializer
attributes :name # list all desired attributes here
has_many :players
end
# app/serializers/player_serializer.rb
class PlayerSerializer < ActiveModel::Serializer
attributes :name # list all desired attributes here
has_many :contracts
end
# app/serializers/contract_serializer.rb
class ContractSerializer < ActiveModel::Serializer
attributes :start_date # list all desired attributes here
has_many :seasons
end
# app/serializers/season_serializer.rb
class SeasonSerializer < ActiveModel::Serializer
attributes :year, :salary # list all desired attributes here
end
TeamController
def show
#team = Team
.preload(players: [contracts: :seasons])
.find(params[:id])
render json: #team,
include: ['players.contracts.seasons'],
adapter: :attributes
end
Note 1: Using preload (or includes) will help you eager-load multiple associations, hence saving you from N + 1 problem.
Note 2: You may wish to read the details of the include option in render method from Doc
I think your approach is right. Looks clean as it could be.
You can use also named_scopes on your Model.
See docs here.
Related
I'm using ActiveModel::Serializer in a rails application to format my model data as a json response, but I would like to change the formatting so that the associations of my main model are not nested. I tried setting root: false and that doesn't work
Expected behavior vs actual behavior
I have a model Account with an association belongs_to :account_status
and I was able to add this association in the AccountSerializer to get that associated data just fine. But do to my api contract requirements, I need the json to be formatted without the association nesting.
So I'm getting this:
{
"account_id": 1
<other account info>
...
"account_status": {
"status_code": 1
"desc": "status description"
....
}
}
But I want this:
{
"account_id": 1
<other account info>
...
"account_status_status_code": 1
"account_status_desc": "status description"
....
}
Model + Serializer code
How can I achieve the expected behavior without writing each account_status field as an individual attribute in the AccountSerializer ??
Controller
class AccountsController < ActionController::API
def show
account = Account.find(params[:account_id])
render json: account
end
end
Model
class Account < ActiveRecord::Base
self.primary_key = :account_id
belongs_to :account_status, foreign_key: :account_status_code, inverse_of: :accounts
validates :account_status_code, presence: true
end
Serializer
class AccountSerializer < ActiveModel::Serializer
attributes(*Account.attribute_names.map(&:to_sym))
belongs_to :account_status,
foreign_key: :account_status_code,
inverse_of: :accounts
end
Environment
OS Type & Version: macOS Catalina v 10.15.7
Rails 6.1.4:
ActiveModelSerializers Version 0.10.0:
Output of ruby -e "puts RUBY_DESCRIPTION":
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-darwin19]
you could replace belongs_to :account_status,... by below code
class AccountSerializer < ActiveModel::Serializer
attributes(*Account.attribute_names.map(&:to_sym))
AccountStatus.attribute_names.each do |attr_name|
key = "account_status_#{attr_name}"
define_method(key) do
# i checked and see that `object.account_status` just call one time
object.account_status.send(attr_name)
end
attribute key.to_sym
end
end
Instead of using an assocation in your serializer you can setup delegation in your model:
class Account < ApplicationRecord
delegate :desc, ...,
to: :account_status,
prefix: true
end
This will create a account_status_desc method which you can simply call from your serializer:
class AccountSerializer < ActiveModel::Serializer
attributes(
:foo,
:bar,
:baz,
:account_status_code, # this is already a attribute of Account
:account_status_desc
# ...
)
end
Another way of doing this is by simply adding methods to your serializer:
class AccountSerializer < ActiveModel::Serializer
attributes :foo
def foo
object.bar.do_something
end
end
This is a good alternative when the result of serialization does not align with the internal representation in the model.
In my Rails (api only) learning project, I have 2 models, Group and Album, that have a one-to-many relationship. When I try to save the group with the nested (already existing) albums, I get the following error, ActiveRecord::RecordNotFound (Couldn't find Album with ID=108 for Group with ID=). I'm using the jsonapi-serializer gem. Below is my current set up. Any help is appreciated.
Models
class Group < ApplicationRecord
has_many :albums
accepts_nested_attributes_for :albums
end
class Album < ApplicationRecord
belongs_to :group
end
GroupsController#create
def create
group = Group.new(group_params)
if group.save
render json: GroupSerializer.new(group).serializable_hash
else
render json: { error: group.errors.messages }, status: 422
end
end
GroupsController#group_params
def group_params
params.require(:group)
.permit(:name, :notes, albums_attributes: [:id, :group_id])
end
Serializers
class GroupSerializer
include JSONAPI::Serializer
attributes :name, :notes
has_many :albums
end
class AlbumSerializer
include JSONAPI::Serializer
attributes :title, :group_id, :release_date, :release_date_accuracy, :notes
belongs_to :group
end
Example JSON payload
{
"group": {
"name": "Pink Floyd",
"notes": "",
"albums_attributes": [
{ "id": "108" }, { "id": "109" }
]
}
}
If the albums already exist, then accepts_nested_attributes is not necessary.
You could save them like this:
Group.new(name: group_params[:name], notes: group_params[:notes], album_ids: group_params[:album_ids])
You will want to extract the album_ids as an array when passing it here.
I have a rails API that currently has quite a few N+1 queries that I'd like to reduce.
As you can see it's going through quite a few loops before returning the data.
The relationships are as follows:
Company Model
class Company < ApplicationRecord
has_many :jobs, dependent: :destroy
has_many :contacts, dependent: :destroy
has_many :listings
end
Job Model
class Job < ApplicationRecord
belongs_to :company
has_many :listings
has_and_belongs_to_many :technologies
has_and_belongs_to_many :tools
scope :category, -> ( category ) { where category: category }
end
Listing Modal
class Listing < ApplicationRecord
belongs_to :job, dependent: :destroy
belongs_to :company, dependent: :destroy
scope :is_active, -> ( active ) { where is_active: active }
end
Job Serializer
class SimpleJobSerializer < ActiveModel::Serializer
attributes :id,
:title,
:company_name,
attribute :technology_list, if: :technologies_exist
attribute :tool_list, if: :tools_exist
def technology_list
custom_technologies = []
object.technologies.each do |technology|
custom_technology = { label: technology.label, icon: technology.icon }
custom_technologies.push(custom_technology)
end
return custom_technologies
end
def tool_list
custom_tools = []
object.tools.each do |tool|
custom_tool = { label: tool.label, icon: tool.icon }
custom_tools.push(custom_tool)
end
return custom_tools
end
def tools_exist
return object.tools.any?
end
def technologies_exist
return object.technologies.any?
end
def company_name
object.company.name
end
end
Current query in controller
Job.eager_load(:listings).order("listings.live_date DESC").where(category: "developer", listings: { is_active: true }).first(90)
I've tried to use eager_load to join the listings to the Jobs to make the request more efficient but i'm unsure how to handle this when some of the n+1 queries are coming from inside the serializer as it tries to look at tools and technologies.
Any help would be much appreciated!
You might was well eager load tools and technologies since you know that the serializer is going to use them:
Job.eager_load(:listings, :tools, :technologies)
.order("listings.live_date DESC")
.where(category: "developer", listings: { is_active: true })
.first(90)
After that you really need to refactor that serializer. #each should only be used when you are only interested in the side effects of the iteration and not the return value. Use #map, #each_with_object, #inject etc. These calls can be optimized. return is implicit in ruby so you only explicitly return if you are bailing early.
class SimpleJobSerializer < ActiveModel::Serializer
# ...
def tool_list
object.tools.map { |t| { label: tool.label, icon: tool.icon } }
end
# ...
end
Try nested preload:
Job.preload(:technologies, :tools, company: :listings).order(...).where(...)
Database tables, first table contain tags (id, name) the second table contain relation between items and tags.
tags
id name
1 TagA
2 TagB
3 TagC
tags_items
item_id tag_id
1 1
1 2
1 3
2 1
2 3
Active reocrds :
class Tag < ActiveRecord::Base
has_many :tags_itemses
validates_presence_of :name
validates_length_of :name, :maximum => 15
end
class TagsItems < ActiveRecord::Base
has_many :tags
end
In my controller i have index method:
def index
items = TagItems.all.includes(:tags)
render json: items,
status: 200
end
How the controller should looks like to get following json ?
[{item_id :1, tags: [{id:1, name: TagA}, {id:2, name: TagB}, {id:3, name: TagC}]},
{item_id :2, tags: [{id:1, name: TagA}, {id:3, name: TagC}]}]
You can customize the JSON output with the include option:
class TagsController
def index
items = TagItems.all.includes(:tags)
render json: items, includes: {
tags: {
only: [:id, :name]
}
}, status: 200
end
end
But this can get very repetitive though and bloats your controllers - active_model_serializers can help here.
However this will still not work since your modeling is way off! Models names should always be in singular! tags_items would be appropriate if it was a has_and_belongs_to_many relationship but that is a very special case since that is a join table without an associated model.
What you want here is to use a has_many :through relationship to setup a many to many between tags and items:
class Item < ActiveRecord::Base
has_many :tag_items # you're not Gollum!
has_many :tags, through: :tag_items
end
class Tag < ActiveRecord::Base
has_many :tag_items
has_many :items, through: :tag_items
end
class TagItem < ActiveRecord::Base
belongs_to :tag
belongs_to :item
end
You also need to correct the name of the table! Create a migration with rails g migration RenameTagsItems and modify the contents:
class RenameTagsItemsMigration < ActiveRecord::Migration
def change
rename_table :tags_items, :tag_items
end
end
Then run the migration (rake db:migrate).
I'm trying to build a JSON representation of some Rails models using Active Model Serializer, where some models embed others. For example, I have Event and Attendees, Event has_and_belongs_to_many Attendees.
class EventSerializer < ActiveModel::Serializer
attributes :name
has_many :attendees, serializer: AttendeeSerializer
end
class AttendeeSerializer < ActiveModel::Serializer
attributes :name
end
This would result in JSON like { name: 'Event One', attendees: [{ name: 'Alice' }, { name: 'Bob' }] }.
Now, I'd like to add what the attendees have said about the event. Let's say, Comment belongs_to Event, belongs_to Attendee. I'd like to include said comments in the serialized output of event, so it would become { name: 'Event One', attendees: [{ name: 'Alice', comments: [{ text: 'Event One was great!'}] }, { name: 'Bob', comments: [] }] }.
I could have
class AttendeeSerializer < ActiveModel::Serializer
attributes :name
has_many :comments
end
but that would select all the comments by this attendee for all the events - not what I want. I'd like to write this, but how do I find the particular event for which I'm doing serialization? Can I somehow access the 'parent' object, or maybe pass options to a has_many serializer?
class AttendeeSerializer < ActiveModel::Serializer
attributes :name
has_many :comments
def comments
object.comments.where(event_id: the_event_in_this_context.id)
end
end
Is this something that can be done, or should I just build the JSON in another way for this particular use case?
I'd do things manually to get control:
class EventSerializer < ActiveModel::Serializer
attributes :name, :attendees
def attendees
object.attendees.map do |attendee|
AttendeeSerializer.new(attendee, scope: scope, root: false, event: object)
end
end
end
class AttendeeSerializer < ActiveModel::Serializer
attributes :name, :comments
def comments
object.comments.where(event_id: #options[:event].id).map do |comment|
CommentSerializer.new(comment, scope: scope, root: false)
end
end
end