Rails源码研究之ActiveRecord:一,基本架构、CRUD封装与数据库连接

Rails的ORM框架ActiveRecord是马大叔的[url=http://www.martinfowler.com/eaaCatalog/activeRecord.html]ActiveRecord模式[/url]的实现+associations+[url=http://wiki.rubyonrails.org/rails/pages/SingleTableInheritance]SingleTableInheritance[/url]
ActiveRecord的作者也是Rails的作者--David Heinemeier Hansson
ActiveRecord的key features:
1,零Meta Data,不需要XML配置文件
2,Database Support,现在支持mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase frontbase,[url=http://wiki.rubyonrails.org/rails/pages/New+database+adapter]写一个新的database adapter[/url]不会超过100行代码
3,线程安全,本地Ruby Web服务器,如WEBrick/Cerise,用线程处理请求
4,速度快,对100个对象循环查找一个值做benchmark,速度为直接数据库查询速度的50%
5,事务支持,使用事务来确保级联删除自动执行,同时也支持自己写事务安全的方法
6,简洁的关联,使用natural-language macros,如has_many、belongs_to
7,内建validations支持
8,自定义值对象

让我们深入研读一下ActiveRecord的核心源码
1,activerecord-1.15.3\lib\active_record.rb:
[code]
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))

unless defined?(ActiveSupport)
begin
$:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
require 'active_support'
rescue LoadError
require 'rubygems'
gem 'activesupport'
end
end

require 'active_record/base'
require 'active_record/observer'
require 'active_record/validations'
require 'active_record/callbacks'
require 'active_record/reflection'
require 'active_record/associations'
require 'active_record/aggregations'
require 'active_record/transactions'
require 'active_record/timestamp'
require 'active_record/acts/list'
require 'active_record/acts/tree'
require 'active_record/acts/nested_set'
require 'active_record/locking/optimistic'
require 'active_record/locking/pessimistic'
require 'active_record/migration'
require 'active_record/schema'
require 'active_record/calculations'
require 'active_record/xml_serialization'
require 'active_record/attribute_methods'

ActiveRecord::Base.class_eval do
include ActiveRecord::Validations
include ActiveRecord::Locking::Optimistic
include ActiveRecord::Locking::Pessimistic
include ActiveRecord::Callbacks
include ActiveRecord::Observing
include ActiveRecord::Timestamp
include ActiveRecord::Associations
include ActiveRecord::Aggregations
include ActiveRecord::Transactions
include ActiveRecord::Reflection
include ActiveRecord::Acts::Tree
include ActiveRecord::Acts::List
include ActiveRecord::Acts::NestedSet
include ActiveRecord::Calculations
include ActiveRecord::XmlSerialization
include ActiveRecord::AttributeMethods
end

unless defined?(RAILS_CONNECTION_ADAPTERS)
RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase frontbase )
end

RAILS_CONNECTION_ADAPTERS.each do |adapter|
require "active_record/connection_adapters/" + adapter + "_adapter"
end

require 'active_record/query_cache'
require 'active_record/schema_dumper'
[/code]
首先$:.unshift一句将当前文件加入动态库路径,然后确保加载ActiveSupport
然后将active_record/base/observer/validations.../attribute_methods等子目录下的文件require进来
然后用ActiveRecord::Base.class_eval将ActiveRecord::Validations/Locking/.../AttributeMethods等子模块include进来
RAILS_CONNECTION_ADAPTERS定义了ActiveRecord支持的database adapters的名字数组,然后循环将每个adapter文件require进来
最后将query_cache和schema_dumper这两个文件require进来

2,activerecord-1.15.3\lib\active_record\base.rb:
[code]
module ActiveRecord

class Base

class << self # Class methods

def find(*args)
options = extract_options_from_args!(args)
validate_find_options(options)
set_readonly_option!(options)

case args.first
when :first then find_initial(options)
when :all then find_every(options)
else find_from_ids(args, options)
end
end

def find_by_sql(sql)
connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
end

def exists?(id_or_conditions)
!find(:first, :conditions => expand_id_conditions(id_or_conditions)).nil?
rescue ActiveRecord::ActiveRecordError
false
end

def create(attributes = nil)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr) }
else
object = new(attributes)
scope(:create).each { |att,value| object.send("#{att}=", value) } if scoped?(:create)
object.save
object
end
end

