I lamented earlier about the absence of a racecondition-free find_or_create implementation and somewhat promised to check back with a working implementation. YES, WE CAN: NOW is the time, folks!
As stated there the first thing missing is an advisory locking implementation. With the database as the natural choice for a synchronisation point that implementation would be DB dependant, the following being a MySQL-implementation:
module ActiveRecord::ConnectionAdapters::MysqlAdapter::AdvisoryLock
TIMEOUT=10
def locked(lock)
lock = "#{current_database}_#{lock}"
begin
execute "SELECT GET_LOCK(#{quote(lock)},#{TIMEOUT})"
yield
ensure
execute "SELECT RELEASE_LOCK(#{quote(lock)})"
end
end
end
class ActiveRecord::ConnectionAdapters::MysqlAdapter
include AdvisoryLock
end
Now, whats still left is a find_or_create implementation. I like that one:
ActiveRecord::Base
class ActiveRecord::Base
def self.find_first_by_hash(hash)
find :first, :conditions => hash
end
def self.find_or_create(hash)
find(hash) || connection.locked("#{self.table_name}.find_or_create") do
find(hash) || create(hash)
end
end
end
Where is the “find_or_create_by_attribute_names”, you might ask? Well, I am not too fond of those anyways, because writing
find_by_name_and zipcode name, zipcode
adds several hundred line to the ActiveRecord implementation, doesn’t work with attributes that have an underscore in its name, and saves only a few typestrokes over
find :first, :name => name, :zipcode => zipcode
which could even be reduced down to one by an extended find() implementation, which takes a Hash as a possible first argument.