Improve MPTT

- Remove the order on MPTT
- Make room with queries using left/right instead of ids
- Call _update_tree multiple times for small list instead of rebuild the all
  tree
- Improve test for with many roots
- Factorize the test to update MPTT

issue1892
review1007002
This commit is contained in:
Cédric Krier committed 2013-09-03 10:26:23 +02:00
1 parent a7e7b3b91e
commit 0210370575
4 files changed
+80 -189

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
* Remove the default order on MPTT
* Add grouped attribute to data tag
* Store selection tree state
* Add order to Action Window
+66 -129
View File
@@ -381,17 +381,8 @@ class ModelSQL(ModelStorage):
records = cls.browse(new_ids)
cls._validate(records)
# Check for Modified Preorder Tree Traversal
for k in cls._fields:
field = cls._fields[k]
if (isinstance(field, fields.Many2One)
and field.model_name == cls.__name__
and field.left and field.right):
if len(new_ids) == 1:
cls._update_tree(new_ids[0], k, field.left, field.right)
else:
with Transaction().set_user(0):
cls._rebuild_tree(k, False, 0)
field_names = cls._fields.keys()
cls._update_mptt(field_names, [new_ids] * len(field_names))
cls.trigger_create(records)
return records
@@ -826,21 +817,8 @@ class ModelSQL(ModelStorage):
cls._validate(records)
# Check for Modified Preorder Tree Traversal
for k in cls._fields:
field = cls._fields[k]
if isinstance(field, fields.Many2One) \
and field.model_name == cls.__name__ \
and field.left and field.right:
if field.left in values or field.right in values:
raise Exception('ValidateError',
'You can not update fields: "%s", "%s"' %
(field.left, field.right))
if len(ids) == 1:
cls._update_tree(ids[0], k, field.left, field.right)
else:
with Transaction().set_user(0):
cls._rebuild_tree(k, False, 0)
field_names = cls._fields.keys()
cls._update_mptt(field_names, [ids] * len(field_names), values)
cls.trigger_write(trigger_eligibles)
@@ -1017,13 +995,7 @@ class ModelSQL(ModelStorage):
'(id, write_uid, write_date) VALUES (%s, %s, %s)',
(obj_id, Transaction().user, datetime.datetime.now()))
for k in tree_ids.keys():
field = cls._fields[k]
if len(tree_ids[k]) == 1:
cls._update_tree(tree_ids[k][0], k, field.left, field.right)
else:
with Transaction().set_user(0):
cls._rebuild_tree(k, False, 0)
cls._update_mptt(tree_ids.keys(), tree_ids.values())
@classmethod
def search(cls, domain, offset=0, limit=None, order=None, count=False,
@@ -1894,6 +1866,31 @@ class ModelSQL(ModelStorage):
raise Exception('Error', 'Wrong field name (%s) for %s in order!' %
(field, cls.__name__))
@classmethod
def _update_mptt(cls, field_names, list_ids, values=None):
cursor = Transaction().cursor
count = None
for field_name, ids in zip(field_names, list_ids):
field = cls._fields[field_name]
if (isinstance(field, fields.Many2One)
and field.model_name == cls.__name__
and field.left and field.right):
if (values is not None
and (field.left in values or field.right in values)):
raise Exception('ValidateError',
'You can not update fields: "%s", "%s"' %
(field.left, field.right))
if count is None:
cursor.execute('SELECT COUNT(1) FROM "' + cls._table + '"')
count, = cursor.fetchone()
if len(ids) < count / 4:
for id_ in ids:
cls._update_tree(id_, field_name,
field.left, field.right)
else:
with Transaction().set_user(0):
cls._rebuild_tree(field_name, None, 0)
@classmethod
def _rebuild_tree(cls, parent, parent_id, left):
'''
@@ -1902,10 +1899,9 @@ class ModelSQL(ModelStorage):
cursor = Transaction().cursor
right = left + 1
with Transaction().set_user(0):
childs = cls.search([
(parent, '=', parent_id),
])
childs = cls.search([
(parent, '=', parent_id),
], order=[])
for child in childs:
right = cls._rebuild_tree(parent, child.id, right)
@@ -1920,35 +1916,36 @@ class ModelSQL(ModelStorage):
return right + 1
@classmethod
def _update_tree(cls, object_id, field_name, left, right):
def _update_tree(cls, record_id, field_name, left, right):
'''
Update left, right values for the tree.
Remarks:
- the value (right - left - 1) / 2 will not give
the number of children node
- the order of the tree respects the default _order
'''
cursor = Transaction().cursor
cursor.execute('SELECT "' + left + '", "' + right + '" '
cursor.execute('SELECT "' + left + '", "' + right + '", '
'"' + field_name + '" '
'FROM "' + cls._table + '" '
'WHERE id = %s', (object_id,))
'WHERE id = %s', (record_id,))
fetchone = cursor.fetchone()
if not fetchone:
return
old_left, old_right = fetchone
if old_left == old_right:
old_left, old_right, parent_id = fetchone
if old_left == old_right == 0:
cursor.execute('SELECT MAX("' + right + '") '
'FROM "' + cls._table + '" '
'WHERE "' + field_name + '" IS NULL')
old_left, = cursor.fetchone()
old_left += 1
old_right = old_left + 1
cursor.execute('UPDATE "' + cls._table + '" '
'SET "' + right + '" = "' + right + '" + 1 '
'WHERE id = %s', (object_id,))
old_right += 1
'SET "' + left + '" = %s, "' + right + '" = %s '
'WHERE id = %s', (old_left, old_right, record_id))
size = old_right - old_left + 1
parent_right = 1
cursor.execute('SELECT "' + field_name + '" '
'FROM "' + cls._table + '" '
'WHERE id = %s', (object_id,))
parent_id = cursor.fetchone()[0] or False
if parent_id:
cursor.execute('SELECT "' + right + '" '
'FROM "' + cls._table + '" '
@@ -1962,85 +1959,25 @@ class ModelSQL(ModelStorage):
if fetchone:
parent_right = fetchone[0] + 1
cursor.execute('SELECT id FROM "' + cls._table + '" '
'WHERE "' + left + '" >= %s AND "' + right + '" <= %s',
(old_left, old_right))
child_ids = [x[0] for x in cursor.fetchall()]
if len(child_ids) > cursor.IN_MAX:
with Transaction().set_user(0):
return cls._rebuild_tree(field_name, False, 0)
red_child_sql, red_child_ids = reduce_ids('id', child_ids)
# ids for left update
cursor.execute('SELECT id FROM "' + cls._table + '" '
'WHERE "' + left + '" >= %s '
'AND NOT ' + red_child_sql,
[parent_right] + red_child_ids)
left_ids = [x[0] for x in cursor.fetchall()]
# ids for right update
cursor.execute('SELECT id FROM "' + cls._table + '" '
'WHERE "' + right + '" >= %s '
'AND NOT ' + red_child_sql,
[parent_right] + red_child_ids)
right_ids = [x[0] for x in cursor.fetchall()]
if left_ids:
for i in range(0, len(left_ids), cursor.IN_MAX):
sub_ids = left_ids[i:i + cursor.IN_MAX]
red_sub_sql, red_sub_ids = reduce_ids('id', sub_ids)
cursor.execute('UPDATE "' + cls._table + '" '
'SET "' + left + '" = "' + left + '" + '
+ str(old_right - old_left + 1) + ' '
'WHERE ' + red_sub_sql, red_sub_ids)
if right_ids:
for i in range(0, len(right_ids), cursor.IN_MAX):
sub_ids = right_ids[i:i + cursor.IN_MAX]
red_sub_sql, red_sub_ids = reduce_ids('id', sub_ids)
cursor.execute('UPDATE "' + cls._table + '" '
'SET "' + right + '" = "' + right + '" + '
+ str(old_right - old_left + 1) + ' '
'WHERE ' + red_sub_sql, red_sub_ids)
cursor.execute('UPDATE "' + cls._table + '" '
'SET "' + left + '" = "' + left + '" + '
+ str(parent_right - old_left) + ', '
'"' + right + '" = "' + right + '" + '
+ str(parent_right - old_left) + ' '
'WHERE ' + red_child_sql, red_child_ids)
# Use root user to by-pass rules
with contextlib.nested(Transaction().set_user(0),
Transaction().set_context(active_test=False)):
brother_ids = map(int, cls.search([
(field_name, '=', parent_id),
]))
if brother_ids[-1] != object_id:
next_id = brother_ids[brother_ids.index(object_id) + 1]
cursor.execute('SELECT "' + left + '" '
'FROM "' + cls._table + '" '
'WHERE id = %s', (next_id,))
next_left = cursor.fetchone()[0]
cursor.execute('SELECT "' + left + '" '
'FROM "' + cls._table + '" '
'WHERE id = %s', (object_id,))
current_left = cursor.fetchone()[0]
cursor.execute('UPDATE "' + cls._table + '" '
'SET "' + left + '" = "' + left + '" + '
+ str(old_right - old_left + 1) + ', '
'"' + right + '" = "' + right + '" + '
+ str(old_right - old_left + 1) + ' '
'WHERE "' + left + '" >= %s AND "' + right + '" <= %s',
(next_left, current_left))
cursor.execute('UPDATE "' + cls._table + '" '
'SET "' + left + '" = "' + left + '" - '
+ str(current_left - next_left) + ', '
'"' + right + '" = "' + right + '" - '
+ str(current_left - next_left) + ' '
'WHERE ' + red_child_sql, red_child_ids)
'SET "' + left + '" = "' + left + '" + %s '
'WHERE "' + left + '" >= %s', (size, parent_right))
cursor.execute('UPDATE "' + cls._table + '" '
'SET "' + right + '" = "' + right + '" + %s '
'WHERE "' + right + '" >= %s', (size, parent_right))
if old_left < parent_right:
params = (parent_right - old_left,
parent_right - old_left,
old_left, old_right)
else:
params = (parent_right - old_left - size,
parent_right - old_left - size,
old_left + size, old_right + size)
cursor.execute('UPDATE "' + cls._table + '" '
'SET "' + left + '" = "' + left + '" + %s, '
'"' + right + '" = "' + right + '" + %s '
'WHERE "' + left + '" >= %s '
'AND "' + right + '" <= %s', params)
@classmethod
def validate(cls, records):
-8
View File
@@ -12,9 +12,6 @@ class MPTT(ModelSQL, ModelView):
'Modified Preorder Tree Traversal'
__name__ = 'test.mptt'
name = fields.Char('Name', required=True)
sequence = fields.Integer('Sequence',
order_field='(%(table)s.sequence IS NULL) %(order)s, '
'%(table)s.sequence %(order)s')
parent = fields.Many2One('test.mptt', "Parent", select=True,
left="left", right="right")
left = fields.Integer('Left', required=True, select=True)
@@ -22,11 +19,6 @@ class MPTT(ModelSQL, ModelView):
childs = fields.One2Many('test.mptt', 'parent', 'Children')
active = fields.Boolean('Active')
@classmethod
def __setup__(cls):
super(MPTT, cls).__setup__()
cls._order.insert(0, ('sequence', 'ASC'))
@classmethod
def validate(cls, record):
super(MPTT, cls).validate(record)
+13 -52
View File
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
#This file is part of Tryton. The COPYRIGHT file at the top level of
#this repository contains the full copyright notices and license terms.
import sys
import unittest
from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \
install_module
@@ -17,12 +18,10 @@ class MPTTTestCase(unittest.TestCase):
install_module('test')
self.mptt = POOL.get('test.mptt')
def CheckTree(self, parent_id=False, left=0, right=0):
def CheckTree(self, parent_id=False, left=-1, right=sys.maxint):
childs = self.mptt.search([
('parent', '=', parent_id),
])
child_index = dict((x.id, i) for i, x in enumerate(childs))
childs.sort(key=lambda x: child_index[x.id])
], order=[('left', 'ASC')])
for child in childs:
assert child.left > left, \
'%s: left %d <= parent left %d' % \
@@ -30,21 +29,21 @@ class MPTTTestCase(unittest.TestCase):
assert child.left < child.right, \
'%s: left %d >= right %d' % \
(child, child.left, child.right)
assert right == 0 or child.right < right, \
assert child.right < right, \
'%s: right %d >= parent right %d' % \
(child, child.right, right)
self.CheckTree(child.id, left=child.left,
right=child.right)
next_left = 0
next_left = -1
for child in childs:
assert child.left > next_left, \
'%s: left %d <= next left %d' % \
(child, child.left, next_left)
next_left = child.right
childs.reverse()
previous_right = 0
previous_right = sys.maxint
for child in childs:
assert previous_right == 0 or child.right < previous_right, \
assert child.right < previous_right, \
'%s: right %d >= previous right %d' \
% (child, child.right, previous_right)
previous_right = child.left
@@ -65,62 +64,28 @@ class MPTTTestCase(unittest.TestCase):
for record in records:
self.ReParent(record)
def ReOrder(self, parent=None):
records = self.mptt.search([
('parent', '=', parent),
])
if not records:
return
i = len(records)
for record in records:
record.sequence = i
record.save()
i -= 1
i = 0
for record in records:
record.sequence = i
record.save()
i += 1
for record in records:
self.ReOrder(record)
records = self.mptt.search([])
self.mptt.write(records, {
'sequence': 0,
})
def test0010create(self):
'''
Create tree.
'''
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
new_records = [None]
new_records = [None, None, None]
for j in range(3):
parent_records = new_records
new_records = []
k = 0
for parent_record in parent_records:
for i in range(3):
new_records += self.mptt.create([{
'name': 'Test %d %d %d' % (j, k, i),
'parent': (parent_record.id
if parent_record else None),
}])
new_records += self.mptt.create([{
'name': 'Test %d %d %d' % (j, k, i),
'parent': (parent_record.id
if parent_record else None),
} for i in range(3)])
k += 1
self.CheckTree()
transaction.cursor.commit()
def test0020reorder(self):
'''
Re-order.
'''
with Transaction().start(DB_NAME, USER,
context=CONTEXT) as transaction:
self.ReOrder()
transaction.cursor.rollback()
def test0030reparent(self):
'''
Re-parent.
@@ -141,8 +106,6 @@ class MPTTTestCase(unittest.TestCase):
self.CheckTree()
self.ReParent()
self.CheckTree()
self.ReOrder()
self.CheckTree()
transaction.cursor.rollback()
@@ -164,8 +127,6 @@ class MPTTTestCase(unittest.TestCase):
})
self.ReParent()
self.CheckTree()
self.ReOrder()
self.CheckTree()
transaction.cursor.rollback()