【问题标题】:Rails custom ActiveRecord::Type fails when using `class_name` in has_many :through association在 has_many 中使用 `class_name` 时,Rails 自定义 ActiveRecord::Type 失败:通过关联
【发布时间】:2020-05-31 05:00:29
【问题描述】:

我在我的 Rails 应用程序中使用 KSUIDs 作为 UUID 的替代品。 michaelherold/ksuid-ruby 将 KSUID 移植到 Ruby 并将它们实现为 ::ActiveRecord::Type::String。使用has_many :through associationsclass_name 时,除了一个小错误之外,一切都运行良好。

我能够创建两个 rspec 测试来演示该错误。

工作测试

https://github.com/mattes/ksuid-ruby/blob/e545b1b251bd6430c454509475963a7845b1da0f/spec/cast1_spec.rb#L50-L58

# code excerpt from link above
class Patient < ActiveRecord::Base
  act_as_ksuid :id
  has_many :appointments
  has_many :physicians, through: :appointments
end

bundle exec rspec ./spec/cast1_spec.rb # works as expected, tests pass

测试失败

当我将患者的预约关联更新为使用class_name 时,将导致TypeError: can't cast KSUID::Type

https://github.com/mattes/ksuid-ruby/blob/e545b1b251bd6430c454509475963a7845b1da0f/spec/cast2_spec.rb#L46

# code excerpt from link above
class Patient < ActiveRecord::Base
  act_as_ksuid :id
  has_many :foobar, class_name: "Appointment" # <---- using class_name here
  has_many :physicians, through: :foobar
end

bundle exec rspec ./spec/cast2_spec.rb # test fails

TypeError:
  can't cast KSUID::Type
# ./spec/cast2_spec.rb:59:in `block (2 levels) in <top (required)>'

您能帮我找出问题并修复测试吗?重现自己运行:

git clone https://github.com/mattes/ksuid-ruby.git
cd ksuid-ruby
git checkout cast_error
bundle install
bundle exec rspec ./spec/cast1_spec.rb # works
bundle exec rspec ./spec/cast2_spec.rb # fails

【问题讨论】:

  • 请在链接到您的 github 存储库时使用永久链接,以防止链接失效。您可以通过右键单击任何代码行并选择“复制永久链接”来获取永久链接。如果可能的话,您还应该包含足够的有问题的代码来重现问题。
  • 我将代码放在我不打算更改的分支中。我已经尝试尽可能地简化它。我不认为在这种特殊情况下包含更多代码会有所帮助。
  • 是的,但如果这些链接被破坏,它确实可以防止问题在未来变得完全无用。
  • 已更新永久链接

标签: ruby-on-rails ruby has-many-through ruby-on-rails-6 ksuid


【解决方案1】:

这看起来像一个 Rails 错误。

在解析through-association (patient.physicians) 时,rails 会查找与连接表名称相同的关系,并且由于没有 - 回退到作为字符串进行类型转换(=不需要类型转换,因此出现错误) .

使示例工作的一个技巧是添加关系:

class Physician < ActiveRecord::Base
  act_as_ksuid :id

  has_many :foobar, class_name: "Appointment"
  has_many :patients, through: :foobar

  has_many :appointments # <= this is not used in app, but rails now can correctly resolve types
end

Rails master (6.1-alpha) 已合并 pull request 36847,这修复了某些情况并输出更易于理解的错误:

NotImplementedError:为了正确键入 cast Patient.id,Physician 需要定义一个 :appointments 关联。

但它是appears to break some other cases,所以不确定 6.1 版本中会出现什么。因此,就目前而言,上述带有 Rails 版本保护的 hack 或猴子补丁看起来是一个可行的解决方案。

附言。无论实际的适配器是什么,您的 ksuid/activerecord/schema_statements 都会将类型添加到 PostgreSQLAdapter

PPS。您可以使用bundler/inlineminitest/autorun 创建一个独立的示例(只需ruby filename.rb 即可运行):

require 'bundler/inline'

gemfile(ENV['INSTALL']=='1') do
  source 'https://rubygems.org'
  gem 'activerecord', '~>6.0.2' # also tested '~>5.2', '5.0' with same result, master with different error
  gem 'sqlite3' # use , '~> 1.3.6' # for rails 5
  gem 'ksuid', github: 'mattes/ksuid-ruby', ref:'e545b1b251bd6430c454509475963a7845b1da0f'

  gem 'minitest'
end

require "active_record"
require "logger"

require "ksuid/activerecord"
require "ksuid/activerecord/table_definition"

# require "rails"
# require "ksuid/activerecord/schema_statements" # commented out to not load rails only to check Rails.env, instead:
require "active_record/connection_adapters/sqlite3_adapter"
::ActiveRecord::ConnectionAdapters::SQLite3Adapter::NATIVE_DATABASE_TYPES[:ksuid] = { name: "varchar", limit: 27 }

# require "ksuid/activerecord/quoting" # monkey-patch that fixes the error


ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(IO::NULL)
ActiveRecord::Schema.verbose = false

ActiveRecord::Schema.define do
  create_table(:physicians,   force: true, id: :ksuid)
  create_table(:patients,     force: true, id: :ksuid)
  create_table(:appointments, force: true, id: :ksuid) {|t| t.ksuid :physician_id, :patient_id }
end

class Physician < ActiveRecord::Base
  act_as_ksuid :id
  has_many :foobar, class_name: "Appointment"
  has_many :patients, through: :foobar

  has_many :appointments # the hack
end

class Appointment < ActiveRecord::Base
  act_as_ksuids :id, :physician_id, :patient_id
  belongs_to :physician
  belongs_to :patient
end

class Patient < ActiveRecord::Base
  act_as_ksuid :id
  has_many :appointments
  has_many :physicians, through: :appointments
end

ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)

require "minitest/autorun"

describe "ActiveRecord integration" do
  it "loads all associations correctly" do
    patient = Patient.create!
    physician = Physician.create!
    appointment = Appointment.create!(patient_id: patient.id, physician_id: physician.id)
    expect(patient.id.class).must_equal KSUID::Type

    expect(patient.physicians.first).must_equal physician
    expect(physician.patients.first).must_equal patient
  end
end

【讨论】:

  • 这很棒。也非常感谢您找到 rails 错误报告。我一定错过了他们。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-06
  • 1970-01-01
  • 2014-07-06
相关资源
最近更新 更多