[IMP] base: don't prefetch translate field by default

Issue
-----
Via the field prefetch mechanism, when we need a value of one field
(not in cache of course), the ORM will prefetch all fields
(which has the attribute to `prefetch=True`, the default value of this
attribute is `True`) for all record ids in `_prefetch_ids`.
Then, for each translate fields (where translate is not a callable)
the ORM need to make a `LEFT JOIN` on the `ir_translation` to fetch the
translated value. For big model, it leads to a simple `SELECT` with
several `LEFT JOIN` on ir_translation but each LEFT JOIN have a cost
in the planner time (a small cost in the execution time) of PostgreSQL.

By example, for `product.template` (stock/sale/purchase installed),
there are 6 LEFT JOIN to get all translated fields (5 of this
fields are rarely used).

Proposed solution
-----------------
Deactivate the prefetch by default for all translate fields expect if
this field is the `_rec_name` of the model (which is more likely to
be used).

In the example on the `product.template`:
Without prefetching the translated fields, there is only one LEFT JOIN
(the name, which is translated but is the `_rec_name` of the model).
With the 6 translated fields to fetch, the
query takes 5 ms to plan and 2 ms to execute VS with 1 translate field,
it 1 ms to plan and 1.5 ms to execute.

Side change note
----------------
- All translate of fields of `website.seo.metadata` should be prefetch
to avoid lot of website errors (it is because, website put in cache data
in sudo before reading it without sudo)
- `description` (`mail.message.subtype`), `subject` (`mail.template`),
`body_html` (`mail.template`) should be prefetch to avoid lot of extra
query from mail module.
- `vat_label` (`res.country`) should be prefetch to avoid a extra query
for each website page.
- Increase some queryCount (when it is legit, due to `subtitle` of
`blog_post` or `description` of `event.type.ticket`, etc)

task-2738029

closes odoo/odoo#82896

