Saltar al contenido principal

Campo relacionado con Odoo obsoleto: Guía de reparación de invalidación de caché

¿El campo relacionado muestra un valor obsoleto en Odoo 17/18/19? Corrija la invalidación de caché con invalidate_recordset() y declaraciones @api.depends adecuadas en producción.

E
ECOSIRE Research and Development Team
|4 de mayo de 20267 min de lectura1.2k Palabras|

Este artículo actualmente está disponible solo en inglés. La traducción estará disponible próximamente.

You update a field on a parent record and the related field on a child record still shows the old value:

You update a field on a parent record and the related field on a child record still shows the old value:

# In a shell
order.partner_id.email  # shows new value: 'new@x.com'
order.line_ids[0].partner_email  # shows OLD value: 'old@x.com'
# where partner_email is fields.Char(related='order_id.partner_id.email')

There is no exception, no error log — just wrong data on screen and in reports. This is one of the most insidious Odoo bugs. It bites Odoo 17.0/18.0/19.0 alike, and it almost always points at a cache invalidation gap, not a database problem.

Quick Fix

Force the cache to clear before the read:

self.invalidate_recordset(['partner_email'])
fresh_value = self.partner_email

For related fields, the durable fix is usually to switch to a stored compute with explicit @api.depends:

# WRONG — related field, cache invalidation can lag
partner_email = fields.Char(related='order_id.partner_id.email')

# RIGHT — explicit, predictable
partner_email = fields.Char(
    compute='_compute_partner_email',
    store=True,
)

@api.depends('order_id.partner_id.email')
def _compute_partner_email(self):
    for line in self:
        line.partner_email = line.order_id.partner_id.email or False

Why This Happens

Odoo keeps a per-cursor cache of field values. When you write to a field, Odoo invalidates the cache for that field and any field that depends on it via @api.depends. Cache invalidation breaks down when:

  1. The dependency graph is incomplete. A related field's invalidation chain is field <- intermediate <- root. If the chain is long or crosses a compute that has wrong @api.depends, invalidation stops short.
  2. A direct SQL UPDATE bypassed the ORM. The database row is current, but the in-memory cache still has the old value.
  3. Multi-cursor isolation. A request handler's cursor sees a value committed by another worker after the request started. Until the next request, the cache shows the older value.
  4. browse() after write() in the same flow without a flush. The cache returns the value from before the write because no read has refreshed it.
  5. prefetch_fields has loaded a stale value into the prefetch cache, and subsequent accesses use it.

Step-by-Step Diagnosis

1. Confirm the database is correct. Run direct SQL:

SELECT email FROM res_partner WHERE id = 7;

If the database has the new value but Odoo shows the old value, it is a cache problem, not a data problem.

2. Check the dependency graph.

field = env['sale.order.line']._fields['partner_email']
print(field._depends)  # the full transitive depend tree
print(field.related)   # if it is a related field

Look for gaps. If partner_email is related='order_id.partner_id.email' but order_id was set after the line was created, the cache may not have linked them.

3. Force invalidation manually.

self.env.invalidate_all()  # nuclear — clears entire cache
fresh = self.partner_email

If fresh shows the right value, you have confirmed it is a cache-invalidation gap.

4. Check for direct SQL writes. Search your codebase for cr.execute("UPDATE ..."). Each one is suspect — direct SQL bypasses the ORM cache.

5. Test in a fresh request. Reload the browser tab. If the value is now correct, the bug is intra-request — invalidation never fired during that request's flow.

Permanent Fix

Replace fragile related with stored compute fields:

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

    partner_email = fields.Char(
        compute='_compute_partner_email',
        store=True,
        readonly=True,
    )

    @api.depends('order_id.partner_id.email')
    def _compute_partner_email(self):
        for line in self:
            line.partner_email = line.order_id.partner_id.email or False

Stored computes with explicit deps are predictable, indexable, and survive every Odoo upgrade. related fields are syntactic sugar with looser invalidation semantics — fine for trivial cases, fragile when the chain crosses module boundaries.

For places where you wrote and immediately read, explicitly flush:

order.partner_id = new_partner
self.env.flush_all()       # writes any pending changes
self.env.invalidate_all()  # drops cache so next read goes to DB
print(order.line_ids[0].partner_email)   # fresh from DB

Use flush_all() and invalidate_all() sparingly — they cost performance — but they are the right tools when correctness matters.

For direct SQL writes, wrap them so cache invalidation always follows:

def _bulk_update_emails(self, partner_ids, new_email):
    self.env.cr.execute("""
        UPDATE res_partner SET email = %s WHERE id = ANY(%s)
    """, (new_email, partner_ids))
    # CRITICAL: invalidate the affected records' cache
    self.env['res.partner'].browse(partner_ids).invalidate_recordset(['email'])

If you do not invalidate after raw SQL, every code path that already has those records cached will read stale values until the cursor closes.

How to Prevent It

  • Avoid related for cross-module dependencies. A related field that crosses module boundaries is fragile. Use a stored compute with explicit @api.depends instead.
  • Never bypass the ORM unless you also invalidate. Make this a code-review rule: every cr.execute("UPDATE") must be followed by an explicit invalidate_recordset() or invalidate_all().
  • Lint for @api.depends completeness. OCA's pylint-odoo checks that @api.depends lists match the fields actually accessed in the compute body. Wire it into CI.
  • Test cache invalidation explicitly. A unit test that writes a parent field and reads the related child field on the same cursor catches invalidation bugs cheaply. We ship one for every related field in ECOSIRE modules.
  • Use .flush_all() before tests assert. Vitest-style assertion right after a write can read cached pre-write values. Flushing first removes the false negatives.
  • Avoid long related chains. Anything more than 2 hops (a_id.b_id.field) is asking for trouble. Flatten with a compute.

Frequently Asked Questions

What is the difference between invalidate_all() and invalidate_recordset()?

invalidate_all() drops the entire ORM cache for the current cursor — every record, every field. It is correct after operations that touched many models. invalidate_recordset(['field_a', 'field_b']) is surgical: only the listed fields on the current recordset get evicted. Prefer the surgical version unless you know you need the full clear.

Does flush_all() commit to the database?

No. flush_all() writes the in-memory pending changes to the cursor (so subsequent SQL queries see them), but the transaction itself only commits at the end of the request or when you call cr.commit() explicitly. Cache invalidation and transaction commit are different operations.

Can I disable the cache?

You can, with env['model'].with_context(prefetch_fields=False), but the performance cost is severe — every field access becomes a DB round-trip. The cache is correct in 99 percent of cases; chase the bug, not the architecture.

Studio fields are stored as ordinary ir.model.fields rows but with the compute defined as a Python expression evaluated at runtime. They are particularly prone to invalidation gaps because Studio sometimes generates incomplete @api.depends. The fix is to convert the field to a hand-written compute in a custom module — Studio is fine for prototypes but should be rewritten as Python for production.

Does read_group see stale cache?

Yes, if a write happened earlier in the same transaction without a flush. read_group queries SQL directly, but Odoo writes are buffered in the cache and only flushed at commit. Call self.env.flush_all() before read_group to ensure the SQL sees the latest data.

Can I see what is in the cache for debugging?

Yes:

print(self.env.cache._data.keys())   # field tuples currently cached

Or for a specific field on a specific record:

print(self.env.cache.get(rec, rec._fields['partner_email']))

Useful when chasing "why is this value wrong" through several writes.


Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.

Compartir:
E

Escrito por

ECOSIRE Team

Technical Writing

The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.

ECOSIRE

Transforme su negocio con Odoo ERP

Implementación, personalización y soporte experto de Odoo para optimizar sus operaciones.