Rails - Which is a better approach to save model updates - ruby-on-rails

I've found that both methods work for saving data. Is there an advantage to using one method over the other? What are the advantages/disadvantages?
First version:
begin
#user.save!
render json: "User #{#user.email} added", status: :created
rescue StandardError => e
render json: #user.errors.full_messages, status: :unprocessable_entity
end
Second version:
if #user.valid?
#user.save!
render json: "User #{#user.email} added", status: :created
else
render json: #user.errors.full_messages, status: :unprocessable_entity
end

I would go with a third alternative, because on save, the model is validated automatically:
if #user.save
render json: "User #{#user.email} added", status: :created
else
render json: #user.errors.full_messages, status: :unprocessable_entity
end

Its comes down to performance. Your first approach is more expensive because the full call stack has to be unwound to construct the exception details and you don't even use it.
Exceptions are expensive
When you catch an exception, the exception has the full stack trace where exactly the error occurred and what was the sequence of method calls that led to that event. Building this information requires Ruby to go to the previous method call and then to the previous-to-previous method call and so on recursively. This is a fairly expensive operation and since you are already within the method, you don't really need this information.
Validating twice is unnecessary
So, out of your two approaches, the second version is better. However jvperrin's answer is even better. In your second approach, you call #user.isvalid? which runs through all the model validations. And when you call #user.save, it again runs through the same validations. Instead you could just call #user.save directly and look at the return value, which is true when everything went well and false when there were validation errors.
Hope that helps.

You can use like this
if #user.valid?
if #user.save!
render json: "User #{#user.email} added", status: :created
else
render json: #user.errors.full_messages, status: :unprocessable_entity
end
else
##some code
end
Because if user is not valid no need of entering save block.

Couldn't you just add validations to your User model as a 3rd, simpler alternative?
Something like,
class User < Activerecord::Base
validates :email, presence: true
end

Related

Rails: Find or initialize and then merge in params

I use find_or_initialize_by in my controller and then would like to apply the additional params to the record, whether new or existing. Is this possible to do in a Railsy way or do I have to loop through the parameters?
def create
re_evaluation = #client.re_evaluations.find_or_initialize_by(program_id: re_evaluation_params[:program_id], check_in_id: re_evaluation_params[:check_in_id])
# apply remaining re_evaluation_params here
if re_evaluation.save
render json: re_evaluation, status: :created
else
render json: re_evaluation.errors.full_messages, status: :unprocessable_entity
end
end
You should be able to assign the parameters as you'd do normally.
re_evaluation.assign_attributes(re_evaluation_params)
if re_evaluation.save
...

how to rescue ActiveRecord::RecordNotUnique error in controller and re-render form?

