How to write activemodel serializer for many to many relationship? - ruby-on-rails

Trying to set up the backend for an ember-cli app. Here's how the models look like in Ember:
post.js
export default DS.Model.extend({
heading: DS.attr('string'),
content: DS.attr(''),
imageUrl: DS.attr('string'),
pageId: DS.belongsTo('page'),
tagIds: DS.hasMany('tag')
});
tag.js
export default DS.Model.extend({
name: DS.attr('string'),
postIds: DS.hasMany('post')
});
The models in Rails and Active Record are just Post, Tag, and Theme. Theme joins Post and Tag. (ie: Post has_many :tags, through: :themes)
Here's what my serializers look like:
class PostSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, :heading, :content, :image_url
has_many :tags
end
class TagSerializer < ActiveModel::Serializer
embed :ids, include: true
attributes :id, :name
end
This works in one direction: searching posts will get all the tags as well. Doesn't work in the other because I don't have a has_many in the TagSerializer. However, if I put a has_many in both serializers, there will be a stack level too deep error.
So I guess my question is: What is the typical way to implement a many-to-many relationship with ActiveModel serializer? I can't seem to find any resources on how to set this up in a Rails back end. Any help would be appreciated. Thanks!

You're getting a "stack level too deep" error because each serializer is recursively embedding the other.
I'd start by making sure you're using includes in your Rails controller:
# posts controller
def show
post = Post.includes(:tags).find_by id: params[:id]
render json: post
end
# tags controller
def show
tag = Tag.includes(:posts).find_by id: params[:id]
render json: post
end
Then, in your serializer, tell it to conditionally include tags / posts only if the association has been loaded:
# post serializer
def include_tags?
object.association(:tags).loaded?
end
# tag serializer
def include_posts?
object.association(:posts).loaded?
end
After this, it should only cascade down one level.
As a side note, you'll probably want to rename the tagIds and postIds properties in your ember models to tags and posts.

Related

Rails - How get first record of associated table?

