Active Record 是什么?
Active Record 是 MVC 中的 M(模型),处理数据和业务逻辑。Active Record 负责创建和使用需要持久存入数据库中的数据。Active Record 实现了 Active Record 模式,是一种对象关系映射系统。
Active Record 是 Rails 最核心的功能,也是 Rails 在web开发领域取得成功的关键因素之一,它的作者也是 DHH 本人。Active Record 的主要优点有:
- 不依赖于特定的数据库,支持多种数据库
- 以面向对象的方式操作数据,无需直接编写sql语句
- 事务支持
- 简介的关联关系(例如 has_many, belongs_to)
- 内建 validations
接下来我们看看 Active Record 的核心源码:
#rails/activerecord/lib/active_record.rb
require 'active_support'
require 'active_support/rails'
require 'active_model'
require 'arel'
require 'active_record/version'
module ActiveRecord
extend ActiveSupport::Autoload
autoload :Base
autoload :Callbacks
autoload :Core
autoload :ConnectionHandling
autoload :CounterCache
autoload :DynamicMatchers
autoload :Explain
autoload :Inheritance
autoload :Integration
autoload :Migration
autoload :Migrator, 'active_record/migration'
autoload :ModelSchema
autoload :NestedAttributes
autoload :Persistence
autoload :QueryCache
autoload :Querying
autoload :ReadonlyAttributes
autoload :Reflection
autoload :RuntimeRegistry
autoload :Sanitization
autoload :Schema
autoload :SchemaDumper
autoload :SchemaMigration
autoload :Scoping
autoload :Serialization
autoload :StatementCache
autoload :Store
autoload :Timestamp
autoload :Transactions
autoload :Translation
autoload :Validations
.....
ActiveRecord 模块会自动加载不同功能的模块,包括常见的数据库连接,迁移,查询,序列化,验证模块等。
以数据库查询模块Querying为例:
#rails/activerecord/lib/active_record/querying.rb
module ActiveRecord
module Querying
delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all
delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
delegate :find_by, :find_by!, to: :all
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
delegate :find_each, :find_in_batches, to: :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
:where, :preload, :eager_load, :includes, :from, :lock, :readonly,
:having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
delegate :pluck, :ids, to: :all
.......
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
# Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }]
def find_by_sql(sql, binds = [])
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
column_types = {}
if result_set.respond_to? :column_types
column_types = result_set.column_types
else
ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`"
end
result_set.map { |record| instantiate(record, column_types) }
end
....
def count_by_sql(sql)
sql = sanitize_conditions(sql)
connection.select_value(sql, "#{name} Count").to_i
end
end
end
delegate 方法在 rails 源码中使用的非常多,它可以把所包含对象的 public method 变成自己的。例如:
class Greeter < ActiveRecord::Base
def hello
'hello'
end
def goodbye
'goodbye'
end
end
class Foo < ActiveRecord::Base
belongs_to :greeter
delegate :hello, to: :greeter
end
Foo.new.hello # => "hello"
Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
委托方法在rails中应用广泛的原因很明显,我们很难在rails代码中找到动辄几千行的代码,也没有存在某个类拥有大量的方法,这样的代码一定会让可读性和维护性变差,而rails通过把一堆方法分成不同的组放在不同的文件,然后通过代理方法把它们汇总在一起,很巧妙的避免了这个问题。例如查询模块,rails把数据查询,数据的删除和更新等操作分成不同的组放入不同的文件,再在Querying模块把它们汇集在一起。那么为什么不使用include方法呢?我们知道,如果在一个module A 中inlude另外一个module B,那么B 中的方法将成为A的实例方法。显然include和delegate有着相同的作用,但是有些情况下,我只需要B中的某个或一部分方法,使用include把所有模块的方法都取过来显然是不科学的,而delegate可以做到按需索取。
ActiveRecord提供了如此多的方法可以供我们使用,那么它又是如何连接数据库的呢?
#rails/activerecord/lib/active_record/connection_adapters/connection_specification.rb
module ActiveRecord
module ConnectionAdapters
class ConnectionSpecification #:nodoc:
attr_reader :config, :adapter_method
.....
def resolve_hash_connection(spec) # :nodoc:
spec = spec.symbolize_keys
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
begin
require path_to_adapter
rescue Gem::LoadError => e
raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile."
rescue LoadError => e
raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
end
adapter_method = "#{spec[:adapter]}_connection"
ConnectionSpecification.new(spec, adapter_method)
end
rails server 或 rails console启动的时候,rails会自动的去读取 database.yml 文件,此处的spec参数就是文件中的内容被转化成hash,通过读取adapter中的值确定使用哪个数据库适配器。以mysql为例:
#rails/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
module ActiveRecord
module ConnectionHandling # :nodoc:
# Establishes a connection to the database that's used by all Active Record objects.
def mysql_connection(config)
config = config.symbolize_keys
host = config[:host]
port = config[:port]
socket = config[:socket]
username = config[:username] ? config[:username].to_s : 'root'
password = config[:password].to_s
database = config[:database]
mysql = Mysql.init
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
options = [host, username, password, database, port, socket, default_flags]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
end
end
.....
rails找到mysql_adapter.rb 文件,该文件包含了获取数据库连接的相关方法。mysql_connection 方法回再次读取database.yml文件,获取数据库配置,建立数据库连接,active_record 有一个 ConnectionPool的类来管理数据库连接。
Active Record的缺点也是显而易见的,它把数据库中的数据映射成相应的对象,对于数据库的批量操作,势必会消耗大量的内存;Active Record的关联关系使用起来的很简单,但是如果对其不够了解,一个普通的操作背后可能就是大量的数据库操作,带来性能问题。Rails通过牺牲性能为代价换取了极高的开发效率,一方面使其在web开发领域占据一席之地,另一方面也让其饱受诟病,而DHH开发rails的初衷之一就是以效率至上,通过其他技术和增加硬件资源来弥补在性能上的缺失,毕竟人力资源总是比机器资源更昂贵。
method_missing
method_missing 在 ruby元编程 一书中被称作幽灵方法,它可以动态的创建方法. rails 大量的使用 method_missing,最重要的用途之一就是可以减少代码量。很多人会好奇为什么rails可以把数据库字段映射成响应的方法供 active record 对象调用,其背后的秘密就在于 method_missing 的使用。
#rails/activerecord/lib/active_record/attribute_methods.rb
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = match_attribute_method?(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def attribute_missing(match, *args, &block)
__send__(match.target, match.attr_name, *args, &block)
end
在ruby中,send 可以动态的调用方法,代码在运行期间可以动态的决定到底调用哪一个方法;其另外一个作用就是它可以调用对象的私有方法。send方法和__send__的作用是一样的,只是在某些情况下,send方法容易被覆盖。
在ruby当中,method_missing 也和我们关系密切,只是它待在一个极为隐秘的地方,你几乎察觉不到它的存在。
class A
end
A.new.private_methods.grep(/miss/)
=> [:method_missing]
A.new.hello
NoMethodError: undefined method `hello' for #<A:0x007f9a0911d6e0>
A.ancestors
=> [A, Object, Kernel, BasicObject]
ruby会沿着A的祖先链去搜寻hello方法,一直都没有找到,最后来到Kernel模块,ruby解释器要求A对象调用 method_missing 方法,该方法抛出 NoMethodError 的错误。因此ruby中method_missing和我们的关系密切,它就像一座监狱,所有不合法的方法都会被关押在这里统一处理,你可以通过覆写它,让这些不合法的方法重获自由。
comments powered by Disqus对于一般的ruby编程,我并不推荐使用 method_missing 这样的花哨技巧,因为它增加了代码的复杂度,使代码维护性降低。它很适合应用于编写框架,就像rails,把细节隐藏在背后,我们在意框架如何使用,而不会过多的关心框架的底层实现。