簡體   English   中英

如何在 Rails 5 中創建 ActiveRecord 無表模型?

[英]How to create ActiveRecord tableless Model in Rails 5?

我嘗試創建新模型,該模型在數據庫中沒有表的情況下具有自動類型轉換。 我試圖從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

無法實現隱式類型轉換。

您可以使用

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

然后您將獲得 activerecord 模型的許多功能,例如驗證。

我能夠通過 Rails 4 中的一個小補丁和 Rails 5 中的一個更大補丁來實現這一點。在 Rails 5 中,列信息直接從數據庫中檢索,除了覆蓋load_schema! 方法。 至少我還沒有找到方法。

我個人希望看到一個更好的開箱即用的解決方案,因為我發現它在某些我們不需要存儲數據的情況下很有用。 也許更好的方法是為 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

玩得開心!

我找到 了一篇描述如何執行此操作的文章。

我認為重要的部分是

extend ActiveModel::Naming

而不是使用

< ActiveRecord::Base

希望這可以幫助:)

最后我決定離開那個代碼並繼續前進。 但隨着時間的推移,我認為它應該重寫為關系解決方案或使用 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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM