2

I'm trying to do a "virtual relationship" (what I call it - don't know if there's a real term for it) in Rails. Here's one that I have that is definitely working: Address belongs_to Business and Business has_many Addresses. Addresses have a 'category' column: either Mailing or Physical. In theory, a business could have any number of each type, but in practice they have exactly one of each. So my models look like this:

class Address < ActiveRecord::Base
  belongs_to :business
end

class Business < ActiveRecord::Base
  has_many :addresses
  has_one  :physical_address, :class_name => 'Address', :conditions => ["category = ?", "Physical"]
  has_one  :mailing_address, :class_name => 'Address', :conditions => ["category = ?", "Mailing"]
end

Thus, I have two "virtual" has_one's, so that I can conveniently access either one (as business.physical_address or business.mailing_address) without having to do the work of separating them myself.

Now I'm trying to do something similar, but with a has_many :through. Here's my new scenario. I have a simple many-to-many relationship with a join. Let's say A has many J's, B also has many J's, and J belongs_to both A and B. However, A also has_many B's through J. So I can access them as a.bs.

However, the entries in B also have a 'category'. So as with the Business/Address example above, I want to have "virtual" has-one/many-through relationships in A, so I don't have to manually separate the B's by category. I tried something like this (assuming A's have exactly one B with category='Type 1'):

class A < ActiveRecord::Base
  has_many :js
  has_many :bs, :through => js
  has_one  :b_type1, :class_name => 'B', :through => :js, :conditions => ["category = ?", "Type 1"]
end

However, when I do a find on A's, the resulting objects do not have a b_type1 property in them.

Is it even possible to do what I'm trying to do?? Any help is appreciated.

Edit: I am needing this to be serialized to an Adobe Flex frontend (using the RestfulX framework). According to this thread, I can't use just a method as @Andrew suggests below because the resulting object will not get properly serialized without some heavy-duty effort.

Community
  • 1
  • 1
istrasci
  • 1,331
  • 1
  • 18
  • 40
  • What you're trying to do has both polymorphic relationship and single-table-inheritance aspects, both of which Rails supports. The topics are broad and well-traveled elsewhere on the web. Do some googlin' on how to do those two things in Rails and I think you'll find what you're after. – Yardboy Jun 11 '11 at 03:24
  • No, this has nothing to do with polymorphism. *J* is the join table between *A* and *B*, not *J* polymorphically belongs to either *A* or *B*. – istrasci Jun 12 '11 at 00:43
  • Sorry, I read "...and J belongs_to both A and B" as indicating pm. – Yardboy Jun 13 '11 at 18:30

2 Answers2

2

The has_one association you wrote should work exactly as you want it, however Rails won't be able to guess the field from :b_type1.

Just as you explicitly specified the class name, try specifying the source:

has_one :b_type1, :class_name => 'B', :through => :js, :source => bs, :conditions => ["category = ?", "Type 1"]

I tested a similar finder and it worked as you would expect it to.

Felix
  • 801
  • 5
  • 9
  • OK, I'll try it with :source and see what happens. I was messing around with it a lot today and found :source, but of all the combinations of things I tried, I didn't try it exactly this way. – istrasci Jun 14 '11 at 04:38
  • Yes, it seems that it worked just by adding the :source. I guess I was really close. One thing (I'll editing the OP to reflect this) is that I needed to serialize this value to a Flex frontend. So maybe I had it right at some point and I was just not serializing it correctly. Anyway, this seems to solve the problem. Thank you! – istrasci Jun 14 '11 at 20:37
1

There are two "railsier" ways I can think of to solve this.

First Way: Why not just make these methods?

In the first example you gave, try this in your Business class:

def physical_address
   self.address.where(:category => 'physical').first
end

Now calling business.physical_address would return the address object. In the same way you can do this with your HABTM association.

class A < ActiveRecord::Base
  ...
  def type_1
    self.bs.where(:category=>'type_1').first
  end
  ...
end

Now calling a.type_1 would return the single B having a category of 'Type_1' (or the first if there were more than one).

Second Way: More useful if you thought there would ever be more than one match for a given 'virtual relationship', you could just make these conditions a scope. So, for the second example:

class B < ActiveRecord::Base
  ...
  scope :type_1 { where(:category=>'type_1') }
  ...
end

Now if you called a.bs.type_1.first you'd get the B you were looking for. You could also put .first in the scope if you will never have more than one.

To enforce there being only one you can also create a uniqueness validation, validating uniqueness of a category with a given parent object.

Using either of these options seems to me to be much closer to the "Rails Way" of solving your problem than building these "virtual" relationships is. Maybe it's just me though.

Please try this and let me know if you have any trouble :)

Andrew
  • 42,517
  • 51
  • 181
  • 281