如何在 Rails 5 中创建 ActiveRecord 无表模型?
How to create ActiveRecord tableless Model in Rails 5?
我尝试在数据库中创建没有 table 的自动类型转换的新模型。我试图从 ActiveRecord::Base
继承它抛出的异常 ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: relation "people" does not exist
Class 实施:
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 模型的许多功能,例如验证。
我发现 an article 描述了如何执行此操作。
我认为重要的是
extend ActiveModel::Naming
而不是使用
< ActiveRecord::Base
希望这对您有所帮助:)
我能够通过 Rails 4 中的一个小补丁和 Rails 5 中的一个更大的补丁来实现这一点。在 Rails 5 中,直接从数据库中检索的列信息没有除了覆盖 load_schema!
方法之外,我们有机会中断这个过程。至少我还没有找到办法。
我个人希望看到更好的开箱即用解决方案,因为我发现它在某些不需要存储数据的情况下很有用。也许更好的方法是为 NullDatabase 实现一个适配器,但我们的用例非常简单,这个解决方案对我们来说效果很好。
请注意,我没有对 Rails 5 解决方案进行太多测试,我现在正在将一个应用程序从 4 升级到 5,只是重写它以与 Rails 5 一起使用。
Rails 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
Rails 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
玩得开心!
最后我决定离开那个代码继续前进。但随着时间的推移,我认为它应该重写为关系解决方案或使用 JSON 字段。
Rails 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
我尝试在数据库中创建没有 table 的自动类型转换的新模型。我试图从 ActiveRecord::Base
继承它抛出的异常 ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: relation "people" does not exist
Class 实施:
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 模型的许多功能,例如验证。
我发现 an article 描述了如何执行此操作。
我认为重要的是
extend ActiveModel::Naming
而不是使用
< ActiveRecord::Base
希望这对您有所帮助:)
我能够通过 Rails 4 中的一个小补丁和 Rails 5 中的一个更大的补丁来实现这一点。在 Rails 5 中,直接从数据库中检索的列信息没有除了覆盖 load_schema!
方法之外,我们有机会中断这个过程。至少我还没有找到办法。
我个人希望看到更好的开箱即用解决方案,因为我发现它在某些不需要存储数据的情况下很有用。也许更好的方法是为 NullDatabase 实现一个适配器,但我们的用例非常简单,这个解决方案对我们来说效果很好。
请注意,我没有对 Rails 5 解决方案进行太多测试,我现在正在将一个应用程序从 4 升级到 5,只是重写它以与 Rails 5 一起使用。
Rails 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
Rails 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
玩得开心!
最后我决定离开那个代码继续前进。但随着时间的推移,我认为它应该重写为关系解决方案或使用 JSON 字段。
Rails 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