saving multiple tables at once in rails - ruby-on-rails

I have an action that update three tables at once like this:
def action_save
#user.update(param_param_list1)
#application.update(param_list2)
#college.update(param_list3)
end
but to make the program better, I want to either save all three together at once or not at all

Use an ActiveRecord::Transaction:
def action_save
#college.transaction do
#user.update!(param_param_list1)
#application.update!(param_list2)
#college.update!(param_list3)
end
end
A transaction ensures that all the database action within that block are performed. Or if there is an error, then the whole transaction is rolled back.

Related

Rails multiple record update

The following array of boolean attributes for multiple records
{"utf8"=>"✓","_method"=>"patch", "authenticity_token"=>"...",
"ts"=>
{"1"=>{"go"=>"0", "pickup"=>"0", "delivery"=>"1"},
"2"=>{"go"=>"0", "pickup"=>"0", "delivery"=>"1"},
"3"=>{"go"=>"0", "pickup"=>"0", "delivery"=>"1"},
[...]},
"commit"=>"Save changes"}
is being posted from one controller to a child controller with the following action that has un-conventional naming for the parameters.
def update_all
params[:ts].keys.each do |id|
#daystruttimeslot = Daystruttimeslot.find(id.to_i)
#daystruttimeslot.update(ts_params)
end
end
is hitting the error undefined local variable or method 'ts_params' for #<DaystruttimeslotsController:0x00007fa118f262f8> Did you mean? to_param params #_params
How can these parameters be properly processed by this action?
def update_all
ts = params.require(:ts)
#daystruttimeslots = Daystruttimeslot.where(id: ts.keys)
#daystruttimeslots.each do |d|
d.update(ts.fetch(d.id.to_s).permit(:go, :pickup, :delivery))
end
end
This does a single read operation instead of fetching each record separately and also provides a ivar that actually makes sense instead of whatever is at the end of the loop.
If you need to validate that all the ids are correct compare ts.keys.length to #daystruttimeslots.size. You also might want to consider wrapping this in a transaction so that the changes are rolled back if any of the updates fail instead of just leaving the job half done.

flexible system to destroy each records in batch

