是否有可能获得 Rails 与 return 子类而不是超类的关联?

Is it possible to get Rails associations to return subclasses instead of the superclass?

首先,继承方式可能不正确。如果是这样,请解释不同的方法。

这是一个设置示例。

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

在我的 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

我不想 redefine/define 主要应用程序中的所有关联。

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

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

意思是,我希望我的主应用 User#dogs 成为 return 主应用 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

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

如果您知道所有 MyEngine::Dog 都“响应”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

重要说明即使没有继承也不能查询user.dogs.with_spots。您的意思可能是在 user.dogs.to_a.

中获取 Dog 的数组

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

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

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

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

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

我们所做的,我们告诉 Rails,这两个表可能被不同的 classes 使用(通过扩展)。通过向表中添加 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 列中显式写入 class 名称来实现的。

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">

我只想post跟进:

虽然@dimakura 是正确的,但它仍然无法满足我的要求。这是因为 Rails 不允许 STI 的链接作用域。最后,我删除了 STI 并使用了以下内容:

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