Rails的ORM框架ActiveRecord是马大叔的ActiveRecord模式的实现+associations+SingleTableInheritance
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,写一个新的database adapter不会超过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:
1. $:.unshift(File.dirname(__FILE__)) unless
2. $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3.
4. unless defined?(ActiveSupport)
5. begin
6. $:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
7. require 'active_support'
8. rescue LoadError
9. require 'rubygems'
10. gem 'activesupport'
11. end
12. end
13.
14. require 'active_record/base'
15. require 'active_record/observer'
16. require 'active_record/validations'
17. require 'active_record/callbacks'
18. require 'active_record/reflection'
19. require 'active_record/associations'
20. require 'active_record/aggregations'
21. require 'active_record/transactions'
22. require 'active_record/timestamp'
23. require 'active_record/acts/list'
24. require 'active_record/acts/tree'
25. require 'active_record/acts/nested_set'
26. require 'active_record/locking/optimistic'
27. require 'active_record/locking/pessimistic'
28. require 'active_record/migration'
29. require 'active_record/schema'
30. require 'active_record/calculations'
31. require 'active_record/xml_serialization'
32. require 'active_record/attribute_methods'
33.
34. ActiveRecord::Base.class_eval do
35. include ActiveRecord::Validations
36. include ActiveRecord::Locking::Optimistic
37. include ActiveRecord::Locking::Pessimistic
38. include ActiveRecord::Callbacks
39. include ActiveRecord::Observing
40. include ActiveRecord::Timestamp
41. include ActiveRecord::Associations
42. include ActiveRecord::Aggregations
43. include ActiveRecord::Transactions
44. include ActiveRecord::Reflection
45. include ActiveRecord::Acts::Tree
46. include ActiveRecord::Acts::List
47. include ActiveRecord::Acts::NestedSet
48. include ActiveRecord::Calculations
49. include ActiveRecord::XmlSerialization
50. include ActiveRecord::AttributeMethods
51. end
52.
53. unless defined?(RAILS_CONNECTION_ADAPTERS)
54. RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase frontbase )
55. end
56.
57. RAILS_CONNECTION_ADAPTERS.each do |adapter|
58. require "active_record/connection_adapters/" + adapter + "_adapter"
59. end
60.
61. require 'active_record/query_cache'
62. require 'active_record/schema_dumper'
首先$:.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:
1. module ActiveRecord
2.
3. class Base
4.
5. class << self # Class methods
6.
7. def find(*args)
8. options = extract_options_from_args!(args)
9. validate_find_options(options)
10. set_readonly_option!(options)
11.
12. case args.first
13. when :first then find_initial(options)
14. when :all then find_every(options)
15. else find_from_ids(args, options)
16. end
17. end
18.
19. def find_by_sql(sql)
20. connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
21. end
22.
23. def exists?(id_or_conditions)
24. !find(:first, :conditions => expand_id_conditions(id_or_conditions)).nil?
25. rescue ActiveRecord::ActiveRecordError
26. false
27. end
28.
29. def create(attributes = nil)
30. if attributes.is_a?(Array)
31. attributes.collect { |attr| create(attr) }
32. else
33. object = new(attributes)
34. scope(:create).each { |att,value| object.send("#{att}=", value) } if scoped?(:create)
35. object.save
36. object
37. end
38. end
39.
40. def update(id, attributes)
41. if id.is_a?(Array)
42. idx = -1
43. id.collect { |id| idx += 1; update(id, attributes[idx]) }
44. else
45. object = find(id)
46. object.update_attributes(attributes)
47. object
48. end
49. end
50.
51. def delete(id)
52. delete_all([ "#{connection.quote_column_name(primary_key)} IN (?)", id ])
53. end
54.
55. def destroy(id)
56. id.is_a?(Array) ? id.each { |id| destroy(id) } : find(id).destroy
57. end
58.
59. def update_all(updates, conditions = nil)
60. sql = "UPDATE #{table_name} SET #{sanitize_sql(updates)} "
61. add_conditions!(sql, conditions, scope(:find))
62. connection.update(sql, "#{name} Update")
63. end
64.
65. def destroy_all(conditions = nil)
66. find(:all, :conditions => conditions).each { |object| object.destroy }
67. end
68.
69. def delete_all(conditions = nil)
70. sql = "DELETE FROM #{table_name} "
71. add_conditions!(sql, conditions, scope(:find))
72. connection.delete(sql, "#{name} Delete all")
73. end
74.
75. def count_by_sql(sql)
76. sql = sanitize_conditions(sql)
77. connection.select_value(sql, "#{name} Count").to_i
78. end
79.
80. private
81.
82. def find_initial(options)
83. options.update(:limit => 1) unless options[:include]
84. find_every(options).first
85. end
86.
87. def find_every(options)
88. records = scoped?(:find, :include) || options[:include] ?
89. find_with_associations(options) :
90. find_by_sql(construct_finder_sql(options))
91.
92. records.each { |record| record.readonly! } if options[:readonly]
93.
94. records
95. end
96.
97. def find_from_ids(ids, options)
98. expects_array = ids.first.kind_of?(Array)
99. return ids.first if expects_array && ids.first.empty?
100.
101. ids = ids.flatten.compact.uniq
102.
103. case ids.size
104. when 0
105. raise RecordNotFound, "Couldn't find #{name} without an ID"
106. when 1
107. result = find_one(ids.first, options)
108. expects_array ? [ result ] : result
109. else
110. find_some(ids, options)
111. end
112. end
113.
114. def find_one(id, options)
115. conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
116. options.update :conditions => "#{table_name}.#{connection.quote_column_name(primary_key)} = #{quote_value(id,columns_hash[primary_key])}#{conditions}"
117.
118. if result = find_every(options).first
119. result
120. else
121. raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
122. end
123. end
124.
125. def find_some(ids, options)
126. conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
127. ids_list = ids.map { |id| quote_value(id,columns_hash[primary_key]) }.join(',')
128. options.update :conditions => "#{table_name}.#{connection.quote_column_name(primary_key)} IN (#{ids_list})#{conditions}"
129.
130. result = find_every(options)
131.
132. if result.size == ids.size
133. result
134. else
135. raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
136. end
137. end
138.
139. def method_missing(method_id, *arguments)
140. if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s)
141. finder, deprecated_finder = determine_finder(match), determine_deprecated_finder(match)
142.
143. attribute_names = extract_attribute_names_from_match(match)
144. super unless all_attributes_exists?(attribute_names)
145.
146. attributes = construct_attributes_from_arguments(attribute_names, arguments)
147.
148. case extra_options = arguments[attribute_names.size]
149. when nil
150. options = { :conditions => attributes }
151. set_readonly_option!(options)
152. ActiveSupport::Deprecation.silence { send(finder, options) }
153.
154. when Hash
155. finder_options = extra_options.merge(:conditions => attributes)
156. validate_find_options(finder_options)
157. set_readonly_option!(finder_options)
158.
159. if extra_options[:conditions]
160. with_scope(:find => { :conditions => extra_options[:conditions] }) do
161. ActiveSupport::Deprecation.silence { send(finder, finder_options) }
162. end
163. else
164. ActiveSupport::Deprecation.silence { send(finder, finder_options) }
165. end
166.
167. else
168. ActiveSupport::Deprecation.silence do
169. send(deprecated_finder, sanitize_sql(attributes), *arguments[attribute_names.length..-1])
170. end
171. end
172. elsif match = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(method_id.to_s)
173. instantiator = determine_instantiator(match)
174. attribute_names = extract_attribute_names_from_match(match)
175. super unless all_attributes_exists?(attribute_names)
176.
177. attributes = construct_attributes_from_arguments(attribute_names, arguments)
178. options = { :conditions => attributes }
179. set_readonly_option!(options)
180.
181. find_initial(options) || send(instantiator, attributes)
182. else
183. super
184. end
185. end
186.
187. def extract_attribute_names_from_match(match)
188. match.captures.last.split('_and_')
189. end
190.
191. def construct_attributes_from_arguments(attribute_names, arguments)
192. attributes = {}
193. attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
194. attributes
195. end
196.
197. protected
198.
199. def sanitize_sql(condition)
200. case condition
201. when Array; sanitize_sql_array(condition)
202. when Hash; sanitize_sql_hash(condition)
203. else condition
204. end
205. end
206.
207. def sanitize_sql_hash(attrs)
208. conditions = attrs.map do |attr, value|
209. "#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}"
210. end.join(' AND ')
211.
212. replace_bind_variables(conditions, expand_range_bind_variables(attrs.values))
213. end
214.
215. def sanitize_sql_array(ary)
216. statement, *values = ary
217. if values.first.is_a?(Hash) and statement =~ /:\w+/
218. replace_named_bind_variables(statement, values.first)
219. elsif statement.include?('?')
220. replace_bind_variables(statement, values)
221. else
222. statement % values.collect { |value| connection.quote_string(value.to_s) }
223. end
224. end
225.
226. alias_method :sanitize_conditions, :sanitize_sql
227.
228. end
229.
230. public
231.
232. def save
233. create_or_update
234. end
235.
236. def save!
237. create_or_update || raise(RecordNotSaved)
238. end
239.
240. def destroy
241. unless new_record?
242. connection.delete <<-end_sql, "#{self.class.name} Destroy"
243. DELETE FROM #{self.class.table_name}
244. WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id}
245. end_sql
246. end
247.
248. freeze
249. end
250.
251. def update_attribute(name, value)
252. send(name.to_s + '=', value)
253. save
254. end
255.
256. def update_attributes(attributes)
257. self.attributes = attributes
258. save
259. end
260.
261. def update_attributes!(attributes)
262. self.attributes = attributes
263. save!
264. end
265.
266. private
267.
268. def create_or_update
269. raise ReadOnlyRecord if readonly?
270. result = new_record? ? create : update
271. result != false
272. end
273.
274. def update
275. connection.update(
276. "UPDATE #{self.class.table_name} " +
277. "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
278. "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}",
279. "#{self.class.name} Update"
280. )
281. end
282.
283. def create
284. if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
285. self.id = connection.next_sequence_value(self.class.sequence_name)
286. end
287.
288. self.id = connection.insert(
289. "INSERT INTO #{self.class.table_name} " +
290. "(#{quoted_column_names.join(', ')}) " +
291. "VALUES(#{attributes_with_quotes.values.join(', ')})",
292. "#{self.class.name} Create",
293. self.class.primary_key, self.id, self.class.sequence_name
294. )
295.
296. @new_record = false
297. id
298. end
299.
300. def method_missing(method_id, *args, &block)
301. method_name = method_id.to_s
302. if @attributes.include?(method_name) or
303. (md = /\?$/.match(method_name) and
304. @attributes.include?(query_method_name = md.pre_match) and
305. method_name = query_method_name)
306. define_read_methods if self.class.read_methods.empty? && self.class.generate_read_methods
307. md ? query_attribute(method_name) : read_attribute(method_name)
308. elsif self.class.primary_key.to_s == method_name
309. id
310. elsif md = self.class.match_attribute_method?(method_name)
311. attribute_name, method_type = md.pre_match, md.to_s
312. if @attributes.include?(attribute_name)
313. __send__("attribute#{method_type}", attribute_name, *args, &block)
314. else
315. super
316. end
317. else
318. super
319. end
320. end
321.
322. end
323.
324. end
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:
1. module ActiveRecord
2. class Base
3. class ConnectionSpecification
4. attr_reader :config, :adapter_method
5. def initialize (config, adapter_method)
6. @config, @adapter_method = config, adapter_method
7. end
8. end
9.
10. class << self
11.
12. def connection
13. self.class.connection
14. end
15.
16. def self.establish_connection(spec = nil)
17. case spec
18. when nil
19. raise AdapterNotSpecified unless defined? RAILS_ENV
20. establish_connection(RAILS_ENV)
21. when ConnectionSpecification
22. clear_active_connection_name
23. @active_connection_name = name
24. @@defined_connections[name] = spec
25. when Symbol, String
26. if configuration = configurations[spec.to_s]
27. establish_connection(configuration)
28. else
29. raise AdapterNotSpecified, "#{spec} database is not configured"
30. end
31. else
32. spec = spec.symbolize_keys
33. unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
34. adapter_method = "#{spec[:adapter]}_connection"
35. unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end
36. remove_connection
37. establish_connection(ConnectionSpecification.new(spec, adapter_method))
38. end
39. end
40. end
41. end
connection_specification.rb文件定义了ActiveRecord::Base建立获取数据库连接相关的方法
4,activerecord-1.15.3\lib\active_record\connection_adapters\mysql_adapter.rb:
1. module ActiveRecord
2. class Base
3. def self.mysql_connection(config)
4. config = config.symbolize_keys
5. host = config[:host]
6. port = config[:port]
7. socket = config[:socket]
8. username = config[:username]
9. password = config[:password]
10.
11. if config.has_key?(:database)
12. database = config[:database]
13. else
14. raise ArgumentError, "No database specified. Missing argument: database."
15. end
16.
17. require_mysql
18. mysql = Mysql.init
19. mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
20.
21. ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
22. end
23. end
24.
25. module ConnectionAdapters
26. class MysqlAdapter < AbstractAdapter
27. def initialize(connection, logger, connection_options, config)
28. super(connection, logger)
29. @connection_options, @config = connection_options, config
30.
31. connect
32. end
33.
34. def execute(sql, name = nil) #:nodoc:
35. log(sql, name) { @connection.query(sql) }
36. rescue ActiveRecord::StatementInvalid => exception
37. if exception.message.split(":").first =~ /Packets out of order/
38. 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."
39. else
40. raise
41. end
42. end
43.
44. def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
45. execute(sql, name = nil)
46. id_value || @connection.insert_id
47. end
48.
49. def update(sql, name = nil) #:nodoc:
50. execute(sql, name)
51. @connection.affected_rows
52. end
53.
54. private
55. def connect
56. encoding = @config[:encoding]
57. if encoding
58. @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
59. end
60. @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
61. @connection.real_connect(*@connection_options)
62. execute("SET NAMES '#{encoding}'") if encoding
63.
64. execute("SET SQL_AUTO_IS_NULL=0")
65. end
66. end
67. end
68. end
这个文件是mysql的数据库adapter的例子,其中mysql_connection->connect->real_connect方法会在establish_connection中调用
5,activerecord-1.15.3\lib\active_record\vendor\mysql.rb:
1. class Mysql
2.
3. def initialize(*args)
4. @client_flag = 0
5. @max_allowed_packet = MAX_ALLOWED_PACKET
6. @query_with_result = true
7. @status = :STATUS_READY
8. if args[0] != :INIT then
9. real_connect(*args)
10. end
11. end
12.
13. def real_connect(host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil)
14. @server_status = SERVER_STATUS_AUTOCOMMIT
15. if (host == nil or host == "localhost") and defined? UNIXSocket then
16. unix_socket = socket || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_ADDR
17. sock = UNIXSocket::new(unix_socket)
18. @host_info = Error::err(Error::CR_LOCALHOST_CONNECTION)
19. @unix_socket = unix_socket
20. else
21. sock = TCPSocket::new(host, port||ENV["MYSQL_TCP_PORT"]||(Socket::getservbyname("mysql","tcp") rescue MYSQL_PORT))
22. @host_info = sprintf Error::err(Error::CR_TCP_CONNECTION), host
23. end
24. @host = host ? host.dup : nil
25. sock.setsockopt Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true
26. @net = Net::new sock
27.
28. a = read
29. @protocol_version = a.slice!(0)
30. @server_version, a = a.split(/\0/,2)
31. @thread_id, @scramble_buff = a.slice!(0,13).unpack("La8")
32. if a.size >= 2 then
33. @server_capabilities, = a.slice!(0,2).unpack("v")
34. end
35. if a.size >= 16 then
36. @server_language, @server_status = a.slice!(0,3).unpack("cv")
37. end
38.
39. flag = 0 if flag == nil
40. flag |= @client_flag | CLIENT_CAPABILITIES
41. flag |= CLIENT_CONNECT_WITH_DB if db
42.
43. @pre_411 = (0 == @server_capabilities & PROTO_AUTH41)
44. if @pre_411
45. data = Net::int2str(flag)+Net::int3str(@max_allowed_packet)+
46. (user||"")+"\0"+
47. scramble(passwd, @scramble_buff, @protocol_version==9)
48. else
49. dummy, @salt2 = a.unpack("a13a12")
50. @scramble_buff += @salt2
51. flag |= PROTO_AUTH41
52. data = Net::int4str(flag) + Net::int4str(@max_allowed_packet) +
53. ([8] + Array.new(23, 0)).pack("c24") + (user||"")+"\0"+
54. scramble41(passwd, @scramble_buff)
55. end
56.
57. if db and @server_capabilities & CLIENT_CONNECT_WITH_DB != 0
58. data << "\0" if @pre_411
59. data << db
60. @db = db.dup
61. end
62. write data
63. pkt = read
64. handle_auth_fallback(pkt, passwd)
65. ObjectSpace.define_finalizer(self, Mysql.finalizer(@net))
66. self
67. end
68.
69. alias :connect :real_connect
70.
71. def real_query(query)
72. command COM_QUERY, query, true
73. read_query_result
74. self
75. end
76.
77. def query(query)
78. real_query query
79. if not @query_with_result then
80. return self
81. end
82. if @field_count == 0 then
83. return nil
84. end
85. store_result
86. end
87.
88. end
其中mysql.rb里的real_connect定义了Mysql数据库真正建立连接的方法
这次主要研究了ActiveRecord的基本架构、CRUD方法的封装以及以Mysql为例子的数据库连接相关的代码,歇会再聊,咳咳