[REF] models: refactor code to update parent_left/parent_right

Put the code to update the MPTT in specific methods, and reduce the number of
queries being made (from 5-6 queries to 2-3 queries).  Add test on MPTT to
validate the refactoring.
This commit is contained in:
Raphael Collet
2018-01-16 10:47:13 +01:00
parent 869f062e08
commit 2ed137af3e
5 changed files with 277 additions and 112 deletions
+1
View File
@@ -443,6 +443,7 @@ class TestParentStore(TransactionCase):
super(TestParentStore, self).setUp()
# pretend the pool has finished loading to avoid deferring parent_store computation
self.patch(self.registry, '_init', False)
self.registry.do_parent_store(self.cr)
# force res_partner_category.copy() to copy children
category = self.env['res.partner.category']
+7 -1
View File
@@ -10,10 +10,16 @@ from odoo.tools import pycompat
class Category(models.Model):
_name = 'test_new_api.category'
_order = 'name'
_parent_store = True
_parent_name = 'parent'
_parent_order = 'name'
name = fields.Char(required=True)
color = fields.Integer('Color Index')
parent = fields.Many2one('test_new_api.category')
parent = fields.Many2one('test_new_api.category', ondelete='cascade')
parent_left = fields.Integer("Left Parent", index=True)
parent_right = fields.Integer("Right Parent", index=True)
root_categ = fields.Many2one(_name, compute='_compute_root_categ')
display_name = fields.Char(compute='_compute_display_name', inverse='_inverse_display_name')
dummy = fields.Char(store=False)
@@ -3,7 +3,7 @@
#
from datetime import date, datetime
from odoo.exceptions import AccessError, except_orm
from odoo.exceptions import AccessError, UserError, except_orm
from odoo.tests import common
from odoo.tools import mute_logger, float_repr, pycompat
@@ -937,3 +937,123 @@ class TestMagicFields(common.TransactionCase):
record = self.env['test_new_api.discussion'].create({'name': 'Booba'})
self.assertEqual(record.create_uid, self.env.user)
self.assertEqual(record.write_uid, self.env.user)
class Tree(object):
def __init__(self, node, *children):
self.node = node
self.children = children
def __str__(self):
return "%s(%s, %s)" % (self.node.name, self.node.parent_left, self.node.parent_right)
class TestParentStore(common.TransactionCase):
def setUp(self):
super(TestParentStore, self).setUp()
# pretend the pool has finished loading to avoid deferring parent_store
# computation
self.patch(self.registry, '_init', False)
self.registry.do_parent_store(self.cr)
def assertTree(self, tree):
self.assertLess(tree.node.parent_left, tree.node.parent_right,
"incorrect node %s" % tree)
for child in tree.children:
self.assertLess(tree.node.parent_left, child.node.parent_left,
"incorrect parent %s - child %s" % (tree, child))
self.assertLess(child.node.parent_right, tree.node.parent_right,
"incorrect parent %s - child %s" % (tree, child))
self.assertTrees(*tree.children)
def assertTrees(self, *trees):
for tree in trees:
self.assertTree(tree)
for tree1, tree2 in pycompat.izip(trees, trees[1:]):
self.assertLess(tree1.node.parent_right, tree2.node.parent_left,
"wrong node order: %s < %s" % (tree1, tree2))
def test_parent_store(self):
""" Test parent_left/parent_right computation. """
Category = self.env['test_new_api.category']
def descendants(recs):
return Category.search([('id', 'child_of', recs.ids)])
def ascendants(recs):
return Category.search([('id', 'parent_of', recs.ids)])
# create root nodes
c = Category.create({'name': 'c'})
a = Category.create({'name': 'a'})
b = Category.create({'name': 'b'})
self.assertTrees(Tree(a), Tree(b), Tree(c))
# create nodes d, e, f under b
f = Category.create({'name': 'f', 'parent': b.id})
d = Category.create({'name': 'd', 'parent': b.id})
e = Category.create({'name': 'e', 'parent': b.id})
self.assertTrees(Tree(a), Tree(b, Tree(d), Tree(e), Tree(f)), Tree(c))
self.assertEqual(descendants(a), a)
self.assertEqual(descendants(b), b + d + e + f)
self.assertEqual(descendants(c), c)
self.assertEqual(descendants(d), d)
self.assertEqual(descendants(e), e)
self.assertEqual(descendants(f), f)
self.assertEqual(ascendants(a), a)
self.assertEqual(ascendants(b), b)
self.assertEqual(ascendants(c), c)
self.assertEqual(ascendants(d), b + d)
self.assertEqual(ascendants(e), b + e)
self.assertEqual(ascendants(f), b + f)
# move d, f under c
(f + d).write({'parent': c.id})
self.assertTrees(Tree(a), Tree(b, Tree(e)), Tree(c, Tree(d), Tree(f)))
self.assertEqual(descendants(a), a)
self.assertEqual(descendants(b), b + e)
self.assertEqual(descendants(c), c + d + f)
self.assertEqual(descendants(d), d)
self.assertEqual(descendants(e), e)
self.assertEqual(descendants(f), f)
self.assertEqual(ascendants(a), a)
self.assertEqual(ascendants(b), b)
self.assertEqual(ascendants(c), c)
self.assertEqual(ascendants(d), c + d)
self.assertEqual(ascendants(e), b + e)
self.assertEqual(ascendants(f), c + f)
# move b, c under a
(b + c).write({'parent': a.id})
self.assertTrees(Tree(a, Tree(b, Tree(e)), Tree(c, Tree(d), Tree(f))))
self.assertEqual(descendants(a), a + b + c + d + e + f)
self.assertEqual(descendants(b), b + e)
self.assertEqual(descendants(c), c + d + f)
self.assertEqual(descendants(d), d)
self.assertEqual(descendants(e), e)
self.assertEqual(descendants(f), f)
self.assertEqual(ascendants(a), a)
self.assertEqual(ascendants(b), a + b)
self.assertEqual(ascendants(c), a + c)
self.assertEqual(ascendants(d), a + c + d)
self.assertEqual(ascendants(e), a + b + e)
self.assertEqual(ascendants(f), a + c + f)
# remove node d
d.unlink()
self.assertTrees(Tree(a, Tree(b, Tree(e)), Tree(c, Tree(f))))
self.assertEqual(descendants(a), a + b + c + e + f)
self.assertEqual(descendants(b), b + e)
self.assertEqual(descendants(c), c + f)
self.assertEqual(descendants(e), e)
self.assertEqual(descendants(f), f)
self.assertEqual(ascendants(a), a)
self.assertEqual(ascendants(b), a + b)
self.assertEqual(ascendants(c), a + c)
self.assertEqual(ascendants(e), a + b + e)
self.assertEqual(ascendants(f), a + c + f)
# not cycle should occur
with self.assertRaises(UserError):
a.parent = e
+147 -109
View File
@@ -3044,24 +3044,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
cr = self._cr
# determine records that require updating parent_left, parent_right
parents_changed = []
if self._parent_store and (self._parent_name in vals) and \
not self._context.get('defer_parent_store_computation'):
# The parent_left/right computation may take up to 5 seconds. No
# need to recompute the values if the parent is the same.
#
# Note: to respect parent_order, nodes must be processed in
# order, so ``parents_changed`` must be ordered properly.
parent_val = vals[self._parent_name]
if parent_val:
query = "SELECT id FROM %s WHERE id IN %%s AND (%s != %%s OR %s IS NULL) ORDER BY %s" % \
(self._table, self._parent_name, self._parent_name, self._parent_order)
cr.execute(query, (tuple(self.ids), parent_val))
else:
query = "SELECT id FROM %s WHERE id IN %%s AND (%s IS NOT NULL) ORDER BY %s" % \
(self._table, self._parent_name, self._parent_order)
cr.execute(query, (tuple(self.ids),))
parents_changed = [x[0] for x in cr.fetchall()]
parent_records = self._parent_store_update_prepare(vals)
# determine SQL values
columns = [] # list of (column_name, format, value)
@@ -3149,63 +3132,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# check Python constraints
self._validate_fields(vals)
# TODO: use _order to set dest at the right position and not first node of parent
# We can't defer parent_store computation because the stored function
# fields that are computer may refer (directly or indirectly) to
# parent_left/right (via a child_of domain)
if parents_changed:
if self.pool._init:
self.pool._init_parent[self._name] = True
else:
parent_val = vals[self._parent_name]
if parent_val:
clause, params = '%s=%%s' % self._parent_name, (parent_val,)
else:
clause, params = '%s IS NULL' % self._parent_name, ()
for id in parents_changed:
# determine old parent_left, parent_right of current record
cr.execute('SELECT parent_left, parent_right FROM %s WHERE id=%%s' % self._table, (id,))
pleft0, pright0 = cr.fetchone()
width = pright0 - pleft0 + 1
# determine new parent_left of current record; it comes
# right after the parent_right of its closest left sibling
# (this CANNOT be fetched outside the loop, as it needs to
# be refreshed after each update, in case several nodes are
# sequentially inserted one next to the other)
pleft1 = None
cr.execute('SELECT id, parent_right FROM %s WHERE %s ORDER BY %s' % \
(self._table, clause, self._parent_order), params)
for (sibling_id, sibling_parent_right) in cr.fetchall():
if sibling_id == id:
break
pleft1 = (sibling_parent_right or 0) + 1
if not pleft1:
# the current record is the first node of the parent
if not parent_val:
pleft1 = 0 # the first node starts at 0
else:
cr.execute('SELECT parent_left FROM %s WHERE id=%%s' % self._table, (parent_val,))
pleft1 = cr.fetchone()[0] + 1
if pleft0 < pleft1 <= pright0:
raise UserError(_('Recursivity Detected.'))
# make some room for parent_left and parent_right at the new position
cr.execute('UPDATE %s SET parent_left=parent_left+%%s WHERE %%s<=parent_left' % self._table, (width, pleft1))
cr.execute('UPDATE %s SET parent_right=parent_right+%%s WHERE %%s<=parent_right' % self._table, (width, pleft1))
# slide the subtree of the current record to its new position
if pleft0 < pleft1:
cr.execute('''UPDATE %s SET parent_left=parent_left+%%s, parent_right=parent_right+%%s
WHERE %%s<=parent_left AND parent_left<%%s''' % self._table,
(pleft1 - pleft0, pleft1 - pleft0, pleft0, pright0))
else:
cr.execute('''UPDATE %s SET parent_left=parent_left-%%s, parent_right=parent_right-%%s
WHERE %%s<=parent_left AND parent_left<%%s''' % self._table,
(pleft0 - pleft1 + width, pleft0 - pleft1 + width, pleft0 + width, pright0 + width))
self.invalidate_cache(['parent_left', 'parent_right'])
# update parent_left/parent_right
if parent_records:
parent_records._parent_store_update(vals)
return True
@@ -3296,12 +3225,16 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
@api.model
def _create(self, vals):
self = self.browse()
cr = self.env.cr
# set boolean fields to False by default (to make search more powerful)
for name, field in self._fields.items():
if field.type == 'boolean' and field.store:
vals.setdefault(name, False)
# prepare the update of parent_left, parent_right
parent_store = self._parent_store_create_prepare(vals)
# determine SQL values
columns = [] # list of (column_name, format, value)
other_fields = [] # list of non-column fields
@@ -3332,7 +3265,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
columns.append(('write_date', '%s', AsIs("(now() at time zone 'UTC')")))
# insert a row for this record
cr = self._cr
query = """INSERT INTO "%s" (%s) VALUES(%s) RETURNING id""" % (
self._table,
', '.join('"%s"' % column[0] for column in columns),
@@ -3344,39 +3276,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
self = self.browse(cr.fetchone()[0])
# update parent_left, parent_right
if self._parent_store and not self._context.get('defer_parent_store_computation'):
if self.pool._init:
self.pool._init_parent[self._name] = True
else:
parent_val = vals.get(self._parent_name)
if parent_val:
# determine parent_left: it comes right after the
# parent_right of its closest left sibling
pleft = None
cr.execute("SELECT parent_right FROM %s WHERE %s=%%s ORDER BY %s" % \
(self._table, self._parent_name, self._parent_order),
(parent_val,))
for (pright,) in cr.fetchall():
if not pright:
break
pleft = pright + 1
if not pleft:
# this is the leftmost child of its parent
cr.execute("SELECT parent_left FROM %s WHERE id=%%s" % self._table, (parent_val,))
pleft = cr.fetchone()[0] + 1
else:
# determine parent_left: it comes after all top-level parent_right
cr.execute("SELECT MAX(parent_right) FROM %s" % self._table)
pleft = (cr.fetchone()[0] or 0) + 1
# make some room for the new node, and insert it in the MPTT
cr.execute("UPDATE %s SET parent_left=parent_left+2 WHERE parent_left>=%%s" % self._table,
(pleft,))
cr.execute("UPDATE %s SET parent_right=parent_right+2 WHERE parent_right>=%%s" % self._table,
(pleft,))
cr.execute("UPDATE %s SET parent_left=%%s, parent_right=%%s WHERE id=%%s" % self._table,
(pleft, pleft + 1, record_id))
self.invalidate_cache(['parent_left', 'parent_right'])
if parent_store:
self._parent_store_update(vals)
with self.env.protecting(protected_fields, self):
# mark fields to recompute; do this before setting other fields,
@@ -3417,6 +3318,143 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
return self
def _parent_store_create_prepare(self, vals):
""" Prepare the creation of a record, and return whether its
parent_left/parent_right fields must be updated after creation.
"""
if not self._parent_store or self._context.get('defer_parent_store_computation'):
return False
if self.pool._init:
self.pool._init_parent.add(self._name)
return False
# temporarily put the node at the top-level rightmost position
query = "SELECT COALESCE(MAX(parent_right) + 1, 0) FROM {}"
self._cr.execute(query.format(self._table))
parent_left = self._cr.fetchone()[0]
vals.setdefault(self._parent_name, False)
vals['parent_left'] = parent_left
vals['parent_right'] = parent_left + 1
return True
def _parent_store_update_prepare(self, vals):
""" Return the records in ``self`` that must update their parent_left,
parent_right fields, in their sibling order. This must be called
before updating the parent field.
"""
if not self._parent_store or self._parent_name not in vals or \
self._context.get('defer_parent_store_computation'):
return self.browse()
if self.pool._init:
self.pool._init_parent.add(self._name)
return self.browse()
# The parent_left/right computation may take up to 5 seconds. No need to
# recompute the values if the parent is the same.
#
# Note: to respect parent_order, nodes must be processed in order, so
# the result must be ordered properly.
parent_val = vals[self._parent_name]
if parent_val:
query = """ SELECT id FROM {0}
WHERE id IN %s AND ({1} != %s OR {1} IS NULL)
ORDER BY {2} """
params = [tuple(self.ids), parent_val]
else:
query = """ SELECT id FROM {0}
WHERE id IN %s AND {1} IS NOT NULL
ORDER BY {2} """
params = [tuple(self.ids)]
query = query.format(self._table, self._parent_name, self._parent_order)
self._cr.execute(query, params)
return self.browse([row[0] for row in self._cr.fetchall()])
def _parent_store_update(self, vals):
""" Update the parent_left/parent_right fields of ``self``. """
cr = self.env.cr
parent_val = vals[self._parent_name]
if parent_val:
clause, params = '{}=%s'.format(self._parent_name), [parent_val]
else:
clause, params = '{} IS NULL'.format(self._parent_name), []
modified_ids = set()
for record in self:
# retrieve record's siblings data (this CANNOT be fetched
# outside the loop, as it needs to be refreshed after each
# update, in case several nodes are sequentially inserted one
# next to the other)
query = """ SELECT id, parent_left, parent_right FROM {}
WHERE {} ORDER BY {} """
cr.execute(query.format(self._table, clause, self._parent_order), params)
siblings = cr.fetchall()
# determine record's position among its siblings
index = next(pos
for pos, data in enumerate(siblings)
if data[0] == record.id)
# retrieve record's current data
pleft0, pright0 = siblings[index][1:]
width = pright0 - pleft0 + 1
# determine new parent_left of current record
if index:
# record comes right after its closest left sibling
pleft1 = (siblings[index - 1][2] or 0) + 1
else:
# record is the first node of its parent
if not parent_val:
pleft1 = 0 # the first node starts at 0
else:
query = "SELECT parent_left FROM {} WHERE id=%s"
cr.execute(query.format(self._table), [parent_val])
pleft1 = cr.fetchone()[0] + 1
if pleft0 == pleft1:
continue
if pleft0 < pleft1 <= pright0:
raise UserError(_('Recursivity Detected.'))
# We have to slide values in the interval [x,x+a+b) in order to
# swap the subsets in [x,x+a) and [x+a,x+a+b), respectively.
#
# x x+a x+b x+a+b
# |-----------|-----------|-----------|
# before: A A A A A A B B B B B B B B B B B B
# after: B B B B B B B B B B B B A A A A A A
#
query1 = """
UPDATE {}
SET parent_left = parent_left + (CASE
WHEN parent_left<%(x)s THEN 0
WHEN parent_left<%(x)s+%(a)s THEN %(b)s
WHEN parent_left<%(x)s+%(a)s+%(b)s THEN -%(a)s
ELSE 0 END),
parent_right = parent_right + (CASE
WHEN parent_right<%(x)s THEN 0
WHEN parent_right<%(x)s+%(a)s THEN %(b)s
WHEN parent_right<%(x)s+%(a)s+%(b)s THEN -%(a)s
ELSE 0 END)
WHERE (parent_left BETWEEN %(x)s AND %(x)s+%(a)s+%(b)s-1)
OR (parent_right BETWEEN %(x)s AND %(x)s+%(a)s+%(b)s-1)
RETURNING id
"""
if pleft0 < pleft1:
# x = pleft0, a = width, x+a+b = pleft1
params1 = dict(x=pleft0, a=width, b=pleft1 - pleft0 - width)
else:
# x = pleft1, x+a = pleft0, b = width
params1 = dict(x=pleft1, a=pleft0 - pleft1, b=width)
cr.execute(query1.format(self._table), params1)
modified_ids.update(row[0] for row in cr.fetchall())
self.browse(modified_ids).modified(['parent_left', 'parent_right'])
# TODO: ameliorer avec NULL
@api.model
def _where_calc(self, domain, active_test=True):
+1 -1
View File
@@ -111,7 +111,7 @@ class Registry(Mapping):
self.models = {} # model name/model instance mapping
self._sql_error = {}
self._init = True
self._init_parent = {}
self._init_parent = set()
self._assertion_report = assertion_report.assertion_report()
self._fields_by_model = None
self._post_init_queue = deque()