What's the Rails way to save an object with associatons? - ruby-on-rails

I have a couple of classes with a usual one-to-many relationship, like:
class Blog < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :blog
validates :blog_id, presence: true
end
when I build a blog and add to it a bunch of posts:
blog = Blog.new
blog.posts = [Post.new, Post.new]
I cannot just save the blog, like this:
blog.save
because blog_id in the posts is blank, so validation for the posts are failing. I thought Rails was smart enough to either check the id or the presence of the object, but it isn't.
What's the proper Rails way to solve this? Something better than:
saved = true
Blog.transaction do
saved &&= #blog.save
if saved && params[:blog][:posts].responds_to?(:each)
params[:blog][:posts].each do |post|
p = Post.new({blog: #blog}, without_protection: true)
saved &&= p.save
end
end
end

My solution, so far, is to change the validation line from:
validates :blog_id, presence: true
to
validate { errors.add(:blog, "can't be blank") if blog_id.blank? && blog.blank? }
which makes me wonder, why isn't Rails doing that for associations? Is there any danger in doing that?

You can do it two ways
blog.posts.create(attributes_of_new_post)
or
blog.posts << Post.new(attributes_of_new_post)
And change your has_many to be
has_many :posts, :inverse_of => :blog

Related

How to get object for `reject_if`? [duplicate]

Creating a record with nested associations fails to save any of the associated records if one of the records fails validations.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
accepts_nested_attributes_for :episodes
end
class Episode < ActiveRecord::Base
belongs_to :podcast, inverse_of: :episodes
validates :podcast, :some_attr, presence: true
end
# Creates a podcast with one episode.
case_1 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
]
}
# Creates a podcast without any episodes.
case_2 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
{title: "ep2"} # <- Invalid Episode
]
}
I'd expect case_1 to save successfully with one created episode.
I'd expect case_2 to do one of two things:
Save with one episode
Fail to save with validation errors.
Instead the podcast saves but neither episode does.
I'd like the podcast to save with any valid episode saved as well.
I thought to reject invalid episodes by changing the accepts nested attributes line to
accepts_nested_attributes_for :episodes, reject_if: proc { |attributes| !Episode.new(attributes).valid? }
but every episode would be invalid because they don't yet have podcast_id's, so they would fail validates :podcast, presence: true
Try this pattern: Use the :reject_if param in your accepts_nested_attributes_for directive (docs) and pass a method to discover if the attributes are valid. This would let you offload the validation to the Episode model.
Something like...
accepts_nested_attributes_for :episodes, :reject_if => :reject_episode_attributes?
def reject_episode_attributes?( attributes )
!Episode.attributes_are_valid?( attributes )
end
Then in Episode you make a method that tests those however you like. You could even create a new record and use existing validations.
def self.attributes_are_valid?( attributes )
new_e = Episode.new( attributes )
new_e.valid?
end
You can use validates_associated to cause the second option (Fail to save with validation errors)
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
validates_associated :episodes
accepts_nested_attributes_for :episodes
end
UPDATE:
To do option one (save with one episode) you could do something like this:
1. Add the validates_associated :episodes
2. Add code in your controller's create action after the save of #podcast fails. First, inspect the #podcast.errors object to see if the failure is caused by validation error of episodes (and only that) otherwise handle as normal. If caused by validation error on episodes then do something like #podcast.episodes.each {|e| #podcast.episodes.delete(e) unless e.errors.empty?} Then save again.
This would look something like:
def create
#podcast = Podcast.new(params[:podcast])
if #podcast.save
redirect_to #podcast
else
if #some conditions looking at #podcast.errors to see that it's failed because of the validates episodes
#podcast.episodes.each do |episode|
#podcast.episodes.delete(episode) unless episode.errors.empty?
end
if #podcast.save
redirect_to #podcast
else
render :new
end
else
render :new
end
end
end
To get the first option, try turning autosaving on for your episodes.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast, autosave: true
accepts_nested_attributes_for :episodes
end

Rails 5 Model.where(user_id) - Two levels up

