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
Related
Im using Rails 5. I have a Product model defined as follows
class Product < ActiveRecord::Base
validates :image_url, presence: true
def image_url
super || 'http://someplaceholderimage.com'
end
end
Every product has an image_url attribute. Now, when i run the below query:
Product.all.where.not(image_url: nil)
im expecting it to return all products even if the image_url was nil in the database for any of them. But the default value i defined in the model is not being used and im getting only those products whose image_url column has a non nil value in the database. How can i fix this ?
Thanks
That's an instance method. It'll work on any instance of the Product class.
Every single query made to your database through the models is a class method. So they're not going to work as you expected.
What you can do is to add a default value to that column in your database. So, whenever a record is inserted, with nil (NULL) image_url, it'll take the value you defined as default.
It also exists the possibility to add a default value using the SQL CASE expression, which adds more flexibility regarding the need you have:
Product.select("CASE WHEN image_url IS NULL
THEN 'http://someplaceholderimage.com'
ELSE image_url END, *")
(this is PostgreSQL syntax)
Combining both ways you won't have to add more code to your model.
For changing the default value of the column, you can use change_column_default:
class ChangeImageUrlDefaultProducts < ActiveRecord::Migration[6.0]
def change
change_column_default :products, :image_url, from: nil, to: 'http://someplaceholderimage.com'
end
end
This is reversible by using the from and to option arguments.
In Rails .where() is structuring some SQL to be called on the database. So the code is doing what you asked: it's going to the database and asking for all records where image_url is not nil.
What you are defining on the model is a method that:
looks for an image_url in the database
returns it if there is one
returns 'http://someplaceholderimage.com' if there isn't one
To achieve your stated goal:
im expecting it to return all products even if the image_url was nil in the database
You simply need to call Product.all
I'm not sure why you would add .where.not() if you didn't want that to be a condition.
To really drive the point home: Expecting the database to return records where :image_url is either nil or not nil:
Product.where(image_url: nil).or(Product.where.not(image_url: nil)) is the same as calling Product.all
If you want to make sure every record has a URL defined in the database, you could do a before_validation callback in the Model:
class Product < ActiveRecord::Base
validates :image_url, presence: true
before_save :default_image_url, if: ->{ image_url.blank? }
# def image_url
# super || 'http://someplaceholderimage.com'
# end
private
def default_image_url
image_url ||= 'http://someplaceholderimage.com'
end
end
If you want to be able to tell which records are returning the default :image_url and which records have an :image_url saved in the database, you need to rename the method so it doesn't conflict with the database column name:
class Product < ActiveRecord::Base
validates :image_url, presence: true
def image
image_url || 'http://someplaceholderimage.com'
end
end
Now calling #product.image_url will return whatever is in the database and calling #product.image will either return the dB value or the placeholder value.
But, there's another issue:
Your validates :image_url, presence: true means the record will never save if the :image_url is blank.
So asking the database to look for a potentially nil value when you've already ensured that every record can't have a nil value is a bit of a head-scratcher.
Final tangent:
If you must have the method name and database column name match, I don't think super is a good option.
I think you should use this:
def image_url
read_attribute(:image_url) || 'http://someplaceholderimage.com'
end
If nothing else, it's a bit more clear than super as to what is happening.
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.
How does one check to see if some data already exists in one of the tables in the controller?
So for example lets say in the create action you want to prevent the following from happening:
#equip = #petowner.equips.new(params[:equip])
this variable contains data that already exists in the equips table.
I would like to prevent the data of a given inventory from being applied a second time.
while #intable == 1, #intable not end of file, #intable++
if #equip.inventory_id == #intable.inventory_id
# Render a failure message that returns back to new saying that this data already has been applied to a a given pet.
end
If it succeds and doesn't find the information then it should continue on forward.
In Equips controller is where I want the check to occur, but don't know how to accomplish it is Rails 3.2.13
Any ideas?
You could use the .exists?() function
if Model.exists?(id) then
#do stuff
end
You might want to also put some validations in the model itself, such as validates uniqueness:
class Account < ActiveRecord::Base
validates :email, uniqueness: true
end
I am pretty new to rails (and development) and have a requirement to create a change log. Let's say you have an employees table. On that table you have an employee reference number, a first name, and a last name. When either the first name or last name changes, I need to log it to a table somewhere for later reporting. I only need to log the change, so if employee ref 1 changes from Bill to Bob, then I need to put the reference number and first name into a table. The change table can have all the columns that mnight change, but most only be populated with the reference number and the changed field. I don't need the previous value either, just the new one. hope that makes sense.
Looked at gems such as paper trail, but they seem very complicated for what I need. I don't ever need to manipulate the model or move versions etc, I just need to track which fields have changed, when, and by whom.
I'd appreciate your recommendations.
If you insist on building your own changelog, based on your requirements you can do so using a few callbacks. First create your log table:
def up
create_table :employee_change_logs do |t|
t.references :employee
# as per your spec - copy all column definitions from your employees table
end
end
In your Employee model:
class Employee < ActiveRecord::Base
has_many :employee_change_logs
before_update :capture_changed_columns
after_update :log_changed_columns
# capture the changes before the update occurs
def capture_changed_columns
#changed_columns = changed
end
def log_changed_columns
return if #changed_columns.empty?
log_entry = employee_change_logs.build
#changed_columns.each{|c| log_entry.send(:"#{c}=", self.send(c))}
log_entry.save!
end
end
I recommend the gem vestal_versions.
To version an ActiveRecord model, simply add versioned to your class like so:
class User < ActiveRecord::Base
versioned
validates_presence_of :first_name, :last_name
def name
"#{first_name} #{last_name}"
end
end
And use like this:
#user.update_attributes(:last_name => "Jobs", :updated_by => "Tyler")
#user.version # => 2
#user.versions.last.user # => "Tyler"
The first thing we did was put an around filter in the application controller. This was how I get the current_employee into the employee model, which was the challenge, especially for a newbie like me!
around_filter :set_employee_for_log, :if => Proc.new { #current_account &&
#current_account.log_employee_changes? && #current_employee }
def set_employee_for_log
Thread.current[:current_employee] = #current_employee.id
begin
yield
ensure
Thread.current[:current_employee ] = nil
end
end
end
Next, in the employee model I defined which fields I was interested in monitoring
CHECK_FIELDS = ['first_name', 'last_name', 'middle_name']
then I added some hooks to actually capture the changes IF logging is enabled at the account level
before_update :capture_changed_columns
after_update :log_changed_columns, :if => Proc.new { self.account.log_employee_changes? }
def capture_changed_columns
#changed_columns = changed
#changes = changes
end
def log_changed_columns
e = EmployeeChangeLog.new
Employee::CHECK_FIELDS.each do |field|
if self.send("#{field}_changed?")
e.send("#{field}=", self.send(field))
end
end
if e.changed?
e.update_attribute(:account_id, self.account.id)
e.update_attribute(:employee_id, self.id)
e.update_attribute(:employee_ref, self.employee_ref)
e.update_attribute(:user_id, Thread.current[:current_employee])
e.save
else return
end
end
And that;s it. If the account enables it, the app keeps an eye on specific fields and then all changes to those fields are logged to a table, creating an simple audit trail.
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