i am trying to create an api for my mobile app.
I have posts and images tables. For my api, i can send all posts with:
#posts = Post.all
render json: #posts
Output: [{"id":20,"title":"Title 1", "body":" first post ", "user_id":1 }]
But it does not contain images at all. In order to show a showcase image in homepage of my app, i just need the first image of associated images.
The output which i need is (the name of showcase_image attribute does not matter) :
Output: [{"id":20, "title":"Title 1", "body":" first post ", "showcase_image": 'first_image.jpg' , "user_id":1 }]
I need to include first image from associated images table to my json response..
Thanks in advance !
I would suggest using a serializer. Active Model Serializer is pretty standard and easy to use, but is not receiving any updates and has a bad performance. You can choose any other serializer (I recommend Blueprinter) or use the AMS.
Through the AMS you coudl define the relation you want to serialize and it would build the json you're expecting
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body, :showcase_image, :user_id
def showcase_image
object.images.first.name # don't know what is the attribute you're looking for
end
end
And on your controller:
#posts = Post.includes(:images).all # Use includes to avoid N+1 problems
render json: #posts, serialize_collection: PostSerializer
You can include associations with the :include option when calling as_json.
render json: #posts.as_json(include: :images)
You could limit this to one image by adding a new association to Post.
class Post < ApplicationRecord
has_many :images
has_one :showcase_image, class_name: 'Image'
end
This would allow you to use the :showcase_image instead.
render json: #posts.as_json(include: :showcase_image)
You could also use Jbuilder to solve the issue at hand without adding an additional association.
# app/views/posts/index.json.jbuilder
# Get images that belong to posts, group them by post_id and
# return the minimum image id for each post_id.
images = Images.where(post_id: #posts.select(:id)).group(:post_id).minimum(:id)
# Request the full image data for all image ids returned above.
images = images.keys.zip(Image.find(images.values)).to_h
json.array! #posts do |post|
json.extract! post, :id, :title, :body, :...
json.showcase_image do
image = images[post.id]
if image
json.extract! image, :id, :name, :location, :...
else
json.null!
end
end
end
Without calling a specific render, Rails will default to the app/views/posts/index file, and select the file matching the request. (If you request HTML it will look for an HTML file, if you request JSON it looks for JSON, etc.)
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
#posts = Post.all
end
end
Now when you request /posts.json or /posts with the header Accept: application/json your application should return the JSON response build by Jbuilder.

Embeds_many child property not persisted when saving parent

I've been looking for some days without finding the exact answer to my problem which is as simple as that : I have a simple model, with books and authors. A book embeds many authors, and an author is embedded in book. But whenever I'm saving a new book, the author array is not persisted.
What I have is an angular 7 application, calling a ROR API. My Rails versions is 5.2.2. I am using mongoid 7.0 for persistence.
My API was generated with rails g scaffold, and with the --api and --skip-active-record flags.
I first had a problem with the mapping of my properties. My Angular APP sends JSON in lowerCamelCase, when Rails awaits form lower_snake_case vars. I managed to bypass this problem by adding a middleware (correct me if I'm wrong on this one) in my initializers which converts camelCase to snake_case.
# Transform JSON request param keys from JSON-conventional camelCase to
# Rails-conventional snake_case:
ActionDispatch::Request.parameter_parsers[:json] = -> (raw_post) {
# Modified from action_dispatch/http/parameters.rb
data = ActiveSupport::JSON.decode(raw_post)
data = {:_json => data} unless data.is_a?(Hash)
# Transform camelCase param keys to snake_case:
data.deep_transform_keys!(&:underscore)
}
From what I found looking for my problem, it could have been a problem with strong params, so I tried to get awat with this in my book_params
def book_params
#params.fetch(:book, {})
params.require(:book).permit(:title, :release_date, authors_attributes: [:name, :last_name, :birth_date])
end
These are my model :
class Person
include Mongoid::Document
field :last_name, type: String
field :first_name, type: String
field :birth_date, type: Date
end
class Author < Person
include Mongoid::Document
embedded_in :book
end
class Book
include Mongoid::Document
field :title, type: String
field :release_date, type: Date
embeds_many :authors
accepts_nested_attributes_for :authors
end
And this is POST in my book controller (generated with Rails)
# POST /books
def create
#book = Book.new(book_params)
if #book.save
render json: #book, status: :created, location: #book
else
render json: #book.errors, status: :unprocessable_entity
end
end
And here are exemple of a body sent, received and how it is processed by Rails :
Request sent by angular app
Request received and processed by Rails
We can see in the book object
"book"=>{"title"=>"azerty", "release_date"=>"2019-01-21T16:10:19.515Z"}}
That the authors have disappeared, though they were present in the request received by the server.
My question is then : what is the solution to this, or at least what am I missing ? Doesn't Mongoid automatically save children when using embedded documents and accepts_nested_attributes_for ? Should I manually save the children each time a parent is saved in my controller ?
Thanks in advance for helping me
You have to use nested attributes to save children records
Add following line in book model
accepts_nested_attributes_for :authors
And pass authors parameters in author_attributes, for exa:
{title: 'test', release_date: '', author_attributes: [{first_name: '', other_attributes of author}, {first_name: '', , other_attributes of author}]}
for more details please check Mongoid: Nested attributes
Pass perameters in this format
{"title"=>"test", "release_date"=>"2019-01-22", "book"=>{"title"=>"test", "release_date"=>"2019-01-22", "authors_attributes"=>[{"first_name"=>"test name", "last_name"=>"test", "birth_date"=>"2019-01-22T09:43:39.698Z"}]}}
Permit book params
def book_params
params.require(:book).premit(:first_name, :last_name, authors_attributes: %i[first_name last_name birth_date])
end

Rails query of parent model doesn't retrieve child models

I have parent and child models on rails 5 with mongoid. When I query the parent, with .includes command - I can see rails trying to query mongo db - but the result json does not return the child objects.
Parent Model:
class Activity
include Mongoid::Document
field :title, type: String
has_many :activity_pictures
end
Child Model:
class ActivityPicture
include Mongoid::Document
field :name, type: String
belongs_to :activity, :class_name => 'Activity'
end
The controller methods:
def index
#activities = Activity.includes(:activity_pictures).all
end
def show
Activity.includes(:activity_pictures)
end
off course, I have updated activity_params:
def activity_params
params.require(:activity).permit(:title, :activity_pictures)
end
How do i get the full json data from http://localhost:3000/activities.json and the single object links?
Whilst the associations are being loaded with the use of includes, you need to specifically call the loaded association in order for it to render. Try
def index
#activities = Activity.includes(:activity_pictures).all
render json: #activities, include :activity_pictures
end
The answer above by margo was the right lead. I am using jbuilder though, so the solution was to change the file
index.json.jbuilder
as follows:
json.array! #activities do |activity|
json.title activity.title
json.activity_pictures activity.activity_pictures do |activity_picture|
json.name activity_picture.name
end
end

Present subset of an object with ActiveModel Serializer

I am using ActiveModel Serializers in a Rails project.
The default serializer for the object is fairly large, and nesting an object in API responses result in rather large JSON objects.
Sometimes, I want to embed an object, but only need a small subset of the object's attributes to be present in the JSON.
Obviously, I could do something like this:
render json: #user, serializer: SmallerUserSerializer
but that would lead to a lot of duplication.
Is there an option that I can pass to the serializer so that it will only include a subset of the serializers attributes? Eg:
class BlogSerializer
# This is pseudocode. Does not actually work.
has_one :user, only_show: [:user_id, :profile_url]
end
Create a method and call to_json on the user object. Then add that method name to your list of attributes. The method can be called user also.
class BlogSerializer
attributes :id, :user
def user
object.user.to_json( only: [ :id, :profile_url ] )
end
end
Use the active model serialzers gem.
Your pseudo code will become the following simple modularized code:
class BlogSerializer < ActiveModel::Serializer
attributes :user_id, :profile_url
end
Guide: http://railscasts.com/episodes/409-active-model-serializers
Create a method and call to_json on the user object. Then add that method name to your list of attributes. The method can be called user also.
class BlogSerializer
require 'json'
attributes :id, :user
def user
JSON.parse "#{object.user.to_json( only: [ :id, :profile_url ] )}"
end
end

Rails 4 Not Updating Nested Attributes Via JSON

I've scoured related questions and still have a problem updating nested attributes in rails 4 through JSON returned from my AngularJS front-end.
Question: The code below outlines JSON passed from AngularJS to the Candidate model in my Rails4 app. The Candidate model has many Works, and I'm trying to update the Works model through the Candidate model. For some reason the Works model fails to update, and I'm hoping someone can point out what I'm missing. Thanks for your help.
Here's the json in the AngularJS front-end for the candidate:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}]}
Rails then translates this JSON into the following by adding the candidate header, but does not include the nested attributes under the candidate header and fails to update the works_attributes through the candidate model:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}],
"candidate"=>{"id"=>"13", "nickname"=>"New Candidate"}}
The candidate_controller.rb contains a simple update:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
def update
respond_with Candidate.update(params[:id], candidate_params)
end
private
def candidate_params
params.require(:candidate).permit(:nickname,
works_attributes: [:id, :title, :description])
end
end
The candidate.rb model includes the following code defining the has_many relationship with the works model:
class Candidate < ActiveRecord::Base
## Model Relationships
belongs_to :users
has_many :works, :dependent => :destroy
## Nested model attributes
accepts_nested_attributes_for :works, allow_destroy: true
## Validations
validates_presence_of :nickname
validates_uniqueness_of :user_id
end
And finally, the works.rb model defines the other side of the has_many relationship:
class Work < ActiveRecord::Base
belongs_to :candidate
end
I appreciate any help you may be able to provide as I'm sure that I'm missing something rather simple.
Thanks!
I've also been working with a JSON API between Rails and AngularJS. I used the same solution as RTPnomad, but found a way to not have to hardcode the include attributes:
class CandidatesController < ApplicationController
respond_to :json
nested_attributes_names = Candidate.nested_attributes_options.keys.map do |key|
key.to_s.concat('_attributes').to_sym
end
wrap_parameters include: Candidate.attribute_names + nested_attributes_names,
format: :json
# ...
end
Refer to this issue in Rails to see if/when they fix this problem.
Update 10/17
Pending a PR merge here: rails/rails#19254.
I figured out one way to resolve my issue based on the rails documentation at: http://edgeapi.rubyonrails.org/classes/ActionController/ParamsWrapper.html
Basically, Rails ParamsWrapper is enabled by default to wrap JSON from the front-end with a root element for consumption in Rails since AngularJS does not return data in a root wrapped element. The above documentation contains the following:
"On ActiveRecord models with no :include or :exclude option set, it will only wrap the parameters returned by the class method attribute_names."
Which means that I must explicitly include nested attributes with the following statement to ensure Rails includes all of the elements:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
wrap_parameters include: [:id, :nickname, :works_attributes]
...
Please add another answer to this question if there is a better way to pass JSON data between AngularJS and Rails
You can also monkey patch parameter wrapping to always include nested_attributes by putting this into eg wrap_parameters.rb initializer:
module ActionController
module ParamsWrapper
Options.class_eval do
def include
return super if #include_set
m = model
synchronize do
return super if #include_set
#include_set = true
unless super || exclude
if m.respond_to?(:attribute_names) && m.attribute_names.any?
self.include = m.attribute_names + nested_attributes_names_array_of(m)
end
end
end
end
private
# added method. by default code was equivalent to this equaling to []
def nested_attributes_names_array_of model
model.nested_attributes_options.keys.map { |nested_attribute_name|
nested_attribute_name.to_s + '_attributes'
}
end
end
end
end

Resources