[IMP] base: batch XID generation during export
* add UUIDs to XID sections (4 bytes / 8 hex digits) so it's not
necessary to handle collisions & add a fallback generator
* use COPY for performances over executemany: executemany just
performs an implicit loop on all statements (in psycopg2)
* ~pure SQL was possible:
INSERT INTO ir_model_data (module, model, name, res_id)
SELECT
'__export__',
'{model}',
'{table}_' || A.id || '_' || uuid_generate_v4(),
A.id
FROM {table} A
LEFT JOIN ir_model_data B
ON A.id = B.res_id AND B.model = %s
WHERE B.res_id IS NULL;
but would have required installing pg extensions (problematic
especially in stable) and performances are about the same as
the COPY version
task 36343
Fixes #22493
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
|
import unittest
|
||||||
|
from cProfile import Profile
|
||||||
|
|
||||||
from odoo.tests import common
|
from odoo.tests import common
|
||||||
from odoo.tools import pycompat
|
from odoo.tools import pycompat
|
||||||
@@ -71,6 +73,16 @@ class test_integer_field(CreatorCase):
|
|||||||
self.export(2**31-1),
|
self.export(2**31-1),
|
||||||
[[pycompat.text_type(2**31-1)]])
|
[[pycompat.text_type(2**31-1)]])
|
||||||
|
|
||||||
|
@unittest.skip("Only benches/profiles")
|
||||||
|
def test_xid_perfs(self):
|
||||||
|
for i in range(10000):
|
||||||
|
self.make(i)
|
||||||
|
self.model.invalidate_cache()
|
||||||
|
records = self.model.search([])
|
||||||
|
|
||||||
|
p = Profile()
|
||||||
|
p.runcall(records._export_rows, [['id'], ['value']])
|
||||||
|
p.dump_stats('xid_perfs.pstats')
|
||||||
|
|
||||||
class test_float_field(CreatorCase):
|
class test_float_field(CreatorCase):
|
||||||
model_name = 'export.float'
|
model_name = 'export.float'
|
||||||
@@ -325,11 +337,9 @@ class test_m2o(CreatorCase):
|
|||||||
record = self.env['export.integer'].create({'value': 42})
|
record = self.env['export.integer'].create({'value': 42})
|
||||||
# Expecting the m2o target model name in the external id,
|
# Expecting the m2o target model name in the external id,
|
||||||
# not this model's name
|
# not this model's name
|
||||||
external_id = u'__export__.export_integer_%d' % record.id
|
self.assertRegexpMatches(
|
||||||
self.assertEqual(
|
self.export(record.id, fields=['value/id'])[0][0],
|
||||||
self.export(record.id, fields=['value/id']),
|
u'__export__.export_integer_%d_[0-9a-f]{8}' % record.id)
|
||||||
[[external_id]])
|
|
||||||
|
|
||||||
|
|
||||||
class test_o2m(CreatorCase):
|
class test_o2m(CreatorCase):
|
||||||
model_name = 'export.one2many'
|
model_name = 'export.one2many'
|
||||||
|
|||||||
+67
-26
@@ -27,10 +27,12 @@ import collections
|
|||||||
import dateutil
|
import dateutil
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
import pytz
|
import pytz
|
||||||
import re
|
import re
|
||||||
|
import uuid
|
||||||
from collections import defaultdict, MutableMapping, OrderedDict
|
from collections import defaultdict, MutableMapping, OrderedDict
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from inspect import getmembers, currentframe
|
from inspect import getmembers, currentframe
|
||||||
@@ -615,33 +617,67 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
|||||||
def _is_an_ordinary_table(self):
|
def _is_an_ordinary_table(self):
|
||||||
return tools.table_kind(self.env.cr, self._table) == 'r'
|
return tools.table_kind(self.env.cr, self._table) == 'r'
|
||||||
|
|
||||||
def __export_xml_id(self):
|
def __ensure_xml_id(self, skip=False):
|
||||||
""" Return a valid xml_id for the record ``self``. """
|
""" Create missing external ids for records in ``self``, and return an
|
||||||
|
iterator of pairs ``(record, xmlid)`` for the records in ``self``.
|
||||||
|
|
||||||
|
:rtype: Iterable[Model, str | None]
|
||||||
|
"""
|
||||||
|
if skip:
|
||||||
|
return ((record, None) for record in self)
|
||||||
|
|
||||||
|
if not self:
|
||||||
|
return iter([])
|
||||||
|
|
||||||
if not self._is_an_ordinary_table():
|
if not self._is_an_ordinary_table():
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"You can not export the column ID of model %s, because the "
|
"You can not export the column ID of model %s, because the "
|
||||||
"table %s is not an ordinary table."
|
"table %s is not an ordinary table."
|
||||||
% (self._name, self._table))
|
% (self._name, self._table))
|
||||||
ir_model_data = self.sudo().env['ir.model.data']
|
|
||||||
data = ir_model_data.search([('model', '=', self._name), ('res_id', '=', self.id)])
|
modname = '__export__'
|
||||||
if data:
|
|
||||||
if data[0].module:
|
cr = self.env.cr
|
||||||
return '%s.%s' % (data[0].module, data[0].name)
|
cr.execute("""
|
||||||
else:
|
SELECT res_id, module, name
|
||||||
return data[0].name
|
FROM ir_model_data
|
||||||
else:
|
WHERE model = %s AND res_id in %s
|
||||||
postfix = 0
|
""", (self._name, tuple(self.ids)))
|
||||||
name = '%s_%s' % (self._table, self.id)
|
xids = {
|
||||||
while ir_model_data.search([('module', '=', '__export__'), ('name', '=', name)]):
|
res_id: (module, name)
|
||||||
postfix += 1
|
for res_id, module, name in cr.fetchall()
|
||||||
name = '%s_%s_%s' % (self._table, self.id, postfix)
|
}
|
||||||
ir_model_data.create({
|
|
||||||
'model': self._name,
|
# create missing xml ids
|
||||||
'res_id': self.id,
|
missing = self.filtered(lambda r: r.id not in xids)
|
||||||
'module': '__export__',
|
xids.update(
|
||||||
'name': name,
|
(r.id, (modname, '%s_%s_%s' % (
|
||||||
})
|
r._table,
|
||||||
return '__export__.' + name
|
r.id,
|
||||||
|
uuid.uuid4().hex[:8],
|
||||||
|
)))
|
||||||
|
for r in missing
|
||||||
|
)
|
||||||
|
cr.copy_from(io.StringIO(
|
||||||
|
u'\n'.join(
|
||||||
|
u"%s\t%s\t%s\t%d" % (
|
||||||
|
modname,
|
||||||
|
record._name,
|
||||||
|
xids[record.id][1],
|
||||||
|
record.id,
|
||||||
|
)
|
||||||
|
for record in missing
|
||||||
|
)),
|
||||||
|
table='ir_model_data',
|
||||||
|
columns=['module', 'model', 'name', 'res_id'],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.invalidate_cache()
|
||||||
|
|
||||||
|
return (
|
||||||
|
(record, '%s.%s' % xids[record.id])
|
||||||
|
for record in self
|
||||||
|
)
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _export_rows(self, fields):
|
def _export_rows(self, fields):
|
||||||
@@ -659,13 +695,16 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
|||||||
from the cache after it's been iterated in full
|
from the cache after it's been iterated in full
|
||||||
"""
|
"""
|
||||||
for idx in range(0, len(rs), 1000):
|
for idx in range(0, len(rs), 1000):
|
||||||
sub = rs[idx: idx+1000]
|
sub = rs[idx:idx+1000]
|
||||||
for rec in sub:
|
for rec in sub:
|
||||||
yield rec
|
yield rec
|
||||||
rs.invalidate_cache(ids=sub.ids)
|
rs.invalidate_cache(ids=sub.ids)
|
||||||
|
|
||||||
|
# both _ensure_xml_id and the splitter want to work on recordsets but
|
||||||
|
# neither returns one, so can't really be composed...
|
||||||
|
xids = dict(self.__ensure_xml_id(skip=['id'] not in fields))
|
||||||
# memory stable but ends up prefetching 275 fields (???)
|
# memory stable but ends up prefetching 275 fields (???)
|
||||||
for idx, record in enumerate(splittor(self)):
|
for record in splittor(self):
|
||||||
# main line of record, initially empty
|
# main line of record, initially empty
|
||||||
current = [''] * len(fields)
|
current = [''] * len(fields)
|
||||||
lines.append(current)
|
lines.append(current)
|
||||||
@@ -685,7 +724,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
|||||||
if name == '.id':
|
if name == '.id':
|
||||||
current[i] = str(record.id)
|
current[i] = str(record.id)
|
||||||
elif name == 'id':
|
elif name == 'id':
|
||||||
current[i] = record.__export_xml_id()
|
xid = xids.get(record)
|
||||||
|
assert xid, "no xid was generated for the record %s" % record
|
||||||
|
current[i] = xid
|
||||||
else:
|
else:
|
||||||
field = record._fields[name]
|
field = record._fields[name]
|
||||||
value = record[name]
|
value = record[name]
|
||||||
@@ -700,7 +741,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
|||||||
# in import_compat mode, m2m should always be exported as
|
# in import_compat mode, m2m should always be exported as
|
||||||
# a comma-separated list of xids in a single cell
|
# a comma-separated list of xids in a single cell
|
||||||
if import_compatible and field.type == 'many2many' and len(path) > 1 and path[1] == 'id':
|
if import_compatible and field.type == 'many2many' and len(path) > 1 and path[1] == 'id':
|
||||||
xml_ids = [r.__export_xml_id() for r in value]
|
xml_ids = [xid for _, xid in value.__ensure_xml_id()]
|
||||||
current[i] = ','.join(xml_ids) or False
|
current[i] = ','.join(xml_ids) or False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user