【问题标题】:Avoiding STI in Rails在 Rails 中避免 STI
【发布时间】:2011-01-18 03:11:55
【问题描述】:
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

在这种情况下,我真的看不出有任何理由拥有子类。位置对象的行为是相同的,唯一的一点是使关联更容易。我还希望将数据存储为 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

这很明显,因为无法将 has_one 声明中的 :conditions 选项转换为等于 1 的类型。

我可以将 id 嵌入到视图中出现这些字段的任何位置,但这似乎也是错误的:

<%= f.hidden_field :type, LOCATION_TYPES[:location] %>

有什么方法可以避免额外的子类或使 LOCATION_TYPES 方法起作用?

在我们的特定情况下,应用程序非常了解位置,并且对象可以有许多不同类型的位置。我是不是很奇怪不想要所有这些子类?

感谢您的任何建议,如果您愿意,请告诉我我疯了,但是您是否希望看到 10 多个不同的位置模型在应用程序/模型周围浮动?

【问题讨论】:

    标签: ruby-on-rails single-table-inheritance


    【解决方案1】:

    为什么不使用 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
    

    我认为,您仍然需要在创建时设置类型。

    【讨论】:

      【解决方案2】:

      据我所知,位置就是位置。您所指的不同“子类”(IdealLocation,Birthplace)似乎只是在描述该位置与特定用户的关系。如果我弄错了那部分,请阻止我。

      知道了这一点,我可以看到两种解决方案。

      第一个是将位置视为值对象而不是实体。 (有关条款的更多信息:Value vs Entity objects (Domain Driven Design))。在上面的示例中,您似乎将位置设置为“Cincinnati, OH”,而不是从数据库中查找“Cincinnati, OH”对象。在这种情况下,如果 Cincinnati 中存在许多不同的用户,那么您的数据库中将有相同数量的“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 模型,但只需使用与 User 的关系来定义关系“类型”,如下所示:

      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 属性。

      【讨论】:

      • 你是对的,子类仅仅描述了位置与用户的关系。感谢您指向关于值对象与实体的文章。你是对的,使用我目前的解决方案,我最终会在位置表中复制很多行。不幸的是,值对象场景无法解决,因为 Location 模型实际上有更多的行为并且需要通过 SQL 访问。您的 belongs_to 方法似乎一针见血,我不知道为什么我首先使用 has_one 来避免它。
      【解决方案3】:

      尝试添加 before_save 钩子

      class Location
        def before_save
          self.type = 1
        end
      end
      

      对于其他类型的位置也是如此

      【讨论】:

      • 我不想有其他类型的位置是重点。感谢您真正阅读我发布的内容。
      • 我试图建议如何使您失败的示例(使用 location.type => nil)工作。
      【解决方案4】:

      您可以使用模块封装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
      

      Rails guides: add-an-acts-as-method-to-active-record

      【讨论】:

      【解决方案5】:

      也许我在这里遗漏了一些重要的东西,但我认为你可以这样命名你的关系:

      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
      

      【讨论】:

      • 来自 Rails API:has_one 指定与另一个类的一对一关联。仅当其他类包含外键时才应使用此方法。如果当前类包含外键,那么您应该改用belongs_to。这样的事情可能使用belongs_to 来代替,chrisdinn 刚刚发布了类似的东西,但是使用belongs_to,我将不得不评估他的帖子。谢谢。
      猜你喜欢
      • 1970-01-01
      • 2011-04-02
      • 1970-01-01
      • 2021-12-12
      • 2015-04-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多