【问题标题】:Is it possible to get Rails associations to return subclasses instead of the superclass?是否可以让 Rails 关联返回子类而不是超类?
【发布时间】:2015-08-31 15:45:52
【问题描述】:

首先,继承方法可能不正确。如果是这样,请解释另一种方法。

这是一个设置示例。

除了使用完全相同的数据库之外,我还有许多未连接的应用程序。为了使代码库枯竭,我有一个包含所有活动记录类的引擎。我想在主要应用程序中保留应用程序特定的范围和方法。

在我的 Rails 引擎中,

class MyEngine::User < ActiveRecord::Base
  has_many :dogs
end

class MyEngine::Dog < ActiveRecord::Base
  belongs_to :user
end

在我的主应用中,

class User < MyEngine::User
end

class Dog < MyEngine::Dog
  # EDITED TO SHOW EXAMPLE OF HOW SCOPE DOESN'T BELONG IN ENGINE
  DOGS_WITH_SPOTS_IDS = [ 1, 2, 3, 4 ]

  scope :with_spots, -> { where(id: DOGS_WITH_SPOTS_IDS) }
end

我的主应用程序中的问题,

user = User.last
user.dogs
# => #<ActiveRecord::Associations::CollectionProxy[#<MyEngine::Dog spots: true>]>
user.dogs.with_spots
NoMethodError: undefined method 'with_spots' for #<MyEngine::Dog::ActiveRecord_Association_CollectionProxy:0x007fa4bf838e50>

虽然这可行

class User < MyEngine::User
  has_many :dogs
end

我不希望重新定义/定义主应用程序中的所有关联。

这种类型的问题只会在我所有的主要应用程序中以不同的形式出现。这让我想到可能有办法重新定义关联。

有没有一种方法可以评估子类而不是超类的关联,就像 STI 模式一样,让 Rails 助手识别不同的子类?

意思是,我希望我的主应用 User#dogs 返回主应用 Dog 而不是 MyEngine::Dog。

# may not be the exact code, just off the top of my head

instance_eval do
  def model_name
    self.class.name
  end
end

【问题讨论】:

    标签: ruby ruby-on-rails-4 activerecord associations metaprogramming


    【解决方案1】:

    重要提示你不能查询user.dogs.with_spots,即使没有继承。您可能的意思是在user.dogs.to_a 中获取Dogs 的数组。

    解决方案 解决问题的最通用方法是使用Single Table Inheritance

    让我们看看它是如何工作的。

    我假设已经有表my_engine_usersmy_engine_dogs 定义了所有必需的列(就MyEngine 模块而言)。

    MyEngine::UserMyEngine::Dog 类保持不变。

    UserDog 类只是扩展了各自的引擎类,没有任何关于它们之间关系的额外声明。

    我们告诉 Rails,这两个表可能被不同的类使用(通过扩展)。通过将type 列添加到表中很容易实现。

    add_column :my_engine_users, :type, :string
    

     add_column :my_engine_dogs, :type, :string
    

    现在您可以与您的用户和狗一起做所有美妙的事情。

    dimakura = User.create(username: 'dimakura')
    dora = Dog.create(name: 'Dora', owner: dimakura)
    fanta = Dog.create(name: 'Fanta', owner: dimakura)
    dimakura.dogs.to_a # => array of Dogs, not MyEngine::Dogs
    

    这个魔法是通过在type 列中显式写入类名来实现的。

    p dimakura #=> #<User id: 1, username: "dimakura", type: "User">
    p dora #=> #<Dog id: 1, owner_id: 1, name: "Dora", type: "Dog">
    p fanta #=> #<Dog id: 2, owner_id: 1, name: "Fanta", type: "Dog">
    

    【讨论】:

    • 我从来没有把我真的只是在使用 STI 放在一起。非常感谢!
    【解决方案2】:

    MyEngine::User.dogs 返回 [由关联实例代理] 一组 MyEngine::Dog 实例,通常可能没有 with_spots 范围。

    如果您知道所有MyEngine::Dogs “响应”到with_spots,只需将范围定义移动到MyEngine::Dog。如果不是这种情况,则在 MyEngine::User.dogs 上调用此范围是没有意义的。

    UPD添加的示例可能会解决:

    class MyEngine::Dog < ActiveRecord::Base
      belongs_to :user
    
      scope :with_spots, -> { self.class.const_get(:SCOPE_PROC) }
    end
    
    class Dog < MyEngine::Dog
      DOGS_WITH_SPOTS_IDS = [ 1, 2, 3, 4 ]
      SCOPE_PROC = -> { where(name: DOGS_WITH_SPOTS_IDS }
    end
    

    【讨论】:

    • 您是对的,但并非所有 MyEngine::Dog 实例都响应 with_spots。我的现实生活情况比这要复杂一些。我将更改我的示例以更好地匹配它。
    • 虽然该解决方案确实有效,但我根本不希望引擎知道斑点。
    • 引擎不知道范围⇒引擎关系不知道范围。就这么简单。
    • 对不起,如果我不够清楚。感谢您的帮助。
    【解决方案3】:

    我只是想发布一个跟进:

    虽然@dimakura 是正确的,但它仍然不适合我想要做的事情。这是因为 Rails 不允许 STI 的链接范围。最后,我删除了 STI 并使用了以下内容:

    user = User.first
    user.dogs.merge(Dog.with_spots)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-14
      • 1970-01-01
      • 2018-05-09
      • 2019-11-14
      • 2019-05-09
      相关资源
      最近更新 更多