【问题标题】:Rails STI association with subclassesRails STI 与子类的关联
【发布时间】:2012-06-20 11:21:05
【问题描述】:

在使用 STI 时,从与 rails 3 的 has_many 关联中获取集合时,我遇到了一些奇怪的行为。我有:

class Branch < ActiveRecord::Base
   has_many :employees, class_name: 'User::Employee'
   has_many :admins, class_name: 'User::BranchAdmin'
end

class User < ActiveRecord::Base
end

class User::Employee < User
  belongs_to :branch
end

class User::BranchAdmin < User::Employee
end

期望的行为是 branch.employees 返回所有员工,包括分支机构管理员。只有当branch.admins 访问过分支管理员时,它们似乎才被“加载”到此集合下,这是控制台的输出:

Branch.first.employees.count
=> 2

Branch.first.admins.count
=> 1

Branch.first.employees.count
=> 3

在生成的SQL中可以看到,第一次:

SELECT COUNT(*) FROM "users" WHERE "users"."type" IN ('User::Employee') AND "users"."branch_id" = 1

第二次:

SELECT COUNT(*) FROM "users" WHERE "users"."type" IN ('User::Employee', 'User::BranchAdmin') AND "users"."branch_id" = 1

我可以通过指定来解决这个问题:

class Branch < ActiveRecord::Base
   has_many :employees, class_name: 'User'
   has_many :admins, class_name: 'User::BranchAdmin'
end

因为它们都是从它们的 branch_id 中找到的,但是如果我想做branch.employees.build,这会在控制器中产生问题,那么该类将默认为User,我必须在某处修改类型列。我现在已经解决了这个问题:

  has_many :employees, class_name: 'User::Employee', 
    finder_sql: Proc.new{
      %Q(SELECT users.* FROM users WHERE users.type IN          ('User::Employee','User::BranchAdmin') AND users.branch_id = #{id})
    },
    counter_sql: Proc.new{
      %Q(SELECT COUNT(*) FROM "users" WHERE "users"."type" IN ('User::Employee', 'User::BranchAdmin') AND "users"."branch_id" = #{id})
    }

但如果可能的话,我真的很想避免这种情况。任何人,有什么想法吗?

编辑:

finder_sql 和 counter_sql 并没有真正为我解决这个问题,因为似乎父关联不使用它,所以 organisation.employees has_many :employees, through: :branches 将再次只在选择中包含 User::Employee 类。

【问题讨论】:

    标签: sql ruby-on-rails activerecord sti


    【解决方案1】:

    基本上,问题只存在于按需加载类的开发环境中。 (在生产中,类被加载并保持可用。)

    问题出现是因为解释器在您第一次运行Employee.find 等调用时还没有看到AdminsEmployee 的一种类型。

    (注意后面会用到IN ('User::Employee', 'User::BranchAdmin')

    每次使用深度超过一层的模型类时都会发生这种情况,但仅限于开发模式。

    子类总是自动加载其父层次结构。基类不会自动加载其子层次结构。

    黑客修复:

    您可以通过明确要求基类 rb 文件中的所有子类来强制在开发模式下执行正确的行为。

    【讨论】:

    • 这是一个很好的收获,谢谢。模型结构实际上已经改变,所以问题消失了,但我认为我什至不会认为这是环境的影响!
    • 您确定这只是处于开发模式吗?我相信我在生产中遇到过这种行为。运行轨道 5.2
    【解决方案2】:

    你能用:conditions吗?

    class Branch < ActiveRecord::Base
       has_many :employees, class_name: 'User::Employee', :conditions => {:type => "User::Employee"}
       has_many :admins, class_name: 'User::BranchAdmin', :conditions => {:type => "User::BranchAdmin"}
    end
    

    这将是我的首选方法。另一种方法可能是为多态模型添加默认范围。

    class User::BranchAdmin < User::Employee
      default_scope where("type = ?", name)
    end
    

    【讨论】:

    • 我确实尝试过使用条件,但也遇到了问题。应用程序的结构现在已经改变,所以我不需要担心这个,这可能已经在 rails 3.2.7 中修复了。
    【解决方案3】:

    类似的问题在 Rails 6 中仍然存在。

    此链接概述了issue and workaround。它包含以下解释和代码sn-p:

    Active Record 需要完全加载 STI 层次结构才能生成正确的 SQL。 Zeitwerk 中的预加载是为此用例设计的:

    通过预加载树的叶子,自动加载将处理父类之后的整个层次结构。

    这些文件将在启动时和每次重新加载时预加载。

    # config/initializers/preload_vehicle_sti.rb
    
    autoloader = Rails.autoloaders.main
    sti_leaves = %w(car motorbike truck)
    
    sti_leaves.each do |leaf|
      autoloader.preload("#{Rails.root}/app/models/#{leaf}.rb")
    end
    

    您可能需要spring stop 才能进行配置更改。

    【讨论】:

      【解决方案4】:

      确实,这是 gem 早期的计划,但很快就被放弃了(在 2019 年,在 Rails 6 发布之前)。预加载已被弃用很长时间,并已在即将发布的 Zeitwerk 2.5 中删除。

      在 Rails 应用程序中,您可以这样做:

      # config/initializers/preload_vehicle_sti.rb
      Rails.application.config.to_prepare do
        Car
        Motorbike
        Truck
      end
      

      也就是说,您只需使用 to_prepare 块中的常量来“预加载”。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-12-16
        • 1970-01-01
        • 2011-09-28
        相关资源
        最近更新 更多