after_commit callback is being called several times - ruby-on-rails

update:
Is it the case that a call to update_attributes gets it's own transaction?
I've looked at this question and for reasons in addition to that question, i've decided to go with after_commit as the proper hook. The problem is it's being called multiple (exactly three) times. The code is a little complex to explain, but basically there is a profile model that has
include Traits::Blobs::Holder
in holder.rb I have:
module ClassMethods
def belongs_to_blob(name, options = {})
clazz = options[:class_name] ? options[:class_name].constantize : Blob
foreign_key = options[:foreign_key] || :"#{name}_id"
define_method "save_#{name}" do
blob = self.send(name)
if self.errors.any? && blob && blob.valid?
after_transaction do
blob.save!
#self[foreign_key] = blob.id
#save resume anyway
self.update_attribute(foreign_key, blob.id)
end
end
end
after_validation "save_#{name}"
belongs_to name, options
accepts_nested_attributes_for name
end
end
finally in profile.rb itself I have:
after_commit :send_messages_after_registration!
protected
def send_messages_after_registration!
Rails.logger.debug("ENTERED : send_messages_after_registration " + self.owner.email.to_s)
if self.completed?
Rails.logger.debug("completed? is true " + self.owner.email.to_s)
JobSeekerNotifier.webinar_notification(self.owner.id).deliver
Resque.enqueue_in(48.hours, TrackReminderWorker, self.owner.id)
end
end
it appears that the method is entered 3 times. I've been trying to figure this out for a few days so any guidance you can provide will be appreciated.
controller code:
def create
#user = Customer.new(params[:customer].merge(
:source => cookies[:source]
))
#user.require_password = true
respond_to do |f|
if #user.save
promote_provisional_user(#user) if cookies[:provisional_user_id]
#user.profile.update_attributes(:firsttime => true, :last_job_title => params[:job_title]) unless params[:job_title].blank?
if params[:resume]
#user.profile.firsttime = true
#user.profile.build_resume(:file => params[:resume])
#user.profile.resume.save
#user.profile.save
end
...
end

So it's happening 3 times because the profile is being saved 3 times: once when the user is saved (I assume that User accepts_nested_attributes_for :profile, once when you call update_attributes(:first_time => true,...) and once when you call save in the if params[:resume] block. Every save creates a new transaction (unless one is already in progress) you end up with multiple calls to after_commit
after_commit does take an :on option (which can take the values :create, :update, :destroy) so that you can limit it to new records. This would obviously fire on the first save so you wouldn't be able to see the profile's resumé and so on.
You could in addition wrap the entirety of those updates in a single transaction, in that case after_commit only gets called once, no matter how many saves take place inside the transaction by doing something like
User.transaction do
if #user.save
...
end
end
The transaction will get rolled back if an exception is raised (you can raise ActiveRecord::Rollback if you want to bail out)

Related

Custom Validator on: :create not running on rails app

I have a rails application for creating volumes and have written two custom validators using ActiveModel::Validator.
volume.rb:
class Volume < ActiveRecord::Base
include UrlSafeCode
include PgSearch::Model
include ActiveModel::Validations
validates :user_id, presence: true
validates_with Validators::VolumeValidator
validates_with Validators::CreateVolumeValidator, on: :create
def self.digest text
Digest::SHA256.hexdigest(text)
end
def text=(new_text)
new_text.rstrip!
new_text.downcase!
self.text_digest = Volume.digest(new_text)
super(new_text)
end
My Problem: The CreateVolumeValidator checks if a record with the same text_digest is already in the database. I only want to run this when creating a new volume so that I can still update existing volumes. However, adding on: :create to the CustomVolumeValidator causes the validator to stop working.
I've read through a lot of the other entries about similar issues and haven't found a solution. I am pretty sure I am missing something about when different attributes are getting created, validated, and saved, but I haven't worked with custom validations much, and I'm lost.
Here is the other relevant code.
volumes_controller.rb
def new
#volume = Volume.new
end
def create
our_params = params
.permit(:text, :description)
if params[:text].nil?
render :retry
return
end
text = params[:text].read.to_s
text_digest = Volume.digest(text)
#description = our_params[:description]
begin
#volume = Volume.where(text_digest: text_digest)
.first_or_create(text: text, user: current_user, description: our_params[:description])
rescue ActiveRecord::RecordNotUnique
retry
end
if #volume.invalid?
render :retry
return
end
render :create
end
def edit
get_volume
end
def update
get_volume
unless #volume
render nothing: true, status: :not_found
return
end
#volume.update(params.require(:volume).permit(:text, :description))
if #volume.save
redirect_to volume_path(#volume.code)
else
flash[:notice] = #volume.errors.full_messages.join('\n')
render :edit
end
end
def get_volume
#volume = Volume.where(code: params.require(:code)).first
end
create_volume_validator.rb
class Validators::CreateVolumeValidator < ActiveModel::Validator
def validate(volume)
existing_volume = Volume.where(text_digest: volume.text_digest).first
if existing_volume
existing_volume_link = "<a href='#{Rails.application.routes.url_helpers.volume_path(existing_volume.code)}'>here</a>."
volume.errors.add :base, ("This volume is already part of the referral archive and is available " + existing_volume_link).html_safe
end
end
end
If your goal is for all Volume records to have unique text_digest, you are better off with a simple :uniqueness validator (and associated DB unique index).
However, the reason your existing code isn't working is:
Volume.where(text_digest: text_digest).first_or_create(...)
This returns either the first Volume with the matching text_digest or creates a new one. But that means if there is a conflict, no object is created, and therefore your (on: :create) validation doesn't run. Instead, it simply sets #volume to the existing object, which is, by definition, valid. If there is no matching record, it does call your validator, but there's nothing to validate because you've already proved there is no text_digest conflict.
You could resolve by replacing the first_or_create with create, but again, you are vastly better off with a unique index & validator (with custom message if you like).

Infinite Loop, what is causing this?

In my Order model I have:
validates_uniqueness_of :store_order_id, on: :create
before_validation :generate_id, on: :create if :manual_order?
def manual_order?
order_type == "manual"
end
def generate_id
begin
self.store_order_id = "M-#{(SecureRandom.random_number(9e5) + 1e5).to_i}"
end while self.class.find_by(store_order_id: store_order_id)
end
controller create method:
def create
#order = Order.new
#order.order_type = "manual"
respond_to do |format|
if #order.save!
For some reason, this is causing a loop of the :generate_id
What is causing this?
I use similar code to generate a token for each order, the only difference is I don't use a validates_uniquness_of with it like i do for :generate_id. But I don't see how this is causing this loop?
I do need the validates_uniqueness_of :store_order_id, on: :create because it's also used for webhook id's in case the webhook fires twice, which is does on occasion . I do this to avoid multiple identical records. Because of this uniqueness validaton, when I create manual orders it won't save because the store_order_id needs to be unique. So i created the generate_id method, but with issues.
Assuming it was possible the uniquness was causing this, because there is no difference from code I have used many times, i tried:
validates_uniqueness_of :store_order_id, on: :create unless :manual_order?
but this didn't solve it.

rails - left shift "<<" operator saves record automatically

Need help understanding this code, as what to my knowledge I know "<<" append to a collection but here it saves the record correctly, how come it does without calling .save method?
#user.rb
has_many :saved_properties, through: :property_saves, source: :property
#users_controller.rb
def update
if #user.saved_properties << Property.find(params[:saved_property_id])
render plain: "Property saved"
end
In the has_many documentation it says:
Adds one or more objects to the collection by setting their foreign
keys to the collection's primary key. Note that this operation
instantly fires update SQL without waiting for the save or update
call on the parent object, unless the parent object is a new record.
Maybe looking at the source code will help you. This is my trail of searches based on the << method in activerecord:
def <<(*records)
proxy_association.concat(records) && self
end
rails/collection_proxy.rb at 5053d5251fb8c03e666f1f8b765464ec33e3066e · rails/rails · GitHub
def concat(*records)
records = records.flatten
if owner.new_record?
load_target
concat_records(records)
else
transaction { concat_records(records) }
end
end
rails/collection_association.rb at 5053d5251fb8c03e666f1f8b765464ec33e3066e · rails/rails · GitHub
def concat_records(records, should_raise = false)
result = true
records.each do |record|
raise_on_type_mismatch!(record)
add_to_target(record) do |rec|
result &&= insert_record(rec, true, should_raise) unless owner.new_record?
end
end
result && records
end
rails/collection_association.rb at 5053d5251fb8c03e666f1f8b765464ec33e3066e · rails/rails · GitHub
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
set_inverse_instance(record)
if raise
record.save!(validate: validate)
else
record.save(validate: validate)
end
end
https://github.com/rails/rails/blob/5053d5251fb8c03e666f1f8b765464ec33e3066e/activerecord/lib/active_record/associations/has_many_association.rb#L32
def insert_record(record, validate = true, raise = false)
ensure_not_nested
if record.new_record? || record.has_changes_to_save?
if raise
record.save!(validate: validate)
else
return unless record.save(validate: validate)
end
end
save_through_record(record)
record
end
https://github.com/rails/rails/blob/5053d5251fb8c03e666f1f8b765464ec33e3066e/activerecord/lib/active_record/associations/has_many_through_association.rb#L38
As you can see, in the end, it does call the save method.
Disclaimer: I'm not that familiar with Rails souce code, but you have interesting question.
In a has_many relationship the link information is saved in the target record. This means that << would have to modify that record in order to add it to the set.
Perhaps intending convenience, ActiveRecord automatically saves these for you when making an assignment if the assignment was successful. The exception is for new records, the record they're being associated with doesn't have any identifier so that has to be delayed. They are saved when the record they're associated with is finally created.
This can be a little confusing, perhaps unexpected, but it's actually the thing you'd want to happen 99% of the time. If you don't want that to happen you should manipulate the linkage manually:
property = Property.find(params[:saved_property_id])
property.user = #user
property.save!
That's basically equivalent but a lot more verbose.

Use before_destroy to delete another model's entry?

So I have a model, let's call it Notes. On the notes, you can but several posts. So the notes model has the line:
has_many :posts
And the posts model has the line
belongs_to :note
Now, when a User destroys a post, I want for the note to be destroyed IF it not longer has any other posts.
I thought i would write this code into the post model with before_destroy:
def delete_note_if_last_post
if self.note.posts.count == 1
self.note.destroy
end
end
This doesn't work. It shuts down the server based on an "Illegal Instruction". Is there some way to accomplish what I am trying to do?
EDIT: changed the code, as I noticed an error, and now the problem is slightly different.
you can return false to prevent a model from destruction in before_destroy filter
before_destroy :has_no_post
then in has_no_post
def has_no_post
#You can prevent this from deletion by using these options
#Option1 return false on certain condition
return false if posts.any?
#or add an error to errors
errors << "Can not delete note if it has post" if posts.any?
#raise an exception
raise "Cant delete ..." if blah blah
end
I would suggest putting this kind of logic into an observer. Something like
class PostObserver < ActiveRecord::Observer
def after_destroy(post)
note = Note.find(post.note_id)
note.destroy if note.posts.count == 0
end
end
You'd have to register the observer in your config/application.rb file as well. One thing to note is that if your callback returns any value that can be evaluated as false (e.g. nil or false) the rest of your callbacks will not run.

ActiveRecord custom validation problem

I'm having a problem with validation in my RoR Model:
def save
self.accessed = Time.now.to_s
self.modified = accessed
validate_username
super
end
def validate_username
if User.find(:first, :select => :id, :conditions => ["userid = '#{self.userid}'"])
self.errors.add(:userid, "already exists")
end
end
As you can see, I've replaced the Model's save method with my own, calling validate_username before I call the parent .save method. My Problem is, that, even though the error is being added, Rails still tries to insert the new row into the database, even if the user name is a duplicate. What am I doing wrong here?
PS: I'm not using validate_uniqueness_of because of the following issue with case sensitivity: https://rails.lighthouseapp.com/projects/8994/tickets/2503-validates_uniqueness_of-is-horribly-inefficient-in-mysql
Update: I tried weppos solution, and it works, but not quite as I'd like it to. Now, the field gets marked as incorrect, but only if all other fields are correct. What I mean is, if I enter a wrong E-Mail address for example, the email field is marked es faulty, the userid field is not. When I submit a correct email address then, the userid fields gets marked as incorrect. Hope you guys understand what I mean :D
Update2: The data should be validated in a way, that it should not be possible to insert duplicate user ids into the database, case insensitive. The user ids have the format "user-domain", eg. "test-something.net". Unfortunately, validates_uniqueness_of :userid does not work, it tries to insert "test-something.net" into the database even though there already is an "Test-something.net". validate_username was supposed to be my (quick) workaround for this problem, but it didn't work. weppos solution did work, but not quite as I want it to (as explained in my first update).
Haven't figured this out yet... anyone?
Best regards,
x3ro
Why don't you use a callback and leave the save method untouched?
Also, avoid direct SQL value interpolation.
class ... < ActiveRecord::Base
before_save :set_defaults
before_create :validate_username
protected
def set_defaults
self.accessed = Time.now.to_s
self.modified = accessed
end
def validate_username
errors.add(:userid, "already exists") if User.exists?(:userid => self.userid)
errors.empty?
end
end
How about calling super only if validate_username returns true or something similar?
def save
self.accessed = Time.now.to_s
self.modified = accessed
super if validate_username
end
def validate_username
if User.find(:first, :select => :id, :conditions => ["userid = '#{self.userid}'"])
self.errors.add(:userid, "already exists")
return false
end
end
... I think that you could also remove totally the super call. Not sure, but you could test it out.

Resources