Creating 2 Models in Controller Action With Transaction - Rails 4 - ruby-on-rails

There are multiple answers that explain how you can have nested resources, however, my use case is a bit different.
Batches belong to orders and an order has many batches.
I can understand how it works if you have a form for an order and can create batches within that form, but cannot comprehend a good way for my situation.
I have a form for a nested resource (batch) where the parent (order) may or may not exist. They may select whether or not it exists via radio buttons. If it exists, they then just simply select which order it belongs to .. simple. If it doesn't exist, I show fields for the order and submit order params alongside of batch params. I want to make sure to rollback the order creation if the batch does not save.
Here is the code I have thus far.
def create
#batch = Batch.new(batch_params)
Batch.transaction do
if params[:new_order] == "newOrder"
#order = Order.new(order_params)
#order.project_id = params[:batch][:project_id]
begin
#order.save!
rescue
respond_to do |format|
format.html { render action: 'new' }
format.json { render json: {order: #order.errors}, status: :unprocessable_entity }
format.js { render json: {order: #order.errors}, status: :unprocessable_entity }
end
raise ActiveRecord::Rollback
return
end
##batch.order_id = #order.id
end
respond_to do |format|
begin
#batch.save!
format.html { redirect_to #batch, notice: 'Batch was successfully created.' }
format.json { render json: #batch }
format.js { render json: #batch }
rescue
binding.pry
raise ActiveRecord.Rollback
format.html { render action: 'new' }
format.json { render json: {batch: #batch.errors}, status: :unprocessable_entity }
format.js { render json: {batch: #batch.errors}, status: :unprocessable_entity }
end
end
end
end
This isn't behaving quite like I want it and seems quite ugly. I have a feeling I'm making it more difficult than I need to. What's the best approach in a situation like this? Much appreciated!

It seems like this is a great opportunity to use a Service Object: https://www.engineyard.com/blog/keeping-your-rails-controllers-dry-with-services .
This pattern is very useful for keeping Model and Controllers clean, and making sure those parts of the application are keeping to the Single Responsibility Principle.
What I would do in this case is create a service class called CreateBatch that takes in the parameters and performs the correct logic for each case. You can then render the correct output in the controller. This will also help clean up the conditionals and early returns you have.
For example:
# app/controllers/batches_controller.rb
def create
project_id = params[:batch][:project_id]
new_order = params[:new_order]
result = CreateBatch.new(new_order, batch_params, order_params, project_id).call
if result.errors
# handle errors with correct format
else
# handle successful response with correct format
end
end
# app/services/create_batch.rb
class CreateBatch
def initialize(new_order, batch_params, order_params, project_id)
#new_order = new_order
#batch_params = batch_params
#order_params = order_params
#project_id = project_id
end
def call
if new_order?
create_new_order
else
add_batch_to_existing_order
end
end
private
def new_order?
#new_order
end
def create_new_order
order_params = #order_params.merge(project_id: #project_id)
Order.save(order_params)
end
def add_batch_to_existing_order
Batch.create(#batch_params)
end
end
I did not run this so it may take a bit of tweaking to work, however, I hope it's a good starting point. One of the awesome things about this refactor is you now have 1 conditional for the logic and 1 conditional for the response, no need for adding in Transaction blocks, and no early returns. It may make sense to break the call method into 2 different methods that you can call from the controller. Using service classes like this makes the code much easier to unit test as well.

Why not move error handling and response rendering outside of the transaction?
def create
#batch = Batch.new(batch_params)
Batch.transaction do
if params[:new_order] == "newOrder"
#order = Order.new(order_params)
#order.project_id = params[:batch][:project_id]
#order.save!
#batch.order_id = #order.id
#batch.save!
end
end
respond_to do |format|
format.html { redirect_to #batch, notice: 'Batch was successfully created.' }
format.json { render json: #batch }
format.js { render json: #batch }
end
rescue StandardError => error
#error = error
format.html { render action: 'new' }
format.json { render json: {error: #error, batch: #batch.errors}, status: :unprocessable_entity }
format.js { render json: {error: #error, batch: #batch.errors}, status: :unprocessable_entity }
end
It's still quite complex, but it's definitely more readable. The next step would be to extract the whole transaction block to a service.

Related

Rails store to different tables within the same controller

I have two models, Livestock and History
a livestock has many histories and history belongs to livestock
This is the create method inside the LivestockController
# POST /livestocks
# POST /livestocks.json
def create
#livestock = Livestock.new(livestock_params.permit!)
respond_to do |format|
if #livestock.save
format.html { redirect_to #livestock }
flash[:success] = "Livestock was successfully created"
format.json { render :show, status: :created, location: #livestock }
else
format.html { render :new }
format.json { render json: #livestock.errors, status: :unprocessable_entity }
end
end
end
I wanted to create a record in the histories table with
history = History.new(livestock_id: #livestock.id, event: "Purchased", event_date: #livestock.purchase_date, image: #livestock.image)
history.save!
inside the create method
How can I do it? I can't put it in the create method because it says
Validation failed: Livestock must exist
apparently #livestock has not yet have the id attribute
Edit:
it still raises the same exception when I put it after
if #livestock.save
However I found a work around by using the session variable. Since it is redirected to the show page, I created the following inside the create method
session[:created] = "created"
And in my show method
# GET /livestocks/1
# GET /livestocks/1.json
def show
if session[:created] == "created"
history = History.new(livestock_id: params[:id], event: "Purchased", event_date: #livestock.purchase_date, image: #livestock.image)
history.save!
session.delete(:created)
end
end
Now I am wondering what are consequences if I use this approach.
Livestock record is created when you call save (and there is no validation error). So one option is to create the history inside this if condition:
if #livestock.save
Another option is to use after_create callback in the livestock model that will create a history object right after creating livestock. You have to be careful because the callback might be called when you do not need it (i.e. when importing data).
The last option is to create a separate service object that will create livestock and all other required objects. That's probably the best approach, but it will require more customized code.
Update
Please also make sure to move if/else block outside the respond_to block:
if #livestock.save
# create history object here
respond_to do |format|
format.html { redirect_to #livestock }
flash[:success] = "Livestock was successfully created"
format.json { render :show, status: :created, location: #livestock}
end
else
respond_to do |format|
format.html { render :new }
format.json { render json: #livestock.errors, status: :unprocessable_entity }
end
end

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

Create more than one object at once using the standard create method in Ruby on Rails

I am trying to use the standard create method created for Ruby/Rails projects and simply pass in an additional form field that tells the method how many objects to create (vs just creating one object). The standard create method looks like so:
def create
#micropost = Micropost.new(micropost_params)
respond_to do |format|
if #micropost.save
format.html { redirect_to #micropost, notice: 'Micropost was successfully created.' }
format.json { render :show, status: :created, location: #micropost }
else
format.html { render :new }
format.json { render json: #micropost.errors, status: :unprocessable_entity }
end
end
end
I want to pass in an additional data (form field called number_to_create) which tells the method how many of the microposts to create. I just added a new form field like this, in addition to the other micropost form field params:
<%= text_field_tag :number_to_create %>
My question is how do I modify the create method code such that it creates N number of micropost objects vs. just one. So if I pass in 3 from the form along with the other micropost attributes, the method creates 3 identical micropost objects, not just one as it currently does.
Thanks in advance for your help on this.
You could use the param as times
#microposts = Micropost.transaction do
[].tap do |microposts|
param[:number_to_create].times do
microposts << Micropost.create(micropost_params)
end
end
end
respond_to do |format|
if #microposts.all? &:persisted?
format.html { redirect_to #micropost, notice: 'Micropost was successfully created.' }
format.json { render :show, status: :created, location: #micropost }
else
format.html { render :new }
format.json { render json: #micropost.errors, status: :unprocessable_entity }
end
end
The transaction block is to make sure that either all of them gets saved, or none of them gets saved, this way you can fix your errors and recreate them without worrying of getting any stray saved objects

best way to redirect to another action in controller

I am building a "shopping cart" and i am looking to allow the user to update quantities of an item in the cart. I have a remove link for the cart item, but the user could technically put the quantity to "0" rather than hitting the remove button. So on "update" i want to check if #cart.item.quantity == 0, and if so, then just redirect to the destroy action in the controller.
what is the cleanest solution to this? Here is what i tried but it doesn't work, and i am thinking its not the cleanest approach...
In my controller:
def update
if #cart_item.quantity == 0
render action: :destroy
else
respond_to do |format|
if #cart_item.update(cart_item_params)
format.js#on { head :no_content }
else
format.json { render json: #cart_item.errors, status: :unprocessable_entity }
end
end
end
end
def destroy
#cart_item.destroy
respond_to do |format|
format.js
end
end
render will not execute the action. Its probably better to just destroy the cart item in update itself instead of redirecting to destroy action
def update
if #cart_item.quantity == 0
#cart_item.destroy
else
respond_to do |format|
if #cart_item.update(cart_item_params)
format.json { head :no_content }
else
format.json { render json: #cart_item.errors, status: :unprocessable_entity }
end
end
end
end
Using Redirect:
def update
if #cart_item.quantity == 0
redirect_to action: :destroy, id: #cart_item.id
else
respond_to do |format|
if #cart_item.update(cart_item_params)
format.js#on { head :no_content }
else
format.json { render json: #cart_item.errors, status: :unprocessable_entity }
end
end
end
end
def destroy
#card_item = CartItem.find(params[:id]) #not sure if you are doing this elsewhere
#cart_item.destroy
respond_to do |format|
format.js
end
end
The first answer is correct that render will not execute the action. Render takes the current state of your controller and then draws the view. However, if you call his function as is you will have a problem. It will destroy the item but then not render the delete view so, assuming this is an ajax call, it will not update the view correctly.
If you want to execute the controller function you have two choices:
redirect_to your_destroy_action_path(#cart_item) - The one thing I'm not sure of here is how well ajax works when you make an ajax call and then follow it up with a redirect. I think the action will still execute but I'm not sure if the ajax view code is handled correctly with that level of indirection. (I'd like to think it would be but have never tried coding it myself.)
Call the destroy function in the place where you currently have:
render action: :destroy
So it becomes:
def update
if #cart_item.quantity == 0
destroy
else
respond_to do |format|
if #cart_item.update(cart_item_params)
format.js#on { head :no_content }
else
format.json { render json: #cart_item.errors, status: :unprocessable_entity }
end
end
end
end
def destroy
#cart_item.destroy
respond_to do |format|
format.js
end
end
I like to keep my controllers clean as much as possible.
If I will develop this, I will create a Jquery function that check if the quantity is zero and if yes, change the form action to DELETE instead PUT.

How do I call update action from another action in rails 3?

So I'm writing a basic member modifying action, and I figured, lets stay DRY and just modify the params hash then pass along to our update method but it doesn't seem to work. I guess there is some rails magic going on that I can't find... From what I've read this should work. I'm using Rails 3.2.
Here's an example of what I'm trying to do:
# POST /tasks/1/toggle_done
def toggle_done
#task = Task.find(params[:id])
puts "<<<<<", params
# invert done bool value
params[:done] = !(#task.done)
# thought maybe update_attributes retured a full set of
# attributes in the params...
#params[:name] = #task.name + "...test."
# thought maybe the method call to update was getting
# filtered or something. Doesn't seem to help.
#params[:_method] = "put"
# redirect to update with these new params
puts ">>>>>", params
# Why bother rewriting task.done = x; task.save;
# redirect_to show; etc when update already does that.
update
end
# PUT /tasks/1
# PUT /tasks/1.json
def update
#task = Task.find(params[:id])
puts "======", params
respond_to do |format|
if #task.update_attributes(params[:task])
format.html { redirect_to #task, notice: 'Task was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: #task.errors, status: :unprocessable_entity }
end
end
end
I get the following console output:
<<<<<
{"_method"=>"post", "authenticity_token"=>"CVqzsJfSVgM7Bq/kXlrjzkWVoA7Pbne4GNEHqbQB42s=", "action"=>"toggle_done", "controller"=>"tasks", "id"=>"1"}
>>>>>
{"_method"=>"put", "authenticity_token"=>"CVqzsJfSVgM7Bq/kXlrjzkWVoA7Pbne4GNEHqbQB42s=", "action"=>"toggle_done", "controller"=>"tasks", "id"=>"1", "done"=>false, "name"=>"Put Done button in index view...test."}
======
{"_method"=>"put", "authenticity_token"=>"CVqzsJfSVgM7Bq/kXlrjzkWVoA7Pbne4GNEHqbQB42s=", "action"=>"toggle_done", "controller"=>"tasks", "id"=>"1", "done"=>false, "name"=>"Put Done button in index view...test."}
So it seems like the params array is set right. It renders the regular show view with the flash message "Task was successfully updated.", so it seems like the whole method gets executed but non of the model properties are getting changed. I guess something inside update_attributes is failing. Can anyone shed some light on this for me?
Also is this a crazy thing to do? Should I be setting and saving inside my toggle_done method instead of chaining to update?
Rails saves the attributes for the task object in the hash params[:task]. So you in your toggle_done method you need to save the result in params[:task][:done] otherwise rails cannot associate the done attribute with the task.
def toggle_done
#task = Task.find(params[:id])
params[:task] = { done: !(#task.done) }
update
end
But with calling the update method you make 3 database queries where only 2 are neccessary - And the first 2 are identically because you load the Task with the ID in the toggle_done method as well as in update.
To avoid this you can put the save and redirect part into a protected method and call it when you want to save it. Like this:
def toggle_done
#task = Task.find(params[:id])
params[:task] = { done: !(#task.done) }
save_updated
end
def update
#task = Task.find(params[:id])
save_updated
end
protected
def save_updated
respond_to do |format|
if #task.update_attributes(params[:task])
format.html { redirect_to #task, notice: 'Task was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: "edit" }
format.json { render json: #task.errors, status: :unprocessable_entity }
end
end
You're passing params[:task] to update_attributes, which doesn't exist. Try:
params[:task] = {:done => !(#task.done)}

Resources