How about find(:exactly_one, ...)?

All,

Is there a safe, one-line way of finding a record that:

1) Must be present 2) Must be unique (according to the conditions)

Bootstrapping data (often stored as a constant) often spurs this need, e.g.

class Status < ActiveRecord::Base   APPROVED = Status.find(:first, :conditions => ["name = 'approved'"])   ... end

class AccountType < ActiveRecord::Base   AccountType.find(:first, :conditions => ["type = 'commercial'"]) end

or

us_state = UsState.find(:first, :conditions => ["abbrev = ?", abbreviation])

I don't want the burden of error-checking the results, e.g. 'x.nil?' or 'x.size == 1', every time I issue a query of this type.

I haven't seen any good solution so far, and I'm considering overriding 'find' to take an ':exactly_one' option in addition to :first and :all, raising RecordNotFoundException or TooManyRecordsException. Before I go down that road I wanted to ask the mailing list if I'm missing something.

Thanks!

Brian Hartin

Brian Hartin wrote:

All,

Is there a safe, one-line way of finding a record that:

1) Must be present 2) Must be unique (according to the conditions)

Bootstrapping data (often stored as a constant) often spurs this need, e.g.

class Status < ActiveRecord::Base   APPROVED = Status.find(:first, :conditions => ["name = 'approved'"])   ... end

class AccountType < ActiveRecord::Base   AccountType.find(:first, :conditions => ["type = 'commercial'"]) end

or

us_state = UsState.find(:first, :conditions => ["abbrev = ?", abbreviation])

I don't want the burden of error-checking the results, e.g. 'x.nil?' or 'x.size == 1', every time I issue a query of this type.

I haven't seen any good solution so far, and I'm considering overriding 'find' to take an ':exactly_one' option in addition to :first and :all, raising RecordNotFoundException or TooManyRecordsException. Before I go down that road I wanted to ask the mailing list if I'm missing something.

Thanks!

Brian Hartin

The access you are asking for would be given by

class ActiveRecord::Base   def self.find_exactly_one(conditions)     return nil unless self.count(:conditions => conditions) == 1     self.find(:first, :conditions => conditions)   end end

(or something close to this)

I don't know if you want to bother into one sql-statement.

Can you not put a uniqueness constraint on the column?

Stephan

Stephan Wehner wrote:

Brian Hartin wrote:

All,

Is there a safe, one-line way of finding a record that:

1) Must be present 2) Must be unique (according to the conditions)

Bootstrapping data (often stored as a constant) often spurs this need, e.g.

class Status < ActiveRecord::Base   APPROVED = Status.find(:first, :conditions => ["name = 'approved'"])   ... end

class AccountType < ActiveRecord::Base   AccountType.find(:first, :conditions => ["type = 'commercial'"]) end

or

us_state = UsState.find(:first, :conditions => ["abbrev = ?", abbreviation])

I don't want the burden of error-checking the results, e.g. 'x.nil?' or 'x.size == 1', every time I issue a query of this type.

I haven't seen any good solution so far, and I'm considering overriding 'find' to take an ':exactly_one' option in addition to :first and :all, raising RecordNotFoundException or TooManyRecordsException. Before I go down that road I wanted to ask the mailing list if I'm missing something.

Thanks!

Brian Hartin

The access you are asking for would be given by

class ActiveRecord::Base   def self.find_exactly_one(conditions)     return nil unless self.count(:conditions => conditions) == 1     self.find(:first, :conditions => conditions)   end end

(or something close to this)

I don't know if you want to bother into one sql-statement.

Can you not put a uniqueness constraint on the column?

Stephan

There is a uniqueness constraint, but that doesn't cover the case in which the record doesn't exist. The behavior I'm shooting for is:

