Michael Guterl
Michael Guterl

Reputation: 853

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, :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?

Upvotes: 1

Views: 2248

Answers (5)

kikito
kikito

Reputation: 52621

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

Upvotes: 0

chrisdinn
chrisdinn

Reputation: 751

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.

Upvotes: 1

Luke Francl
Luke Francl

Reputation: 31444

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.

Upvotes: 2

nanda
nanda

Reputation: 1313

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

Upvotes: 0

dan
dan

Reputation: 45602

Try adding before_save hooks

class Location
  def before_save
    self.type = 1
  end
end

and likewise for the other types of location

Upvotes: 0

Related Questions