I need to check if a similar record exist in database before save, if true then update the existing record without saving, else create a new one
In rails 5:
returning false in a hook method doesn't halt callbacks and "throw :abort" is used instead.
the problem is using "throw :abort" rolls back any changes made in the before_save callback.
what I am trying to do is to check for a similar recored in "before_save" and if a similar record exist I need to update the current record and stop saving the new one.
I used
before_save :check
def check
if (similar record exist..)
update current...
return false <==========
end
true
end
but this is not working any more in Rails 5 so returning false doesn't stop it from saving the new record too.
and I tried
before_save :check
def check
if (exist..)
update current...
throw :abort <========
end
true
end
this stops saving current record to db but it perform "rollback" so the updated recored is missed !!
how can I do that ?
I think this is one possible way. This example if with a Product model looking for same name.
before_create :rollback_if_similar_exists
after_rollback :update_existing_record
def rollback_if_similar_exists
throw :abort if Product.exists? name: self.name
end
def update_existing_record
# do here what you need
puts name
puts "find the existing record"
puts "update data"
end
Here is a slightly different approach you could take:
Instead of using before_save, create your own validation and use assign_attributes instead of update or create since assign_attributes won't actually write to the database. You could then call the valid? function on your record to execute your validations. If you get a duplicate record error from the validation you defined, then have your code handle updating the existing record in the logic of your error handling.
Something like this in your controller:
#record.assign_attributes(my_parameter: params[:my_parameter])
if #record.valid?
#record.save
else
puts #record.errors.messages.inspect
#update your existing record instead.
end
Then in your model:
validate :my_validation
def my_validation
if record_already_exists
return errors.add :base, "Your custom error message here."
end
end
I'd recommend using #find_or_initialize_by or #find_or_create_by to instantiate your instances. Instead of placing record swapping code inside a callback. This means you'll do something like this (example controller create):
class Post < ApplicationController
def create
#post = Post.find_or_initialize_by(title: param[:title])
if #post.update(post_params)
redirect_to #post
else
render :new
end
end
end
Pair this with a validation that doesn't allow you to create double records with similar attributes and you're set.
create_table :posts do |t|
t.string :title, null: false
t.text :body
end
add_index :posts, :title, unique: true
class Post < ApplicationRecord
validates :title, presence: true, uniqueness: true
end
I don't recommend the following code, but you could set the id of your instance to match the record with similar data. However you'll have to bypass persistence (keeps track of new and persistent records) and dirty (keeps track of attribute changes). Otherwise you'll create a new record or update the current id instead of the similar record id:
class Post < ApplicationRecord
before_save :set_similar_id
private
def set_similar_id
similar_record = Post.find_by(title: title)
return unless similar_record
#attributes['id'].instance_variable_set :#value, similar_record.id
#new_record = false
end
end
Keep in mind that only changes are submitted to the database when creating a new record. For new records these are only the attributes of which the attributes are set, attributes with value nil are not submitted and will keep their old value.
For existing records theses are the attributes that are not the same as there older variant and the rule old_value != new_value (not actual variable names) applies.
Related
Is there was a way to assign attr_readonly after update?
attr_readonly, on: :update
If not, perhaps a method
#post.update(content: params[:content])
#post.readonly
You could override readonly? in that model like this:
def readonly?
super || created_at != updated_at
end
Rails checks if a record is readonly before it tries to saves an updated record to the database and would raise an ActiveRecord::ReadOnlyRecord exception if the record is marked as readonly. This overridden readonly? method protects a record from being changed twice, by returning always true if the record was changed at least once (indicated by different timestamp on updated_at and created_at).
Furthermore this allows you to check in your view item.readonly? to hide links to the edit page.
you can create a before_update
before_update :forbid_second_update
def forbid_second_update
if created_at != updated_at_was
errors.add :base, "Cannot updated!"
false
end
end
first time update will be successful as created_at and updated_at will be same
second time it will fail
or alternatively if you want to lock some attributes and don't want to fail the update, you can just add for eg.
self.email = self.email_was
this will override the email attribute to its old value
You can add a count into your Model
rails g scaffold sport name
rails g migration add_modified_count_to_sports modified_count:integer
I'm assigning a default value
class AddModifiedCountToSports < ActiveRecord::Migration
def change
add_column :sports, :modified_count, :integer, default: 0
end
end
rake db:migrate
On my Sport model I create a before_update sort of validation
class Sport < ActiveRecord::Base
before_update :validate_update_status
def validate_update_status
unless self.modified_count.eql?(1)
#if the field 'modified_count = 0'
self.modified_count = 1
else
errors.add(:name,'You can only modified your account once')
false
end#end unless
end#def
end#class
You could also implement the same with a State Machine like gem (i.e. assm)
voilĂ !
I want to make an attribute read only for each record after the first update, is there a way to do this in Rails 4 using attr_readonly? Or another method?
It has to do with Transaction security...
attr_readonly is a class method, so it would "freeze" the attribute on all instances of the model, whether they've been updated or not.
The sanest way to do what you want, I think, would be to add a some_attribute_is_frozen boolean attribute to your model, and then set it to true in a before_update callback. Then you can have a validation that will only run if some_attribute_is_frozen? is true, and which will fail if the "frozen" attribute has changed.
Something like this (for the sake of an example I've arbitrarily chosen "Customer" as the name of the model and address as the name of the attribute you want to "freeze"):
# $ rails generate migration AddAddressIsFrozenToCustomers
# => db/migrate/2014XXXX_add_address_is_frozen_to_customers.rb
class AddAddressIsFrozenToCustomers < ActiveRecord::Migration
def change
add_column :customers, :address_is_frozen, :boolean,
null: false, default: false
end
end
# app/models/customer.rb
class Customer < ActiveRecord::Base
before_update :mark_address_as_frozen
validate :frozen_address_cannot_be_changed, if: :address_is_frozen?
# ...snip...
private
def mark_address_as_frozen
self.address_is_frozen = true
end
def frozen_address_cannot_be_changed
return unless address_changed?
errors.add :address, "is frozen and cannot be changed"
end
end
Since the before_update callback runs after validation, the very first time the record is updated, address_is_frozen? will return false and the validation will be skipped. On the next update, though, address_is_frozen? will return true and so the validation will run, and if address has changed the validation will fail with a useful error message.
I hope that's helpful!
I have a subscribe method in my controller
def subscribe
#subscription = current_user.subscriptions.create(params[:subscription])
#subscription.title = #stream.title
#subscription.save
redirect_to stream_path(#stream)
end
Before I set the subscription title to the stream title, how can I check if a subscription already exists with the same title?
Like so :
current_user.subscriptions.where(title: #stream.title).present?
But assuming you only want to save it if it is not included elsewhere you can do this :
if current_user.subscriptions.where(title: #stream.title).blank?
#subscription.title = #stream.title
#subscription.save
end
An elegant way to perform this is to add the logic to your controller so that it validates uniqueness ( and also prevents race conditions ) :
class Subscription < ActiveRecord::Base
validates_uniqueness_of :title, scope: :user
end
And then you can check validation in model.save ( again, assuming you don't want it to save if it shares the same title )
To help you out, the suggestion by #Sergio Tulentsev basically means you create a unique index on a column in your db, like this:
#db/migrate/[timestamp]_new_migration.rb
add_index :table, :column, unique: true
This creates an index on your data table, which basically means you cannot make any duplicate data in it. The bottom line - it means you can only insert the data once.
--
The save! method is there to update your record if it exists already. First time I've seen this method, so it's been a good lesson for me. If you're having problems with it, it will probably be that your database will not have the index you need
I have a model, Person, that requires a schedule if it's type is "scheduled". In it's controller (which inherits from InheritedResources::Base):
def create
super do
#person.schedule = Schedule.create params[:schedule] if #person.scheduled?
end
end
The thing is, I want to validate that all People of type "scheduled" have a schedule. Something like this:
validates :schedule, :presence => true, :if => :scheduled?
in the Person model. But because a schedule belongs_to a Person, it needs the person to be created prior to being created itself (so the person will have an ID). So with this in my controller, the person validation fails, since the schedule needs to be created later.
Is there something in Rails that I don't know about, which will enable me to perform these validations? If I used accepts_nested_attributes_for, will that allow these validations to pass?
Maybe just don't create it beforehand?
#person.schedule = Schedule.new params[:schedule] if #person.scheduled?
So #person and assigned Schedule shall be saved at the same time (transaction).
I think this is the only correct way.
UPDATED (due to super :create conception):
super action
def create(&block)
...
yield #person if block_given?
#person.save # line where #person get saved
end
inherited action
def create
super do |person|
person.schedule = Schedule.new params[:schedule] if person.scheduled?
end
end
I wanted some advice about how to handle to_param in regards to permalinks
Basically this is what happens.
Create a new company
The company :name is then parameterized and saved as a :permalink in the db
Updating an existing company enables you to change the :permalink
There are validations to ensure user updated :permalink is unique
The problem I'm having is occurring when updating the company's :permalink to something that already exists. The uniqueness validation works which is great, but it changes the params[:id] to the invalid permalink instead of reseting and using the existing params[:id]
When I try to edit the permalink to something else I get a flash validation error of "Name already taken" because it thinks I'm editing the company of the already existing :permalink (company). The URL reflects the change in permalink since my companies_controller.rb is using #company = Company.find_by_permalink[:id])
I wanted to know the best way to handle this issue?
class Companies < ActiveRecord::Base
before_create :set_permalink
before_update :update_permalink
attr_accessible :name, :permalink
validates :name, :permalink, uniqueness: { message: 'already taken' }
def to_param
permalink
end
private
def set_permalink_url
self.permalink = name.parameterize
end
def update_permalink_url
self.permalink = permalink.parameterize
end
end
Apologies if I'm not making too much sense.
Thanks in advance.
you could try to handle this with an after_rollback callback.
after_rollback :restore_permalink
def restore_permalink
self.permalink = permalink_was if permalink_changed?
end
here's how it works : every update / destroy in Rails is wrapped in a transaction. If the save fails, the transaction rollbacks and triggers the callback.
The callback then restores the old value (permalink_was) if it was changed since the record has been loaded.
See ActiveModel::Dirty and ActiveRecord::Transactions for more info.
EDIT
On the other hand, there may be another solution (untested) - just define your accessor like this :
def permalink=( value )
permalink_will_change! unless #permalink == value
#permalink = value
end
This way, the permalink will not be marked as dirty if the new value is identical to the old one, and so AR will not try to update the column.
Explanation:
i don't know on which version of rails it was implemented (it is relatively recent), but here's how "dirtyness" works :
your "standard" (automagically generated) attribute setters basicly call
#{your_attribute}_will_change! before setting the associated
instance variable (even if you set the exact same value than before)
when you call save, ActiveRecords looks for attributes that have changed ("dirty") and builds the SQL UPDATE query using ONLY these attributes (for performance reasons, mostly)
so if you want to avoid your permalink to appear in the query when it is unchanged, i think you have to override the standard setter - or avoid mass-assignment and only set permalink if it has changed