1) One and only one query is issued (your example issues two) 2) The conditions will usually reference a column(s) with a unique index, but not necessarily (some dev shops don't use them - sigh) 3) It must raise a NoRecordFound if no record is found 4) It must raise a TooManyRecordsFound if more than one record is found 5) It must not interfere with other, expected find behavior

So far, I've got:

def self.find_exactly_one(conditions)   results = self.find(:all, :conditions => conditions)   raise RecordNotFound if results.size == 0   raise TooManyRecordsFound, "#{results.size} found." if results.size > 1   results[0] # Found just one end

But this is only because I'm not yet ready to tackle replacing 'find' itself. Ideally, I could do:

find(:exactly_one, :conditions => ...)

Thanks for the response!

Brian

Patching it into find should be easy. Something like below (which is aircoded and probably not correct) should get you started:

class ActionView::Base

  # this method will become "find"   def find_with_exactly_one(*args)     options = args.extract_options!     case args.first        when :exactly_one then find_exactly_one(options)        else find_without_exactly_one(args, options)      end   end

  # this makes "find_with_exactly_one" into "find"   # and turn the original "find" method into "find_without_exactly_one"   alias_method_chain :find, :exactly_one

  def self.find_exactly_one(conditions)     results = self.find(:all, :conditions => conditions)     raise RecordNotFound if results.size == 0     raise TooManyRecordsFound, "#{results.size} found." if results.size

1     results[0] # Found just one   end end

Brian Hartin wrote:

...but that doesn't cover the case in

which the record doesn't exist. The behavior I'm shooting for is:

1) One and only one query is issued (your example issues two) 2) The conditions will usually reference a column(s) with a unique index, but not necessarily (some dev shops don't use them - sigh) 3) It must raise a NoRecordFound if no record is found 4) It must raise a TooManyRecordsFound if more than one record is found 5) It must not interfere with other, expected find behavior

How something like this one:

class ActiveRecord::Base   def self.find_exactly_one(conditions)     results = self.find(:all, :condition => conditions)     raise 'None found' if results.blank? # might also be results==     raise 'Too many' if results.length > 1     results.first   end end

Stephan

I think that typically you use alias_method_chain when you want to inject something into the call chain (ie., before/after processing, adding callbacks) rather than simply extending the method. If you only need to extend it, you're probably better adding an extension in lib (often lib/core_ext when extending a core object) and then including the functionality where you need it.

The find_exactly_on code is good (should probably be protected), though you could simply:

def self.find   options = args.extract_options!   return find_exactly_one if args.first==:exactly_one # add your new method when the conditions are right   super # give way to default if not end

AndyV wrote:

I think that typically you use alias_method_chain when you want to inject something into the call chain (ie., before/after processing, adding callbacks) rather than simply extending the method. If you only need to extend it, you're probably better adding an extension in lib (often lib/core_ext when extending a core object) and then including the functionality where you need it.

The find_exactly_on code is good (should probably be protected), though you could simply:

def self.find   options = args.extract_options!   return find_exactly_one if args.first==:exactly_one # add your new method when the conditions are right   super # give way to default if not end

On Apr 18, 8:04 pm, Nathan Esquenazi <rails-mailing-l...@andreas-

That implies that we either do this in a particular model or use a common base class for all our models, no? For a monkeypatch, I'd have to use alias_method_chain, right?

I prefer the common base-class approach. I'll give that a shot.

Thanks!

Brian

Brian Hartin wrote:

AndyV wrote:

I think that typically you use alias_method_chain when you want to inject something into the call chain (ie., before/after processing, adding callbacks) rather than simply extending the method. If you only need to extend it, you're probably better adding an extension in lib (often lib/core_ext when extending a core object) and then including the functionality where you need it.

The find_exactly_on code is good (should probably be protected), though you could simply:

def self.find   options = args.extract_options!   return find_exactly_one if args.first==:exactly_one # add your new method when the conditions are right   super # give way to default if not end

On Apr 18, 8:04 pm, Nathan Esquenazi <rails-mailing-l...@andreas-

That implies that we either do this in a particular model or use a common base class for all our models, no? For a monkeypatch, I'd have to use alias_method_chain, right?

I prefer the common base-class approach. I'll give that a shot.

Thanks!

Brian

I've done this as a monkeypatch for now. Here's what I have:

# Add common behaviors to models module ActiveRecord   class TooManyRecordsFound < ActiveRecordError   end

  class Base     class << self       # Adds support for the new :exactly_one option       def find_with_exactly_one(*args)         options = args.last.is_a?(Hash) ? args.last : {}         if args.first == :exactly_one           find_exactly_one(options)         else           find_without_exactly_one(*args)         end       end

      alias_method_chain :find, :exactly_one

      protected           # Raises either TooManyRecordsFound or           # RecordNotFound if we don't find exactly           # one record.           def find_exactly_one(options)             results = find(:all, options)             raise RecordNotFound if results.size == 0             raise TooManyRecordsFound, "#{results.size} found." if results.size > 1             results[0] # Found just one           end     end   end end

Thanks for all your responses,

Brian Hartin

I am not sure but I actually think Andy was right. I am not sure alias_method_chain is the best method to use here BUT it works so I am happy you at least got that worked up. Although I think Andy might have been pointing you to a better solution (i.e included module extension)