Rails 원본 연구의ActiveRecord: 첫째, 기본 구조, CRUD 봉인과 데이터베이스 연결
26003 단어 sqlmysqlSQL ServerRailsActiveRecord
4ActiveRecord 모드의 실현 +associations+
SingleTableInheritance
Active Record 저자 역시 Rails 저자 - David Heinemeier Hansson
ActiveRecord의 key features:
1, Meta Data 없음, XML 구성 파일 필요 없음
2, Database Support, mysql postgresql sqlite firebird sqlserverdb2 oracle sybase openbase frontbase를 지원합니다.
4새 데이터베이스 adapter 쓰기 100행 코드 없음
3, 스레드 보안, 로컬 Ruby 웹 서버, 예를 들어 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:
$:.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'
먼저 $:.unshift 현재 파일을 동적 라이브러리 경로에 추가한 다음ActiveSupport 로드 확인
그리고 activerecord/base/observer/validations.../attribute_methods 등자 디렉터리에 있는 파일 Require 들어오기
그런 다음 ActiveRecord::Base를 사용합니다.class_eval에서 ActiveRecord::Validations/Locking/.../AttributeMethods 등자 모듈 include 들어오기
RAILS_CONNECTION_ADAPTERS는ActiveRecord가 지원하는 데이터베이스 adapters의 이름 배열을 정의한 다음, 각adapter 파일 Require를 순환해서 들어옵니다
마지막으로querycache 및 schemadumper 이 두 파일 Require 들어오세요.
2,activerecord-1.15.3\lib\active_record\base.rb:
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
base.rb 이 파일은 비교적 커서 Base 클래스의Class Method를 먼저 정의했다.find,find 를 포함한다by_ql,create,update,destroy 등
그리고find 와 같은private 방법을 정의했다initial、find_every、find_from_ids 등 방법
예상대로private 작용역에서methodMissing 메서드, find 지원by_username、find_by_username_and_password、find_or_create_by_username 등 동적 증가 방법
보호된 역할 영역에서sanitizeql 등 보조 방법, 이런 종류 (즉 우리 모델) 에서도 이러한 보호법 을 사용할 수 있다
그리고 Base 클래스의public의Instance Method,예를 들어save,destroy,updateattribute、update_attributes 등
그리고 Base 클래스의private의Instance Method를 정의했습니다. 예를 들어public의save 방법으로 호출되는createor_업데이트,create,업데이트 등 방법
그리고private의method 을 정의했습니다.Missing 실례 방법, 본 클래스 내의 다른 실례 방법으로 본 클래스의attributes에 접근할 수 있도록 합니다
3,activerecord-1.15.3\lib\active_record\connection_adapters\abstract\connection_specification.rb:
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
connection_specification.rb 파일은ActiveRecord:::Base 구축 데이터베이스 연결 획득 방법 정의
4,activerecord-1.15.3\lib\active_record\connection_adapters\mysql_adapter.rb:
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
이 파일은 mysql의 데이터베이스 adapter의 예입니다. 그 중에서 mysqlconnection->connect->real_connect 방법은 establishconnection에서 호출
5,activerecord-1.15.3\lib\active_record\vendor\mysql.rb:
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
그중에 mysql.rb의 리얼connect는 Mysql 데이터베이스에서 진정으로 연결을 구축하는 방법을 정의했다
이번에는Active Record의 기본 구조, CRUD 방법의 봉인 및Mysql을 예로 들어 데이터베이스 연결과 관련된 코드를 연구했습니다. 좀 쉬었다가 다시 이야기합시다. 콜록콜록
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
깊이 중첩된 객체를 정확히 일치 검색 - PostgreSQL목차 * 🚀 * 🎯 * 🏁 * 🙏 JSON 객체 예시 따라서 우리의 현재 목표는 "고용주"사용자가 입력한 검색어(이 경우에는 '요리')를 얻고 이 용어와 정확히 일치하는 모든 사용자 프로필을 찾는 것입니다. 즐거운 ...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.