Files
odoo_source/odoo/addons/test_new_api/tests/test_many2many.py
T
Raphael ColletandDidier Debondt 036ef5b96d [FIX] core: use EXISTS in domain conditions on many2many fields
This fixes two problems that occur with conditions on many2many fields:
the subtle bug caused by NULL values that are returned by subqueries,
and the performance of those subqueries.

The problem with NULL values returned by subqueries is the fact that
they make the SQL condition undetermined instead of false, and this
discards some of the results returned by a query.  Simply consider:

    id NOT IN (1, 2, 3)

The value id=3 makes the condition false, while i=4 makes it true.  Now
consider that the set also includes a NULL value, like:

    id NOT IN (1, 2, 3, NULL)

The value id=3 makes the condition false, but the value id=4 makes it
undetermined.  Therefore this condition is actually undetermined for all
possible values of id, and a query containing that condition simply
returns nothing!

Regarding performance, consider a domain like [('tag_ids', '=', False)].
It is currently translated in SQL as

    "model".id NOT IN (
        SELECT "model_id"
        FROM "model_tag_rel"
        WHERE "model_id" IS NOT NULL
    )

PostgreSQL does not handle well this "NOT IN" expression when the
relation table is large, and uses some very inefficient query plan in
that case.  The inefficient query plan is possibly related to the
special handling of NULL values, by the way.  This commit changes the
above expression to a "NOT EXISTS" expression (as below), which is
executed with a much more efficient query plan, even with large tables.

    NOT EXISTS (
        SELECT 1
        FROM "model_tag_rel"
        WHERE "model_tag_rel"."model_id" = "model".id
    )

This change was motivated by a use-case with 2.7M account moves, 11M
account move lines and a many2many relation table with 2.3M lines
(`account_move_line_account_tax_rel`).  The query was timing out (15
minutes), and it now takes 15 seconds.

We have replaced all the conditions on many2many fields to use the
relational operators EXISTS and NOT EXISTS.

A test has been added to reflect a case where the many2many table
contains NULL values, like the one for channel_partner in regards to
partner_id and guest_id.

task-2753782

X-original-commit: 334b2aa2aa437d2ed86130f2ed426025f8fe4b86
Part-of: odoo/odoo#87285
Co-authored-by: Didier Debondt <did@odoo.com>
2022-03-25 17:47:43 +01:00

32 lines
1.2 KiB
Python

# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
class Many2manyCase(TransactionCase):
def setUp(self):
super().setUp()
self.ship = self.env['test_new_api.ship'].create({'name': 'Colombus'})
# the ship contains one prisoner
self.env['test_new_api.prisoner'].create({
'name': 'Brian',
'ship_ids': self.ship.ids,
})
# the ship contains one pirate
self.blackbeard = self.env['test_new_api.pirate'].create({
'name': 'Black Beard',
'ship_ids': self.ship.ids,
})
self.redbeard = self.env['test_new_api.pirate'].create({'name': 'Red Beard'})
def test_not_in_relation(self):
pirates = self.env['test_new_api.pirate'].search([('ship_ids', 'not in', self.ship.ids)])
self.assertEqual(len(pirates), 1)
self.assertEqual(pirates, self.redbeard)
def test_not_in_relation_as_query(self):
# ship_ids is a Query object
ship_ids = self.env['test_new_api.ship']._search([('name', '=', 'Colombus')])
pirates = self.env['test_new_api.pirate'].search([('ship_ids', 'not in', ship_ids)])
self.assertEqual(len(pirates), 1)
self.assertEqual(pirates, self.redbeard)