We are looking to save JSON API puts with Rails 5 (we are using ember.js on the front end).
We are struggling to work out the best way to save/create relationship data.
Our model:
class ProductSpec < ApplicationRecord
belongs_to :organisation
end
Our serializer:
class ProductSpecSerializer < ActiveModel::Serializer
attributes :id, :name
has_one :organisation
end
Our controller:
class ProductSpecsController < ApplicationController
before_action :set_product_spec, only: [:show, :update, :destroy]
{...}
# PATCH/PUT /product_specs/1
def update
if #product_spec.update(product_spec_params)
render json: #product_spec
else
render json: #product_spec.errors, status: :unprocessable_entity
end
end
{...}
private
def set_product_spec
#product_spec = ProductSpec.find(params[:id])
end
def product_spec_params
params.require(:data)
.require(:attributes)
.permit(
:name
)
end
end
And our JSON
Parameters: {"data"=>{"id"=>"7", "attributes"=>{"name"=>"Tester"}, "relationships"=>{"organisation"=>{"data"=>{"type"=>"organisations", "id"=>"1"}}}, "type"=>"product-specs"}, "id"=>"7", "product_spec"=>{}}
Any changes to name are nicely saved. The question how to best design our controller so that we save the 'organisation' relationship (realise that we also have to permit this, too).
Related
I'm using Rails5 API and Ember 3.
On the Rails side the model I'd like to save is defined as follows:
class ShopLanguage < ApplicationRecord
belongs_to :shop
belongs_to :language
end
Here is the Shop model:
class Shop < ApplicationRecord
has_many :shop_languages, inverse_of: :shop, dependent: :destroy
has_many :languages, through: :shop_languages
end
Here is the ShopLanguage serializer:
class ShopLanguageSerializer < ActiveModel::Serializer
attributes :id, :shop_id, :language_id, :modified_by
belongs_to :shop
belongs_to :language
end
The problem is that when I'm trying to create a new shop_languagefor a specified shop from Ember:
# controllers/shop-languages.js
actions: {
saveLanguage(aLanguage) {
let shopLanguage = this.store.createRecord('shop-language', {
language: aLanguage,
shop: this.get('currentShop.shop'),
modifiedBy: this.get('currentUser.user').get('username')
});
shopLanguage.save().then(function() {
this.get('flashMessages').info('New language added with success');
});
the language_id is not passed in in params hash in the ShopLanguagesController on the Rails side:
# shop_langauges_controller.rb
class ShopLanguagesController < ApplicationController
before_action :find_shop
before_action :find_language, only: [:create, :destroy]
def create
#shop.languages << #language
json_response(#shop.languages, :created)
end
private
def language_params
params.permit(:language_id, :shop_id, :id, :modified_by)
end
def find_shop
#shop = Shop.find(params[:shop_id])
end
def find_language
#language = Language.find_by!(id: params[:language_id])
end
end
When I check the URL hit by Ember app, it seems to be OK:
POST http://localhost:4200/shops/613/languages
The error comes from find_language method because language_id is null.
Why so ? What is wrong with that ? Thank you
This one is a bit confusing.
I think the line that is problematic is in the controller and it's this line in particular:
recipe_tools = (recipe.recipe_tools + RecipeTool.generic)
My models:
class Recipe < ActiveRecord::Base
...
has_many :recipe_tools, dependent: :destroy
...
end
class RecipeTool < ActiveRecord::Base
belongs_to :story
end
class Story < ActiveRecord::Base
...
has_many :recipe_tools, dependent: :destroy
..
end
This is my controller:
module Api
module Recipes
class RecipeToolsController < Api::BaseController
before_filter :set_cache_buster
def index
# expires_in 30.minutes, public: true
recipe = Recipe.find(params[:recipe_id])
recipe_tools = (recipe.recipe_tools + RecipeTool.generic)
binding.pry
render json: recipe_tools, each_serializer: Api::V20150315::RecipeToolSerializer
end
end
end
end
This is my serializer:
module Api
module V20150315
class RecipeToolSerializer < ActiveModel::Serializer
cached
delegate :cache_key, to: :object
attributes :id,
:display_name,
:images,
:display_price,
:description,
:main_image,
:subtitle
def display_name
object.display_name
end
def images
object.story.get_spree_product.master.images
end
def display_price
object.story.get_spree_product.master.display_price
end
def description
object.story.description
end
def main_image
object.story.main_image
end
def subtitle
object.story.get_spree_product.subtitle
end
def spree_product
binding.pry
spree_product.nil? ? nil : spree_product.to_hash
end
private
def recipe_tool_spree_product
#spree_product ||= object.story.get_spree_product
end
end
end
end
This is my RecipeTool model:
class RecipeTool < ActiveRecord::Base
...
scope :generic, -> { where(generic: true) }
end
In the controller, we call recipe.recipe_tool only once and so I don't think we need to includes recipe_tool. We're not iterating through a collection of recipes and calling recipe_tool on each one so no N+1 problem.
However, we are creating a collection of recipe_tools in the controller by concatenating two collections of recipe_tools together. Recipe.generic is also a SQL query that generates generic recipe_tools.
I think the N+1 problem is happening in generating the JSON response via the serializer. We call recipe_tool.story a lot which would generate a SQL queries each time we call #story and we do that on a collection of recipe_tools.
First, I would fix your associations using :inverse_of, so that I wouldn't have to worry about rails reloading the objects if it happened to traverse back up to a parent object. ie
class Recipe < ActiveRecord::Base
...
has_many :recipe_tools, dependent: :destroy, :inverse_of=>:recipe
...
end
class RecipeTool < ActiveRecord::Base
belongs_to :story, :inverse_of => :recipe_tools
belongs_to :recipe, :inverse_of => :recipe_tools ## this one was missing???
end
class Story < ActiveRecord::Base
...
has_many :recipe_tools, dependent: :destroy, :inverse_of=>:story
..
end
Next I would eager_load the appropriate associations in the controller, something like:
ActiveRecord::Associations::Preloader.new.preload(recipe_tools, :story =>:recipe_tools, :recipe=>:recipe_tools)
before calling the serializer.
I'm having an issue with limiting the level of associations serialized within an active model resource.
For example:
A Game has many Teams which has many Players
class GameSerializer < ActiveModel::Serializer
attributes :id
has_many :teams
end
class TeamSerializer < ActiveModel::Serializer
attributes :id
has_many :players
end
class PlayerSerializer < ActiveModel::Serializer
attributes :id, :name
end
When I retrieve the JSON for the Team, it includes all the players in a sub array, as desired.
When I retrieve the JSON for the Game, it includes all the Teams in a sub array, excellent, but also all the players for each Team. This is the expected behaviour but is it possible to limit the level of associations? Have Game only return the serialized Teams without the Players?
Another option is to abuse Rails' eager loading to determine which associations to render:
In your rails controller:
def show
#post = Post.includes(:comments).find(params[:id])
render json: #post
end
then in AMS land:
class PostSerializer < ActiveModel::Serializer
attributes :id, :title
has_many :comments, embed: :id, serializer: CommentSerializer, include: true
def include_comments?
# would include because the association is hydrated
object.association(:comments).loaded?
end
end
Probably not the cleanest solution, but it works nicely for me!
You can create another Serializer:
class ShortTeamSerializer < ActiveModel::Serializer
attributes :id
end
Then:
class GameSerializer < ActiveModel::Serializer
attributes :id
has_many :teams, serializer: ShortTeamSerializer
end
Or you can define a include_teams? in GameSerializer:
class GameSerializer < ActiveModel::Serializer
attributes :id
has_many :teams
def include_teams?
#options[:include_teams]
end
end
I was wondering what the best implementation would be to programatically generate a single child object for a parent(s) without the use of a form.
In my case I have an existing forum system that I would like to tie into my Upload system via comments. I would like to manually create a child Forum object to discuss said upload in the same create action that the Upload is created. The two models have a relationship as so:
Child forum:
class Forum < ActiveRecord::Base
...
belongs_to :upload
end
Parent upload:
class Upload < ActiveRecord::Base
...
has_one :forum
end
I was thinking of something along the lines of:
class UploadsController < ApplicationController
...
def create
#Create the upload and a new forum + initial post to discuss the upload
#upload = Upload.new(params[:upload])
#forum = Forum.new(:upload_id => #upload.id, :title => #upload.name...)
#first_post = Post.new(:forum_id => #forum.id....)
if #upload.save && #topic.save && #first_post.save
redirect_to :action => "show", :id => #upload.id
else
redirect_to :action => "new"
end
end
end
Which is fairly close to what I wanted to do but the parent ids aren't generated until the parent objects are saved. I could probably do something like:
#upload.save
#forum = Forum.new(:upload_id => #upload.id...
#forum.save....
But I thought it might be cleaner to only persist the objects if they all validated. I'm not sure, does anybody else know of a better implementation?
I would recommend moving the forum creation from the controller to the model. The forum will only be created on the successful creation of the Upload.
class Upload < ActiveRecord::Base
...
has_one :forum
after_create :create_forum
...
def create_forum
Forum.create(:upload_id => self.id, :title => self.name...)
end
end
class Forum < ActiveRecord::Base
...
has_many :posts
after_create :create_first_post
...
def create_first_post
Post.new(:forum_id => self.id)
# or self.posts << Post.new()
end
end
I am trying to create a Active Record tableless Model. My user.rb looks like this
class User < ActiveRecord::Base
class_inheritable_accessor :columns
def self.columns
#columns ||= [];
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(
name.to_s,
default,
sql_type.to_s,
null
)
end
column :name, :text
column :exception, :text
serialize :exception
end
When creating the new object in controller
#user = User.new
I am getting the error
Mysql2::Error: Table 'Sampledb.users' doesn't exist: SHOW FIELDS FROM users
class Tableless
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
def self.attr_accessor(*vars)
#attributes ||= []
#attributes.concat( vars )
super
end
def self.attributes
#attributes
end
def initialize(attributes={})
attributes && attributes.each do |name, value|
send("#{name}=", value) if respond_to? name.to_sym
end
end
def persisted?
false
end
def self.inspect
"#<#{ self.to_s} #{ self.attributes.collect{ |e| ":#{ e }" }.join(', ') }>"
end
end
Few things:
Firstly you are using the Rails2 approach outlined in Railscast 193 when really you should be using the Rails 3 approach, outlined in Railscast 219
You probably don't want to inherit from ActiveRecord::Base when doing this sort of thing.
Read Yehuda Katz's blog post on this.
As mentioned by stephenmurdoch in rails 3.0+ you can use the method outlined in railscasts 219
I had to make a slight modification to get this to work:
class Message
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :name, :email, :content
validates_presence_of :name
validates_format_of :email, :with => /^[-a-z0-9_+\.]+\#([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
validates_length_of :content, :maximum => 500
def initialize(attributes = {})
unless attributes.nil?
attributes.each do |name, value|
send("#{name}=", value)
end
end
end
def persisted?
false
end
end
Don't inherit your class from ActiveRecord::Base.
If a model inherits from ActiveRecord::Base as you would expect a model class to,it wants to have a database back-end.
Just remove:
class_inheritable_accessor :columns
And it should work, even with associations just like a model with a table.
Just for anyone still struggling with this. For rails 2.x.x
class TestImp < ActiveRecord::Base
def self.columns
#columns ||= []
end
end
For rails 3.1.x you can either include ActiveModel (as explained by #ducktyped) without inheriting from ActiveRecord or If you do need to inherit from ActiveRecord::Base due to some reason then the above with one other addition:
class TestImp < ActiveRecord::Base
def attributes_from_column_definition
[]
end
def self.columns
#columns ||= []
end
end
For Rails >= 3.2 there is the activerecord-tableless gem. Its a gem to create tableless ActiveRecord models, so it has support for validations, associations, types.
When you are using the recommended way to do it in Rails 3.x there is no support for association nor types.