Terribly worded, but I'm confusing it.
I have a User model who has_many Clients and has_many statements, through: :clients and then statements which belongs_to clients and belongs to user
In Console I can do all the queries I want. User.statements User.client.first.statements etc - What I'm struggling on is Controller restrictions
For now it's simple - A user should only be able to see Clients and Statements in which they own.
For Clients I did
Client Controller
def index
#clients = Client.where(user_id: current_user.id)
end
Which seems to work perfectly. Client has a field for user_id
I'm kind of stuck on how to emulate this for Statements. Statements do -not- have a user_id field. I'm not quite sure I want them too since in the very-soon-future I want clients to belongs_to_many :users and Statements to not be bound.
Statement Controller
def index
#clients = Client.where(user_id: current_user.id)
#statements = Statement.where(params[:client_id])
end
I'm just genuinely not sure what to put - I know the params[:client_id] doesn't make sense, but what is the proper way to fulfill this? Am I going about it an unsecure way?
Client Model
class Client < ApplicationRecord
has_many :statements
has_many :client_notes, inverse_of: :client
belongs_to :user
validates :name, presence: true
validates :status, presence: true
accepts_nested_attributes_for :client_notes, reject_if: :all_blank, allow_destroy: true
end
Statement Model
class Statement < ApplicationRecord
belongs_to :client
belongs_to :user
validates :name, presence: true
validates :statement_type, presence: true
validates :client_id, presence: true
validates :start_date, presence: true
validates :end_date, presence: true
end
User Model
class User < ApplicationRecord
has_many :clients
has_many :statements, through: :clients
end
Based on the reply provided below I am using
def index
if params[:client][:user_id] == #current_user.id
#clients = Client.includes(:statements).where(user_id: params[:client][:user_id])
#statements = #clients.statements
else
return 'error'
end
end
Unsure if this logic is proper
Use includes to avoid [N+1] queries.
And regarding "A user should only be able to see Clients and Statements in which they own".
if params[:client][:user_id] == #current_user.id
#clients = Client.includes(:statements).where(user_id: params[:client][:user_id])
# do more
else
# Type your error message
end
Additionally, you might need to use strong params and scope.
The best way to do it is using includes:
#clients = Client.where(user_id: current_user.id)
#statements = Statement.includes(clients: :users}).where('users.id = ?', current_user.id)
You can take a look in here: https://apidock.com/rails/ActiveRecord/QueryMethods/includes
In this case, thanks to the reminder that current_user is a helper from Devise, and the relational structure I showed, it was actually just as simple as
def index
#statements = current_user.statements
end
resolved my issue.
Due to the [N+1] Queries issue that #BigB has brought to my attention, while this method works, I wouldn't suggest it for a sizable transaction.

validates_associated to validate the association between two models

I have two models Post model and Comment model..First if a create a post it will have a post id of 1 then while creating a comment i can give association to the post using post_id equal to 1 but if i create a comment with post id of 2 which does not exist it would still go ahead and create a comment but with id of 'nil'..I want to ensure that the comment will be created only if the respective post_id is present.
class Post < ActiveRecord::Base
has_many :comments, dependent: destroy
end
class Comment < ActiveRecord::Base
belongs_to :post
validates_associated: post
end
As per my understanding validates_associated checks whether the validations in post model passes before creating a comment. Clarify me if i am wrong and what would be a appropriate solution for the above scenario?
First, the preferred way of setting the association b/w Post-Comment here is by :
def new
#product = Product.first
#comment = #product.comments.build
end
def create
#product = Product.find(params[:comment][:post_id])
#comment = #product.comments.create(comment_params)
end
For your particular scenario, I'm assuming that post_id is coming in params via some form or something, and then you wish to create a comment only if the post with that particular post_id exists. This can be done by adding following in Comment model:
validates :post, presence: true, allow_blank: false
OR
validate :post_presence, on: :create
def post_presence
errors.add(:post_id, "Post doesn't exist") unless Post.find(post_id).present?
end
You can even do the same thing at controller-side with before_action/before_filter hooks.
You can do this to validate the presence of post_id
class Comment < ActiveRecord::Base
belongs_to :post
validates :post_id, :presence => true
end
or to validate association, you can use
class Comment < ActiveRecord::Base
belongs_to :post
validates_presence_of :post
end

