Tuesday, March 18, 2008

Logger ใน ActiveRecord

จากโจทย์ที่ apirak ถามที่ codenone, ผมจะลองเล่าให้ฟังว่า ถ้าผมไม่ลักไก่ค้นด้วย google แล้ว
ผมจะมีวิธีไล่หามันใน rails code ได้อย่างไร

เรามาลองไล่ code ของ ActiveRecord กัน
เริ่มจากบรรทัดแรก

ActiveRecord::Base.establish_connection(
:adapter => 'mysql',
:host => 'localhost',
:username => 'root',
:password => '',
:database => 'test'
)

จะเห็นว่ามันเรียก class method จาก class ActiveRecord::Base
แต่ถ้าเราไปลองเปิด file "base.rb" ไล่หา method นี้ดู ก็จะหาไม่เจอ
ที่เป็นเช่นนี้ ก็เพราะเจ้า ruby มัน open class ได้ คนที่เขียน rails ก็เลยอาศัยคุณสมบัตินี้ มา implement
code ในลักษณะ Partition class ออกเป็นส่วนๆ ตามความรับผิดชอบ
ดังนั้น เจ้าเนื้อหาใน ActiveRecord::Base ก็เลยกระจายอยู่ในหลายๆ file

ลอง grep หาดู

$ grep -Rl establish_connection *
abstract/connection_specification.rb

ลองเปิด connection_specification.rb มาดู ก็จะพบ method หน้าตาแบบนี้
    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]
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

เห็น switch เยอะๆแล้วตาลายนิดหน่อย case ของเราจะตกลงตรงช่อง else
  # เริ่มด้วยการแปลง key ของ Hash table เราให้เป็น symbol ก่อน
spec = spec.symbolize_keys
# parameter ที่บังคับใส่ก็คือ :adapter
unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
# หา adapter_method ที่รับผิดชอบ connection ของ database นี้
# กรณี mysql ก็จะเป็น mysql_connection
adapter_method = "#{spec[:adapter]}_connection"
# check ว่ามี class method นี้อยู่จริงหรือไม่
unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end
remove_connection
# ตรงนี้แหล่ะที่ทำผมแปลกใจ มัน recursive call ตัวเองอีกครั้งด้วย parameter ที่ห่อไว้ใน ConnectionSpecification object
establish_connection(ConnectionSpecification.new(spec, adapter_method))

แกะแล้วมี surprise เล็กน้อย เนื่องจากมัน recursive call ตัวเองด้วย parameter ที่ถูกแปลงไปแล้ว
(เจ้า ruby มันเป็น dynamic type ดังนั้น code ในลักษณะ Polymorphism ก็เลยต้องเขียนออกมาอย่างนี้)

ในการ recursive ครั้งที่สอง มันก็จะมาตก code ส่วนนี้

clear_active_connection_name
@active_connection_name = name
# spec ก็คือ ConnectionSpecification object
@@defined_connections[name] = spec

จะเห็นว่าหลังจากจบคำสั่ง establish_connection, เจ้า activerecord มันไม่ได้รีบร้อนจะเปิด connection ต่อ database ให้เรา
แต่อย่างไร, มันจะรอให้เราต้องการ access ข้อมูลจริงๆก่อน จึงจะเริ่มต้นเปิด connection ให้เรา

ตัว key ที่สำคัญใน code ข้างบน ก็คือ adapter_method ที่ชื่อ mysql_connection
ลอง grep หาดู ก็จะพบว่ามัน define ไว้ใน file "mysql_adapter.rb"
ลองเปิดดู
module ActiveRecord
class Base

...
...

def self.mysql_connection(config) # :nodoc:
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

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
end

จะเห็นว่ามี keyword ตัวหนึ่งที่ชื่อ logger
แล้ว เจ้า logger หล่ะมาจากไหน
เนื่องจากเจ้า method mysql_connection มันอยู่ใน scope ของ class ActiveRecord::Base
ก็ลองไปเปิด file "base.rb" ดู และหาคำว่า logger ก็จะพบบรรทัดนี้
  class Base    
# Accepts a logger conforming to the interface of Log4r or
# the default Ruby 1.8+ Logger class, which is then passed
# on to any new database connections made and which can be
# retrieved on both a class and instance level by calling +logger+.
cattr_accessor :logger, :instance_writer => false

Bingo!!
ถ้าเราต้องการ Logger ก็เพียงแต่ supply มันให้ เจ้า ActiveRecord::Base แบบนี้
require 'logger'
ActiveRecord::Base.logger = Logger.new(STDOUT)

Related link from Roti

2 comments:

Apirak said...

เปิดหูเปิดตามากๆ ขอบคุณมากครับ

ข่า said...

ไล่ code กันหลายไฟล์เลย =='