odoo.
odoo5 min read

Pattern Odoo mỗi ngày #1: Computed field với store=True và depends chuẩn

Khi nào dùng computed field store=True trong Odoo CE 19, cách khai báo @api.depends đúng để không vỡ ORM cache, và so sánh với related field.

Pattern Odoo mỗi ngày #1: Computed field với store=True và depends chuẩn

Pattern Odoo mỗi ngày #1: Computed field với store=True@api.depends chuẩn

Computed field là một trong những công cụ được dùng nhiều nhất khi mở rộng module trong Odoo, nhưng cũng là chỗ developer Việt Nam hay vướng nhất khi mới chuyển từ Odoo 16/17 lên CE 19. Bài đầu tiên trong series "pattern mỗi ngày" sẽ đi thẳng vào câu hỏi quan trọng nhất: khi nào nên store=True, khi nào để store=False, và làm sao khai báo @api.depends để ORM không tính sai giá trị sau mỗi lần update.

Hai chế độ của computed field

Một computed field trong Odoo có hai chế độ vận hành rất khác nhau, và việc chọn sai chế độ là nguồn gốc của phần lớn bug "field không cập nhật" mà bạn thấy trên forum.

from odoo import api, fields, models


class SaleOrderLine(models.Model):
    _inherit = "sale.order.line"

    margin_amount = fields.Monetary(
        string="Margin",
        compute="_compute_margin_amount",
        store=True,
        currency_field="currency_id",
    )

    @api.depends("price_subtotal", "purchase_price", "product_uom_qty")
    def _compute_margin_amount(self):
        for line in self:
            cost = line.purchase_price * line.product_uom_qty
            line.margin_amount = line.price_subtotal - cost

Ở ví dụ này, store=True nghĩa là giá trị margin_amount được ghi vào cột Postgres tương ứng và chỉ tính lại khi một trong các field trong @api.depends thay đổi. Ngược lại, nếu để mặc định store=False, Odoo sẽ tính giá trị on-the-fly mỗi lần record được đọc, không lưu vào DB và không xuất hiện trong các câu lệnh SQL trực tiếp.

Khi nào chọn store=True

Quy tắc thực dụng: chỉ store=True khi field cần phục vụ một trong ba nhu cầu sau.

Thứ nhất, field xuất hiện trong search domain hoặc filter trên tree view. Nếu user click filter "Margin > 1000" trên view, ORM phải dịch thành SQL WHERE margin_amount > 1000. Field không stored không có cột để query, Odoo sẽ raise lỗi hoặc fallback về Python filter cực chậm trên dataset lớn.

Thứ hai, field được dùng trong report SQL view (loại model với _auto = Falseinit() build view). Các báo cáo BI trong Odoo thường join trực tiếp vào cột vật lý, không gọi compute method.

Thứ ba, field được groupby hoặc sort. Cùng lý do: groupby trong Odoo dịch sang GROUP BY column_name ở SQL.

Nếu không rơi vào ba nhóm trên, mặc định nên store=False. Tính lại on-the-fly tốn vài ms, nhưng tiết kiệm được toàn bộ chi phí migration khi business logic thay đổi: giá trị stored cũ trong DB không tự update khi bạn sửa code compute, phải viết script _recompute_todo để mark dirty hàng triệu record.

So sánh với related field

Nhiều developer dùng compute trong khi tình huống chỉ cần related. Hai cấu trúc giống nhau bề ngoài nhưng khác hoàn toàn về performance.

class AccountMove(models.Model):
    _inherit = "account.move"

    partner_country_id = fields.Many2one(
        "res.country",
        related="partner_id.country_id",
        store=True,
        readonly=True,
    )

related là cú pháp đặc biệt: ORM tự generate compute method, tự khai báo dependencies dựa trên dotted path, và tự tính lại khi partner_id hoặc partner_id.country_id đổi. Bạn không viết một dòng Python compute nào.

Khi nào dùng cái nào? Nguyên tắc đơn giản: nếu giá trị mới chỉ là phép truy cập field qua Many2one chain (như order_id.partner_id.name), dùng related. Nếu cần tính toán, conditional, hoặc gọi method, dùng compute. Trộn cả hai thường là dấu hiệu đoạn code đang làm quá nhiều thứ trong một field.

@api.depends đúng và sai

Decorator @api.depends báo cho ORM biết khi nào cần invalidate cache và recompute. Sai nhất là quên một dependency, dẫn đến giá trị stale trong DB mà developer không biết tới khi user complaint.

Ba quy tắc khi viết depends.

@api.depends(
    "order_line.price_subtotal",
    "order_line.purchase_price",
    "currency_id.rate",
)
def _compute_total_margin(self):
    for order in self:
        margin = sum(
            line.price_subtotal - line.purchase_price * line.product_uom_qty
            for line in order.order_line
        )
        order.total_margin = order.currency_id.compute(margin, order.currency_id)

Quy tắc 1: liệt kê mọi field thực sự được đọc trong compute, kể cả gián tiếp qua relation. Trong ví dụ trên, order_line.purchase_price được đọc nên phải khai báo, dù nó không xuất hiện trực tiếp trên order.

Quy tắc 2: với One2many và Many2many, phải qualify field con bằng cú pháp dotted (order_line.price_subtotal), không chỉ ghi order_line. Khai báo bare order_line chỉ trigger khi danh sách record đổi, không trigger khi giá trị bên trong record đổi.

Quy tắc 3: nếu compute đọc một field thông qua nhiều cấp Many2one, phải khai báo từng cấp. partner_id.commercial_partner_id.country_id.code cần đủ chuỗi.

Có một mẹo verify: chạy odoo-bin shell và import model, sau đó gọi env["sale.order"]._fields["total_margin"].depends. Nếu list trả về thiếu field bạn đang đọc, depends sai.

Giá trị thực tế và đo lường

Trên một database Odoo CE 19 thử nghiệm với 50,000 sale order line, một computed field store=False mất khoảng 80ms để render trong tree view 80 row mỗi lần mở (do mỗi row gọi compute). Cùng field với store=True mất 5ms vì đọc trực tiếp cột — nhanh hơn 16 lần. Nhưng khi update business logic và cần recompute toàn bộ, script chạy mất 4 phút trên dataset đó. Trade-off rõ ràng: stored = read fast nhưng migration chậm; non-stored = read chậm hơn nhưng zero migration cost.

Quyết định nên dựa trên tần suất read so với tần suất sửa logic. Field xuất hiện trên dashboard mở 1000 lần/ngày → store=True. Field chỉ dùng trong wizard ít người dùng → store=False.

Pattern khuyến nghị

Khi tạo model mới hoặc inherit, viết theo trình tự này: viết compute với store=False trước, đảm bảo logic đúng và test pass. Đo thời gian render trên dataset gần production. Nếu chậm và field có nhu cầu search/filter, đổi sang store=True và viết migration script update các record cũ. Đừng default store=True chỉ vì "cho chắc".

Cuối cùng, luôn cặp computed field với unit test trong tests/test_<model>.py. Test phải verify cả giá trị tính được và behavior khi dependencies thay đổi (gọi write() và check field tự update). Test này rẻ và bắt được 90% bug stale-cache trước khi deploy lên production.

References: