[IMP] base: add prefetching group feature

The field prefetching mechanism was poorly customizable.  Before this,
we could only tell if a field was prefetched with other fields or not at
all.  We have no way to inform the framework, like: "When I need data of
that field, prefetch these other fields, which are likely be used in the
same transaction".

From now on, the `prefetch` attribute is used as a grouping key for
prefetching fields.  When a field is fetched, all the fields with the
same value for `prefetch` are taken for prefetching.

For example, consider a small set of fields that are rarely used, except
for one flow A using them.  You want to prefetch those fields only in
the flow A, and you want to fetch them in a single query.  With the new
feature, simply set `prefetch=A` for some string `A` on those fields,
and they will be grouped for prefetching.

closes odoo/odoo#85220

Signed-off-by: Rémy Voet <ryv@odoo.com>
This commit is contained in:
Rémy Voet (ryv)
2022-03-03 11:03:55 +00:00
parent d2b68d186e
commit e1785e820a
7 changed files with 73 additions and 16 deletions
+3 -3
View File
@@ -17,11 +17,11 @@ class EventStage(models.Model):
string='End Stage', default=False,
help='Events will automatically be moved into this stage when they are finished. The event moved into this stage will automatically be set as green.')
legend_blocked = fields.Char(
'Red Kanban Label', default=lambda s: _('Blocked'), translate=True, prefetch=True, required=True,
'Red Kanban Label', default=lambda s: _('Blocked'), translate=True, prefetch='legend', required=True,
help='Override the default value displayed for the blocked state for kanban selection.')
legend_done = fields.Char(
'Green Kanban Label', default=lambda s: _('Ready for Next Stage'), translate=True, prefetch=True, required=True,
'Green Kanban Label', default=lambda s: _('Ready for Next Stage'), translate=True, prefetch='legend', required=True,
help='Override the default value displayed for the done state for kanban selection.')
legend_normal = fields.Char(
'Grey Kanban Label', default=lambda s: _('In Progress'), translate=True, prefetch=True, required=True,
'Grey Kanban Label', default=lambda s: _('In Progress'), translate=True, prefetch='legend', required=True,
help='Override the default value displayed for the normal state for kanban selection.')
+3 -3
View File
@@ -23,9 +23,9 @@ class SeoMetadata(models.AbstractModel):
_description = 'SEO metadata'
is_seo_optimized = fields.Boolean("SEO optimized", compute='_compute_is_seo_optimized')
website_meta_title = fields.Char("Website meta title", translate=True)
website_meta_description = fields.Text("Website meta description", translate=True)
website_meta_keywords = fields.Char("Website meta keywords", translate=True)
website_meta_title = fields.Char("Website meta title", translate=True, prefetch="website_meta")
website_meta_description = fields.Text("Website meta description", translate=True, prefetch="website_meta")
website_meta_keywords = fields.Char("Website meta keywords", translate=True, prefetch="website_meta")
website_meta_og_img = fields.Char("Website opengraph image")
seo_name = fields.Char("Seo name", translate=True, prefetch=True)
@@ -1566,12 +1566,17 @@ class PrecomputeMonetary(models.Model):
self.currency_id = 1 # EUR
class PrefetchTranslateField(models.Model):
_name = 'test_new_api.prefetch.translate'
_description = 'A model with some translate fields to check prefetch'
class Prefetch(models.Model):
_name = 'test_new_api.prefetch'
_description = 'A model to check the prefetching of fields (translated and group)'
name = fields.Char('Name', translate=True)
description = fields.Char('Description', translate=True, prefetch=True)
html_description = fields.Html('Styled description', translate=True, prefetch=True)
rare_description = fields.Char('Rare Description', translate=True)
rare_html_description = fields.Html('Rare Styled description', translate=True)
harry = fields.Integer('Harry Potter', prefetch='Harry Potter')
hermione = fields.Char('Hermione Granger', prefetch='Harry Potter')
ron = fields.Float('Ron Weasley', prefetch='Harry Potter')
hansel = fields.Integer('Hansel', prefetch="Hansel and Gretel")
gretel = fields.Char('Gretel', prefetch="Hansel and Gretel")
@@ -87,4 +87,4 @@ access_test_new_api_precompute_combo,access_test_new_api_precompute_combo,model_
access_test_new_api_precompute_editable,access_test_new_api_precompute_editable,model_test_new_api_precompute_editable,,1,0,0,0
access_test_new_api_precompute_required,access_test_new_api_precompute_required,model_test_new_api_precompute_required,,1,0,0,0
access_test_new_api_precompute_monetary,access_test_new_api_precompute_monetary,model_test_new_api_precompute_monetary,,1,0,0,0
access_test_new_api_prefetch_translate,access_test_new_api_prefetch_translate,model_test_new_api_prefetch_translate,,1,0,0,0
access_test_new_api_prefetch,access_test_new_api_prefetch,model_test_new_api_prefetch,,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
87 access_test_new_api_precompute_editable access_test_new_api_precompute_editable model_test_new_api_precompute_editable 1 0 0 0
88 access_test_new_api_precompute_required access_test_new_api_precompute_required model_test_new_api_precompute_required 1 0 0 0
89 access_test_new_api_precompute_monetary access_test_new_api_precompute_monetary model_test_new_api_precompute_monetary 1 0 0 0
90 access_test_new_api_prefetch_translate access_test_new_api_prefetch access_test_new_api_prefetch_translate access_test_new_api_prefetch model_test_new_api_prefetch_translate model_test_new_api_prefetch 1 0 0 0
@@ -2523,7 +2523,7 @@ class TestFields(TransactionCaseWithUserDemo):
record_user.read(['tags'])
def test_98_prefetch_translate(self):
Model = self.registry['test_new_api.prefetch.translate']
Model = self.registry['test_new_api.prefetch']
# translated '_rec_name' field should be prefetched
self.assertTrue(Model.name.prefetch)
@@ -2554,6 +2554,58 @@ class TestFields(TransactionCaseWithUserDemo):
line.with_user(user).unlink()
self.assertEqual(move.quantity, 0)
def test_99_prefetch_group(self):
records = self.env['test_new_api.prefetch'].create([{} for _ in range(10)])
records.flush()
records.invalidate_cache()
with self.assertQueries(["""
SELECT
"test_new_api_prefetch"."id" AS "id",
"test_new_api_prefetch"."name" AS "name",
"test_new_api_prefetch"."description" AS "description",
"test_new_api_prefetch"."html_description" AS "html_description",
"test_new_api_prefetch"."create_uid" AS "create_uid",
"test_new_api_prefetch"."create_date" AS "create_date",
"test_new_api_prefetch"."write_uid" AS "write_uid",
"test_new_api_prefetch"."write_date" AS "write_date"
FROM "test_new_api_prefetch"
WHERE "test_new_api_prefetch".id IN %s
"""]):
records.mapped('name') # fetch all fields with prefetch=True
with self.assertQueries(["""
SELECT
"test_new_api_prefetch"."id" AS "id",
"test_new_api_prefetch"."harry" AS "harry",
"test_new_api_prefetch"."hermione" AS "hermione",
"test_new_api_prefetch"."ron" AS "ron"
FROM "test_new_api_prefetch"
WHERE "test_new_api_prefetch".id IN %s
"""]):
records.mapped('harry') # fetch all fields with prefetch='Harry Potter'
records.mapped('hermione') # fetched already
records.mapped('ron') # fetched already
with self.assertQueries(["""
SELECT
"test_new_api_prefetch"."id" AS "id",
"test_new_api_prefetch"."hansel" AS "hansel",
"test_new_api_prefetch"."gretel" AS "gretel"
FROM "test_new_api_prefetch"
WHERE "test_new_api_prefetch".id IN %s
"""]):
records.mapped('hansel') # fetch all fields with prefetch='Hansel and Gretel'
records.mapped('gretel') # fetched already
records.invalidate_cache()
with self.assertQueryCount(4):
records.mapped('name') # fetch all fields with prefetch=True
records.mapped('hansel') # fetch all fields with prefetch='Hansel and Gretel'
records.mapped('harry') # fetch all fields with prefetch='Harry Potter'
records.mapped('rare_description') # fetch that field only
class TestX2many(common.TransactionCase):
def test_definition_many2many(self):
+1 -1
View File
@@ -290,7 +290,7 @@ class Field(MetaField('DummyField', (object,), {})):
related_field = None # corresponding related field
group_operator = None # operator for aggregating values
group_expand = None # name of method to expand groups in read_group()
prefetch = True # whether the field is prefetched
prefetch = True # the prefetch group (False means no group)
default_export_compatible = False # whether the field must be exported by default in an import-compatible export
+4 -4
View File
@@ -3251,7 +3251,7 @@ Fields:
fields = self.check_field_access_rights('read', fields)
# fetch stored fields from the database to the cache
stored_fields = set()
stored_fields = OrderedSet()
for name in fields:
field = self._fields.get(name)
if not field:
@@ -3262,7 +3262,7 @@ Fields:
# optimization: prefetch direct field dependencies
for dotname in self.pool.field_depends[field]:
f = self._fields[dotname.split('.')[0]]
if f.prefetch and (not f.groups or self.user_has_groups(f.groups)):
if f.prefetch is True and (not f.groups or self.user_has_groups(f.groups)):
stored_fields.add(f.name)
self._read(stored_fields)
@@ -3303,8 +3303,8 @@ Fields:
fnames = [
name
for name, f in self._fields.items()
# select fields that can be prefetched
if f.prefetch
# select fields with the same prefetch group
if f.prefetch == field.prefetch
# discard fields with groups that the user may not access
if not (f.groups and not self.user_has_groups(f.groups))
# discard fields that must be recomputed