在 Rails 中创建一个与 STI 一起使用的动态子类

Posted

技术标签:

【中文标题】在 Rails 中创建一个与 STI 一起使用的动态子类【英文标题】:Create a dynamic subclass in Rails that works with STI 【发布时间】:2022-01-02 04:25:20 【问题描述】:

我想结合以下两件事:

我的 STI 结构:

假设 Rails 模型 User。只有两种类型的用户,即AdminCustomer。 STI 列是type,表称为users,但实际上,我们永远不会有和User 对象,只有AdminsCustomers

User(用于存储所有数据和实现通用功能,但从未实例化) Admin Customer < User

动态扩展模型:

在某些情况下,动态扩展模型类很方便,可以暂时拥有一个具有更多能力的对象来执行特定操作。假设我的create 控制器操作检查附加字段“发送欢迎邮件”(由new 表单提供)。该属性是虚拟的,因此我们可以在表单中使用它,就好像它是一个普通的列一样。这可以通过以下方式实现:

extended_user_class = Class.new(User)
extended_user_class.send(:include, MyAwesomeMixins)
extended_user_class.class_eval do
  my_virtual_attribute :send_welcome_mail, default: true
end

model = extended_user_class.new
# send model to the view and it "just works"

两者结合

不幸的是,将这两种技术结合起来并不能以上述方式工作,因为 Rails STI 似乎被匿名类破坏了:

[1] pry(main)> User.where(id: 1).to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`id` = 1"
[2] pry(main)> Class.new(User).where(id: 1).to_sql
=> "SELECT `users`.* FROM `users` WHERE `users`.`type` IS NULL AND `users`.`id` = 1"

编辑:要重现上述内容,必须在生产环境中。

第二个语句是错误的,因为它按类型过滤。第一个语句是预期的行为。

因此,显然匿名类对 STI 不利。如https://dev.to/factorial/a-trick-with-ruby-anonymous-classes-11pp 中所述重新定义常量User 可能会破坏应用程序 - 我希望扩展类仅在显式调用时使用 (extended_user_class) - User 必须不受影响。

这可以在 Ruby 3 / Rails 6 中实现吗?

【问题讨论】:

感觉您描述的模式与大约 10 年前在 Rails 社区中流行的 DCI 模式非常相似。当时我真的不喜欢这种模式——主要是因为它使代码更难理解和使用,因为你不能再确定一个类的实例在你的应用程序的两个不同地方以相同的方式表现。您总是必须仔细检查是否需要以某种方式更改实例才能在特定上下文中工作。 但更重要的是这个模式有a huge impact on your application's performance。 我在“rails 控制台”中没有遇到同样的问题。 Class.new(Client).where(id: 1).to_sql -> "SELECT \"clients\".* FROM \"clients\" WHERE \"clients\".\"id\" = 1" 这真是一个阅读@spickermann的地狱! 这看起来更像是寻找问题的解决方案。如果您想从模型本身中删除处理表单的逻辑,您可以使用表单对象,但我不明白您为什么要动态生成子类,因为它们只有两个。匿名课程不仅对 STI 不利。 ActiveModel 和 ActiveRecord 基于类名的大量假设。雅格尼。 【参考方案1】:

两种可能的解决方案:

单例类

实验表明,单例类(也称为 Eigenclasses)可能会提供解决方案,但性能损失可能低至 25%。

所以动态扩展模型的部分会变成:

    在普通用户类上操作 实例化新用户 使用其单例类将虚拟属性添加到该实例

修补匿名类以假装它是超类

在此解决方案中,匿名类使用 mixin 进行扩展,将以下三个类方法委托给 superclass

finder_needs_type_condition? descendants name

例如:

def name
  superclass.name
end

这样,匿名类变得透明,并且表现得像它继承的类。这个解决方案是一个相当丑陋的黑客,但它似乎在实践中有效。它比单例类解决方案更丑但更快。

但是有一个警告:

User.all == [admin1, admin2, customer1]
admin1.class == Admin
extended_admin1 = extend_using_patch(admin1)
extended_admin1.class.name == "User"

可以看出,匿名类 extended_admin1 扩展了 User,而不是 Admin

【讨论】:

以上是关于在 Rails 中创建一个与 STI 一起使用的动态子类的主要内容,如果未能解决你的问题,请参考以下文章

如何在 STI rails 中使用父类视图

Rails:在父模型的视图中创建一个 has_one 模型?

Rails 5:在连接表记录上保存记录时的 STI 空键

Rails 中的 WebSockets:在使用 websockets 时,我们是不是必须在现有应用程序中创建一个新的 WebSocketController?

如何在没有后备表的 Rails 中创建只读模型

处理 Rails 中 STI 子类的路由的最佳实践