Ассоциация Ruby ActiveRecord генерирует ошибку при доступе к дочернему элементу - PullRequest
0 голосов
/ 27 июня 2018

Введение : я создаю отдельное (не Rails) приложение, которое использует ActiveRecord и ActiveRecord SQL Server Adapter для доступа к базе данных для коммерческого приложения. Я не могу изменить схему базы данных или сервер базы данных так, как мне бы хотелось. Я пытался изменить имена ниже, чтобы защитить виновных.

Базовый класс модели:

class AppRecord < ActiveRecord::Base
  after_initialize :parent_init
  attr_reader :_downcase_field_values
  attr_accessor :downcase_field_values

  self.primary_key = 'IRN'

  def parent_init
    set_downcase_field_values
  end

  def set_downcase_field_values
    @downcase_field_values ||= []
    @_downcase_field_values = self.attributes.keys.select { |att| att if (att.match(/IRN/) || att == "Id") }
    downcase_fields! self
  end

  def self.table_name
    "app.#{self}"
  end

  def to_h
    self.attributes.to_options
  end
end

Модель классов:

class ReportIndex < AppRecord
  after_find :init

  has_many :ReportIndexParameters, class_name: "ReportIndexParameters", foreign_key: "ReportIndexIRN", dependent: :destroy
  has_many :ReportProperties, class_name: "ReportProperties", foreign_key: "ReportIndexIRN", dependent: :destroy
  has_many :ReportLayouts, class_name: "ReportLayouts", foreign_key: "ReportIndexIRN", dependent: :destroy

  has_many :ReportIndexSeries, class_name: "ReportIndexSeries", foreign_key: "ReportIndexesIRN", dependent: :destroy
  has_many :ReportUserDefinedViews, class_name: "ReportUserDefinedViews", foreign_key: "BaseClassID", primary_key: "ClassId"

  def init
    self.downcase_field_values = %w(BaseClassID)
  end
end

class ReportProperties < AppRecord
  belongs_to :ReportIndex
  after_find :init
  has_many :ReportPropertySeriesFilters, class_name: "ReportPropertySeriesFilters", foreign_key: "ReportPropertiesIRN", dependent: :destroy
  has_many :ReportPropertyParameters, class_name: "ReportPropertyParameters", foreign_key: "ReportPropertiesIRN", dependent: :destroy
  def init
    self.downcase_field_values = %w(ClassID)
  end
end

Запрос ActiveRecord:

pp ReportIndex.includes(:ReportProperties).find_by(ReportName: report_name)

Ошибка:

/usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader/association.rb:63:in `block in associated_records_by_owner': undefined method `association' for nil:NilClass (NoMethodError)
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/core.rb:367:in `init_with'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/persistence.rb:69:in `instantiate'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/querying.rb:50:in `block (2 levels) in find_by_sql'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/result.rb:55:in `block in each'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/result.rb:55:in `each'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/result.rb:55:in `each'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/querying.rb:50:in `map'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/querying.rb:50:in `block in find_by_sql'
    from /usr/local/bundle/gems/activesupport-5.1.6/lib/active_support/notifications/instrumenter.rb:21:in `instrument'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/querying.rb:49:in `find_by_sql'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/relation.rb:678:in `exec_queries'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/relation.rb:546:in `load'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader/association.rb:122:in `block in load_records'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader/association.rb:121:in `each'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader/association.rb:121:in `each_slice'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader/association.rb:121:in `each'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader/association.rb:121:in `flat_map'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader/association.rb:121:in `load_records'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader/association.rb:61:in `associated_records_by_owner'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader/collection_association.rb:8:in `preload'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader/association.rb:19:in `run'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:151:in `block (2 levels) in preloaders_for_one'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:149:in `each'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:149:in `map'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:149:in `block in preloaders_for_one'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:148:in `each'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:148:in `flat_map'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:148:in `preloaders_for_one'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:115:in `preloaders_on'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:102:in `block in preload'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:101:in `each'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:101:in `flat_map'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/associations/preloader.rb:101:in `preload'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/relation.rb:686:in `block in exec_queries'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/relation.rb:684:in `each'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/relation.rb:684:in `exec_queries'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/relation.rb:546:in `load'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/relation.rb:255:in `records'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/relation/finder_methods.rb:508:in `find_take'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/relation/finder_methods.rb:100:in `take'
    from /usr/local/bundle/gems/activerecord-5.1.6/lib/active_record/relation/finder_methods.rb:78:in `find_by'
    from /src/artest.rb:13:in `<main>'

Однако SQL, сгенерированный ActiveRecord, кажется правильным:

