Метапрограммированная область действия в модуле, вызывающая NoMethodError - PullRequest
0 голосов
/ 15 апреля 2020

У меня есть система HR с моделью Person и моделью Field. Person имеет некоторые атрибуты, которые хранятся в обычных столбцах базы данных, а некоторые могут быть добавлены динамически.

В таблице полей есть запись для каждого столбца базы данных в таблице сотрудников. Это обязательные для заполнения поля. Администраторы могут добавлять столько полей (не обязательных для заполнения), сколько им нужно при настройке приложения. Они также могут устанавливать свойства для полей, например, являются ли они обязательными.

Для необязательных полей администраторы могут добавить виджет на домашнюю страницу пользователя, который показывает, сколько людей пропустили этот атрибут. Например, администратор может добавить поле: personal_email и виджет, который показывает, сколько людей не ввели это поле.

Поля могут быть добавлены в приложение во время выполнения, а области используются для фильтрации таблицы сотрудников на предмет отсутствия записей. Все это делается с помощью модуля PersonField. Когда добавляется новое поле и запрашивается виджет, приложение выдает ошибку NoMethodError: undefined method `missing_personal_email' for #<Person::ActiveRecord_Relation>.

При перезапуске сервера rails ошибка не появляется. Я думал, что это может быть связано с cache_classes, однако это происходит в разработке, где оно ложно. Как можно реорганизовать модуль PersonField, чтобы избежать этой проблемы?

class Person < ActiveRecord::Base
  include PersonField
end

class Field < ActiveRecord::Base   
    enum field_type: {:boolean => 1, :integer => 2, :string => 3, :date => 4, :time => 5, :datetime => 6, :float => 7, :decimal => 8, :reference => 9, :any => 10, :email => 11, :phone => 12, :text => 13, :currency => 14, :postcode => 15}
    enum widget:  { :not_set => 0, :missing => 1, :not_missing => 2 }

    scope :widget, -> { where.not(widget: 0) }
    scope :system_required, -> {where(system_required: 1)}
    scope :not_system_required, -> {where(system_required: 0)}
end

module PersonField
    included do
        typed_store :data do |s|
            Field.active.each do |f|
                case f.field_type.to_sym
                when :integer, :reference
                    s.integer f.name.to_sym
                when :string, :text, :email, :phone, :postcode
                    s.string f.name.to_sym
                when :datetime
                    s.datetime f.name.to_sym
                # etc for all field types
                else
                    s.any f.name.to_sym
                end
            end
        end
    end

    Field.active.system_required.widget.uniq.each do |f|  
       scope "#{f.widget}_#{f.name}", -> { where("people.#{f.name} IS NULL") } 
    end

    Field.active.not_system_required.widget.pluck(:name).uniq.each do |f|
       # EG for :personal_email field this gives the SQL condition: people.data NOT LIKE '%personal_email%' OR people.data LIKE '%personal_email: \n%' 
       scope "#{f.widget}_#{f.name}", -> { where("people.data NOT LIKE '%#{f.name}%' OR people.data LIKE '%#{f.name}: \n%'") }
    end
end

1 Ответ

0 голосов
/ 15 апреля 2020

Я бы начал с настройки системы EAV с помощью STI:

module DynamicFields
  # Represents the normalized A in EAV
  class FieldType
    self.abstract_class = true
    self.table_name = 'field_types'
  end

  def self.exist?(name)
     FieldType.exist?(name: name)
  end
end

module DynamicFields
  class StringType < FieldType
  end
end

module DynamicFields
  class IntegerType < FieldType
  end
end

# more types ...

module DynamicFields
  # This is the V in EAV
  class FieldValue
    self.table_name = 'field_values'
    belongs_to :person # this is the E in EAV
    belongs_to :field_type # this is the A in EAV
  end
end

class Person
  has_many :field_types, 
    class_name: 'DynamicFields::FieldType'
  has_many :field_values, 
    class_name: 'DynamicFields::FieldValue'
end

Здесь происходит немного, но у вас в основном есть таблица field_types с нормализованными типами полей, например:

id | type            | name            | required
1  | StringType      | display_name    | false
2  | IntegerType     | age             | false

Фактические значения хранятся в таблице field_values EAV:

id | field_type_id  | person_id    | value (JSON)   
1  | 1              | 1            | "Mr Loverman"
2  | 2              | 2            | 21 

То, что вы делаете с missing_personal_email, на самом деле очень похоже на Rails Dynami c Искатели , которые в итоге были исключены из фреймворка и могут быть реализованы через method_missing:

module DynamicFields
  module MissingFinders
    # name is the name of the method that was called
    def method_missing(method_name, *args, **kwargs, &block)
      return super unless method_name.start_with?('missing_')  
      self.joins(field_values: :field_type)
          .where(field_values: { 
             value: nil, 
             field_type: {
               method_name.strip('missing_')
             }
          }
      )
    end
  end
end

Если это действительно хорошая идея, весьма сомнительная, так как в ней много потенциальных ошибок и проблем с производительностью. Я бы просто написал обычный метод, который вместо этого принимает параметры:

module DynamicFields
  module Scopes
    def missing_field(*fields)
       where(field_values: { 
         value: nil, 
         field_type: {
           name: fields
         }
       })
    end
  end
end

Да Person.missing_field(:personal_email) это не так волшебно, как Person.missing_personal_email, но маги c всегда идут со стоимостью.

...