How to validate uniqueness of a field without saving in Rails? - ruby-on-rails

Suppose you have User has_many Books. Each book has a name field.
The user enters their books and it is submitted to the app as an array of names. The array of names will replace any existing books.
If the update fails, then the books should not be changed.
class Book
belongs_to :user
validates_uniquness_of :name, scope: [:user]
How to check the validity of each book without saving?
For example:
['Rails Guide', 'Javascript for Dummies'] would be valid.
['Javascript for Dummies', 'Javascript for Dummies'] would not be valid.
params[:books].each{| b | Book.new(b).valid? } will not work because the book has to be saves to get the uniqueness.
Mongoid

You can use an Active Record Transaction. Start the transaction, call save, and if it fails then the entire transaction will be rolled back. For example:
Book.transaction do
params[:books].each{ |b| Book.new(b).save! }
end
The entire transaction is aborted if there is an exception. You should handle this case by catching ActiveRecord::RecordInvalid.

You can use Array#map to convert array of books attributes to array of books names. Then use Array#uniq to remove duplicates from array of books names, and then check if resulting array has the same size as the original array of books attributes:
are_books_uniq = params[:books].map{|b| b[:name]}.uniq.size == params[:books].size
This way you can perform your check, without touching the database. But to be on a safe side, you should save all the books inside a transaction (see #Aaron's answer).

This turned out to be much more complicated than I imagined.
The solution I came up with looks like:
def update params
names = params.delete( :books )
new_books = names.map{| title | Book.new( name:name )}
validate_books_for new_books
return false if errors.present?
return false unless super( params )
self.books = new_books
self
end
Most of the complexity comes from the coupling of the 2 models. I can see why it is not a good idea to couple models. Perhaps a better design would be to store the books as an array.

Related

Rails ActiveRecord Delete Duplicates and also change on relational table

I need to remove duplicated record on A table and if A table have relation with B table, I should find on B table to record which have removed from duplicated to change other record.
I follow this
Remove duplicate records based on multiple columns?
that link.
This link is works successfully and removed all duplicated records and keep first records on table.
My problem is that when we removed record, that record may used in B table so, I need to find record which is removed and update with the kept record.
def up
grouped = A.all.group_by{|model| [model.name] }
grouped.values.each do |duplicates|
last_one = duplicates.last #also used pop
duplicates.each do |double|
find_in_b = B.find_by(a_id: double.id)
find_in_b.a_id = last_one.id
find_in_b.save!(validate: false)
double.destroy
end
end
end
Thanks in advance!
A couple of things:
you should use .pop and not .last, otherwise all your records will be destroyed. (pop is a destructive method on the array, which is what you want, as you want to keep one record)
If A has_many B, you should use .where instead of find_by on this line:
find_in_b = B.where(a_id: double.id)
I guess the code could use AR associations, but not knowing your exact situation, here is my proposed code:
def up
grouped = A.all.group_by{ |model| [model.name] }
grouped.values.each do |duplicates|
last_one = duplicates.pop
duplicates.each do |double|
find_in_b = B.where(a_id: double.id)
# update_all doesn't run validations
find_in_b.update_all(a_id: last_one.id)
double.destroy
end
end
end
Note that if your A table is very big, it's quite inefficient to group_by in ruby, you should rather use the DB grouping.

Rails checking if a record exists in database

What is the most efficient of way of checking if a database will return a record before processing it. Example: Truck.where("id = ?", id).select('truck_no').first.truck_no
This may or may not return a truck if the truck exists. What is the most efficient way for me to ensure the page will not crash when processing this request. How would I handle this both in the view and the controller if lets say I was using a loop to go through each truck and print out its number.
If the record does not exist I would like to be able to print out a message instead saying no records found.
If you want to check for the existence of an object why not use exists?
if Truck.exists?(10)
# your truck exists in the database
else
# the truck doesn't exist
end
The exists? method has the advantage that is not selecting the record from the database (meaning is faster than selecting the record).
The query looks like:
SELECT 1 FROM trucks where trucks.id = 10
You can find more examples in the Rails documentation for #exists?.
Here is how you can check this.
if Trucks.where(:id => current_truck.id).blank?
# no truck record for this id
else
# at least 1 record for this truck
end
where method returns an ActiveRecord::Relation object (acts like an array which contains the results of the where), it can be empty but never be nil.
OP actual use case solution
The simplest solution is to combine your DB check and retrieval of data into 1 DB query instead of having separate DB calls. Your sample code is close and conveys your intent, but it's a little off in your actual syntax.
If you simple do Truck.where("id = ?", id).select('truck_no').first.truck_no and this record does NOT exists, it will throw a nil error when you call truck_no because first may retrieve a nil record if none are found that match your criteria.
That's because your query will return an array of objects that match your criteria, then you do a first on that array which (if no matching records are found) is nil.
A fairly clean solution:
# Note: using Rails 4 / Ruby 2 syntax
first_truck = Truck.select(:truck_no).find_by(id) # => <Truck id: nil, truck_no: "123"> OR nil if no record matches criteria
if first_truck
truck_number = first_truck.truck_no
# do some processing...
else
# record does not exist with that criteria
end
I recommend using clean syntax that "comments" itself so others know exactly what you're trying to do.
If you really want to go the extra mile, you could add a method to your Truck class that does this for you and conveys your intent:
# truck.rb model
class Truck < ActiveRecord::Base
def self.truck_number_if_exists(record_id)
record = Truck.select(:truck_no).find_by(record_id)
if record
record.truck_no
else
nil # explicit nil so other developers know exactly what's going on
end
end
end
Then you would call it like so:
if truck_number = Truck.truck_number_if_exists(id)
# do processing because record exists and you have the value
else
# no matching criteria
end
The ActiveRecord.find_by method will retrieve the first record that matches your criteria or else returns nil if no record is found with that criteria. Note that the order of the find_by and where methods is important; you must call the select on the Truck model. This is because when you call the where method you're actually returning an ActiveRelation object which is not what you're looking for here.
See ActiveRecord API for 'find_by' method
General solutions using 'exists?' method
As some of the other contributors have already mentioned, the exists? method is engineered specifically to check for the existence of something. It doesn't return the value, just confirms that the DB has a record that matches some criteria.
It is useful if you need to verify uniqueness or accuracy of some piece of data. The nice part is that it allows you to use the ActiveRelation(Record?) where(...) criteria.
For instance, if you have a User model with an email attribute and you need to check if an email already exists in the dB:
User.exists?(email: "test#test.com")
The benefit of using exists? is that the SQL query run is
SELECT 1 AS one FROM "users" WHERE "users"."email" = 'test#test.com' LIMIT 1
which is more efficient than actually returning data.
If you need to actually conditionally retrieve data from the DB this isn't the method to use. However, it works great for simple checking and the syntax is very clear so other developers know exactly what you're doing. Using appropriate syntax is critical in projects with multiple developers. Write clean code and let the code "comment" itself.
If you just want to check whether the record exists or not. Go with the #cristian's answer i.e.
Truck.exists?(truck_id) # returns true or false
But if truck exists and you want to access that truck then you will have to find truck again which will lead to two database queries. If this is the case go with
#truck = Truck.find_by(id: truck_id) #returns nil or truck
#truck.nil? #returns true if no truck in db
#truck.present? #returns true if no truck in db
You could just do:
#truck_no = Truck.where("id = ?", id).pluck(:truck_no).first
This will return nil if no record is found, or truck_no of only the first record otherwise.
Then in your view you could just do something like:
<%= #truck_no || "There are no truck numbers" %>
If you want to fetch and display multiple results, then in your controller:
#truck_nos = Truck.where("id = ?", id).pluck(:truck_no)
and in your view:
<% truck_nos.each do |truck_no| %>
<%= truck_no %>
<% end %>
<%= "No truck numbers to iterate" if truck_nos.blank? %>
Rails has a persisted? method
for using like you want

How to make find_or_initialize case insensitive on a Postgres DB?

In a Rails 3.2 app I have a Post model that belongs to a Category. In the new post form is a text field for assigning a category or creating a new category.
It works via the following method on the Post model
def category_name=(name)
if name.present?
post_cat = category.find_or_initialize_by_name(name)
if post_cat.new_record?
post_cat.save(:validate => false)
self.category = post_cat
else
self.category = post_cat
end
end
end
This is more or less working, but is case sensitive. For example, the database now contains records for "Featured", "featured" and "FEATURED" categories.
How can I make the above find_or_initialize function behave as though it were case insensitive. I am using a Postgres database, and I have a suspicion that this is not quite as easy as "making find_or_initialize case insensitive".
I'm grateful for any advice as to best practice, what to consider, and useful references.
I refer to your comment above: Maybe you could add a second field, containing the value of the string which is displayed to the user, and find_or_initialize by the name column:
post_cat = category.find_or_initialize_by_name(name.downcase)
post_cat.display_name = name if post_cat.new_record?
So the second line ensures, that an existing record doesn't get overriden by another user, who tries to add categories like bRandnAmE. The only pitfall I see is, that the first user, who creates the category, has to spell it correct.

ActiveRecord query returns an incorrect model

I have been scratching my head over this one for a little while, and though I'm sure its a stupid mistake, I've reached the point where I must consult SO if I am to preserve the hair follicles I have left.
I've written a function in Rails (3.1.2) which should return an array populated with ActiveRecord model objects (users, in this case) which meet a certain criterion. The criterion is that the user's current list (denoted by the field active_list_id) must not be nil. The code follows:
def build_list_array
#lists = Array.new
User.all.each do |user|
#active_list_id = user.active_list_id
#lists<< List.find(#active_list_id) if #active_list_id != nil #TODO WHAT?!? WHY IS THIS RETURNING USERS?
end
end
As you can see, I'm initializing an empty array, cycling through all users and adding their active list to the array if the relevant reference on the user record is not nil. The problem is, this is returning user objects, not list objects.
Here are the associations from the user and list models:
user model:
has_many :lists
has_many :tasks
list model:
belongs_to :user
A brief word about the reference to active_list: A user can have many lists, but only one is active at any time. Therefore, I need to reference that list on the user record. The active list is not a foreign key in the typical sense, then.
I appreciate any help you can give me...Thanks =)
As it stands, your build_list_array will return an array of User because of the behavior of each. When iterating over a collection using each, the call to each returns the original collection.
For example,
list = []
# returns => []
[1,2,3,4,5].each { |number| list << number * 10 }
# returns => [1, 2, 3, 4, 5]
list
# returns => [10, 20, 30, 40, 50]
In your code, the last statement in your build_list_array method is the each call, meaning the return value of each is what is returned by the method. If you simply added a return statement at the end of the method you would be good to go.
def build_list_array
#lists = Array.new
User.all.each do |user|
#active_list_id = user.active_list_id
#lists<< List.find(#active_list_id) if #active_list_id
end
return #lists # Actually return #lists
end
That being said, you should probably use something like Bradley's answer as a basis for more "correct" Rails code.
each always returns the collection it iterates on (no matter what happens inside the block). Sounds like you want to return #lists at the end of your method.
You seem to be making a curious use of instance variables. You could also fetch this in one query via a join, something along the lines of
List.joins('inner join users on active_list_id =lists.id')
Activerecord's Arel is your friend here:
User.where(:active_list_id.not_eq => nil)
Extending Steven's answer, to get the Lists
class User
belongs_to :active_list, :class_name => "List"
def build_list_array
#lists = User.where('active_list_id is not null').map(&:active_list).compact

