【问题标题】:How to create ActiveRecord tableless Model in Rails 5?如何在 Rails 5 中创建 ActiveRecord 无表模型?
【发布时间】:2017-05-20 13:57:38
【问题描述】:

我尝试创建具有自动类型转换的新模型,而数据库中没有表。我试图从ActiveRecord::Base继承它抛出异常ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: relation "people" does not exist

类实现:

class Person < ActiveRecord::Base

  def self.columns
    @columns ||= [];
  end

  def self.column(name, sql_type = nil, default = nil, null = true)
    @columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
  end

  columns

  column :from_email, :string
  column :to_email, :string
  column :article_id, :integer
  column :message, :text

  def initialize
  end

end

堆栈跟踪:

ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  relation "people" does not exist
LINE 8:                WHERE a.attrelid = '"people"'::regclass
^
  :               SELECT a.attname, format_type(a.atttypid, a.atttypmod),
                         pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
                         (SELECT c.collname FROM pg_collation c, pg_type t
                         WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation),
                         col_description(a.attrelid, a.attnum) AS comment
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = '"people"'::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum

from /activerecord-5.0.1/lib/active_record/connection_adapters/postgresql/database_statements.rb:88:in `async_exec'
from /activerecord-5.0.1/lib/active_record/connection_adapters/postgresql/database_statements.rb:88:in `block in query'
from /activerecord-5.0.1/lib/active_record/connection_adapters/abstract_adapter.rb:589:in `block in log'
from /activesupport-5.0.1/lib/active_support/notifications/instrumenter.rb:21:in `instrument'
from /activerecord-5.0.1/lib/active_record/connection_adapters/abstract_adapter.rb:583:in `log'
from /activerecord-5.0.1/lib/active_record/connection_adapters/postgresql/database_statements.rb:87:in `query'
from /activerecord-5.0.1/lib/active_record/connection_adapters/postgresql_adapter.rb:739:in `column_definitions'
from /activerecord-5.0.1/lib/active_record/connection_adapters/postgresql/schema_statements.rb:227:in `columns'
from /activerecord-5.0.1/lib/active_record/connection_adapters/schema_cache.rb:56:in `columns'
from /activerecord-5.0.1/lib/active_record/connection_adapters/schema_cache.rb:62:in `columns_hash'
from /activerecord-5.0.1/lib/active_record/model_schema.rb:441:in `load_schema!'
from /activerecord-5.0.1/lib/active_record/attributes.rb:233:in `load_schema!'
from /activerecord-5.0.1/lib/active_record/attribute_decorators.rb:28:in `load_schema!'
from /activerecord-5.0.1/lib/active_record/model_schema.rb:436:in `load_schema'
from /activerecord-5.0.1/lib/active_record/model_schema.rb:349:in `attribute_types'
from /activerecord-5.0.1/lib/active_record/attribute_methods.rb:179:in `has_attribute?'
... 3 levels...
from /railties-5.0.1/lib/rails/commands/console_helper.rb:9:in `start'
from /railties-5.0.1/lib/rails/commands/commands_tasks.rb:78:in `console'
from /railties-5.0.1/lib/rails/commands/commands_tasks.rb:49:in `run_command!'
from /railties-5.0.1/lib/rails/commands.rb:18:in `<top (required)>'
from /activesupport-5.0.1/lib/active_support/dependencies.rb:293:in `require'
from /activesupport-5.0.1/lib/active_support/dependencies.rb:293:in `block in require'
from /activesupport-5.0.1/lib/active_support/dependencies.rb:259:in `load_dependency'
from /activesupport-5.0.1/lib/active_support/dependencies.rb:293:in `require'
from /project/rails/bin/rails:9:in `<top (required)>'
from /activesupport-5.0.1/lib/active_support/dependencies.rb:287:in `load'
from /activesupport-5.0.1/lib/active_support/dependencies.rb:287:in `block in load'
from /activesupport-5.0.1/lib/active_support/dependencies.rb:259:in `load_dependency'
from /activesupport-5.0.1/lib/active_support/dependencies.rb:287:in `load'

编辑:

都没有

extend ActiveModel::Naming

也没有

include ActiveModel::Model

无法实现隐式类型转换。

【问题讨论】:

  • 请解释你的理由。 ActiveRecord 专门用于数据库表。 Active Record 将类连接到关系数据库表,为应用程序建立几乎零配置的持久层。 由于您没有表,也没有持久化的意图,这显然不合适,因此请解释您的场景而不是你的错误。
  • 场景是我想在一个地方设置孔系统,并希望在管理面板中轻松地对其进行更改。我正在使用 PostgreSQL 和 gem“rails-settings-cached”。 Gem 没问题,但是当我从视图获取数据到控制器时,参数只是字符串。我想进行类型转换,所以我认为 ActiveModel 将是最干的方式。你觉得怎么样?
  • 那为什么不实际使用持久化模型呢?否则,您的应用程序在部署中重新启动的所有内容都将消失。除了我们使用预配置的单例类进行设置
  • 检查这个tableless article。您似乎缺少使用 extend ActiveModel::Namingto_modelpersisted? 方法

标签: ruby-on-rails ruby activerecord ruby-on-rails-5 activemodel


【解决方案1】:

你可以使用

class Person
  include ActiveModel::Model
  attr_accessor :name, :email
  ...
end

然后您会获得许多 activerecord 模型的功能,例如验证。

【讨论】:

    【解决方案2】:

    我能够通过 Rails 4 中的一个小补丁和 Rails 5 中的一个更大补丁来实现这一点。在 Rails 5 中,我们直接从数据库中检索列信息,除了覆盖 @987654322 之外,我们没有机会中断这个过程@ 方法。至少我还没有找到方法。

    我个人希望看到更好的开箱即用解决方案,因为我发现它在某些不需要存储数据的情况下很有用。也许更好的方法是为 NullDatabase 实现一个适配器,但我们的用例非常简单,而且这个解决方案对我们来说效果很好。

    请注意,我没有过多地测试 Rails 5 解决方案,我现在正在将应用程序从 4 升级到 5,并且只是重写了它以与 Rails 5 一起使用。

    导轨 5

    class AbstractModel < ApplicationRecord
      self.abstract_class = true
    
      def self.attribute_names
        @attribute_names ||= attribute_types.keys
      end
    
      def self.load_schema!
        @columns_hash ||= Hash.new
    
        # From active_record/attributes.rb
        attributes_to_define_after_schema_loads.each do |name, (type, options)|
          if type.is_a?(Symbol)
            type = ActiveRecord::Type.lookup(type, **options.except(:default))
          end
    
          define_attribute(name, type, **options.slice(:default))
    
          # Improve Model#inspect output
          @columns_hash[name.to_s] = ActiveRecord::ConnectionAdapters::Column.new(name.to_s, options[:default])
        end
    
        # Apply serialize decorators
        attribute_types.each do |name, type|
          decorated_type = attribute_type_decorations.apply(name, type)
          define_attribute(name, decorated_type)
        end
      end
    
      def persisted?
        false
      end
    end
    
    class Market::ContractorSearch < AbstractModel
      attribute :keywords,           :text,    :default => nil
      attribute :rating,             :text,    :default => []
      attribute :city,               :string,  :default => nil
      attribute :state_province_id,  :integer, :default => nil
      attribute :contracted,         :boolean, :default => false
    
      serialize :rating
    
      belongs_to :state_province
    
      has_many :categories, :class_name => 'Market::Category'
      has_many :expertises, :class_name => 'Market::Expertise'
    end
    

    导轨 4

    class AbstractModel < ActiveRecord::Base
      def self.columns
        @columns ||= add_user_provided_columns([])
      end
    
      def self.table_exists?
        false
      end
    
      def persisted?
        false
      end
    end
    
    class Market::ContractorSearch < AbstractModel
      attribute :keywords,           Type::Text.new,    :default => nil
      attribute :rating,             Type::Text.new,    :default => [].to_yaml
      attribute :city,               Type::String.new,  :default => nil
      attribute :state_province_id,  Type::Integer.new, :default => nil
      attribute :contracted,         Type::Boolean.new, :default => false
    
      serialize :rating
    
      belongs_to :state_province
    
      has_many :categories, :class_name => 'Market::Category'
      has_many :expertises, :class_name => 'Market::Expertise'
    end
    

    玩得开心!

    【讨论】:

    • 感谢@alexander-s 的回答。我已经把我的code 放在上面,以防它对你有用
    【解决方案3】:

    我发现 an article 描述了如何执行此操作。

    我认为重要的部分只是

    extend ActiveModel::Naming
    

    而不是使用

    < ActiveRecord::Base
    

    希望这会有所帮助:)

    【讨论】:

      【解决方案4】:

      最后我决定留下那段代码并继续前进。但随着时间的推移,我认为应该将其重写为关系解决方案或使用 JSON 字段。

      导轨 5

      class TableLess
        include ActiveModel::Validations
        include ActiveModel::Conversion
        include ActiveModel::Serialization
        extend ActiveModel::Naming
      
        class Error < StandardError;
        end
      
        module Type
          class JSON < ActiveModel::Type::Value
            def type
              :json
            end
      
            private
            def cast_value(value)
              (value.class == String) ? ::JSON.parse(value) : value
            end
          end
      
          class Symbol < ActiveModel::Type::Value
            def type
              :symbol
            end
      
            private
            def cast_value(value)
              (value.class == String || value.class == Symbol) ? value.to_s : nil
            end
          end
        end
      
        def initialize(attributes = {})
          attributes = self.class.columns.map { |c| [c, nil] }.to_h.merge(attributes)
          attributes.symbolize_keys.each do |name, value|
            send("#{name}=", value)
          end
        end
      
        def self.column(name, sql_type = :string, default = nil, null = true)
          @@columns ||= {}
          @@columns[self.name] ||= []
          @@columns[self.name]<< name.to_sym
          attr_reader name
          caster = case sql_type
                     when :integer
                       ActiveModel::Type::Integer
                     when :string
                       ActiveModel::Type::String
                     when :float
                       ActiveModel::Type::Float
                     when :datetime
                       ActiveModel::Type::DateTime
                     when :boolean
                       ActiveModel::Type::Boolean
                     when :json
                       TableLess::Type::JSON
                     when :symbol
                       TableLess::Type::Symbol
                     when :none
                       ActiveModel::Type::Value
                     else
                       raise TableLess::Error.new('Type unknown')
                   end
          define_column(name, caster, default, null)
        end
      
        def self.define_column(name, caster, default = nil, null = true)
          define_method "#{name}=" do |value|
            casted_value = caster.new.cast(value || default)
            set_attribute_after_cast(name, casted_value)
          end
        end
      
        def self.columns
          @@columns[self.name]
        end
      
        def set_attribute_after_cast(name, casted_value)
          instance_variable_set("@#{name}", casted_value)
        end
      
        def attributes
          kv = self.class.columns.map {|key| [key, send(key)]}
          kv.to_h
        end
      
        def persisted?
          false
        end
      
      end
      

      例子

      class Machine < TableLess
        column :foo, :integer
        column :bar, :float
        column :winamp, :boolean
      end
      

      【讨论】:

        猜你喜欢
        • 2012-02-25
        • 1970-01-01
        • 1970-01-01
        • 2018-01-11
        • 1970-01-01
        • 2010-12-05
        • 1970-01-01
        • 1970-01-01
        • 2021-11-17
        相关资源
        最近更新 更多