[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:
xmo-odoo
2018-04-05 10:34:48 +02:00
committed by GitHub
parent 0cdfae0e25
commit b5c660d8c0
2 changed files with 82 additions and 31 deletions
+15 -5
View File
@@ -2,6 +2,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import itertools
import unittest
from cProfile import Profile
from odoo.tests import common
from odoo.tools import pycompat
@@ -71,6 +73,16 @@ class test_integer_field(CreatorCase):
self.export(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):
model_name = 'export.float'
@@ -325,11 +337,9 @@ class test_m2o(CreatorCase):
record = self.env['export.integer'].create({'value': 42})
# Expecting the m2o target model name in the external id,
# not this model's name
external_id = u'__export__.export_integer_%d' % record.id
self.assertEqual(
self.export(record.id, fields=['value/id']),
[[external_id]])
self.assertRegexpMatches(
self.export(record.id, fields=['value/id'])[0][0],
u'__export__.export_integer_%d_[0-9a-f]{8}' % record.id)
class test_o2m(CreatorCase):
model_name = 'export.one2many'
+67 -26
View File
@@ -27,10 +27,12 @@ import collections
import dateutil
import functools
import itertools
import io
import logging
import operator
import pytz
import re
import uuid
from collections import defaultdict, MutableMapping, OrderedDict
from contextlib import closing
from inspect import getmembers, currentframe
@@ -615,33 +617,67 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
def _is_an_ordinary_table(self):
return tools.table_kind(self.env.cr, self._table) == 'r'
def __export_xml_id(self):
""" Return a valid xml_id for the record ``self``. """
def __ensure_xml_id(self, skip=False):
""" 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():
raise Exception(
"You can not export the column ID of model %s, because the "
"table %s is not an ordinary 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)])
if data:
if data[0].module:
return '%s.%s' % (data[0].module, data[0].name)
else:
return data[0].name
else:
postfix = 0
name = '%s_%s' % (self._table, self.id)
while ir_model_data.search([('module', '=', '__export__'), ('name', '=', name)]):
postfix += 1
name = '%s_%s_%s' % (self._table, self.id, postfix)
ir_model_data.create({
'model': self._name,
'res_id': self.id,
'module': '__export__',
'name': name,
})
return '__export__.' + name
modname = '__export__'
cr = self.env.cr
cr.execute("""
SELECT res_id, module, name
FROM ir_model_data
WHERE model = %s AND res_id in %s
""", (self._name, tuple(self.ids)))
xids = {
res_id: (module, name)
for res_id, module, name in cr.fetchall()
}
# create missing xml ids
missing = self.filtered(lambda r: r.id not in xids)
xids.update(
(r.id, (modname, '%s_%s_%s' % (
r._table,
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
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
"""
for idx in range(0, len(rs), 1000):
sub = rs[idx: idx+1000]
sub = rs[idx:idx+1000]
for rec in sub:
yield rec
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 (???)
for idx, record in enumerate(splittor(self)):
for record in splittor(self):
# main line of record, initially empty
current = [''] * len(fields)
lines.append(current)
@@ -685,7 +724,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if name == '.id':
current[i] = str(record.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:
field = record._fields[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
# 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':
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
continue