Incorporating runtime parameters in has_many :conditions

Hi,
I was wondering if there was any way to include runtime parameters in
the :conditions clause of a has_many relationship?

I’m trying to model the concept of a person who goes by various
nicknames. My Person model contains a “name” attribute containing their
real name, and a “has_many :nicknames” relationship. Through my business
rules, I’m enforcing the requirement that a person’s real name is always
included in the database as a nickname.

(If this seems like a really bad data structure, I’m open to
suggestions. However, having weighed up various alternatives, this seems
to me to be the least worst solution.)

I now want to add an “alternative_nicknames” relationship, which will
comprise all of the person’s nicknames excluding their real name. I’ve
tried

has_many :alternative_nicknames, :class_name => “Nickname”,
:conditions => [“nickname <> ?”, self.name]

  • however, since has_many is a class-level method, the :conditions block
    is evaluated at the point of loading the class, and therefore self.name
    returns “Person” rather than the person’s name. Out of desperation I’ve
    even tried wrapping the has_many declaration in an after_initialize
    callback, but then it complains that has_many is not defined. The
    examples in the Rails docs and the Pragmatic book only demonstrate
    static SQL fragments being used in the :conditions clause, not ones
    incorporating parameters. Any ideas how I can achieve this?

(If it helps explain things any better, I believe this person’s
(unanswered) question is equivalent to mine:
http://groups.google.com/group/rubyonrails-talk/msg/5006b300e4e5a8cc )

  • Matt

Matt Westcott wrote:

incorporating parameters. Any ideas how I can achieve this?

(If it helps explain things any better, I believe this person’s
(unanswered) question is equivalent to mine:
http://groups.google.com/group/rubyonrails-talk/msg/5006b300e4e5a8cc )

Write the condition with single quotes to delay evaluation until
the association load is done in an instance context:

:conditions => 'nickname <> #{name}'

This assumes a sanitized name attribute. If name is instead stored
in an unsanitized form you can try something like:

:conditions => ‘nickname <> #{class.connection.quote(name)}’

It would have been nice to be able to write

:conditions => ‘[“nickname <> ?”, #{name}]’

but conditions are currently sanitized then interpolated, not
interpolated then sanitized, and to make the reverse work
interpolation would have to be changed to handle arrays.


We develop, watch us RoR, in numbers too big to ignore.

On Nov 27, 2006, at 4:37 PM, Matt Westcott wrote:

I’m trying to model the concept of a person who goes by various
nicknames. My Person model contains a “name” attribute containing
their
real name, and a “has_many :nicknames” relationship. Through my
business
rules, I’m enforcing the requirement that a person’s real name is
always
included in the database as a nickname.

Rule #1: If you’re duplicating data, you’re doing the wrong thing,
unless
you have a VERY SERIOUS performance problem and you have
measured
the performance advantage of a denormalization very carefully.

(If this seems like a really bad data structure, I’m open to
suggestions.
However, having weighed up various alternatives, this seems to me
to be the
least worst solution.)

I’d recommend either:

  1. Not storing their real name in nicknames.

or

  1. Removing the “name” attribute from Person and adding an extra
    attribute
    to your Nicknames model named real_name that is a boolean.

I prefer #1 because real names are decided NOT nicknames, so why
would they
be stored there?

If you like #2, then I’d highly recommend renaming that model Names,
because
they’ll decidedly NOT be just nicknames any longer!

I find a lot of DB designers paint themselves into corners by doing
the sort
of thing that you’ve done here. If the tables names don’t fit the
use, it’s
not that you have a special need, it’s because your data model is
wrong. :slight_smile:

If the data model is wrong, you’re going to write a larger volume of
code, and
even worse, a larger volume of complex code to ‘decipher’ the data
that will
become hopelessly entangled over time.

I now want to add an “alternative_nicknames” relationship, which will
comprise all of the person’s nicknames excluding their real name.
I’ve
tried

With the advice above, there’s no need to do this. In #1 real name is
one place
and nicknames are another place, or in #2 the two are very obviously
separate
via the boolean column which will make it easier to pinpoint real
names -vs-
nicknames.

callback, but then it complains that has_many is not defined. The
examples in the Rails docs and the Pragmatic book only demonstrate
static SQL fragments being used in the :conditions clause, not ones
incorporating parameters. Any ideas how I can achieve this?

There’s nothing magic in the operation of has_many for the simplistic
cases, so you could leave out the has_many declaration and just code
an accessor method of your own:

def alternative_nicknames
self.nicknames.find(:all, :conditions => [“nickname <> ?”,
self.name])
end

One thing I think you need to be a bit skeptical of, however, is in the
clear overloading of the Nicknames.nickname column. You’re not just
storing a nickname there, but through usage, you’re also using it to
define a boolean relationship as well, based on variable data in the
parent table.

If you use method #1 above, you could access the various pieces very
simply and without any sort of magic at all.

person.name
person.nicknames
person.all_names

class Person
def all_names
[name,nicknames.collect { |n| n.nickname}]
end
end


– Tom M., CTO
– Engine Y., Ruby on Rails Hosting
– Reliability, Ease of Use, Scalability
– (866) 518-YARD (9273)

Tom M. wrote:

On Nov 27, 2006, at 4:37 PM, Matt Westcott wrote:

I’m trying to model the concept of a person who goes by various
nicknames. My Person model contains a “name” attribute containing
their
real name, and a “has_many :nicknames” relationship. Through my
business
rules, I’m enforcing the requirement that a person’s real name is
always
included in the database as a nickname.

I’d recommend either:

  1. Not storing their real name in nicknames.
    or
  2. Removing the “name” attribute from Person and adding an extra
    attribute
    to your Nicknames model named real_name that is a boolean.

Excellent, thanks - that does make a lot of sense. (Option 2 makes more
sense in my case, as I’ve got other models in the system which define
relations with (nick)names, so I need to keep them all together.) I
think this is one of those occasions where I’m so hung up over a niggly
detail of the data model that it takes another pair of eyes to see the
obvious solution… cheers.

Mark Reginald J. wrote:

Write the condition with single quotes to delay evaluation until
the association load is done in an instance context:
:conditions => ‘nickname <> #{class.connection.quote(name)}’

Thanks for this too - good to know the direct answer for when I need it
in future. I have to admit that my paranoid side is now wondering if
it’s really doing two passes of evaluation (and if so, what happens if
your data itself contains “#{insert malicious ruby code here}”) but I’m
sure it’ll all make sense to me after an hour or two of head-scratching
:slight_smile:

  • Matt

Mark Reginald J. wrote:

:conditions => 'nickname <> #{name}'

Oops, scrub this, name isn’t quoted. Use this insead

:conditions => ‘nickname <> #{class.connection.quote(name)}’


We develop, watch us RoR, in numbers too big to ignore.