Rails 在 has_many 中使用 `class_name` 时自定义 ActiveRecord::Type 失败:通过关联
Rails custom ActiveRecord::Type fails when using `class_name` in has_many :through association
我正在使用 KSUIDs as a replacement for UUIDs in my Rails app. michaelherold/ksuid-ruby 将 KSUID 移植到 Ruby 并将它们实现为 ::ActiveRecord::Type::String
。除了使用 has_many :through associations
和 class_name
时的一个小错误外,一切都很好。
我能够创建两个 rspec 测试来演示该错误。
工作测试
# 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
。
# 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
这看起来像一个 rails 错误。
解析 through
-关联 (patient.physicians
) 时 rails 查找与连接 table 同名的关系,并且因为有 none - 退回到类型转换为字符串(=不需要类型转换,因此是错误)。
使示例正常工作的一个技巧是添加关系:
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: In order to correctly type cast Patient.id, Physician needs to define a :appointments association.
但它 appears to break some other cases,所以不确定 6.1 版本中会出现什么。所以现在上面的 hack 或带有 rails version guard 的 monkey-patch 看起来是一个可行的解决方案。
PS。您的 ksuid/activerecord/schema_statements
将类型添加到 PostgreSQLAdapter
,无论实际适配器是什么。
PPS。您可以使用 bundler/inline
和 minitest/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
我正在使用 KSUIDs as a replacement for UUIDs in my Rails app. michaelherold/ksuid-ruby 将 KSUID 移植到 Ruby 并将它们实现为 ::ActiveRecord::Type::String
。除了使用 has_many :through associations
和 class_name
时的一个小错误外,一切都很好。
我能够创建两个 rspec 测试来演示该错误。
工作测试
# 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
。
# 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
这看起来像一个 rails 错误。
解析 through
-关联 (patient.physicians
) 时 rails 查找与连接 table 同名的关系,并且因为有 none - 退回到类型转换为字符串(=不需要类型转换,因此是错误)。
使示例正常工作的一个技巧是添加关系:
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: In order to correctly type cast Patient.id, Physician needs to define a :appointments association.
但它 appears to break some other cases,所以不确定 6.1 版本中会出现什么。所以现在上面的 hack 或带有 rails version guard 的 monkey-patch 看起来是一个可行的解决方案。
PS。您的 ksuid/activerecord/schema_statements
将类型添加到 PostgreSQLAdapter
,无论实际适配器是什么。
PPS。您可以使用 bundler/inline
和 minitest/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