Một số thủ thuật truy vấn nhanh và chính xác hơn, khắc phục các vấn đề khi sử dụng ORM

orm.jpg

1 - ORM là gì?

ORM là một phương pháp lập trình để chuyển đổi từ mô hình Database sang mô hình đối tượng.

Các ưu điểm nổi bật khi sử dụng ORM :

  • Tự động hóa việc chuyển đổi từ object sang table và từ table sang object, giúp giảm thời gian và chi phí.
  • ORM cần ít code hơn store procedures.
  • Tăng tốc độ thực thi của hệ thống.

Với những ưu điểm như vậy nên ORM được sử dụng rất phổ biến hiện nay.

Tuy nhiên nó có thực sự mang lại hiệu năng tốt nhất, với thời gian, chi phí nhỏ nhất mọi lúc khi xử lý dữ liệu trong ứng dụng của bạn?

Chúng ta cùng xem xét các trường hợp sau đây để có câu trả lời!

2 - Sử dụng những hạn chế trong cơ sở dữ liệu để thực hiện các yêu cầu logic:

Giả sử ta định nghĩa một lớp ActiveRecord User như sau :

class User < ActiveRecord::Base
  validates :email, uniqueness: true
end

Khi ta cố gắng tạo một "user" mới, Rails sẽ làm việc theo các bước sau :

  • BEGIN 1 transaction
  • Thực hiện một SELECT để xem có bất kì người dùng nào có địa chỉ email trùng với email được yêu cầu hay không.
  • Nếu SELECT không tìm được hàng nào thì thực hiện thêm một hàng mới với địa chỉ email được request.
  • COMMIT kết quả.

Điều này khá chậm! Nó cũng làm tăng tải trên các ứng dụng của bạn và thời gian xử lý cơ sở dữ liệu của bạn, vì bạn cần phải thực hiện 4 yêu cầu cho mỗi INSERT.

Giải pháp cho trường hợp này là thay vì phải sử dụng 4 truy vấn kia Bạn chỉ cần dùng 1 truy vấn duy nhất : UNIQUE

CREATE TABLE users (email TEXT UNIQUE);

UNIQUE sẽ coi "email" là một "chìa khóa" duy nhất để kiểm tra mỗi lần INSERT thay vì phải truy vấn qua nhiều bước :

> insert into users (email) values ('[email protected]');
INSERT 0 1
> insert into users (email) values ('[email protected]');
ERROR:  duplicate key value violates unique constraint "users_email_key"
DETAIL:  Key (email)=(foo@example.com) already exists.

Một ví dụ khác, khi bạn muốn đọc một File dữ liệu:

if not os.path.isfile(filename):
    raise ValueError("File does not exist")
with open(filename, 'r') as f:
    f.read()
    ...

Điều gì sẽ xảy ra nếu File dữ liệu bị xóa hoặc đã bị thay đổi trong quá trình bạn chọn File, sẽ có lỗi, và nếu bạn không muốn mất thời gian để xử lý những lỗi này bạn có thể dùng ngoại lệ :

try:
    with open(filename, 'r') as f:
        f.read()
        ...
except IOError:
    raise ValueError("File does not exist")

Một thao tác khá phổ biến khi xử lý dữ liệu là thêm một trường dữ liệu(thông tin) vào một Record đã được tạo trước :

def write_phone_number(number, user_id):
    user = Users.find_by_id(user_id)
    if user is None:
        raise NotFoundError("User not found")
    Number.create(number=number, user_id=user_id)

Ở đây, ta muốn thêm số phone cho một người dùng đã được đăng ký từ trước.

Nếu user đó không tồn tại trong Database thì bạn sẽ nhận được thông báo lỗi!

Tuy nhiên, nếu Record user đã tồn tại thì chi phí thời gian để tìm user đó song song với việc tạo mới record number cũng sẽ khiến bạn phải để tâm.

Và bạn sẽ chẳng phải lo điều đó kể cả với Database lớn, nếu bạn viết lại thế này :

def write_phone_number(number, user_id):
    try
        Number.create(number=number, user_id=user_id)
    except DatabaseError as e:
        if is_foreign_key_error(e):
            raise NotFoundError("Don't know that user id")

3 - Cập nhật dữ liệu

Giả sử Bạn đang cập nhật số dư tài khoản của khách hàng như sau :

def charge_customer(account_id, amount=20):
    account = Accounts.get_by_id(account_id)
    account.balance = account.balance - amount
    if account.balance <= 0:
        throw new ValueError("Negative account balance")
    else
        account.save()
SELECT * FROM accounts WHERE id = ?
UPDATE accounts SET balance = 100 WHERE id = ?;

Điều gì sẽ xảy ra nếu có 2 yêu cầu thay đổi số dư tài khoản với số tiền 30$ và 15$ trong cùng một thời điểm?

Hệ thống của bạn sẽ xử lý như sau:

  • Tính phí 30$. Số dư tài khoản hiện tại 100$
  • Tính phí 15$. Số dư tài khoản hiện tại 100$
  • Trừ phí 30$. Số dư hiện tại 70$
  • Trừ 15$. Số dư hiện tại 85$
  • Cập nhật số dư hiện tại là 70$
  • Cập nhật số dư hiện tại là 85$

Điều này là SAI! Chi phí cần trừ là 45$ thì số dư sau khi tính phí phải là 55$.Tuy nhiên, số dư nhận được lại là 70$ hoặc 85$ tùy vào thời điểm cập nhật .

Một số giải pháp dành cho bạn :

  • Tạo ra một số loại dịch vụ khóa để khóa Record dữ liệu trước khi đọc và sau khi bạn cập nhật. Các chủ đề đọc / cập nhật sẽ được thực hiện tuần tự. Nhưng bạn cũng cần phân quyền thứ tự ưu tiên để thực hiện các dịch vụ.

  • Chạy cập nhật trong transaction với 1 khóa tiềm ẩn được tạo ra.

  • Bỏ qua SELECT và viết truy vấn UPDATE đơn :

UPDATE accounts SET balance = balance - 20 WHERE id = ?;

Thao tác này có thể giúp bạn cập nhật 1 triệu bản ghi dữ liệu cùng lúc, không kể thứ tự và vẫn cho kết quả giống hệt nhau.

Bạn cũng không cần sử dụng transaction và .save() như trên.

Nhưng nếu chỉ vậy thì ta chưa kiểm tra được số dư ở một thời điểm cập nhật nào đó có nhỏ hơn 0$ hay không, vậy nên :

CREATE TABLE accounts (
    id integer primary key,
    balance integer CHECK (balance >= 0),
);

Câu lệnh WHERE cũng rất thuận lợi cho bạn khi xử lý tác vụ này :

UPDATE pickups SET status='submitted' WHERE status='OK' AND id=?;

Một triệu Threads Update cùng một thời điểm nhưng sẽ chỉ có 1 thread thành công và cho ta kết quả chính xác - Điều này thật tuyệt vời!

4 - Cẩn thận khi dùng .save()

Chức năng .save() trong ORM cũng có những điểm bất cập mà ta cần để ý :

  • Trước tiên, để gọi .save() bạn cần lấy một thể hiện của đối tượng thông qua một SELECT. Trong khi, nếu đã có ID và một công cụ đọc dữ liệu thì bạn hoàn toàn có thể bỏ qua điều đó và chỉ cần UPDATE. Tránh việc phải mất thời gian gọi lại dữ liệu cũ nhiều lần.
  • Thứ hai, một số thực thi của .save() sẽ gọi một hành động UPDATE và cập nhật tất cả các cột dữ liệu.

Ta cùng xét ví dụ sau :

UPDATE users SET email='[email protected]', phone_number='newnumber' WHERE id = 1;
UPDATE users SET email='[email protected]', phone_number='oldnumber' WHERE id = 1;

Trong khi UPDATE đầu tiên liên tục cập nhật số điện thoại mới thì các email cũ vẫn tiếp tục bị dồn lại - Điều này thật không hay!

Vậy nên, khi sử dụng .save() bạn hãy chỉ UPDATE cột dữ liệu cần thiết và nên dùng .save! nếu thay đổi đó quan trọng.

5 - Sử dụng chỉ số(INDEX) :

Giả sử ta có một bảng pickups như sau :

CREATE TABLE pickups (
    id integer,
    driver_id INTEGER REFERENCES drivers(id),
    status TEXT
);

Với các trạng thái : ASSIGNED, DRAFT, QUEUED, etc..

Một yêu cầu đặt ra : chỉ có duy nhất một lái xe ASSIGNED tại một thời điểm

Để làm được điều này bạn cần sử dụng transaction và viết code rất cẩn thận, nhưng bạn cũng có thể nhờ Postgres làm giúp bạn như thế này :

CREATE UNIQUE INDEX "only_one_assigned_driver" ON pickups(driver_id) WHERE
    status = 'ASSIGNED';

Và nếu bạn cố tình vi phạm yêu cầu đó thì chuyện gì sẽ xảy ra :

> INSERT INTO pickups (id, driver_id, status) VALUES (1, 101, 'ASSIGNED');
INSERT 0 1
> INSERT INTO pickups (id, driver_id, status) VALUES (2, 101, 'DRAFT');
INSERT 0 1 -- OK, because it's draft; doesn't hit the index.
> INSERT INTO pickups (id, driver_id, status) VALUES (3, 101, 'ASSIGNED');
ERROR:  duplicate key value violates unique constraint "only_one_assigned_driver"
DETAIL:  Key (driver_id)=(101) already exists.

Postgres đã bảo vệ tối ưu dữ liệu của bạn và bạn gần như chẳng phải thao tác phức tạp trên ORM.

_ **Lời kết : **_

  • Trong nhiều trường hợp ORM của bạn có thể tạo ra 2 hay nhiều hơn các truy vấn cùng một thời điểm, điều này có thể dẫn đến sai sót đồng thời. Đó là tin xấu! Tin tốt cho bạn là ta có thể sử dụng các truy vấn trên cơ sở dữ liệu để giải quyết nhanh và chính xác hơn rất nhiều! Hãy cùng thử những thủ thuật trên và cùng cảm nhận khác biệt!

_Tài liệu tham khảo : _