Có một loại bug làm người mới lẫn người cũ đều gãi đầu: bạn chạy một câu UPDATE xuống database, thấy rõ giá trị đã đổi trong psql, nhưng code Odoo ngay dòng dưới đọc field đó vẫn ra giá trị cũ. Không phải database sai, không phải transaction chưa commit. Thủ phạm là một tầng bạn thường không thấy: cache của ORM.

Bài này chỉ ra cache đó là gì, khi nào nó lệch, và ba cách làm mới nó — tất cả bằng dữ liệu thật trên một Odoo 19 đang chạy.

ORM cache field để đỡ chạm database

Khi bạn đọc tv.diem, ORM không chạy SELECT mỗi lần. Lần đầu tiên nó nạp giá trị từ database vào một bộ đệm trong env (và thường nạp luôn cho cả nhóm bản ghi lân cận — gọi là prefetch), rồi những lần đọc sau lấy thẳng từ đó. Đây là một trong những lý do vòng lặp trên recordset trong Odoo không nổ hàng nghìn query như ta lo.

diem = tv.diem     # lần đầu: SELECT ... vào cache
diem = tv.diem     # lần sau: KHÔNG chạm DB, lấy từ cache

Bình thường bạn chẳng bao giờ để ý tới cache này, vì mọi thao tác ghi qua ORM (write, create) đều tự cập nhật cache song song với database. Cache và database luôn khớp — chừng nào bạn còn đi qua ORM.

Cache lệch khi bạn đi vòng qua ORM

Vấn đề xuất hiện đúng lúc bạn ghi xuống database không qua ORM — điển hình là một câu SQL thô bằng env.cr.execute(...). ORM không hề biết bạn vừa động vào dữ liệu, nên cache của nó vẫn giữ giá trị cũ. Xem chuyện đó xảy ra thật:

Ảnh chụp phiên odoo shell blog19 thao tác trên bản ghi VIP-0001 có diem 120, lần đọc một trả về 120 và nạp vào cache, sau đó chạy câu UPDATE SQL thô đặt diem thành 999 cho id 1, lần đọc hai vẫn trả về 120 từ cache và bị đánh dấu STALE lệch với DB, rồi gọi tv chấm invalidate_recordset với danh sách chứa diem, lần đọc cuối trả về 999 khớp với database

Hình 1: Bản ghi VIP-0001 có diem=120. Đọc lần đầu → cache giữ 120. Ta UPDATE ... SET diem=999 thẳng bằng SQL thô. Đọc lại: vẫn 120 — ORM trả từ cache, lệch hẳn với database. Chỉ sau invalidate_recordset(['diem']), lần đọc kế mới ra 999 khớp DB.

Đây không phải lỗi của Odoo — đó là hệ quả tất yếu của việc có cache. Cache chỉ đúng khi mọi đường ghi đều báo cho nó. SQL thô là đường ghi không báo, nên sau nó bạn phải tự làm mới cache.

Ba cách làm mới cache

# ghi thẳng bằng SQL thô — ORM không biết
self.env.cr.execute("UPDATE quan_the_thanh_vien SET diem = 999 WHERE id = %s", (tv.id,))
tv.diem                             # STALE: vẫn giá trị cũ trong cache

self.env.invalidate_all()           # xoá TOÀN BỘ cache của env
tv.invalidate_recordset(['diem'])   # chỉ bỏ vài field của recordset này
self.env.flush_all()                # đẩy các ghi đang chờ xuống DB trước

Ảnh chụp đoạn mã Python nền tối, phần đầu minh hoạ đọc field diem hai lần với chú thích lần đầu vào cache lần sau lấy từ cache, phần giữa dùng env chấm cr chấm execute chạy UPDATE SQL thô rồi đọc tv chấm diem vẫn ra giá trị cũ stale, phần cuối liệt kê ba phương thức làm mới cache gồm invalidate_all xoá toàn bộ, invalidate_recordset bỏ vài field của một recordset, và flush_all đẩy ghi đang chờ xuống database

Hình 2: Ba mức làm mới, dùng đúng mức cần. invalidate_recordset(fnames) là mức hẹp nhất — chỉ bỏ vài field trên đúng recordset đó, ít tốn nhất. invalidate_all() xoá sạch cache của cả env — chắc ăn nhưng lần đọc sau nạp lại từ đầu. flush_all() giải quyết chiều ngược: đẩy dữ liệu đang chờ ghi từ cache xuống DB.

Phân biệt hai chiều dễ nhầm:

  • invalidate_* xử lý chiều DB đổi mà cache chưa biết — bỏ cache để lần đọc sau nạp lại từ DB. Dùng sau khi chạy SQL thô ghi dữ liệu.
  • flush_* xử lý chiều cache đổi mà DB chưa biết — ORM gom nhiều thay đổi lại rồi mới ghi một lượt (lười ghi để gộp query). Nếu bạn định chạy một câu SQL thô đọc bảng đó, phải flush trước, nếu không SQL đọc phải dữ liệu cũ trong DB vì thay đổi còn nằm trong cache.

Khi nào bạn thật sự gặp chuyện này

Trong code nghiệp vụ bình thường (chỉ dùng read/write/create/search), bạn không cần đụng tới cache — ORM lo hết. Cache thành vấn đề ở đúng ba tình huống:

  • Chạy SQL thô để tối ưu một thao tác hàng loạt hay đọc thứ ORM không tiện lấy → nhớ flush trước khi đọc, invalidate sau khi ghi.
  • Test kiểm tra giá trị sau một thao tác cấp thấp — đôi khi phải invalidate để đọc đúng trạng thái DB.
  • Migration script động thẳng vào bảng bằng SQL → làm mới cache trước khi phần ORM của script đọc lại.

Nguyên tắc an toàn: nếu bạn ghi xuống DB không qua ORM, hãy invalidate ngay sau đó; nếu bạn đọc từ DB không qua ORM, hãy flush ngay trước đó.

Ba ý mang về

  1. ORM giữ giá trị field trong cache của env; đọc lần đầu chạm DB, các lần sau lấy từ cache. Ghi qua ORM luôn cập nhật cache song song nên bình thường cache và DB khớp.
  2. SQL thô (env.cr.execute) đi vòng qua ORM → cache lệch: đọc field sau đó vẫn ra giá trị cũ (đã chứng minh: 120 trong khi DB là 999).
  3. Làm mới bằng invalidate_recordset(fnames) (hẹp), invalidate_all() (toàn bộ) cho chiều DB-đổi-cache-chưa-biết; và flush_all() cho chiều ngược trước khi đọc DB bằng SQL thô.

Nhắc tới flush, đó chính là chủ đề tiếp theo — thứ quyết định khoảnh khắc ORM thật sự chạm ổ đĩa: Phần sau mổ xẻ flush và cơ chế "lười ghi" của ORM Odoo — vì sao create xong mà database có khi vẫn chưa có dòng nào.