def update(id, attributes)
if id.is_a?(Array)
idx = -1
id.collect { |id| idx += 1; update(id, attributes[idx]) }
else
object = find(id)
object.update_attributes(attributes)
object
end
end

def delete(id)
delete_all([ "#{connection.quote_column_name(primary_key)} IN (?)", id ])
end

def destroy(id)
id.is_a?(Array) ? id.each { |id| destroy(id) } : find(id).destroy
end

def update_all(updates, conditions = nil)
sql = "UPDATE #{table_name} SET #{sanitize_sql(updates)} "
add_conditions!(sql, conditions, scope(:find))
connection.update(sql, "#{name} Update")
end

def destroy_all(conditions = nil)
find(:all, :conditions => conditions).each { |object| object.destroy }
end

def delete_all(conditions = nil)
sql = "DELETE FROM #{table_name} "
add_conditions!(sql, conditions, scope(:find))
connection.delete(sql, "#{name} Delete all")
end

def count_by_sql(sql)
sql = sanitize_conditions(sql)
connection.select_value(sql, "#{name} Count").to_i
end

private

def find_initial(options)
options.update(:limit => 1) unless options[:include]
find_every(options).first
end

def find_every(options)
records = scoped?(:find, :include) || options[:include] ?
find_with_associations(options) :
find_by_sql(construct_finder_sql(options))

records.each { |record| record.readonly! } if options[:readonly]

records
end

def find_from_ids(ids, options)
expects_array = ids.first.kind_of?(Array)
return ids.first if expects_array && ids.first.empty?

ids = ids.flatten.compact.uniq

case ids.size
when 0
raise RecordNotFound, "Couldn't find #{name} without an ID"
when 1
result = find_one(ids.first, options)
expects_array ? [ result ] : result
else
find_some(ids, options)
end
end

def find_one(id, options)
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
options.update :conditions => "#{table_name}.#{connection.quote_column_name(primary_key)} = #{quote_value(id,columns_hash[primary_key])}#{conditions}"

if result = find_every(options).first
result
else
raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
end
end

def find_some(ids, options)
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
ids_list = ids.map { |id| quote_value(id,columns_hash[primary_key]) }.join(',')
options.update :conditions => "#{table_name}.#{connection.quote_column_name(primary_key)} IN (#{ids_list})#{conditions}"

result = find_every(options)

if result.size == ids.size
result
else
raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
end
end

def method_missing(method_id, *arguments)
if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s)
finder, deprecated_finder = determine_finder(match), determine_deprecated_finder(match)

attribute_names = extract_attribute_names_from_match(match)
super unless all_attributes_exists?(attribute_names)

attributes = construct_attributes_from_arguments(attribute_names, arguments)

case extra_options = arguments[attribute_names.size]
when nil
options = { :conditions => attributes }
set_readonly_option!(options)
ActiveSupport::Deprecation.silence { send(finder, options) }

when Hash
finder_options = extra_options.merge(:conditions => attributes)
validate_find_options(finder_options)
set_readonly_option!(finder_options)

if extra_options[:conditions]
with_scope(:find => { :conditions => extra_options[:conditions] }) do
ActiveSupport::Deprecation.silence { send(finder, finder_options) }
end
else
ActiveSupport::Deprecation.silence { send(finder, finder_options) }
end

else
ActiveSupport::Deprecation.silence do
send(deprecated_finder, sanitize_sql(attributes), *arguments[attribute_names.length..-1])
end
end
elsif match = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(method_id.to_s)
instantiator = determine_instantiator(match)
attribute_names = extract_attribute_names_from_match(match)
super unless all_attributes_exists?(attribute_names)

attributes = construct_attributes_from_arguments(attribute_names, arguments)
options = { :conditions => attributes }
set_readonly_option!(options)

find_initial(options) || send(instantiator, attributes)
else
super
end
end

def extract_attribute_names_from_match(match)
match.captures.last.split('_and_')
end

def construct_attributes_from_arguments(attribute_names, arguments)
attributes = {}
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
attributes
end

protected

def sanitize_sql(condition)
case condition
when Array; sanitize_sql_array(condition)
when Hash; sanitize_sql_hash(condition)
else condition
end
end

def sanitize_sql_hash(attrs)
conditions = attrs.map do |attr, value|
"#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}"
end.join(' AND ')

replace_bind_variables(conditions, expand_range_bind_variables(attrs.values))
end

def sanitize_sql_array(ary)
statement, *values = ary
if values.first.is_a?(Hash) and statement =~ /:\w+/
replace_named_bind_variables(statement, values.first)
elsif statement.include?('?')
replace_bind_variables(statement, values)
else
statement % values.collect { |value| connection.quote_string(value.to_s) }
end
end

alias_method :sanitize_conditions, :sanitize_sql

end

public

def save
create_or_update
end

def save!
create_or_update || raise(RecordNotSaved)
end

def destroy
unless new_record?
connection.delete <<-end_sql, "#{self.class.name} Destroy"
DELETE FROM #{self.class.table_name}
WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id}
end_sql
end

freeze
end

def update_attribute(name, value)
send(name.to_s + '=', value)
save
end

def update_attributes(attributes)
self.attributes = attributes
save
end

def update_attributes!(attributes)
self.attributes = attributes
save!
end

private

def create_or_update
raise ReadOnlyRecord if readonly?
result = new_record? ? create : update
result != false
end

def update
connection.update(
"UPDATE #{self.class.table_name} " +
"SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
"WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}",
"#{self.class.name} Update"
)
end

def create
if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
self.id = connection.next_sequence_value(self.class.sequence_name)
end

self.id = connection.insert(
"INSERT INTO #{self.class.table_name} " +
"(#{quoted_column_names.join(', ')}) " +
"VALUES(#{attributes_with_quotes.values.join(', ')})",
"#{self.class.name} Create",
self.class.primary_key, self.id, self.class.sequence_name
)

@new_record = false
id
end

def method_missing(method_id, *args, &block)
method_name = method_id.to_s
if @attributes.include?(method_name) or
(md = /\?$/.match(method_name) and
@attributes.include?(query_method_name = md.pre_match) and
method_name = query_method_name)
define_read_methods if self.class.read_methods.empty? && self.class.generate_read_methods
md ? query_attribute(method_name) : read_attribute(method_name)
elsif self.class.primary_key.to_s == method_name
id
elsif md = self.class.match_attribute_method?(method_name)
attribute_name, method_type = md.pre_match, md.to_s
if @attributes.include?(attribute_name)
__send__("attribute#{method_type}", attribute_name, *args, &block)
else
super
end
else
super
end
end

end

end
[/code]
base.rb这个文件比较大,它首先定义了Base类的Class Method,包括find、find_by_sql、create、update、destroy等
然后定义了一些private方法,如find_initial、find_every、find_from_ids等方法,它们供public的find方法调用
不出所料,private作用域里还定义了method_missing方法,它支持find_by_username、find_by_username_and_password、find_or_create_by_username等动态增加的方法
protected作用域里定义了sanitize_sql等辅助方法,这样子类(即我们的Model)中也可以使用这些protected方法
然后定义了Base类的public的Instance Method,如save、destroy、update_attribute、update_attributes等
然后定义了Base类的private的Instance Method,如供public的save方法调用的create_or_update、create、update等方法
然后定义了private的method_missing实例方法,供本类内其他实例方法访问本类的attributes

3,activerecord-1.15.3\lib\active_record\connection_adapters\abstract\connection_specification.rb:
[code]
module ActiveRecord
class Base
class ConnectionSpecification
attr_reader :config, :adapter_method
def initialize (config, adapter_method)
@config, @adapter_method = config, adapter_method
end
end

class << self

def connection
self.class.connection
end

def self.establish_connection(spec = nil)
case spec
when nil
raise AdapterNotSpecified unless defined? RAILS_ENV
establish_connection(RAILS_ENV)
when ConnectionSpecification
clear_active_connection_name
@active_connection_name = name
@@defined_connections[name] = spec
when Symbol, String
if configuration = configurations[spec.to_s]
establish_connection(configuration)
else
raise AdapterNotSpecified, "#{spec} database is not configured"
end
else
spec = spec.symbolize_keys
unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
adapter_method = "#{spec[:adapter]}_connection"
unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end
remove_connection
establish_connection(ConnectionSpecification.new(spec, adapter_method))
end
end
end
end
[/code]
connection_specification.rb文件定义了ActiveRecord::Base建立获取数据库连接相关的方法

