Evitando DST em trilhos
-
21-09-2019 - |
Pergunta
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
Não consigo ver nenhum motivo para ter subclasses nessa situação. O comportamento dos objetos de localização é idêntico, o único ponto deles é facilitar as associações. Eu também preferiria armazenar os dados como uma string e não uma string, pois eles permitirão que os índices de banco de dados sejam menores.
Eu imagino algo como o seguinte, mas não consigo completar o pensamento:
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
Com este código, o seguinte falha, basicamente o tornando inútil:
user = User.first
location = user.build_location
location.city = "Cincinnati"
location.state = "Ohio"
location.save!
location.type # => nil
Isso é óbvio, porque não há como traduzir as opções: Condições na declaração Has_One no tipo igual a 1.
Eu poderia incorporar o ID na visão em qualquer lugar que esses campos apareçam, mas isso também parece errado:
<%= f.hidden_field :type, LOCATION_TYPES[:location] %>
Existe alguma maneira de evitar as subclasses extras ou fazer com que a abordagem de localização_types funcione?
Em nosso caso específico, o aplicativo está muito ciente do local e os objetos podem ter muitos tipos diferentes de locais. Estou apenas sendo estranho não querendo todas essas subclasses?
Alguma sugestão que você tem é apreciada, diga -me que estou louco, se você quiser, mas gostaria de ver mais de 10 modelos de localização diferentes flutuando em torno de aplicativos/modelos?
Solução
Até onde eu posso ver, um local é um local é um local. As diferentes "subclasses" a que você está se referindo (ideallocation, local de nascimento) parecem estar apenas descrevendo o relacionamento do local com um usuário em particular. Pare -me se eu tiver essa parte errada.
Sabendo disso, posso ver duas soluções para isso.
O primeiro é tratar os locais como objetos de valor em vez de entidades. (Para mais informações sobre os termos: Valor vs entidade Objetos (design orientado ao domínio)). No exemplo acima, você parece estar definindo o local para "Cincinnati, Oh", em vez de encontrar um objeto "Cincinnati, OH" do banco de dados. Nesse caso, se muitos usuários diferentes existissem em Cincinnati, você teria tantos locais idênticos "Cincinnati, OH" no seu banco de dados, embora exista apenas um Cincinnati, OH. Para mim, isso é um sinal claro de que você está trabalhando com um objeto de valor, não uma entidade.
Como seria essa solução? Provavelmente usando um objeto de localização simples como este:
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"
A outra solução que posso ver é usar o seu modelo de ActiveRecord existente, mas simplesmente usar o relacionamento com o usuário para definir o relacionamento "tipo", assim:
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
Tudo o que você precisa fazer para fazer esse trabalho é incluir atributos Location_ID, ideal_location_id e birthplace_id no seu modelo de usuário.
Outras dicas
Por que não usar o nome do nome?
Algo como:
class User
has_many :locations
end
class Location
named_scope :ideal, :conditions => "type = 'ideal'"
named_scope :birthplace, :conditions => "type = 'birthplace" # or whatever
end
Então no seu código:
user.locations.ideal => # list of ideal locations
user.locations.birthplace => # list of birthplace locations
Você ainda teria que lidar com definir o tipo de criação, eu acho.
Tente adicionar ganchos antes_save
class Location
def before_save
self.type = 1
end
end
E da mesma forma para os outros tipos de localização
Você pode encapsular o comportamento dos objetos de localização usando módulos e usar alguma macro para criar o relacionamento:
has_one <location_class>,: conditions => [ "type =?" LOCATION_TYPES [: location]],: dependent =>: destroy,: as =>: locatable
Você pode usar algo assim no seu módulo:
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
Talvez eu esteja perdendo algo importante aqui, mas pensei que você poderia nomear seus relacionamentos assim:
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