Rails for DevOps engineer
Một số cú pháp, best practice khi sử dụng Active Record cơ bản cho mấy bạn dép-ộp engineer.
1. Basic Active Record
1.1 find
Tham số truyền vào của find
là primary-key, ví dụ User.find(1)
tương đương với SELECT * FROM users WHERE id = 1 LIMIT 1
Cũng có thể nhận nhiều giá trị kiểu User.find(1, 10)
hoặc User.find([1, 10])
sẽ tương đương với SELECT * FROM users WHERE id IN (1, 10)
1.2 take/first/last
take
trả về một hoặc nhiều record tùy vào giá trị truyền vào nhưng KHÔNG SẮP XẾPfirst
mặc định trả về một record và SẮP XẾP GIẢM DẦN dựa trên primary keyORDER BY id ASC
, cũng có thể trả về nhiều giá trịlast
giốngfirst
, chỉ khác là sắp xếp tăng dần.
Theo thứ tự active record và sql sẽ như sau:
User.take
User.take(20)
SELECT * FROM users LIMIT 1;
SELECT * FROM users LIMIT 20;
User.first
User.first(2)
SELECT * FROM users ORDER BY id ASC LIMIT 1
SELECT * FROM users ORDER BY id ASC LIMIT 2
User.last
SELECT * FROM users ORDER BY id DESC LIMIT 1
1.3 find_by
và where().take
find_by
trả về một record match với điều kiện.where
trả về record theo điều kiện.
Ví dụ 2 query sau sẽ như nhau:
User.find_by name: 'Quang'
User.where(name: 'Quang').take
SELECT * FROM users WHERE name = 'Quang' LIMIT 1;
1.4 unscope
và only
unscope
dùng để xóa một số điều kiện cụ thể nào đó, only
thì ngược lại, chỉ định các điều kiện sẽ được giữ lại, ví dụ:
User.where("email LIKE '%@gmail%'").order(:id)
SELECT `users`.* FROM `users` WHERE (email LIKE '%@gmail%') ORDER BY `users`.`id` ASC;
Nhưng giờ mình muốn xóa điều kiện sắp xếp theo id
thì dùng method unscope
như sau:
User.where("email LIKE '%@gmail%'").order(:id).unscope(:order)
SELECT `users`.* FROM `users` WHERE (email LIKE '%@gmail%');
2. Some Active-Record tips
2.1. include
và n + 1 query
Giả sử ta có model User như sau:
class User
has_many :posts
end
=> một user có thể có nhiều post
Với cách query thông thường trong active-record
users = User.all
users.each do |user|
user.posts
end
Đoạn trên sẽ generate ra n + 1 query như sau:
SELECT users.* FROM users;
SELECT posts.* FROM posts WHERE posts.user_id = 1;
SELECT posts.* FROM posts WHERE posts.user_id = 2;
SELECT posts.* FROM posts WHERE posts.user_id = 3;
SELECT posts.* FROM posts WHERE posts.user_id = 4;
...
SELECT posts.* FROM posts WHERE posts.user_id = n;
=> nếu có bao nhiêu user thì generate ra bằng đó + 1 user nên gọi là n + 1
Nếu ta dùng includes
thì sẽ chỉ generate ra 2 query, 1 là load tất cả users và 1 là load tất cả các posts liên quan đến các users đó.
users = User.includes(:posts)
users.each do |user|
user.posts
end
SELECT users.* FROM users;
SELECT posts.* FROM posts WHERE posts.user_id IN (1, 2, 3, 4 ... n);
Ngoài include
cũng có thể sử dụng preload
hoặc eager_load
để tránh n + 1 query
2.2. find_each
khi load một lượng lớn record
Tương tự như việc dùng LIMIT
trong SQL
User.all.each do |user|
puts users
end
# SQL generate
SELECT users.* FROM users;
Thay vì load tất cả các user thì ta có thể LIMIT lượng user hiển thị ra bằng cách dùng find_each
như sau:
User.all.find_each(batch_size: 100) do |u|
puts u
end
# SQL query
SELECT users.* FROM users ORDER BY users.id ASC LIMIT 100;
SELECT users.* FROM users WHERE users.id > 100 ORDER BY users.id ASC LIMIT 100;
SELECT users.* FROM users WHERE users.id > 200 ORDER BY users.id ASC LIMIT 100;
...
Chú ý là các query sau sẽ có thêm điều kiện users.id > batch_size
2.3. Chỉ lấy field cần với pluck
hoặc select
Tương tự trong SQL, không phải lúc nào ta cũng cần tất cả thuộc tính của model, thay vì:
SELECT * FROM users;
Ta chỉ lấy các thuộc tính cần thiết bằng câu query:
SELECT email FROM users;
Trong active-record thì thay vì dùng
user_emails = User.where(status: "active").map(&:email)
# SELECT * FROM users;
Ta sẽ chỉ lấy thuộc tính email của user như sau:
user_emails = User.where(status: "active").pluck(:email)
# SELECT users.email FROM users;
Hoặc xài select
User.select(:name)
or
User.select("name")
2.4. Kiểm tra sự tồn tại của dữ liệu với exist?
thay vì present?
if User.where(email: "[email protected]").present?
puts "There is a user with email address [email protected]"
else
puts "There is no user with email address [email protected]"
end
Câu query SQL sẽ generate thành:
SELECT * FROM users WHERE email = '[email protected]';
Câu query trên sẽ lãng phí bởi ví ta không cần load hết thuộc tính của user để kiểm tra user với email đó có tồn tại hay không. Thay vào đó có thể dùng như sau:
if User.where(email: "[email protected]").exist?
puts "There is a user with email address [email protected]"
else
puts "There is no user with email address [email protected]"
end
Query SQL sẽ thành:
SELECT 1 FROM users WHERE email = '[email protected]' LIMIT 1;
Kết quả chỉ trả về 1 record và không bao gồm bất kỳ thuộc tính nào.
2.5 Bulk
Ví dụ ta phải INSERT/UPDATE/DELETE một lượng lớn dữ liệu, thì bulk INSERT/UPDATE/DELETE sẽ nhanh hơn là thao tác với từng record dữ liệu.
Ví dụ với tạo dữ liệu:
new_users = [
{name: "A", email: "[email protected]"},
{name: "B", email: "[email protected]"},
{name: "C", email: "[email protected]" },
...
{name: "Z", email: "[email protected]" },
]
new_users.each do |u|
User.create(u)
end
-----------------------------------------
INSERT INTO users VALUES ("A", "[email protected]");
INSERT INTO users VALUES ("B", "[email protected]");
INSERT INTO users VALUES ("C", "[email protected]");
...
INSERT INTO users VALUES ("Z", "[email protected]");
Ta có thể bulk INSERT bằng cách:
User.create(new_users)
-------------------------------------------------------------------------------------------
INSERT INTO users VALUES ("A", "[email protected]"), ("B", "[email protected]"), ("C", "[email protected]"), ("Z", "[email protected]");
Ví dụ với cập nhật dữ liệu:
update_users = User.where(user_status: nil)
update_users.each do |u|
u.updute(user_status: "new_user")
end
Ta có thể bulk UPDATE bằng cách:
update_users = User.where(user_status: nil)
update_users.update_all(user_status: "new_user")
Ví dụ với xóa dữ liệu:
banned_users = User.where(user_status: "banned")
banned_users.each do |u|
u.destroy
end
-------------------------------------------------------
SELECT * FROM users WHERE users.user_status = "banned";
DELETE FROM users WHERE id = 1;
DELETE FROM users WHERE id = 2;
DELETE FROM users WHERE id = 3;
DELETE FROM users WHERE id = 4;
=> Lấy tất cả các user cần DELETE sau đó sinh ra n query DELETE từng user dựa trên id của user. Thay vì vậy ta có thể xài bulk DELETE như sau
banned_users = User.where(user_status: "banned")
banned_users.delete_all
-----------------------------------------------------
DELETE FROM users WHERE users.user_status = "banned";
2.6. Map vs Select
Kết quả trả về của map
sẽ là một mảng bằng mảng đầu vào, kết quả của map
là mảng chứa kết quả của từng phần tử đối chiếu với điều kiện trong block.
select
chỉ trả về các kết quả thỏa mãn với điều kiện trong block, select
có thể xem như map
+ compact
> an_array = [0, 1, 2, 3, 4, 5]
> m_array = an_array.map { |e| e if e.even? }
=> [0, nil, 2, nil, 4, nil]
> m_array.compact
=> [0, 2, 4]
> s_array = an_array.select { |e| e if e.even? }
=> [0, 2, 4]
3. Other
3.1. ||=
Ví dụ x ||= y
có nghĩa là x || x = y
. Nếu x = nil
hoặc x = false
gán x = y