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ẾP
  • first mặc định trả về một record và SẮP XẾP GIẢM DẦN dựa trên primary key ORDER BY id ASC, cũng có thể trả về nhiều giá trị
  • last giống first, 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_bywhere().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 unscopeonly

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

4. Ref