views:

328

answers:

5
class User < ActiveRecord::Base
  has_one :location, :dependent => :destroy, :as => :locatable
  has_one :ideal_location, :dependent => :destroy, :as => :locatable
  has_one :birthplace, :dependent => :destroy, :as => :locatable
end

class Location < ActiveRecord::Base
  belongs_to :locatable, :polymorphic => true
end

class IdealLocation < ActiveRecord::Base
end

class Birthplace < ActiveRecord::Base
end

I can't really see any reason to have subclasses in this situation. The behavior of the location objects are identical, the only point of them is to make the associations easy. I also would prefer to store the data as an int and not a string as it will allow the database indexes to be smaller.

I envision something like the following, but I can't complete the thought:

class User < ActiveRecord::Base
  LOCATION_TYPES = { :location => 1, :ideal_location => 2, :birthplace => 3 }

  has_one :location, :conditions => ["type = ?", LOCATION_TYPES[:location]], :dependent => :destroy, :as => :locatable
  has_one :ideal_location, :conditions => ["type = ?", LOCATION_TYPES[:ideal_location]], :dependent => :destroy, :as => :locatable
  has_one :birthplace, :conditions => ["type = ?", LOCATION_TYPES[:birthplace]], :dependent => :destroy, :as => :locatable
end

class Location < ActiveRecord::Base
  belongs_to :locatable, :polymorphic => true
end

With this code the following fails, basically making it useless:

user = User.first
location = user.build_location
location.city = "Cincinnati"
location.state = "Ohio"
location.save!

location.type # => nil

This is obvious because there is no way to translate the :conditions options on the has_one declaration into the type equaling 1.

I could embed the id in the view anywhere these fields appear, but this seems wrong too:

<%= f.hidden_field :type, LOCATION_TYPES[:location] %>

Is there any way to avoid the extra subclasses or make the LOCATION_TYPES approach work?

In our particular case the application is very location aware and objects can have many different types of locations. Am I just being weird not wanting all those subclasses?

Any suggestions you have are appreciated, tell me I'm crazy if you want, but would you want to see 10+ different location models floating around app/models?

A: 

Try adding before_save hooks

class Location
  def before_save
    self.type = 1
  end
end

and likewise for the other types of location

dan
I don't want to have other types of location is the point. Thanks for actually reading what I posted.
Michael Guterl
I was trying to suggest how to make your failing example (with location.type => nil) work.
dan
A: 

You can encapsulate the behavior of Location objects using modules, and use some macro to create the relationship:

has_one <location_class>,: conditions => [ "type =?" LOCATION_TYPES [: location]],: dependent =>: destroy,: as =>: locatable

You can use something like this in your module:

module Orders
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def some_class_method(param)
    end

    def some_other_class_method(param)
    end

    module InstanceMethods
      def some_instance_method
      end
    end
  end
end

Rails guides: add-an-acts-as-method-to-active-record

nanda
Some gist examples for modules:Without acts_as: http://gist.github.com/307290With acts_as: http://gist.github.com/307296
nanda
+2  A: 

Why not use named_scopes?

Something like:

class User
  has_many :locations
end

class Location
  named_scope :ideal, :conditions => "type = 'ideal'"
  named_scope :birthplace, :conditions => "type = 'birthplace" # or whatever
end

Then in your code:

user.locations.ideal => # list of ideal locations
user.locations.birthplace => # list of birthplace locations

You'd still have to handle setting the type on creation, I think.

Luke Francl
A: 

As far as I can see it, a Location is a location is a Location. The different "subclasses" you're referring to (IdealLocation, Birthplace) seem to just be describing the location's relationship to a particular User. Stop me if I've got that part wrong.

Knowing that, I can see two solutions to this.

The first is to treat locations as value objects rather than entities. (For more on the terms: http://stackoverflow.com/questions/75446/value-vs-entity-objects-domain-driven-design). In the example above, you seem to be setting the location to "Cincinnati, OH", rather than finding a "Cincinnati, OH" object from the database. In that case, if many different users existed in Cincinnati, you'd have just as many identical "Cincinnati, OH" locations in your database, though there's only one Cincinnati, OH. To me, that's a clear sign that you're working with a value object, not an entity.

How would this solution look? Likely using a simple Location object like this:

class Location
  attr_accessor :city, :state

  def initialize(options={})
    @city = options[:city]
    @state = options[:state]
  end
end

class User < ActiveRecord::Base
  serialize :location
  serialize :ideal_location
  serialize :birthplace
end

@user.ideal_location = Location.new(:city => "Cincinnati", :state => "OH")
@user.birthplace = Location.new(:city => "Detroit", :state => "MI")
@user.save!

@user.ideal_location.state # => "OH"

The other solution I can see is to use your existing Location ActiveRecord model, but simply use the relationship with User to define the relationship "type", like so:

class User < ActiveRecord::Base
  belongs_to :location, :dependent => :destroy
  belongs_to :ideal_location, :class_name => "Location", :dependent => :destroy
  belongs_to :birthplace, :class_name => "Location", :dependent => :destroy
end

class Location < ActiveRecord::Base
end

All you'd need to do to make this work is include location_id, ideal_location_id, and birthplace_id attributes in your User model.

chrisdinn
You are correct, the subclasses merely describes the location's relationship to the user.Thank you for the pointer to the article on value objects versus entities. You're right, with my current solution I'm going to end up duplicating a lot of rows in the locations table.Unfortunately the value object scenario won't work out as the Location model actually has much more behavior and needs to be accessible via SQL.Your belongs_to approach seems to hit the nail on the head, I'm not sure why I avoided it in the first place by using has_one instead.
Michael Guterl
A: 

Maybe I'm missing something important here, but I thought you could name your relationships like this:

class User < ActiveRecord::Base

  has_one :location, :dependent => :destroy
  #ideal_location_id
  has_one :ideal_location, :class_name => "Location", :dependent => :destroy
  #birthplace_id
  has_one :birthplace, :class_name => "Location", :dependent => :destroy

end

class Location < ActiveRecord::Base
  belongs_to :user # user_id
end
egarcia
From Rails API: has_one specifies a one-to-one association with another class. This method should only be used if the other class contains the foreign key. If the current class contains the foreign key, then you should use belongs_to instead.Something like this might work using belongs_to instead, chrisdinn just posted something similar but with belongs_to, I'll have to evaluate his post. Thanks.
Michael Guterl