client_skipped_day_controller.rb
class ClientSkippedDaysController < ApplicationController
before_action :check_client_on_exist, only: [:create]
def index
#client_skipped_days = ClientSkippedDay.order_by(params[:sort_by], params[:direction])
if params[:date].present?
#client_skipped_days = #client_skipped_days.where('skipped_at = ?', Date.parse(params[:date]))
end
render json: #client_skipped_days, status: :ok
end
def create
#client_skipped_days = ClientSkippedDay.create!(client_skipped_days_params)
render json: #client_skipped_days, status: :created
end
def destroy
end
private
def client_skipped_days_params
params.permit(client_skipped_days: %i[client_id skipped_at])[:client_skipped_days]
end
def check_client_on_exist
client_skipped_days_params.each do |day|
ClientSkippedDay.find_by(day)&.destroy
end
end
end
My code works if I try to delete only one record, like a :
Parameters: {"client_skipped_days"=>[{"client_id"=>533, "skipped_at"=>"2019-02-24"}], "client_skipped_day"=>{}}
But if I try to delete each hash in the array, it's didn't work :(
Parameters: {"client_skipped_days"=>[{"client_id"=>533, "skipped_at"=>"2019-02-24"}, {"client_id"=>512, "skipped_at"=>"2019-02-24"}], "client_skipped_day"=>{}}
Only one record will be deleted, but how to add the ability to delete all records? which coincide with the parameters that come from the controller?
And it must be a flexible system to remove if 1 hash in the array and immediately a collection of hashes in the array. Tell me how to do it.
Instead of looping over the params and finding each record one by one you could also consider using multiple #where queries combining them together with the use of #or and loop over the resulting records.
def client_skipped_days_params
params.permit(client_skipped_days: [:client_id, :skipped_at])
# removed `.values` ^
end
def check_client_on_exist
destroyed_records, undestroyed_records =
client_skipped_days_params
.fetch(:client_skipped_days, []) # get the array or use an empty array as default
.map(&ClientSkippedDay.method(:where)) # build individual queries
.reduce(ClientSkippedDay.none, :or) # stitch the queries together using #or
.partition(&:destroy) # call #destroy on each item in the collection, separating destroyed once from undestroyed once
end
In the above example the resulting destroyed records are present in the destroyed_records variable and the records that could not be destroyed are present in the undestroyed_records variable. If you don't care about the result you can leave this out. If you want to raise an exception if a record cannot be destroyed use #destroy! instead (call upon each collection item).
Alternatively you can destroy all records by calling #destroy_all (called upon the collection), but it will simply return an array of records without differentiating the destroyed records from the undestroyed records. This method will still instantiate the records and destroy them one by one with the advantage that callbacks are still triggered.
The faster option is calling #delete_all (called upon the collection). This will destroy all records with one single query. However records are not instantiated when destroyed, meaning that callbacks will not be triggered.
def check_client_on_exist
destroyed_record_count =
# ...
.reduce(ClientSkippedDay.none, :or)
.delete_all # delete all records with a single query (without instantiation)
end
references:
ActionController::Parameters#fetch
Array#map
ActiveRecord::QueryMethods#none
Enumerable#reduce
Enumerable#partition
You need to loop over your array instead of just taking the first value out of it. I don’t understand the params that you have, so I’m assuming that you want to do your find_by using the Hash of client_id and skipped_at.
Also, Ruby 2.3.0 introduced the safe navigation operator, which is what that &. is if you aren’t used to it. http://mitrev.net/ruby/2015/11/13/the-operator-in-ruby/
Since find_by either returns an ActiveRecord object or nil, it’s a great time to use the safe navigation operator to shorten things up.
def client_skipped_days_params
params.permit(client_skipped_days: %i[client_id skipped_at])[:client_skipped_days]
end
def check_client_on_exist
client_skipped_days_params.each do |day|
ClientSkippedDay.find_by(day)&.destroy
end
end
Note, I’m not sure what your client_skipped_day Hash is. I assumed you’re making it possible to delete a single day, or delete in bulk. I would warn against having it do two things. Just make the client always send an array for this action and things will be easier for you. If you can do that, then you can make client_skipped_days required.
def client_skipped_days_params
params.require(:client_skipped_days).permit(%i[client_id skipped_at])
end
This will raise a 422 error to the client if they don’t provide the client_skipped_days key.
If this isn’t possible, then you’ll need to add an if to check_on_exist to make sure that client_skipped_days_params is not null (because they’re using client_skipped_day).

How can I tell when an ActiveRecord::Base.transaction block commits?

A common problem with background jobs and ActiveRecord is when jobs get enqueued and executed before a needed model is committed to the database.
ActiveRecord models have a nice after_commit callback that can be used for a particular model.
But let's say you've got some business logic that touches a few different models, and it's not really appropriate to cram that logic inside a single model. So, you write some sort of service/command object that performs the logic inside a transaction block:
For example, something along the lines of:
class SomeServiceObject
def execute
thing = create_thing_in_a_tx
# this notification often fires before the above transaction commits.
notify_user(thing)
end
private
def create_thing_in_a_tx
ActiveRecord::Base.transaction do
a = ModelA.new(foo: 'bar')
b = ModelB.new(a_record: a, biz: 'baz')
#... various other logic that doesn't really belong in a model ...
ThingModel.create!(b_record: b)
end
end
def notify_user(thing)
EnqueueJob.process_asyc(thing.id)
end
end
In this case, as far as I can tell, you don't really have access to the handy after_commit callback.
I suppose in the above example, you could have ThingModel enqueue the job inside of its after_commit callback, but then you're spreading what should be the responsibilities of SomeServiceObject across different classes, and that feels wrong.
Given all of the above, is there any reasonable way to know when a ActiveRecord::Base.transaction block commits, without resorting to any particular model's after_commit callback?
Thank you! :-D
(See also: How to force Rails ActiveRecord to commit a transaction flush)
It's simpler than you might think. After the ActiveRecord::Base.transaction block completes, the transaction has been committed.
def create_thing_in_a_tx
begin
ActiveRecord::Base.transaction do
a = ModelA.new(foo: 'bar')
b = ModelB.new(a_record: a, biz: 'baz')
#... various other logic that doesn't really belong in a model ...
ThingModel.create!(b_record: b)
end
# The transaction COMMIT has happened. Do your after commit logic here.
rescue # any exception
# The transaction was aborted with a ROLLBACK.
# Your after commit logic above won't be executed.
end
end

How to implement controller in order to handle the creation of one or more than one record?

I am using Ruby on Rails 4.1. I have a "nested" model and in its controller I would like to make the RESTful create action to handle cases when one or more than one records are submitted. That is, my controller create action is:
def create
#nester = Nester.find(:nester_id)
#nesters_nested_objects = #nester.nested_objects.build(create_params)
if #nnesters_ested_objects.save
# ...
else
# ...
end
end
def create_params
params.require(:nesters_nested_object).permit(:attr_one, :attr_two, :attr_three)
end
I would like it to handle both cases when params contain data related to one object and when it contains data related to more than one object.
How can I make that? Should I implement a new controller action (maybe called create_multiple) or what? There is a common practice in order to handling these cases?
Well, if you insist on creating those records aside from their nest, I can propose to go with something like this (it better be a separate method really):
def create_multiple
#nest = Nester.find(params[:nester])
params[:nested_objects].each do |item|
#nest.nested.new(item.permit(:attr_one, :attr_two, :attr_three))
end
if #nest.save
....
else
....
end
end

Multi-page record creation

puts 'newbie question'
I have an account sign-up that spans multiple pages, but I'm not exactly wrapping my head around creating a new instance that is tied to a database but only adds to the database if all pages are completed.
I have three actions:
def index
#ticket = Ticket.new
end
def signup_a
end
def signup_b
end
The index page only collects a single text field, then passes that to populate the field in signup_a, then to b, which is where the record is finally added to the database. How do I go from passing the variable from index to A to B in a Ticket object without actually adding it to the DB?
Edit---
I think I got tripped up that the line
if #order.save
actually saves the object...I thought it just performed a check.
You can keep it in the session and save it in the database once all the steps are complete .

Resources