Signed-off-by: Raphael Collet <rco@odoo.com>
This commit is contained in:
Rémy Voet (ryv)
2022-01-28 14:09:56 +00:00
parent 1187bb798a
commit 5a573c6f18
14 changed files with 62 additions and 29 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, required=True,
'Red Kanban Label', default=lambda s: _('Blocked'), translate=True, prefetch=True, 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, required=True,
'Green Kanban Label', default=lambda s: _('Ready for Next Stage'), translate=True, prefetch=True, 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, required=True,
'Grey Kanban Label', default=lambda s: _('In Progress'), translate=True, prefetch=True, required=True,
help='Override the default value displayed for the normal state for kanban selection.')
@@ -47,7 +47,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
@users('__system__', 'admin')
@warmup
def test_performance_leave_create(self):
with self.assertQueryCount(__system__=25, admin=26):
with self.assertQueryCount(__system__=26, admin=27):
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
leave.action_refuse()
@@ -56,7 +56,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
def test_performance_leave_confirm(self):
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
leave.action_draft()
with self.assertQueryCount(__system__=18, admin=19):
with self.assertQueryCount(__system__=19, admin=20):
leave.action_confirm()
leave.state = 'refuse'
+1 -1
View File
@@ -20,7 +20,7 @@ class MailMessageSubtype(models.Model):
'change in a process (Stage change). Message subtypes allow to '
'precisely tune the notifications the user want to receive on its wall.')
description = fields.Text(
'Description', translate=True,
'Description', translate=True, prefetch=True,
help='Description that will be added in the message posted for this '
'subtype. If void, the name will be added instead.')
internal = fields.Boolean(
+3 -3
View File
@@ -31,7 +31,7 @@ class MailTemplate(models.Model):
name = fields.Char('Name', translate=True)
model_id = fields.Many2one('ir.model', 'Applies to', help="The type of document this template can be used with")
model = fields.Char('Related Document Model', related='model_id.model', index=True, store=True, readonly=True)
subject = fields.Char('Subject', translate=True, help="Subject (placeholders may be used here)")
subject = fields.Char('Subject', translate=True, prefetch=True, help="Subject (placeholders may be used here)")
email_from = fields.Char('From',
help="Sender address (placeholders may be used here). If not set, the default "
"value will be the author's email alias if configured, or email address.")
@@ -47,12 +47,12 @@ class MailTemplate(models.Model):
email_cc = fields.Char('Cc', help="Carbon copy recipients (placeholders may be used here)")
reply_to = fields.Char('Reply To', help="Email address to which replies will be redirected when sending emails in mass; only used when the reply is not logged in the original discussion thread.")
# content
body_html = fields.Html('Body', render_engine='qweb', translate=True, sanitize=False)
body_html = fields.Html('Body', render_engine='qweb', translate=True, prefetch=True, sanitize=False)
attachment_ids = fields.Many2many('ir.attachment', 'email_template_attachment_rel', 'email_template_id',
'attachment_id', 'Attachments',
help="You may attach files to this template, to be added to all "
"emails created from this template")
report_name = fields.Char('Report Filename', translate=True,
report_name = fields.Char('Report Filename', translate=True, prefetch=True,
help="Name to use for the generated report file (may contain placeholders)\n"
"The extension can be omitted and will then come from the report type.")
report_template = fields.Many2one('ir.actions.report', 'Optional report to print and attach')
+4 -4
View File
@@ -73,7 +73,7 @@ class TestPERF(common.TransactionCase):
@warmup
def test_light_sales_orders_batch_creation_perf_without_taxes(self):
self.products[0].taxes_id = [Command.set([])]
with self.assertQueryCount(admin=58):
with self.assertQueryCount(admin=59):
self.env['sale.order'].create([{
'partner_id': self.partners[0].id,
'user_id': self.salesmans[0].id,
@@ -87,7 +87,7 @@ class TestPERF(common.TransactionCase):
@users('admin')
@warmup
def test_light_sales_orders_batch_creation_perf(self):
with self.assertQueryCount(admin=69): # 68 locally, 69 in nightly runbot
with self.assertQueryCount(admin=70): # 69 locally, 70 in nightly runbot
self.env['sale.order'].create([{
'partner_id': self.partners[0].id,
'user_id': self.salesmans[0].id,
@@ -104,7 +104,7 @@ class TestPERF(common.TransactionCase):
# NOTE: sometimes more queries on runbot,
# do not change without verifying in multi-builds
# (Seems to be a time-based problem, everytime happening around 10PM)
self._test_complex_sales_orders_batch_creation_perf(1502)
self._test_complex_sales_orders_batch_creation_perf(1504)
@users('admin')
@warmup
@@ -114,7 +114,7 @@ class TestPERF(common.TransactionCase):
self.env.user.groups_id += self.env.ref('product.group_discount_per_so_line')
# Verify any modification to this count on nightly runbot builds
self._test_complex_sales_orders_batch_creation_perf(1545)
self._test_complex_sales_orders_batch_creation_perf(1546)
def _test_complex_sales_orders_batch_creation_perf(self, query_count):
MSG = "Model %s, %i records, %s, time %.2f"
@@ -70,7 +70,7 @@ class TestEventPerformance(EventPerformanceCase):
event_type = self.env['event.type'].browse(self.test_event_type.ids)
# complex with type
with freeze_time(self.reference_now), self.assertQueryCount(event_user=785): # tef only: 785 (779) - com runbot: 779 - ent runbot 779
with freeze_time(self.reference_now), self.assertQueryCount(event_user=786): # tef only: 785 (779) - com runbot: 779 - ent runbot 779
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = [
dict(self.event_base_vals,
@@ -144,7 +144,7 @@ class TestEventPerformance(EventPerformanceCase):
has_social = 'social_menu' in self.env['event.event'] # otherwise view may crash in enterprise
# type and website
with freeze_time(self.reference_now), self.assertQueryCount(event_user=783): # tef only: 673 - com runbot: 675
with freeze_time(self.reference_now), self.assertQueryCount(event_user=784): # tef only: 673 - com runbot: 675
self.env.cr._now = self.reference_now # force create_date to check schedulers
with Form(self.env['event.event']) as event_form:
event_form.name = 'Test Event'
@@ -187,7 +187,7 @@ class TestEventPerformance(EventPerformanceCase):
event_type = self.env['event.type'].browse(self.test_event_type.ids)
# complex with type
with freeze_time(self.reference_now), self.assertQueryCount(event_user=81): # tef only: 81 (75) - com runbot: 75 - ent runbot 75
with freeze_time(self.reference_now), self.assertQueryCount(event_user=82): # tef only: 81 (75) - com runbot: 75 - ent runbot 75
self.env.cr._now = self.reference_now # force create_date to check schedulers
event_values = dict(
self.event_base_vals,
+4 -4
View File
@@ -21,11 +21,11 @@ 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=True)
website_meta_description = fields.Text("Website meta description", translate=True, prefetch=True)
website_meta_keywords = fields.Char("Website meta keywords", translate=True, prefetch=True)
website_meta_og_img = fields.Char("Website opengraph image")
seo_name = fields.Char("Seo name", translate=True)
seo_name = fields.Char("Seo name", translate=True, prefetch=True)
def _compute_is_seo_optimized(self):
for record in self:
@@ -13,7 +13,7 @@ class TestBlogPerformance(UtilPerf):
self.env['website'].search([]).channel_id = False
def test_10_perf_sql_blog_standard_data(self):
self.assertEqual(self._get_url_hot_query('/blog'), 26)
self.assertEqual(self._get_url_hot_query('/blog'), 27)
def test_20_perf_sql_blog_bigger_data_scaling(self):
BlogPost = self.env['blog.post']
@@ -25,8 +25,8 @@ class TestBlogPerformance(UtilPerf):
for blog_post in blog_posts:
blog_post.tag_ids += blog_tags
blog_tags = blog_tags[:-1]
self.assertEqual(self._get_url_hot_query('/blog'), 26)
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url), 29)
self.assertEqual(self._get_url_hot_query('/blog'), 27)
self.assertEqual(self._get_url_hot_query(blog_post[0].website_url), 31)
def test_30_perf_sql_blog_bigger_data_scaling(self):
BlogPost = self.env['blog.post']
+1 -1
View File
@@ -69,7 +69,7 @@ class Country(models.Model):
('after', 'After Address'),
], string="Customer Name Position", default="before",
help="Determines where the customer/company name should be placed, i.e. after or before the address.")
vat_label = fields.Char(string='Vat Label', translate=True, help="Use this field if you want to change vat label.")
vat_label = fields.Char(string='Vat Label', translate=True, prefetch=True, help="Use this field if you want to change vat label.")
state_required = fields.Boolean(default=False)
zip_required = fields.Boolean(default=True)
+2 -1
View File
@@ -1647,6 +1647,7 @@ class TestMany2many(TransactionCase):
''']):
self.User.search([('groups_id', 'in', group.ids)], order='id')
group_color = group.color
with self.assertQueries(['''
SELECT "res_users".id
FROM "res_users"
@@ -1659,7 +1660,7 @@ class TestMany2many(TransactionCase):
))
ORDER BY "res_users"."id"
''']):
self.User.search([('groups_id.color', '=', group.color)], order='id')
self.User.search([('groups_id.color', '=', group_color)], order='id')
with self.assertQueries(['''
SELECT "res_users".id
@@ -1514,3 +1514,14 @@ class PrecomputeRequired(models.Model):
partner_id = fields.Many2one('res.partner', required=True)
name = fields.Char(related='partner_id.name', precompute=True, store=True, required=True)
class PrefetchTranslateField(models.Model):
_name = 'test_new_api.prefetch.translate'
_description = 'A model with some translate fields to check prefetch'
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)
@@ -84,3 +84,4 @@ access_test_new_api_precompute_line,access_test_new_api_precompute_line,model_te
access_test_new_api_precompute_combo,access_test_new_api_precompute_combo,model_test_new_api_precompute_combo,,1,0,0,0
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_prefetch_translate,access_test_new_api_prefetch_translate,model_test_new_api_prefetch_translate,,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
84 access_test_new_api_precompute_combo access_test_new_api_precompute_combo model_test_new_api_precompute_combo 1 0 0 0
85 access_test_new_api_precompute_editable access_test_new_api_precompute_editable model_test_new_api_precompute_editable 1 0 0 0
86 access_test_new_api_precompute_required access_test_new_api_precompute_required model_test_new_api_precompute_required 1 0 0 0
87 access_test_new_api_prefetch_translate access_test_new_api_prefetch_translate model_test_new_api_prefetch_translate 1 0 0 0
@@ -2522,6 +2522,20 @@ class TestFields(TransactionCaseWithUserDemo):
with self.assertRaises(AccessError):
record_user.read(['tags'])
def test_98_prefetch_translate(self):
Model = self.registry['test_new_api.prefetch.translate']
# translated '_rec_name' field should be prefetched
self.assertTrue(Model.name.prefetch)
# parameter 'prefetch' can be always overridden
self.assertTrue(Model.description.prefetch)
self.assertTrue(Model.html_description.prefetch)
# translated fields should be prefetch=False by default
self.assertFalse(Model.rare_description.prefetch)
self.assertFalse(Model.rare_html_description.prefetch)
class TestX2many(common.TransactionCase):
def test_definition_many2many(self):
+10 -4
View File
@@ -1601,11 +1601,17 @@ class _String(Field):
kwargs['translate'] = bool(kwargs['translate'])
super(_String, self).__init__(string=string, **kwargs)
def _setup_attrs(self, model_class, name):
super()._setup_attrs(model_class, name)
def setup_nonrelated(self, model):
super().setup_nonrelated(model)
if self.prefetch is None:
# do not prefetch complex translated fields by default
self.prefetch = not callable(self.translate)
# translated fields are not prefetched by default except for _rec_name
self.prefetch = not self.translate or model._rec_name == self.name
def setup_related(self, model):
super().setup_related(model)
if self.prefetch is None:
# translated fields are not prefetched by default except for _rec_name
self.prefetch = not self.translate or model._rec_name == self.name
_related_translate = property(attrgetter('translate'))