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)