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
Related
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.
I'm changing existing serializer in ruby on rails(4.2) project adding new custom attribute to serializer(test123), which is not in model. But this attribute is not rendered in json answer which is formed via this serializer. Also i can change values of existing attributes (like author_name), which is in model.
Model contains of id and author_name.
Json is formed in controller of a class which has_many Examples.
My serializer:
class ExampleSerializer < ActiveModel::Serializer
attributes :id,
:author_name,
:test123
def test123
Rails.logger.debug("???!!!")
"test"
end
def author_name
"test"
end
end
Part of my controller:
def show
render json: #system,
include: %w[examples and other included data],
adapter: :json_api_secured,
each_serializer: detect_serializer
end
Server calls test123 (i see it by print ???!!! in logs), but in formed answer there is no field test123.
What can be a problem?
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
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.
I need to get some info about creating new objects in Rails with validation. For example, there is the following code:
def create
#user = User.new(params[:user])
if #user.save
# some actions: redirect, render, etc
else
render 'new'
end
end
But if there is 2 models with has_one association, for example Club and Place. I need to create both this objects from params in the same 'create' action, because I've got the same form for inputing data for it(params[:club] and params[:club][:place]). I don't know how I should save this objects, because for building a place (#club.build_place(params[:club][:place])) I should save #club in database. Please, give me example of the code for my problem. Thanks in advance.
If you're creating multiple objects from a single form you'd probably be best off putting this logic into a "Form Object"... See the article "7 Patterns to Refactor Fat ActiveRecord Models" from the CodeClimate blog found here (look for Section #3 on extracting Form Objects): http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models.
Railscasts also has a good episode on form objects, though it is a "Pro Episode" (i.e. requires subscription). http://railscasts.com/episodes/416-form-objects
In short, you create a custom model including some of the necessary ActiveModel modules then create a custom save method, e.g. (this is directly from the article which has a lot of great advice).
class Signup
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :company
attribute :name, String
attribute :company_name, String
attribute :email, String
validates :email, presence: true
# … more validations …
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
#company = Company.create!(name: company_name)
#user = #company.users.create!(name: name, email: email)
end
end
This gives you much more control and a much cleaner interface.