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
精彩评论