12/08/2018, 13:25

Inside ActiveRecord

Trong phần trước chúng ta đã nghiên cứu qua về mô hình của ActiveRecord, trong phần này chúng ta sẽ tiếp tục tìm hiểu về ActiveRecord với 2 điểm chính là: Thuộc tính động(Dynamic Attributes) Các phương thức tìm kiếm động(Dynamic Finders) Dynamic Attributes Chúng ta ví dụ về các thuộc ...

Trong phần trước chúng ta đã nghiên cứu qua về mô hình của ActiveRecord, trong phần này chúng ta sẽ tiếp tục tìm hiểu về ActiveRecord với 2 điểm chính là:

  • Thuộc tính động(Dynamic Attributes)
  • Các phương thức tìm kiếm động(Dynamic Finders)

Dynamic Attributes

Chúng ta ví dụ về các thuộc tính động của ActiveRecord, bằng các tạo ra một database chứa một bảng tên là tasks.

require 'activerecord'
ActiveRecord::Base.establish_connection :adapter => "sqlite3" ,
:database => "dbfile"
ActiveRecord::Base.connection.create_table :tasks do |t|
  t.string :description
  t.boolean :completed
end

Và chúng ta cũng đồng thời khai báo một class tên là Task, kế thừa từ ActiveRecord::Base và tạo một đối tượng kết nối với database đã tạo ở trên.

class Task < ActiveRecord::Base; end
task = Task.new
task.description = 'Clean up garage'
task.completed = true
task.save
task.description # => "Clean up garage"
task.completed? # => true

Ở trên, chúng ta đã sử dụng các phương thức cơ bản để thao tác với database từ đối tượng là write, 'read' và question. Những phương thức này đều được kế thừa từ module AttributeMethods

module ActiveRecord
  module AttributeMethods
  ...
  def read_attribute(attr_name)
  # ...
  def write_attribute(attr_name, value)
  # ...
  def query_attribute(attr_name)

Ghost Attributes Incarnated

Chúng ta sẽ bắt đầu bằng cách tập trung nửa đầu của phương thức method_mising()

def method_missing(method_id, *args, &block)
  if !self.class.generated_methods?
  self.class.define_attribute_methods
  if self.class.generated_methods.include?(method_name)
    return self.send(method_id, *args, &block)
  end
end
# ...

khi chúng ta gọi một phương thức như Task#description lần đầu tiên, thi nó sẽ chuyển đến phương thức method_missing đầu tiên, trước khi nó làm việc phương thức method_missing() đảm bảo rằng nó sẽ không được sử dụng để tránh các bao đóng hay gọi các phương thức private. Sau đó nó sẽ gọi phương thức define_attribute_methods()

phương thức define_attribute_methods() sẽ định nghia các phương thức động như write, reader, hay question để thao tác với tất cả các cột trong cơ sở dữ liệu. khi chung ta gọi thuộc tính description( hay bất kì accessor khác đã map đến các cột trong database, các cuộc gọi sẽ không được xử lý bởi phương thức method_missing, thay vào đó chúng sẽ gọi phương thức nonghost, khi chúng ta bước vào method_missing description() sẽ được coi là một ghost method.

Defining Accessors

ở đây chúng ta sẽ nghiên cứu define_attribute_methods()

def define_attribute_methods
  return if generated_methods?
  unless instance_method_already_implemented?("#{name}=" )
    if create_time_zone_conversion_attribute?(name, column)
      define_write_method_for_time_zone_conversion(name)
    else
      define_write_method(name.to_sym)
    end
  end
end

phương thức instance_method_already_implemented? được dùng để ngăn chặn các Monkeypatches( sẽ được nói ở phần sau . và một số mã thì đại diện cho một số phương thức thực tế như define_read_method() hay define_write_method(). như phương thức define_write_method() sẽ có một số xử lý quan trọng

def define_write_method(attr_name)
  evaluate_attribute_method attr_name,
    "def #{attr_name}=(new_value);write_attribute('#{attr_name}',   new_value);end",
  "#{attr_name}="
end

phương thức này được xây dựng như String of code theo thương thức class_eval(), như chúng ta gọi phương thức description chúng ta sẽ có một đoạn code

def description=(new_value);write_attribute('description' , new_value);end

khi đó phương thức description sẽ được sinh ra, đây cũng là cách thức hoạt động của các accessors cho tất cả các column trong database.

Attributes That Stay Dynamic

Giống như đã nói, trong một số trường hợp ActiveRecord không muốn định nghĩa các accessors, ví dụ như các thuộc tính không có quan hệ với các cột trong database mà chi làm nhiệm vụ như tính toán...

my_query = "tasks.*, (description like '%garage%') as heavy_job"
task = Task.find(:first, :select => my_query)
task.heavy_job? # => true

thuộc tính heavy_job có thế khác biệt theo tùy đối tượng, vì không có điểm tạo ra các phương thức động để truy cập đến chúng

ActiveRecord::Base#respond_to? ở đây. nếu chúng ta có thể gọi my_task.description(), khi đó kết quả của my_task.respond_to?(:description) sẽ trả về là true, dưới đây là đinh nghĩa của respond_to? trong lớp ActiveRecord::Base

def respond_to?(method, include_private_methods = false)
  method_name = method.to_s
  if super
    return true
  elsif !include_private_methods && super(method, true)
  ....

respond_to?() chứa các code tương tự với phương thức method_missing, bao gồm việc định nghĩa các thuộc tính nếu chúng chưa được định nghĩa. Do vậy, nếu chúng ta gọi phương thức respond_to? trước method_missing thì chúng ta sẽ có một câu trả lời đáng tin cậy.

Dynamic Finders

Đê hiểu rõ hơn về các phương thức tìm kiếm động của ActiveRecord, chúng ta nhìn lại database đã được giới thiệu ở trên

require 'activerecord'
ActiveRecord::Base.establish_connection :adapter => "sqlite3" ,
:database => "dbfile"
ActiveRecord::Base.connection.create_table :tasks do |t|
t.string :description
t.boolean :completed
end
class Task < ActiveRecord::Base; end

Sau khi chúng ta lưu một đối tượng vào trong database và giờ chúng ta muốn lấy lại chúng, ActiveRecord có rất nhiều chức năng để giúp chúng ta tìm, bao gồm phương thức tìm kiếm linh động find()

task = Task.find(:first, :conditions => {:completed => true})
task.description # => "get information"

Ngoài cách trên phương thức find() cũng cung cấp cho chúng ta rất nhiều lựa chọn khác

# Tìm kiếm tất cả các task đã xong
Task.find_all_by_completed(true)
# tìm kiếm task đầu tiên đã hoàn thành có description == 'get'
Task.find_by_description_and_completed('get' , true)
# Tìm kiếm task đầu tiên có description == 'Water',
# hay tạo nó nếu không tồn tại
Task.find_or_create_by_description('Water')
0