My controller's #create action fails because of uniqueness constraint I've added to to the name and title attribute of my Boo model.
def create
#boo = current_user.my_boos.create(boo_params)
respond_to do |format|
if #boo.save
format.html { redirect_to root_path, notice: "Thanks!" }
format.json { render :index, status: :created, location: #boo }
else
format.html { render :new }
format.json { render json: #boo.errors, status: :unprocessable_entity }
end
end
end
Submitting with this action gives a Railsy looking PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_boos_on_name_and_title"error, which I think would be better displayed as an error message and a re-rendered page.
Everything I'm reading says that rescuing from Exception is bad, while rescuing from StandardError is good. But I've yet to find anything that explains how to display that error more nicely for the end user without rescuing from Exception. Like, it doesn't seem that StandardError works with the db.
The direct answer is to rescue the specific exception you want to recover from, and then handle it as you see fit... something like:
def create
#boo = current_user.my_boos.new(boo_params)
respond_to do |format|
begin
if #boo.save
format.html { redirect_to root_path, notice: "Thanks!" }
format.json { render :index, status: :created, location: #boo }
else
format.html { render :new }
format.json { render json: #boo.errors, status: :unprocessable_entity }
end
rescue PG::UniqueViolation
format.html { render :new }
format.json { render json: ["We've already got one"], status: :unprocessable_entity }
end
end
end
(rescuing StandardError there should work just as well, but while safe, it's much broader than we need.)
However, I'd suggest that a more "Railsy" solution is to define a uniqueness validation in your model, in addition to the DB constraint, so it'll be handled by the existing if #boo.save conditional.
You can add validation to your Boo model, it'll prevent from trying to save non-valid record and there will be no need to rescue from PG::UniqueViolation error:
class Boo < ApplicationRecord
# ...
validates :name, uniqueness: { scope: :title }
# ...
end
(c) http://guides.rubyonrails.org/active_record_validations.html#uniqueness

Rails rescue standard error doesn't get sent

I have a controller method as follows
def create
#game = Game.create_new_game(game_params)
render 'show', status: 200
rescue StandardError => e
render json: {
status: 500,
error: e.to_s
}
end
I added a binding.pry and I can clearly see the following error in my console:
#<ActiveRecord::RecordInvalid: Validation failed: Name can't be blank, Duration can't be blank>
But it still sends the status:200 to the client side. Is there a different way the errors are supposed to be handled?
EDIT:
create_new_game method in Game
def self.create_new_game(prms)
#player = Player.find(prms[:player].to_i)
#game = #player.games.create!(
name: prms[:name],
duration: prms[:duration]
)
#game.save!
end
ActiveRecord does not normally raise an exception when a record is invalid. Its only when you use the "bang" methods like .create! that an exception is raised. Its also impossible for us to know what is going on inside Game.create_new_game. These are used for example in seed files wherethe developer should be aware that the validation failed or in nested transactions where it should trigger a rollback.
But what you're doing is not a good practice since exceptions should be used for exceptional events - not normal control flow.
Instead you should check the return value of saving/updating the record and determine the response.
def create
#game = Game.create(game_params)
if #game.save
status: :created, location: #game
# or
render json: #game
else
render json: {
status: 500,
error: #game.errors.full_messages
}
end
end

Rails render return do not stop execution

Hi inmy app I has such a situation:
in some_controller.rb I has a code aout this:
def start
method1(param)
....
if some_case
render json: {ok: "ok"}
end
end
def method1
...
if some_case
render json: {error: "Some error"}
return
end
end
The thing is when it's time to render a json with error, I get a double render error. It advices me to use render .. and return. I've tried even that, and still get this error.
Is this because render do not breaks execution itself, but just returns smth to a caller method? If it's so, what can I do in my case? The thing is method1 is actually a big method and I surely want it to be separateed from start method. And in case there are no reasons to render an error there, I want an execution of start to be continued.
Thanx!
Consider using filter instead. This works:
before_action :method1, only: :start
def start
....
if some_case
render json: {ok: "ok"}
end
end
def method1
...
if some_case
render json: {error: "Some error"}
return
end
end
When render occurs in filter, it does not run action itself then, so no double render occurs.
Use this code
def method1
if some_case
render json: {error: "Some error", status: :unprocessable_entity }
end
end

Handling Unique Validation with Status Accepted

I am making Rails backend api for mobile app, and want to validate unique record with accepted status code.
#Why?
When I save entries from mobile app, I want to avoid saving duplicate records in Rails, but I also want to send "accepted status code" from Rails to mobile app instead of error status not to stop running bulk saving from mobile.
Model
class Entry < ActiveRecord::Base
validates :uuid, uniqueness: true
end
Controllers
def create
# Check if the same record already exists
entry = Entry.find_by(uuid: entry_params[:uuid])
if entry.present?
render json: {errors: {message: 'The same uuid already exists.'}}, status: :accepted
return
end
entry = Entry.new
entry.attributes = entry_params
if entry.save
render json: entry, status: :created
else
render json: entry.errors, status: :unprocessable_entity
end
end
The above code works as I want, but I am wondering if I can use validations of Rails properly instead.
This is what I tried, but couldn't get unique uuid error unfortunately. Is there any better way to handle this situation?
ApplicationController
rescue_from ActiveRecord::RecordNotUnique, with: :record_not_unique
def record_not_unique
render json: {errors: {message: 'Record not unique.'}}, status: :accepted
end
The uniqueness validation will not raise an exception. It just sets the errors object on the validated entry record and prevents saving it. You have two options in principle:
Get rid of the uniqueness validation in your model, and set up a UNIQUE constraint on the column in the underlying database table. Then, the save call would raise the ActiveRecord::RecordNotUnique exception triggered by the database. And then you indeed should be able to rescue_from it the way you posted.
Do without exceptions (dump rescue_from) and leave the uniqueness validation in place and process the errors manually in your controller, something along the lines of:
def create
entry = Entry.new(entry_params)
if entry.save
render json: entry, status: :created
elsif entry.errors.count == 1 && entry.errors[:uuid].first == "has already been taken"
render json: {errors: {message: 'The same uuid already exists.'}}, status: :accepted
else
render json: entry.errors, status: :unprocessable_entity
end
end
You will not be able to use rescue_from because a triggered exception will effectively fail-fast your request. You should cache/log these results of successes/failures (non-uniques) instead.
You can send status 201 when a record has been created, and send status 202 when duplicate record was determined. Bucket each status and at the end of the batch, execute any post-processing at the end.

Resources