开发者

Avoiding STI in Rails

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, :c开发者_JS百科onditions => ["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?


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.


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


Try adding before_save hooks

class Location
  def before_save
    self.type = 1
  end
end

and likewise for the other types of location


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


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
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