ActiveRecord
Bài đăng này đã không được cập nhật trong 3 năm
Tóm tắt về Active Record
Các bạn đã làm việc với Active Record rồi, nó thật tuyệt vời. Vậy các bạn có hiểu rõ Active Record? Sau đây mình sẽ cùng các bạn tìm hiểu thêm về Active Record, bản chất và các tính năng của nó. Với các lập trình viên mới làm quen với Ruby on Rails và không thành thạo khi làm việc với cơ sở dữ liệu thì Active Record là siêu nhân giúp bạn giải quyết các vấn đề liên quan tới cơ sở dữ liệu. Về cơ bản thì Active Record làm nhiệm vụ trừu tượng hóa việc thao thác với các bảng trong cơ sở dữ liệu. Nói các khác thì các lập trình viên sẽ không cần phải viết các câu truy vấn trực tiếp vào cơ sở dữ liệu. Điều đó sẽ giúp giảm khổi lượng công việc của lập trình viên và đồng thời làm giảm độ phức tạp của chương trình.
Cụ thể thì mỗi database table sẽ được map tương ứng với một class trong chương trình, theo đó thì các thao tác insert/update/delete ... sẽ được hỗ trợ sẵn.
Ví dụ minh họa để so sánh flow dùng và không dùng Active Record như sau.
Không dùng Active Record
- Bước 1 : khởi tạo conection và query object.
- Bước 2 : Truyền tham số, viết query. (UPDATE {table} SET ColumnA = ‘{0}’ WHERE id = {...})
- Bước 3 : Thực thi query
Dùng Active Record
- Bước 1 : obj = {table}.FindById(id)
- Bước 2 : obj.ColumnA = {0};
- Bước 3 : obj.save()
Như vậy bạn có thể thấy rằng việc thao tác với database đơn giản giống như việc thao tác với các object khác trong chương trình mà thôi.
Thiết kế của ActiveRecord
ActiveRecord là một phần của Rails, nó sẽ mapping các đối tượng Ruby tới các bảng trong cơ sở dữ liệu. Chức năng này được gọi là ánh xạ quan hệ đối tượng. Bạn có thể sử dụng ActiveRecord trong Rails application. Trong Rails application bạn sử dụng ActiveRecord trong model - chính là “M” trong mô hình MVC. Điểm mấu chốt ở đây chính là bạn sẽ làm việc với các đối tượng ruby để quản lý các logic nghiệp vụ.
Trong Active Record thành phần quan trọng nhất đó chính là ActiveRecord::Base, nó không chỉ chứ các phương thức lớp (các phương thức để kết nối cơ sở dữ liệu) Nó còn là lớp cơ sở để map tất cả các lớp khác, ví dụ như lớp User sau đây:
class User < ActiveRecord::Base
validates_length_of :name, :maximum => 6
end
Phương thức validates_length_of( ) là một Class Macro, trong ví dụ trên, nó chắc chắn rằng tên của User sẽ không được dài hơn 6 kí tự. Nếu bạn cố gắng để lưu một User với tên dài hơn 6 kí tự vào database thì bạn sẽ nhận được một error (nếu bạn không dùng phương thức save!)
Theo quy ước, ActiveRecord sẽ tự động ánh xạ các đối tượng User tới bảng users. Bằng cách nhìn vào lược đồ cơ sở dữ liệu, đồng thời nó cũng tìm thấy lớp User có một attribute tên là name, và nó định nghĩa một Ghost Method để truy cập attribute này. Và nhờ đó bạn có thể sử dụng lớp User ngay lập tức như thế này.
new_user = User.new
new_user.name = "Thuong"
new_user.valid? # => true
new_user.save!
Và để hiểu rõ hơn cách hoạt động của nó, ngay sau đây chúng ta sẽ cùng tìm hiểu về ActiveRecord::Base
ActiveRecord::Base
Khi bạn mới làm việc với ActiveRecord, nhìn vào các lớp và các phương thức của nó bạn sẽ có cảm giác khó hiểu. Với ví dụ trước, có thể bạn sẽ nghĩ là có một phương thức validates_length_of () ở bên trong ActiveRecord::Base. Tuy nhiên trong tài liệu mô tả về nó không hề có phương phức đó. Khi tìm hiểu thêm bạn sẽ thấy phương thức validates_length_of() ở bên trong module ActiveRecord :: Validations.
Tuy nhiên, có một logic đơn giản đằng sau cấu trúc dường như phức tạp này. Hãy xem lại ở bên trong source code, bạn có thể thấy dòng đầu tiên là require ’activerecord’. Dòng này sẽ load file activerecord.rb và đồng thời nó cũng load tất cả các module được định nghĩa trong namespace tên là ActiveRecord
module ActiveRecord
autoload :Base, 'active_record/base'
autoload :Batches, 'active_record/batches'
autoload :Calculations, 'active_record/calculations'
autoload :Callbacks, 'active_record/callbacks'
# ...
autoload :Timestamp, 'active_record/timestamp'
autoload :Transactions, 'active_record/transactions'
autoload :Validations, 'active_record/validations'
# ...
end
ActiveRecord loads mỗi module thông qua autoload(). Đây là lõi Kernel Method, nó sẽ giúp bạn thuật tiện khi bạn có rất nhiều tập tin và bạn không muốn load nhiều hơn số tập tin bạn thực sự cần. Với ví dụ mà chúng ta đang để cập, nó nhanh chóng tham chiếu tới ActiveRecord:Base, autoload() load activerecord/base.rb
module ActiveRecord
class Base
class << self # Class methods
def find(*args) # ...
def first(*args) # ...
def last(*args) # ...
# ...
end
public
def id # ...
def save # ...
def save! # ...
def delete # ...
# ...
end
end
ActiveRecord::Base định nghĩa một danh sách dài các phương thức class như là find(), first() ...Và nó cũng định nghĩa một danh sách các instance method như save(), delete ... Và đây chỉ là một phần nhỏ của ActiveRecord::Base. Và phương thức validates_length_of( ) nằm ở đâu, nếu bạn để ý bạn có thể thấy rằng trong ActiveRecord::Base có include một danh sách các modules nữa.
module ActiveRecord
Base.class_eval do
# ...
include Validations
include Locking::Optimistic, Locking::Pessimistic
include AttributeMethods
include Dirty
include Callbacks, Observing, Timestamp
# ...
end
end
Cả hai phương thức valid?() và validates_length_of( ) đều nằm trong module ActiveRecord:: Validations .
module ActiveRecord
module Validations
def self.included(base)
base.extend ClassMethods
base.class_eval do
alias_method_chain :save, :validation
alias_method_chain :save!, :validation
end
base.send :include, ActiveSupport::Callbacks
# ...
end
module ClassMethods
def validates_each(*attrs) # ...
def validates_confirmation_of(*attr_names)
def validates_length_of(*attrs) # ...
# ...
end
# ...
def save_with_validation(perform_validation = true) # ...
def save_with_validation!
# ...
def valid?
# ...
# ...
end
end
Như vậy chúng ta đã tìm thấy phương thức validates_length_of( ) nằm ở đâu và làm thế nào để có thể gọi ra nó. ActiveRecord còn rất rất nhiều phương thức mà chúng ta thường sử dụng, sau đây là một số phương thức hữu dụng mà bạn sẽ sử dụng rất nhiều.
Một số phương thức hữu dụng trong Active record.
-
find_by
Để lấy ra một đối tượng trong Ruby bạn có thể sử dụng các cách sau. Sử dụng find_by_xyz với xyz là các attributes bạn sử dụng để tìm đối tượng.
2.1.5 :024 > User.find_by_name_and_email("Anna Schoen", "example-4@railstutorial.org")
=> #<User id: 5, name: "Anna Schoen", email: "example-4@railstutorial.org", created_at: "2014-01-13 09:23:13", updated_at: "2014-01-13 09:23:13">
Sử dụng where
2.1.5 :025 > User.where(name: "Anna Schoen",email: "example-4@railstutorial.org").first
=> #<User id: 5, name: "Anna Schoen", email: "example-4@railstutorial.org", created_at: "2014-01-13 09:23:13", updated_at: "2014-01-13 09:23:13">
Tuy nhiên mình gợi ý là bạn nên sử dụng find_by và find_by! bởi sự ngắn gọn và đơn giản.
Đây là 2 phương thức được thêm vào tử Rails 4 trở đi.
Trong đó sự khác biệt của 2 phương thức này là trong trường hợp không tìm thấy bản ghi thì find_by sẽ trả về nil, còn find_by! sẽ trả về error.
2.1.5 :027 > User.find_by(name: "Anna Schoen",email: "example-4@railstutorial.org")
=> #<User id: 5, name: "Anna Schoen", email: "example-4@railstutorial.org", created_at: "2014-01-13 09:23:13", updated_at: "2014-01-13 09:23:13">
2.1.5 :028 > User.find_by(name: "Anna Schoen",email: "aaaaa@gmail.com")
=> nil
2.1.5 :029 > User.find_by!(name: "Anna Schoen",email: "aaaaa@gmail.com")
ActiveRecord::RecordNotFound: ActiveRecord::RecordNotFound
-
find_each
Khi bạn muốn thực hiện 1 update cho hàng ngàn records, không nên sử dụng each bởi vì:
- ActiveRecord sẽ thực hiện 1 query lấy ra toàn bộ records
- Đưa toàn bộ số records vào memory
- Thực hiện update cho toàn bộ số records cùng lúc.
Nếu memory của bạn đủ lớn, thì điều này không những không vấn đề gì, mà còn tăng hiệu năng của hệ thống, nhưng ngược lại, nó sẽ là nguyên nhân chính khiến cho hệ thống của bạn bị chậm, hay thậm chí là ngưng hoạt động. Với find_each, nó sẽ thực hiện update cho từng record một, và hệ thống sẽ không phải lưu trữ toàn bộ records trong cùng 1 khoảng thời gian.
2.1.5 :041 > User.where(admin: false).find_each do |user|
2.1.5 :042 > puts "Do something with #{user.name} here!"
2.1.5 :043 > end
Do something with Micheal Gerlach here!
Do something with Dr. Alta Farrell here!
Do something with Elda Frami here!
....
-
pluck
Khi muốn đưa ra array giá trị 1 cột của các records, thông thường ta hay sử dụng.
name_list = User.all.select(:name).map(&:name)
hay là
name_list = User.all.map(&:name)
bạn có thể sử dụng phương thức pluck để thay thế.
name_list = User.all.pluck(:name)
=> ["Example User", "Micheal Gerlach", "Dr. Alta Farrell"]
Và đặc biệt trong Rails 4 trở đi bạn có thể pluck với nhiều attributes.
2.1.5 :052 > User.all.pluck(:name, :email)
=> [["Example User", "example@railstutorial.org"], ["Micheal Gerlach", "example-1@railstutorial.org"], ["Dr. Alta Farrell", "example-2@railstutorial.org"]]
-
merge
Đây là 1 method rất hữu dụng khi sử dụng trong ActiveRecord::Relation. Bạn có thể vừa joins bảng vừa lọc với 1 scope nào đó trong models.
class User < ActiveRecord::Base
# ...
# Returns all the accounts that have unread messages.
def self.with_unread_messages
joins(:messages).merge( Message.unread )
end
end
-
scoping
Bạn có thể viết lại 1 class method riêng cho 1 trường hợp nào đó giống như scopevới models.
User.where(:id=> 1).scoping do
User.first # SELECT * FROM users WHERE post_id = 1
end
-
scoped
khi bạn muốn ActiveRecord::Relation trả về toàn bộ records của class, có thể dùng scoped.
def search(query)
if query.blank?
scoped
else
q = "%#{query}%" where("name like ? or email like ?", q, q)
end
end
Và bạn cũng có thể sử dụng irb để thấy rắng nó cũng trả ra giá trị như khi gọi all. Tuy nhiên với irb sẽ đưa ra cho bạn một cảnh báo và khuyến khích dùng all thay cho scoped.
2.1.5 :055 > User.scoped.count
DEPRECATION WARNING: Model.scoped is deprecated. Please use Model.all instead. (called from irb_binding at (irb):55)
(0.1ms) SELECT COUNT(*) FROM "users"
=> 101
2.1.5 :056 > User.all.count
(0.1ms) SELECT COUNT(*) FROM "users"
=> 101
-
None
Tử rails 4, Active record được bổ sung thêm 1 phương thức là none. Nó sẽ trả về một ActiveRecord::Relation rỗng.
def filter(filter_require)
case filter_require
when "all"
scoped
when "admin"
where(:admin => true)
when "not_admin"
where(:admin => false)
else
none
end
end
2.1.5 :009 > User.filter("")
=> #<ActiveRecord::Relation []>
2.1.5 :013 > User.none
=> #<ActiveRecord::Relation []>
-
first_or_initialize
Muốn lấy ra record nào đó, nếu nó không tồn tại thì tạo mới nó? Sử dụng first_or_initialize Trong Rails 3 thì nó là phương thức find_or_initialize_by_xyz (với xyz là atrribute)
Rails 3: User.find_or_initialize_by_name('Test Test')
Rails 4: User.where(name: 'Test Test').first_or_initialize
=> #<User id: nil, name: "Test Test", email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_token: nil, admin: false>
-
first_or_create
Tương tự như first_or_initialize nhưng bạn muốn tạo mới 1 record và save luôn vào database thì có thể dùng first_or_create.
Rails 3: User.find_or_create_by_name('Test2')
Rails 4: User.where(name: 'Test2').first_or_create
-
to_sql và explain
Khi sử dụng ActiveRecord, các phương thức của nó có thể trả ra kết quả không giống như bạn mong muốn. Vì vậy bạn nên kiểm tra lại nó bằng to_sql và explain để có thể chắc tìm hiểu nguyên nhân hoặc là để bạn có thể thấy sự khác biệt và tìm cách tối ưu query của bạn.
2.1.5 :021 > User.where(:name => 'Test2').to_sql
=> "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"name\" = 'Test2'"
2.1.5 :022 > User.where(:name => 'Test2').explain
=> EXPLAIN for: SELECT "users".* FROM "users" WHERE "users"."name" = 'Test2'
2.1.5 :025 > User.first.followers.to_sql
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
=> "SELECT \"users\".* FROM \"users\" INNER JOIN \"relationships\" ON \"users\".\"id\" = \"relationships\".\"follower_id\" WHERE \"relationships\".\"followed_id\" = ?"
2.1.5 :026 > User.first.followers.explain
=> EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ? [["followed_id", 1]]
2.1.5 :038 > User.joins(:followers).to_sql
=> "SELECT \"users\".* FROM \"users\" INNER JOIN \"relationships\" ON \"relationships\".\"followed_id\" = \"users\".\"id\" INNER JOIN \"users\" \"followers_users\" ON \"followers_users\".\"id\" = \"relationships\".\"follower_id\""
2.1.5 :039 > User.joins(:followers).explain
=> EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "relationships" ON "relationships"."followed_id" = "users"."id" INNER JOIN "users" "followers_users" ON "followers_users"."id" = "relationships"."follower_id"
All rights reserved