Rails пользовательский ActiveRecord :: Type завершается ошибкой при использовании `class_name` в has_many: посредством ассоциации - PullRequest
5 голосов
/ 15 февраля 2020

Я использую KSUIDs в качестве замены UUID в моем приложении Rails. michaelherold / ksuid- ruby портировал KSUID на Ruby и реализовал их как ::ActiveRecord::Type::String. Все отлично работает, кроме одной маленькой ошибки при использовании has_many :through associations в сочетании с class_name.

Мне удалось создать два теста rspe c для демонстрации ошибки.

Рабочий тест

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

1 Ответ

1 голос
/ 18 февраля 2020

Это похоже на ошибку rails.

При разрешении рельсов с through -ассоциацией (patient.physicians) ищет отношение с именем, совпадающим с таблицей соединений, и, поскольку его нет, возвращается к типизации в виде строки (= не требуется приведение типов, поэтому ошибка).

Хак, чтобы сделать пример работоспособным, это добавить отношение:

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 (6.1-альфа) имеет запрос на извлечение 36847 объединен, что исправляет некоторые случаи, а также выдает более понятную ошибку:

NotImplementedError: Для правильного ввода приведения Patient.id врач должен определить: Ассоциация встреч.

Но это , кажется, нарушает некоторые другие случаи , поэтому неясно, что будет в выпуске 6.1. Так что пока вышеприведенный хак или обезьяна-патч с защитой от рельсовых версий выглядят как жизнеспособное решение.

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

...