D, [2018-06-27T17:42:49.256232 #1] DEBUG -- :   SQL (33.3ms)  USE [APP]
D, [2018-06-27T17:42:50.017505 #1] DEBUG -- :   ReportIndex Load (40.5ms)  EXEC sp_executesql N'SELECT  [app].[ReportIndex].* FROM [app].[ReportIndex] WHERE [app].[ReportIndex].[ReportName] = @0  ORDER BY [app].[ReportIndex].[IRN] ASC OFFSET 0 ROWS FETCH NEXT @1 ROWS ONLY', N'@0 nvarchar(80), @1 int', @0 = N'Master Condemnation', @1 = 1  [["ReportName", nil], ["LIMIT", nil]]
D, [2018-06-27T17:42:51.043389 #1] DEBUG -- :   ReportProperties Load (133.9ms)  SELECT [app].[ReportProperties].* FROM [app].[ReportProperties] WHERE [app].[ReportProperties].[ReportIndexIRN] = '3cad6165-221e-4607-b5ad-01bc32f29157'

Я подозреваю, что эти нарушения проистекают из того, что мне удалось архитектурно ошибиться. Код существующей модели отлично работает для операций DELETE и UPDATE, но не для SELECT с. Я приветствую улей, чтобы дать мне два цента.

1 Ответ

0 голосов
/ 29 июня 2018

Что я выучил:

  • Подкласс ActiveRecord::Base обычно плохая идея, если вы действительно не знаете, что делаете, а я нет. Лучший способ поделиться кодом между классами в вашей модели - это использовать модуль. У подклассов ActiveRecord::Base могут быть непреднамеренные побочные эффекты, и, если я не ошибаюсь, в любом случае, вы не сможете поделиться кодом обратного вызова.
  • Насколько я могу судить, эти параметры не действуют в версии 5.1.6 гема activerecord-sqlserver-adapter, в отличие от документации:

    ActiveRecord::ConnectionAdapters::SQLServerAdapter.lowercase_schema_reflection = true ActiveRecord::Base.table_name_prefix = prefix

    Я решил оба этих вопроса, установив self.table_name в блоке included модуля. Может быть, я делаю что-то не так, но я не смог заставить работать ни один из этих вариантов, и фактически не смог определить разницу в сгенерированном SQL. В результате мне все еще приходится иметь дело с именами столбцов CamelCase.
  • В столбцах MSSQL uniqueidentifier регистр не учитывается. Приложение, для которого я пишу этот код, использует строчный вариант с каждым GUID, поэтому у меня сложилось впечатление, что я должен имитировать это поведение, но база данных делает все равно Я потратил много времени, пытаясь заставить ActiveRecord автоматически downcase эти поля, и вроде как заставил его работать, но он не дал хороших результатов. Смотрите следующий пункт.
  • Если вы попытаетесь изменить значение вашего первичного ключа с помощью after_init, after_find и т. Д., Это нарушит ассоциации. В моем случае я пытался изменить первичный ключ с типа данных uniqueidentifier на строку версия с downcase d шестнадцатеричными значениями. Это то, что произвело оригинал ошибка, из-за которой я создал этот пост.

Код:

Примечание: Раздел ActiveSupport::Inflector необходим для того, чтобы убедиться, что вещи, которые не нуждаются во множественном числе, не являются множественными. Для получения дополнительной информации см. ActiveSupport::Inflector документы.
ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.irregular 'index', 'index'
  inflect.irregular 'layoutml', 'layoutml'
  inflect.acronym 'ML'
end

module AppRecord
  extend ActiveSupport::Concern

  included do
    self.primary_key = 'IRN'
    self.table_name = "app.#{self.to_s.pluralize}"
  end

  def to_h
    self.attributes.to_options
  end
end

class ReportIndex < ActiveRecord::Base
  include AppRecord
  has_many :report_index_parameters, foreign_key: "ReportIndexIRN", dependent: :destroy
  has_many :report_properties, foreign_key: "ReportIndexIRN", dependent: :destroy
  has_many :report_layouts, foreign_key: "ReportIndexIRN", dependent: :destroy

  has_many :report_index_series, foreign_key: "ReportIndexesIRN", dependent: :destroy
  has_many :report_user_defined_views, foreign_key: "ClassId", primary_key: "BaseClassID"

  has_many :report_index_param_series_filters, through: :report_index_series
  has_many :report_property_series_filters, through: :report_index_series
  has_many :report_layout_images, through: :report_layouts
  has_many :report_layout_ml, through: :report_layouts
  has_many :report_property_parameters, through: :report_properties
end

class ReportProperty < ActiveRecord::Base
  include AppRecord
  has_many :report_property_series_filters, foreign_key: "ReportPropertiesIRN", dependent: :destroy
  has_many :report_property_parameters, foreign_key: "ReportPropertiesIRN", dependent: :destroy
end

Нерешенные вопросы:

  • Есть ли способ изменить схему, которую ActiveRecord использует для угадывания внешних ключей? Внешние ключи в этом приложении не соответствуют соглашению "#{class.to_s.underscore}_id", а скорее соглашению "#{class.to_s}IRN". Как Я СУШУ свой код и не называю каждый обычный внешний ключ в моих ассоциациях?
  • Есть ли способ объявить, что ВСЕ из моих ассоциаций has_many должны иметь dependent: :destroy? Опять же, это поможет мне высушить мой код.

Спасибо всем, кто прокомментировал.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...