4,activerecord-1.15.3\lib\active_record\connection_adapters\mysql_adapter.rb:
[code]
module ActiveRecord
class Base
def self.mysql_connection(config)
config = config.symbolize_keys
host = config[:host]
port = config[:port]
socket = config[:socket]
username = config[:username]
password = config[:password]

if config.has_key?(:database)
database = config[:database]
else
raise ArgumentError, "No database specified. Missing argument: database."
end

require_mysql
mysql = Mysql.init
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]

ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
end
end

module ConnectionAdapters
class MysqlAdapter < AbstractAdapter
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@connection_options, @config = connection_options, config

connect
end

def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.query(sql) }
rescue ActiveRecord::StatementInvalid => exception
if exception.message.split(":").first =~ /Packets out of order/
raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
else
raise
end
end

def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
execute(sql, name = nil)
id_value || @connection.insert_id
end

def update(sql, name = nil) #:nodoc:
execute(sql, name)
@connection.affected_rows
end

private
def connect
encoding = @config[:encoding]
if encoding
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
end
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
@connection.real_connect(*@connection_options)
execute("SET NAMES '#{encoding}'") if encoding

execute("SET SQL_AUTO_IS_NULL=0")
end
end
end
end
[/code]
这个文件是mysql的数据库adapter的例子,其中mysql_connection->connect->real_connect方法会在establish_connection中调用

5,activerecord-1.15.3\lib\active_record\vendor\mysql.rb:
[code]
class Mysql

def initialize(*args)
@client_flag = 0
@max_allowed_packet = MAX_ALLOWED_PACKET
@query_with_result = true
@status = :STATUS_READY
if args[0] != :INIT then
real_connect(*args)
end
end

def real_connect(host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil)
@server_status = SERVER_STATUS_AUTOCOMMIT
if (host == nil or host == "localhost") and defined? UNIXSocket then
unix_socket = socket || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_ADDR
sock = UNIXSocket::new(unix_socket)
@host_info = Error::err(Error::CR_LOCALHOST_CONNECTION)
@unix_socket = unix_socket
else
sock = TCPSocket::new(host, port||ENV["MYSQL_TCP_PORT"]||(Socket::getservbyname("mysql","tcp") rescue MYSQL_PORT))
@host_info = sprintf Error::err(Error::CR_TCP_CONNECTION), host
end
@host = host ? host.dup : nil
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true
@net = Net::new sock

a = read
@protocol_version = a.slice!(0)
@server_version, a = a.split(/\0/,2)
@thread_id, @scramble_buff = a.slice!(0,13).unpack("La8")
if a.size >= 2 then
@server_capabilities, = a.slice!(0,2).unpack("v")
end
if a.size >= 16 then
@server_language, @server_status = a.slice!(0,3).unpack("cv")
end

flag = 0 if flag == nil
flag |= @client_flag | CLIENT_CAPABILITIES
flag |= CLIENT_CONNECT_WITH_DB if db

@pre_411 = (0 == @server_capabilities & PROTO_AUTH41)
if @pre_411
data = Net::int2str(flag)+Net::int3str(@max_allowed_packet)+
(user||"")+"\0"+
scramble(passwd, @scramble_buff, @protocol_version==9)
else
dummy, @salt2 = a.unpack("a13a12")
@scramble_buff += @salt2
flag |= PROTO_AUTH41
data = Net::int4str(flag) + Net::int4str(@max_allowed_packet) +
([8] + Array.new(23, 0)).pack("c24") + (user||"")+"\0"+
scramble41(passwd, @scramble_buff)
end

if db and @server_capabilities & CLIENT_CONNECT_WITH_DB != 0
data << "\0" if @pre_411
data << db
@db = db.dup
end
write data
pkt = read
handle_auth_fallback(pkt, passwd)
ObjectSpace.define_finalizer(self, Mysql.finalizer(@net))
self
end

alias :connect :real_connect

def real_query(query)
command COM_QUERY, query, true
read_query_result
self
end

def query(query)
real_query query
if not @query_with_result then
return self
end
if @field_count == 0 then
return nil
end
store_result
end

end
[/code]
其中mysql.rb里的real_connect定义了Mysql数据库真正建立连接的方法

这次主要研究了ActiveRecord的基本架构、CRUD方法的封装以及以Mysql为例子的数据库连接相关的代码,歇会再聊,咳咳
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值