[IMP] core: add models defined by a query instead of a table/view
By adding a parameter `_table_query` on the Model, we can now have views that depend on the context. The query is used instead of the table name in ORM operations. This allows to pre-compute some values to improve performance, instead of storing context values in the database. Co-authored-by: william-andre <wan@odoo.com> Co-authored-by: rco-odoo <rco@odoo.com>
This commit is contained in:
committed by
Raphael Collet
co-authored by
rco-odoo
parent
9ad16bf84a
commit
f2ceef0e2f
@@ -80,3 +80,22 @@ class QueryTestCase(BaseCase):
|
||||
self.assertEqual(tmp_cat_cmp_par, 'product_product__product_tmpl_id__product_category_id__56d55687')
|
||||
tmp_cat_stm_par = query.join(tmp_cat_stm, 'partner_id', 'res_partner', 'id', 'partner_id')
|
||||
self.assertEqual(tmp_cat_stm_par, 'product_product__product_tmpl_id__product_category_id__9_363fdd')
|
||||
|
||||
def test_table_expression(self):
|
||||
query = Query(None, 'foo')
|
||||
from_clause, where_clause, where_params = query.get_sql()
|
||||
self.assertEqual(from_clause, '"foo"')
|
||||
|
||||
query = Query(None, 'bar', 'SELECT id FROM foo')
|
||||
from_clause, where_clause, where_params = query.get_sql()
|
||||
self.assertEqual(from_clause, '(SELECT id FROM foo) AS "bar"')
|
||||
|
||||
query = Query(None, 'foo')
|
||||
query.add_table('bar', 'SELECT id FROM foo')
|
||||
from_clause, where_clause, where_params = query.get_sql()
|
||||
self.assertEqual(from_clause, '"foo", (SELECT id FROM foo) AS "bar"')
|
||||
|
||||
query = Query(None, 'foo')
|
||||
query.join('foo', 'bar_id', 'SELECT id FROM foo', 'id', 'bar')
|
||||
from_clause, where_clause, where_params = query.get_sql()
|
||||
self.assertEqual(from_clause, '"foo" JOIN (SELECT id FROM foo) AS "foo__bar" ON ("foo"."bar_id" = "foo__bar"."id")')
|
||||
|
||||
+4
-3
@@ -316,6 +316,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
correspond to the last one (in the inherits list order).
|
||||
"""
|
||||
_table = None #: SQL table name used by model if :attr:`_auto`
|
||||
_table_query = None #: SQL expression of the table's content (optional)
|
||||
_sequence = None #: SQL sequence to use for ID field
|
||||
_sql_constraints = [] #: SQL constraints [(name, sql_def, message)]
|
||||
|
||||
@@ -3058,7 +3059,7 @@ Fields:
|
||||
cr, user, context, su = env.args
|
||||
|
||||
# make a query object for selecting ids, and apply security rules to it
|
||||
query = Query(self.env.cr, self._table)
|
||||
query = Query(self.env.cr, self._table, self._table_query)
|
||||
self._apply_ir_rules(query, 'read')
|
||||
|
||||
# the query may involve several tables: we need fully-qualified names
|
||||
@@ -3321,7 +3322,7 @@ Fields:
|
||||
if not self._ids:
|
||||
return self
|
||||
|
||||
query = Query(self._cr, self._table)
|
||||
query = Query(self.env.cr, self._table, self._table_query)
|
||||
self._apply_ir_rules(query, operation)
|
||||
if not query.where_clause:
|
||||
return self
|
||||
@@ -4153,7 +4154,7 @@ Fields:
|
||||
if domain:
|
||||
return expression.expression(domain, self).query
|
||||
else:
|
||||
return Query(self.env.cr, self._table)
|
||||
return Query(self.env.cr, self._table, self._table_query)
|
||||
|
||||
def _check_qorder(self, word):
|
||||
if not regex_order.match(word):
|
||||
|
||||
@@ -491,7 +491,11 @@ class Registry(Mapping):
|
||||
Verify that all tables are present and try to initialize those that are missing.
|
||||
"""
|
||||
env = odoo.api.Environment(cr, SUPERUSER_ID, {})
|
||||
table2model = {model._table: name for name, model in env.items() if not model._abstract}
|
||||
table2model = {
|
||||
model._table: name
|
||||
for name, model in env.items()
|
||||
if not model._abstract and model.__class__._table_query is None
|
||||
}
|
||||
missing_tables = set(table2model).difference(existing_tables(cr, table2model))
|
||||
|
||||
if missing_tables:
|
||||
|
||||
@@ -432,7 +432,7 @@ class expression(object):
|
||||
self.expression = distribute_not(normalize_domain(domain))
|
||||
|
||||
# this object handles all the joins
|
||||
self.query = Query(model.env.cr, model._table) if query is None else query
|
||||
self.query = Query(model.env.cr, model._table, model._table_query) if query is None else query
|
||||
|
||||
# parse the domain expression
|
||||
self.parse()
|
||||
|
||||
+28
-26
@@ -1,16 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
import warnings
|
||||
from zlib import crc32
|
||||
|
||||
from odoo.tools import lazy_property
|
||||
|
||||
IDENT_RE = re.compile(r'^[a-z_][a-z0-9_$]*$', re.I)
|
||||
|
||||
def _quote(to_quote):
|
||||
if '"' not in to_quote:
|
||||
return '"%s"' % to_quote
|
||||
return to_quote
|
||||
|
||||
def _from_table(table, alias):
|
||||
""" Return a FROM clause element from ``table`` and ``alias``. """
|
||||
if alias == table:
|
||||
return f'"{alias}"'
|
||||
elif IDENT_RE.match(table):
|
||||
return f'"{table}" AS "{alias}"'
|
||||
else:
|
||||
return f'({table}) AS "{alias}"'
|
||||
|
||||
|
||||
def _generate_table_alias(src_table_alias, link):
|
||||
@@ -44,36 +51,36 @@ class Query(object):
|
||||
""" Simple implementation of a query object, managing tables with aliases,
|
||||
join clauses (with aliases, condition and parameters), where clauses (with
|
||||
parameters), order, limit and offset.
|
||||
|
||||
:param cr: database cursor (for lazy evaluation)
|
||||
:param alias: name or alias of the table
|
||||
:param table: if given, a table expression (identifier or query)
|
||||
"""
|
||||
|
||||
def __init__(self, cr, table, where_clause=None, where_params=()):
|
||||
def __init__(self, cr, alias, table=None):
|
||||
# database cursor
|
||||
self._cr = cr
|
||||
|
||||
# tables {alias: table}
|
||||
self._tables = {table: table}
|
||||
self._tables = {alias: table or alias}
|
||||
|
||||
# joins {alias: (kind, table, condition, condition_params)}
|
||||
self._joins = {}
|
||||
|
||||
# holds the list of WHERE clause elements, to be joined with
|
||||
# 'AND' when generating the final query
|
||||
self._where_clauses = [where_clause] if where_clause else []
|
||||
|
||||
# holds the parameters for the formatting of `where_clause`, to be
|
||||
# passed to psycopg's execute method.
|
||||
self._where_params = list(where_params)
|
||||
# holds the list of WHERE clause elements (to be joined with 'AND'), and
|
||||
# the list of parameters
|
||||
self._where_clauses = []
|
||||
self._where_params = []
|
||||
|
||||
# order, limit, offset
|
||||
self.order = None
|
||||
self.limit = None
|
||||
self.offset = None
|
||||
|
||||
def add_table(self, table, alias=None):
|
||||
def add_table(self, alias, table=None):
|
||||
""" Add a table with a given alias to the from clause. """
|
||||
alias = alias or table
|
||||
assert alias not in self._tables and alias not in self._joins, "Alias %r already in %s" % (alias, str(self))
|
||||
self._tables[alias] = table
|
||||
self._tables[alias] = table or alias
|
||||
|
||||
def add_where(self, where_clause, where_params=()):
|
||||
""" Add a condition to the where clause. """
|
||||
@@ -116,7 +123,7 @@ class Query(object):
|
||||
""" Return the SELECT query as a pair ``(query_string, query_params)``. """
|
||||
from_clause, where_clause, params = self.get_sql()
|
||||
query_str = 'SELECT {} FROM {} WHERE {}{}{}{}'.format(
|
||||
", ".join(args or [_quote(next(iter(self._tables))) + ".id"]),
|
||||
", ".join(args or [f'"{next(iter(self._tables))}".id']),
|
||||
from_clause,
|
||||
where_clause or "TRUE",
|
||||
(" ORDER BY %s" % self.order) if self.order else "",
|
||||
@@ -127,13 +134,11 @@ class Query(object):
|
||||
|
||||
def get_sql(self):
|
||||
""" Returns (query_from, query_where, query_params). """
|
||||
tables = []
|
||||
tables = [_from_table(table, alias) for alias, table in self._tables.items()]
|
||||
joins = []
|
||||
params = []
|
||||
for alias, table in self._tables.items():
|
||||
tables.append(f'"{table}"' if alias == table else f'"{table}" AS "{alias}"')
|
||||
for alias, (kind, table, condition, condition_params) in self._joins.items():
|
||||
joins.append(f'{kind} "{table}" AS "{alias}" ON ({condition})')
|
||||
joins.append(f'{kind} {_from_table(table, alias)} ON ({condition})')
|
||||
params.extend(condition_params)
|
||||
|
||||
from_clause = " ".join([", ".join(tables)] + joins)
|
||||
@@ -165,10 +170,7 @@ class Query(object):
|
||||
def tables(self):
|
||||
warnings.warn("deprecated Query.tables, use Query.get_sql() instead",
|
||||
DeprecationWarning)
|
||||
return tuple(
|
||||
f'"{table}"' if alias == table else f'"{table}" AS "{alias}"'
|
||||
for alias, table in self._tables
|
||||
)
|
||||
return tuple(_from_table(table, alias) for alias, table in self._tables.items())
|
||||
|
||||
@property
|
||||
def where_clause(self):
|
||||
@@ -184,4 +186,4 @@ class Query(object):
|
||||
lhs_alias, rhs_table, lhs_column, rhs_column, link = connection
|
||||
kind = '' if implicit else ('LEFT JOIN' if outer else 'JOIN')
|
||||
rhs_alias = self._join(kind, lhs_alias, lhs_column, rhs_table, rhs_column, link, extra, extra_params)
|
||||
return rhs_alias, f'"{rhs_table}" AS "{rhs_alias}"'
|
||||
return rhs_alias, _from_table(rhs_table, rhs_alias)
|
||||
|
||||
Reference in New Issue
Block a user