Only delete from memory not from database

I have an ActiveRecord array containing a list of shops.
shops = Shop.find(:all)
I want to delete a record from shops without deleteting it from the database.
shops.delete(a_shop) would result in a delete SQL query. I just want the shop to be deleted from the ActiveRecord array but not the database.
Can this be done?
Thanks
Beware if you are using has_many relationship the answer by JP will not work.
Extending the example:
class City < ActiveRecord::Base
has_many :shops
end
city = City.find(:name => "Portland")
then
city.shops.delete(city.shops[0])
Will delete from the DB!
Also
theshops = city.shops
theshops.delete(ss[0])
Will delete from the DB
One way to detach from the DB is to use compact or another array function like so:
theshops = city.shops.compact
theshops.delete(ss[0])
Will not delete from the DB
Also, in all cases delete_if Will not delete from the db:
city.shops.delete_if {|s| s.id == city.shops[0]}
Cheers!
Don't forget: If you are in doubt about these sort of things script/console is your friend!
When I did some testing, calling the delete method does not actually delete the element from the database. Only from the array.
shops = Shop.find(:all)
shops.delete(shops[0]) #deletes the first item from the array, not from the DB
To delete from the DB:
shops = Shop.find(:all)
Shop.delete(shops[0].id)
See the distinction? At least, this how it works on my Rails 2.1 setup.
-JP
I don't think you can remove the item from the array.
However, you should be able to modify your find to return the array without that shop initially. Something like:
shops = Shop.find(:all, :conditions => "id NOT #{a_shop.id}")
should be along the lines of what you want. That way you never get the shop in the container in the first place, but it doesn't affect the database.
I would like to suggest such way to delete AR object in memory.
Add an attribute to the appropriate model that is responsible for marking AR object as deleted (e.g. deleted attribute):
class OrderItem < ActiveRecord::Base
...
attr_accessor :deleted
def deleted
#deleted || 'no'
end
end
Mark the appropriate object as deleted:
o.order_items {|oi| oi.deleted = 'yes' if oi.id == 1029}
Filter order_items set only not deleted rows:
o.order_items do |oi|
unless oi.deleted == 'yes'
...
end
end

Resources