Избегание STI в Rails
-
21-09-2019 - |
Вопрос
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
Я действительно не вижу никакой причины иметь подклассы в этой ситуации.Поведение объектов location идентично, единственный смысл их заключается в том, чтобы упростить ассоциации.Я также предпочел бы хранить данные в виде int, а не строки, поскольку это позволит уменьшить индексы базы данных.
Я представляю себе что-то вроде следующего, но я не могу завершить эту мысль:
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
С этим кодом следующий сбой, по сути, делает его бесполезным:
user = User.first
location = user.build_location
location.city = "Cincinnati"
location.state = "Ohio"
location.save!
location.type # => nil
Это очевидно, потому что нет способа перевести параметры :conditions в объявлении has_one в тип, равный 1.
Я мог бы вставить идентификатор в представление в любом месте, где появляются эти поля, но это тоже кажется неправильным:
<%= f.hidden_field :type, LOCATION_TYPES[:location] %>
Есть ли какой-нибудь способ избежать дополнительных подклассов или заставить работать подход LOCATION_TYPES?
В нашем конкретном случае приложение очень хорошо ориентировано на местоположение, и объекты могут иметь множество различных типов местоположений.Я просто веду себя странно, не желая всех этих подклассов?
Любые ваши предложения приветствуются, скажите мне, что я сумасшедший, если хотите, но хотели бы вы видеть более 10 различных моделей местоположения, плавающих вокруг приложения / моделей?
Решение
Насколько я могу судить, Местоположение есть местоположение есть Местоположение.Различные "подклассы", на которые вы ссылаетесь (IdealLocation, Birthplace), похоже, просто описывают отношение местоположения к конкретному пользователю.Остановите меня, если я неправильно понял эту часть.
Зная это, я вижу два решения этой проблемы.
Первый заключается в том, чтобы рассматривать местоположения как объекты ценности, а не сущности.(Подробнее об условиях: Значение в сравнении с объектами сущностей (дизайн, ориентированный на домен)).В приведенном выше примере вы, похоже, задаете местоположение "Cincinnati, OH", вместо того, чтобы находить объект "Cincinnati, OH" из базы данных.В этом случае, если бы в Цинциннати существовало много разных пользователей, в вашей базе данных было бы столько же идентичных местоположений "Цинциннати, Огайо", хотя Цинциннати, ОГАЙО всего одно.Для меня это явный признак того, что вы работаете с объектом-ценностью, а не с сущностью.
Как бы выглядело это решение?Вероятно, используя простой объект Location, подобный этому:
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"
Другое решение, которое я вижу, состоит в том, чтобы использовать вашу существующую модель Location ActiveRecord, но просто использовать связь с пользователем для определения отношения "тип", вот так:
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
Все, что вам нужно сделать, чтобы это сработало, - это включить атрибуты location_id, ideal_location_id и birthplace_id в вашу пользовательскую модель.
Другие советы
Почему бы не использовать named_scopes?
Что - то вроде:
class User
has_many :locations
end
class Location
named_scope :ideal, :conditions => "type = 'ideal'"
named_scope :birthplace, :conditions => "type = 'birthplace" # or whatever
end
Затем в вашем коде:
user.locations.ideal => # list of ideal locations
user.locations.birthplace => # list of birthplace locations
Я думаю, вам все равно пришлось бы обрабатывать настройку типа при создании.
Попробуйте добавить крючки before_save
class Location
def before_save
self.type = 1
end
end
и аналогично для других типов местоположений
Вы можете инкапсулировать поведение объектов Location с помощью модулей и использовать некоторый макрос для создания взаимосвязи:
has_one <location_class>,: conditions => [ "type =?" LOCATION_TYPES [: location]],: dependent =>: destroy,: as =>: locatable
Вы можете использовать что-то подобное в своем модуле:
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
Направляющие рельсы:добавление-действия-как-метода-к-активной-записи
Возможно, я упускаю здесь что-то важное, но я подумал, что вы могли бы назвать свои отношения вот так:
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