Rails first_or_create ActiveRecord method - ruby-on-rails

What does the first_or_create / first_or_create! method do in Rails?
According to the documentation, the method "has no description"...

From the Guides
first_or_create
The first_or_create method checks whether first returns nil or not. If it does return nil, then create is called. This is very powerful when coupled with the where method. Let’s see an example.
Suppose you want to find a client named ‘Andy’, and if there’s none, create one and additionally set his locked attribute to false. You can do so by running:
Client.where(:first_name => 'Andy').first_or_create(:locked => false)
# => #<Client id: 1, first_name: "Andy", orders_count: 0, locked: false, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
The SQL generated by this method looks like this:
SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
BEGIN
INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 0, NULL, '2011-08-30 05:22:57')
COMMIT
first_or_create returns either the record that already exists or the new record. In our case, we didn’t already have a client named Andy so the record is created and returned.
first_or_create!
You can also use first_or_create! to raise an exception if the new record is invalid. Validations are not covered on this guide, but let’s assume for a moment that you temporarily add
validates :orders_count, :presence => true
to your Client model. If you try to create a new Client without passing an orders_count, the record will be invalid and an exception will be raised:
Client.where(:first_name => 'Andy').first_or_create!(:locked => false)
# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank

I believe first_or_create should generally be avoided these days. (Although it's still in Rails 6.1.1.) In 4.0 they added find_or_create_by and friends, which are apparently meant to replace the first_or_create methods. first_or_create was once mentioned in the guides, now it's not. And it is no longer documented in the code (since Rails 4.0). It was introduced in Rails 3.2.19.
The reasons are:
It might seem that first_or_create(attrs) does first(attrs) || create(attrs), where in fact it does first || create(attrs). The proper usage is:
Model.where(search_attrs).first_or_create(create_attrs)
That is, it might confuse people. first(attrs) || create(attrs) is what find_or_create_by does.
Also, using first_or_create introduces a scope, that might affect the create callbacks in an unexpected way.
More on it in the changelog (search for first_or_create).

Gets the first record that matches what you have specified or creates one if there are no matches

If you check the source, you will see that they are almost identical. The only difference is that the first one calls the "create" method and the other one "create!". This means that the second one will raise an exception, if the creation is not successful.

Related

How can I get active record to throw an exception if there are too many records

Following the principle of fail-fast:
When querying the database where there should only ever be one record, I want an exception if .first() (first) encounters more than one record.
I see that there is a first! method that throws if there's less records than expected but I don't see anything for if there's two or more.
How can I get active record to fail early if there are more records than expected?
Is there a reason that active record doesn't work this way?
I'm used to C#'s Single() that will throw if two records are found.
Why would you expect activerecord's first method to fails if there are more than 1 record? it makes no sense for it to work that way.
You can define your own class method the count the records before getting the first one. Something like
def self.first_and_only!
raise "more than 1" if size > 1
first!
end
That will raise an error if there are more than 1 and also if there's no record at all. If there's one and only one it will return it.
It seems ActiveRecord has no methods like that. One useful method I found is one?, you can call it on an ActiveRecord::Relation object. You could do
users = User.where(name: "foo")
raise StandardError unless users.one?
and maybe define your own custom exception
If you care enough about queries performance, you have to avoid ActiveRecord::Relation's count, one?, none?, many?, any? etc, which spawns SQL select count(*) ... query.
So, your could use SQL limit like:
def self.single!
# Only one fast DB query
result = limit(2).to_a
# Array#many? not ActiveRecord::Calculations one
raise TooManySomthError if result.many?
# Array#first not ActiveRecord::FinderMethods one
result.first
end
Also, when you expect to get only one record, you have to use Relation's take instead of first. The last one is for really first record, and can produce useless SQL ORDER BY.
find_sole_by (Rails 7.0+)
Starting from Rails 7.0, there is a find_sole_by method:
Finds the sole matching record. Raises ActiveRecord::RecordNotFound if no record is found. Raises ActiveRecord::SoleRecordExceeded if more than one record is found.
For example:
Product.find_sole_by(["price = %?", price])
Sources:
ActiveRecord::FinderMethods#find_sole_by.
Rails 7 adds ActiveRecord methods #sole and #find_sole_by.
Rails 7.0 adds ActiveRecord::FinderMethods 'sole' and 'find_sole_by'.

Find_or_initialize_by(nil) returns last record

So I'm trying to create a complicated(imo) CSV/Excel import process, which has to create records for 3 separate models + associations, and while debugging the validation for this process I have stumbled upon a confusing concept regarding find_or_initialize_by() when the passed attributes are nil.
According to Rails API Docs, find_or_initialize_by() should do the following:
# File activerecord/lib/active_record/relation.rb, line 222
def find_or_initialize_by(attributes, &block)
find_by(attributes) || new(attributes, &block)
end
Which leads me to believe that if I pass attributes to find_by which are nil or even blank it should then move it to Model.new(nil), however, for me it keeps returning the last record for that model.
Methods such as Model.find_by() and Model.where().find_or_initialize when passed nil, {}, false, "" still return the last record.
The reason I want it to initialize a new record with nil attributes is so that it fails validation and throws and error back to the user that the data entered in that row is invalid. (Since this is not a standard columns = model attributes type of import, I have to parse the passed columns through another method that should return nil if a piece of the entry is bad... at least thats the best way I can think of.)
So would anyone be able to help me understand why this doesn't work as I've explained and what your suggestion might be in this situation?
Thanks!
Which leads me to believe that if I pass attributes to find_by which
are nil or even blank it should then move it to Model.new(nil),
however, for me it keeps returning the last record for that model.
No, that's not right. find_by() always return first record matching the specified conditions, in your case there is no any conditions, so first record is returned.
It is returns nil only if conditions isn't matching:
=> Model.find_by(nil) # empty conditions without column specifying
=> #<Model:0x00561654201c30
=> Model.find_by(foo: nil) # empty conditions with column specifying
=> nil
In order to initialize model with empty atributes, use empty conditions:
=> Model.where(foo: nil, bar: nil).find_or_initialize

Seeds file - adding id column to existing data

My seeds file populated the countries table with a list of countries. But now it needs to be changed to hard-code the id (instead of rails generating the id column for me).
I added the id column and values as per below:
zmb: {id: 103,code: 'ZMB', name: Country.human_attribute_name(:zambia, default: 'Error!'), display_order: nil, create_user: user, update_user: user, eff_date: Time.now, exp_date: default_exp_date},
skn: {id: 104,code: 'SKN', name: Country.human_attribute_name(:st_kitts_and_nevis, default: 'Error!'), display_order: nil, create_user: user, update_user: user, eff_date: Time.now, exp_date: default_exp_date}
countries.each { |key, value| countries_for_later[key] = Country.find_or_initialize_by(id: value[:id]); countries_for_later[key].assign_attributes(value); countries_for_later[key].save!; }
Above it just a snippet. I have added an id: for every country.
But when I run db:seed I get the following error:
ActiveRecord::RecordInvalid: Validation failed: Code has already been taken
I am new to rails so I'm not sure what is causing this - is it because the ID column already exists in the database?
What I think is happening is you have existing data in your database ... let's say
[{id:1 , code: 'ABC'},
{id:2 , code: 'DEF'}]
Now you run your seed file which has {id: 3, 'DEF'} for example.
Because you are using find_or_initialize_by with id you are running into errors. Since you can potentially insert duplicates.
I recon you should just clear your data, but you can try doing find_or_initialize_by using code instead of id. That way you wont ever have a problem of trying to create a duplicate country code.
Country.find_or_initialize_by(code: value[:code])
I think you might run into problems with your ids, but you will have to test that. It's generally bad practice to do what you are doing. Whether they ids change or now should be irrelevant. Your seed file should reference the objects that are being created not ids.
Also make sure you aren't using any default_scopes ... this would affect how find_or_initialize_by works.
The error is about Code: Code has already been taken. You've a validation which says Code should be uniq. You can delete all Countries and load seeds again.
Run this in the rails console:
Country.delete_all
Then re-run the seed:
rake db:seed
Yes, it is due to duplicate entry. In that case run ModelName.delete_all in your rails console and then run rake db:seed again being in the current project directory. Hope this works.
ActiveRecord::RecordInvalid: Validation failed: Code has already been taken
is the default error message for the uniqueness validator for :code.
Running rake db:reset will definitely clear and reseed your database. Not sure about the hardcoded ids though.
Check this : Overriding id on create in ActiveRecord
you will have to disable protection with
save(false)
or
Country.create(attributes_for_country, without_protection: true)
I haven't tested this though, be careful with your validators.
Add the line for
countries_for_later[key].id = value[:id]
the problem is that you can't set :id => value[:id] to Country.new because id is a special attribute, and is automatically protected from mass-assignment
so it will be:
countries.each { |key, value|
countries_for_later[key] = Country.find_or_initialize_by(id: value[:id])
countries_for_later[key].assign_attributes(value)
countries_for_later[key].id = value[:id] if countries_for_later[key].new_record?
countries_for_later[key].save(false)
}
The ids data that you are using in your seeds file: does that have any meaning outside of Rails? Eg
zmb: {id: 103,code: 'ZMB',
is this some external data for Zambia, where 103 is it's ID in some internationally recognised table of country codes? (in my countries database, Zambia's "numcode" value is 894). If it is, then you should rename it to something else, and let Rails decide what the id field should be.
Generally, mucking about with the value of ID in rails is going to be a pain in the ass for you. I'd recommend not doing it. If you need to do tests on data, then use some other unique field (like 'code') to test whether associations etc have been set up, or whatever you want to do, and let Rails worry about what value to use for ID.

Validation: how to check for specific error

I know how to check an attribute for errors:
#post.errors[:title].any?
Is it possible to check which validation failed (for example "uniqueness")?
Recently I came across a situation where I need the same thing: The user can add/edit multiple records at once from a single form.
Since at validation time not all records have been written to the database I cannot use #David's solution. To make things even more complicated it is possible that the records already existing in the database can become duplicates, which are detected by the uniqueness validator.
TL;DR: You can't check for a specific validator, but you can check for a specific error.
I'm using this:
# The record has a duplicate value in `my_attribute`, detected by custom code.
if my_attribute_is_not_unique?
# Check if a previous uniqueness validator has already detected this:
unless #record.errors.added?(:my_attribute, :taken)
# No previous `:taken` error or at least a different text.
#record.errors.add(:my_attribute, :taken)
end
end
Some remarks:
It does work with I18n, but you have to provide the same interpolation parameters to added? as the previous validator did.
This doesn't work if the previous validator has written a custom message instead of the default one (:taken)
Regarding checking for uniqueness validation specifically, this didn't work for me:
#post.errors.added?(:title, :taken)
It seems the behaviour has changed so the value must also be passed. This works:
#post.errors.added?(:title, :taken, value: #post.title)
That's the one to use ^ but these also work:
#post.errors.details[:title].map { |e| e[:error] }.include? :taken
#post.errors.added?(:title, 'has already been taken')
Ref #34629, #34652
By "taken", I assume you mean that the title already exists in the database. I further assume that you have the following line in your Post model:
validates_uniqueness_of :title
Personally, I think that checking to see if the title is already taken by checking the validation errors is going to be fragile. #post.errors[:title] will return something like ["has already been taken"]. But what if you decide to change the error message or if you internationalize your application? I think you'd be better off writing a method to do the test:
class Post < ActiveRecord::Base
def title_unique?
Post.where(:title => self.title).count == 0
end
end
Then you can test if the title is unique with #post.title_unique?. I wouldn't be surprised if there's already a Rubygem that dynamically adds a method like this to ActiveRecord models.
If you're using Rails 5+ you can use errors.details. For earlier Rails versions, use the backport gem: https://github.com/cowbell/active_model-errors_details
is_duplicate_title = #post.errors.details[:title].any? do |detail|
detail[:error] == :uniqueness
end
Rails Guide: http://guides.rubyonrails.org/active_record_validations.html#working-with-validation-errors-errors-details

Why "Object#id will be deprecated; use Object#object_id" when accessing AR record ID? [duplicate]

This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
Rails primary key and object id
I am baffled as to why I cannot refer an object's attributes; hope someone can help...
I need to build a hash that will associate Marker.marker_name with it's id from the database, which are already stored (the record id will serve as a foreign key in another table).
So, first I retrieve the Marker record via this named scope:
class Marker < ActiveRecord::Base
named_scope :by_name, lambda { |marker_name|
{:conditions => ["marker_name = ?", marker_name]}}
which is called from my Uploads model, like this (marker_name has the value "Amelogenin"):
this_marker = Marker.by_name(marker_name)
I know this worked, because when I Use the debugger, I can see what is in this_marker, which looks like:
(rdb:2) y this_marker
!ruby/object:Marker attributes:
created_at: 2011-03-14 22:21:27.244885
updated_at: 2011-03-14 22:21:27.244885
id: "11"
marker_name: Amelogenin attributes_cache: {}
Yet, I cannot assign the record id in my hash, like this:
$markers[marker_name] = this_marker.id
I cannot seem to refer directly to the id in this way; because, even in the debugger, I get this error:
(rdb:2) p this_marker.id
(__DELEGATION__):2: warning: Object#id will be deprecated; use Object#object_id
Is there some kind of different Ruby syntax I need to be using or what? How can I associate the marker_name with its record id?
Thanks in advance....
This Marker.by_name(marker_name) returns an array of makers. You should write:
this_marker = Marker.by_name(marker_name).first
This kind of error happens when one calls id method on non ActiveRecord object. So make sure this_marker is AR object instance.
this_marker = Marker.by_name(marker_name) are you sure this returns one Marker object? There is no call to all or first.
That is a very confusing error message, (and I bet it's a fairly common problem too).
Marker.by_name(marker_name) does not return an active record object, but a scope, which does not have an active record id, only the Object#id method, which is deprecated (and gone in Ruby 1.9.2).
Scopes are lazy - they won't access the database until you try to use them (or print them, as in your case).
Try Marker.by_name(marker_name).first

Resources