According to the documentation Rails has_many association has clear method. Looks like it executes sql delete query immediately after it performs. Is there a canonical way to delete all the child objects and update association only at the moment of save method? For example:
#cart.container_items.delete_all_example # looks like `clear` execute sql at this line
if #cart.save
# do smth
else
#do smth
end
it is necessary because of many changes at the parent object and they must be committed all or none of them.
You don't want to delete_all, you want to destroy_all.
Calling delete_all executes a simple SQL delete, ignoring any callbacks and dependent records.
Using destroy_all invokes the destroy method on each object, allowing :dependent => :destroy to work as expected, cleaning up child records.
This does not destroy all objects at the point of save, and there is no canonical way to do that as you're not saving the record. Rails persists destroys at the point of the method call, not at a later save. If you need many destroys to be transactional, wrap them in a transaction:
Cart.begin do
#cart.container_items.delete_all_example
end
Try this:
Cart.transaction do
#cart.container_items.delete_all_example # looks like `clear` execute sql at this line
if #cart.save
# success
else
# error
raise ActiveRecord::Rollback
end
end
ActiveRecord::Rollback is not propagated outside the transaction block. It simply terminates the transaction.
Looks like i'm trying to do a transaction. Some articles to learn more about it:
Transations in Rails
Active record transactions
Related
I have a model named runningmenu i want to inactive it on destroy but not hard delete, and also i want to inactive all its dependent records e.g orders
i am using ruby 2.4.0 and rails 5 i have tried using active record concerns, on before destroy i am calling concern stop destroy method to flip delete_status and throw abort. when i abort on dependent records it sends failed to destroy exception to runningmenu destroy action. But it just aborted on first record and not make all dependent records inactive.
module Deletable
extend ActiveSupport::Concern
def stop_destroy
self.deleted!
throw(:abort)
end
def stop_destroy_for_orders
self.update_column(:status, Order.statuses[:cancelled])
throw(:abort)
end
end
ON Orders model i have:
before_destroy -> { stop_destroy_for_orders }
on runningmenu model i have:
has_many :orders, dependent: :destroy
before_destroy -> { stop_destroy }
expected result is to make all runningmenu dependent childs inactive on delete but not hard delete. But now only first instance status set to cancelled and roll backed.
I don't know if you're happy using gems or not, but one of the more popular ones is paranoia:
https://github.com/rubysherpas/paranoia
bin/rails generate migration AddDeletedAtToOrders deleted_at:datetime:index
Add the deleted_at column to your orders, then run rake db:migrate to add the column to your database. In your orders model:
class Order < ActiveRecord::Base
acts_as_paranoid
# ...
end
Now when you call Order.destroy, the record will not be deleted, but will have its deleted_at column updated with the time of 'deletion'. This will add a default scope to Orders, meaning if you do Order.all, it will only return orders with a nil deleted_at. More commands for how to really delete a record and include it in scopes is included in their github above.
I have trouble with trailblazer when setting up a simple show all Things view.
operation
class Thing < ApplicationRecord
class ShowAll < Trailblazer::Operation
include Model
model Thing, :all #why :all is not working here?
def process
end
end
end
controller
class PageController < ApplicationController
def index
run Word::ShowAll
end
end
why is :all not working for getting all Things from the db but :find works to get one via its id?
The best place to ask TRB questions is actually on Github channel.
I'm not sure where you found that example, as it is not supposed to work AFAIK, :find is a shortcut I believe, I've never actually used it.
All your logic should be defined inside the process method. http://trailblazer.to/gems/operation/1.1/api.html#process
Having said that, trying to get all records without some kind of pagination is a really bad idea, unless you are 100% sure that your table won't grow beyond several dozen records. Unless you know you don't have a large load. So to define that kind of shortcut is dangerous.
Calling Trailblazer::Model#model as you're doing there is just a shortcut for overriding the TrailBlazer::Operaration#model! method. So what you seem to want to do is:
class Thing < ApplicationRecord
class ShowAll < Trailblazer::Operation
def model!(params)
Thing.all # add any filtering or pagination here
end
end
end
And in your controller call present instead of run so that it sets up the model but doesn't call the process method of the operation.
class PageController < ApplicationController
def index
present Word::ShowAll
end
end
I'm attempting to build a table which will act as a queue of batched syncs to a third party service.
The following method should speak for itself; but to be clear, its intention is to add a new updatable object (a polymorphic relationship) with status: :queued to the delayed_syncs table.
There is a uniqueness constraint on the polymorphic relationship + status (updatable_id, updatable_type, status) which should cause updatable objects already in the queue with the :queued status to fail here and fall into the rescue block.
The issue I am seeing is that whenever the SELECT generated by find_by is fired, this entire method fails with a:
ActiveRecord::StatementInvalid
error.
Information I've found around this suggests a ROLLBACK or RELEASE SAVEPOINT after the failed INSERT, but I'm not sure how I would accomplish that here.
The aforementioned method:
def self.enqueue(updatable:, action:)
DelayedSync.create(updatable: updatable, status: :queued, action: action)
rescue ActiveRecord::RecordNotUnique
queued_update = DelayedSync.find_by(updatable: updatable, status: :queued, action: :sync_update)
if action == :sync_delete && queued_update.present?
queued_update.sync_delete!
else
Rails.logger.debug "#{updatable.class.name} #{updatable.id} already queued for sync, skipping."
end
end
Rather than rely on exception handling for logic, you can use ActiveRecord transactions to ensure all-or-nothing updates.
Like this:
ActiveRecord::Base.transaction do
DelayedSync.create!(updatable: updatable, status: :queued, action: action)
end
You can still safely utilize rescue to handle logging cleanup.
Docs that have much more detail about this can be found here.
After further digging, I uncovered the problem was due to how I was invoking this method from an after_save callback. Rails invokes after_save and after_destroy callbacks before the transaction has been closed. Rescuing the ActiveRecord::RecordNotUnique error invoked from this callback and attempting to execute more queries is impossible with Postgres, since it invalidates the entire transaction. My solution was to transition to the after_commit callback which provides the same control as after_save and after_destroy with the on: [:create, :destroy] parameter, with the benefit of being executed after the transaction (invalid or not) has closed.
This blog post is a bit dated, but the information near the bottom was immensely helpful and still holds true: http://markdaggett.com/blog/2011/12/01/transactions-in-rails/
This is something I'm trying to do now: A has_many Bs. B has certain callbacks that need to be triggered. Just, when I save from A, I want something to be updated in Bs. Since the Bs can be 10000, I would like not to load them into memory, and still have the benefit of seeing the callbacks triggered. What is the best strategy for this?
And please, no "find_each" or any find by batches variant, I'm aware of them and they will be my last resort in case nothing else works.
When I encountered this problem, I use this solution
define "callback methods" in a class and use they with ids,
define really callbacks in a instance and pass id of record in "class callback"
example of code:
class Post < AR
has_many :comments
after_save do |post|
Post.recalculate_counters(post.comment_ids)
end
end
class Comment < AR
belongs_to :post
after_save :recalculate_couters
def self.recalculate_couters(ids)
... huge and slow update statement ...
end
def recalcuate_couters
self.class.recalculate_couters([id])
end
end
I don't think there's any way you can have the callbacks executed without loading the models. However, if you give up using callback you can use update_all that performs really fast.
You just use:
B.update_all({:timestamp => Time.now}, { :a_id => id })
I have a Post, to which a migration adds a new attribute and table column short_url. This attribute is either provided by the user, or, if left blank, automatically created:
class Post < ActiveRecord::Base
before_create :create_short_url
private
def create_short_url
if short_url.blank? || already_exists?(short_url)
write_attribute :short_url, random_string(6)
end
end
def random_string(length)
#innards are irrelevant for this question
end
end
In the migration, I want to run through all posts and have the short_url created and saved.
problem: Post.find(:all).each {|post| post.create_short_url} in the self.up is not possible, due to the private scope of the create_short_url method.
problem: Looping through posts and update!-ing them does not invoke the before_create :create_short_url, because it is not before create. Once migrated, I prefer to not have any before_update hooks in place: I don't need to change anything on update.
How would you tackle this? Copy over the random_string() and associated methods to the migration? Add specific migration helper methods to the Post?
Just use the Object method send (it doesn't check protected/private).
Post.all.each do |post|
post.send :create_short_url
post.save!
end
An alternative would be (but that could interfere with other migrations running in the same Ruby-process after that):
Post.before_save :create_short_url
Post.all.each(&:save!)
Visibility tip: Most of the time what you really mean is protected (see here). I recommend to use protected instead of private in this case.