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 H.

Brian H. 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 H.

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 W. wrote:

Brian H. 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 H.

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

Brian H. 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

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

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 E. <rails-mailing-l…@andreas-

Brian H. 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 E. <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 H.

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 E. <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 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)

This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.

| Privacy Policy | Terms of Service | Remote Ruby Jobs