Rails accepts_nested_attributes_for saves no nested records if one record is invalid

Creating a record with nested associations fails to save any of the associated records if one of the records fails validations.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
accepts_nested_attributes_for :episodes
end
class Episode < ActiveRecord::Base
belongs_to :podcast, inverse_of: :episodes
validates :podcast, :some_attr, presence: true
end
# Creates a podcast with one episode.
case_1 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
]
}
# Creates a podcast without any episodes.
case_2 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
{title: "ep2"} # <- Invalid Episode
]
}
I'd expect case_1 to save successfully with one created episode.
I'd expect case_2 to do one of two things:
Save with one episode
Fail to save with validation errors.
Instead the podcast saves but neither episode does.
I'd like the podcast to save with any valid episode saved as well.
I thought to reject invalid episodes by changing the accepts nested attributes line to
accepts_nested_attributes_for :episodes, reject_if: proc { |attributes| !Episode.new(attributes).valid? }
but every episode would be invalid because they don't yet have podcast_id's, so they would fail validates :podcast, presence: true
Try this pattern: Use the :reject_if param in your accepts_nested_attributes_for directive (docs) and pass a method to discover if the attributes are valid. This would let you offload the validation to the Episode model.
Something like...
accepts_nested_attributes_for :episodes, :reject_if => :reject_episode_attributes?
def reject_episode_attributes?( attributes )
!Episode.attributes_are_valid?( attributes )
end
Then in Episode you make a method that tests those however you like. You could even create a new record and use existing validations.
def self.attributes_are_valid?( attributes )
new_e = Episode.new( attributes )
new_e.valid?
end
You can use validates_associated to cause the second option (Fail to save with validation errors)
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
validates_associated :episodes
accepts_nested_attributes_for :episodes
end
UPDATE:
To do option one (save with one episode) you could do something like this:
1. Add the validates_associated :episodes
2. Add code in your controller's create action after the save of #podcast fails. First, inspect the #podcast.errors object to see if the failure is caused by validation error of episodes (and only that) otherwise handle as normal. If caused by validation error on episodes then do something like #podcast.episodes.each {|e| #podcast.episodes.delete(e) unless e.errors.empty?} Then save again.
This would look something like:
def create
#podcast = Podcast.new(params[:podcast])
if #podcast.save
redirect_to #podcast
else
if #some conditions looking at #podcast.errors to see that it's failed because of the validates episodes
#podcast.episodes.each do |episode|
#podcast.episodes.delete(episode) unless episode.errors.empty?
end
if #podcast.save
redirect_to #podcast
else
render :new
end
else
render :new
end
end
end
To get the first option, try turning autosaving on for your episodes.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast, autosave: true
accepts_nested_attributes_for :episodes
end

Custom validation in Rails

I have a Post has_many Comments association. Post has boolean attribute published.
When post.published is false, the new comment shouldn't be valid.
What is the best practise to accomplish this kind of validation?
I've tried to do it by this way, but sadly, it doesn't work correctly. It is still possible to create new comment for unpublished post.
class Comment < ActiveRecord::Base
validates :post_id, presence: true, if: :post_is_published
...
def post_is_publised
post && post.published
end
end
Hmm.. I think you have syntax errors in your code... Try this:
class Comment < ActiveRecord::Base
validates :post_id, :presence => true, :if => :post_is_published
def post_is_publised
post.try(:published)
end
end
After reading your console output and checking your question one more time:
class Comment < ActiveRecord::Base
validate :post_has_to_be_published
def post_has_to_be_published
unless post.try(:published)
self.errors.add(:base, "you can add comments only to published posts")
end
end
end
I understand that you don't want to allow adding comments to unpublished posts. Above code should accomplish that.

Resources