[REF] models: refactoring of foreign keys
This commit is contained in:
+51
-29
@@ -1936,19 +1936,31 @@ class Many2one(_Relational):
|
||||
self.delegate = name in model._inherits.values()
|
||||
|
||||
def update_db(self, model, columns):
|
||||
if self.comodel_name not in model.env:
|
||||
raise ValueError(_('There is no reference available for %s') % (self.comodel_name,))
|
||||
comodel = model.env[self.comodel_name]
|
||||
if not model.is_transient() and comodel.is_transient():
|
||||
raise ValueError('Many2one %s from Model to TransientModel is forbidden' % self)
|
||||
if model.is_transient() and not comodel.is_transient():
|
||||
# Many2one relations from TransientModel Model are annoying because
|
||||
# they can block deletion due to foreign keys. So unless stated
|
||||
# otherwise, we default them to ondelete='cascade'.
|
||||
self.ondelete = self.ondelete or 'cascade'
|
||||
return super(Many2one, self).update_db(model, columns)
|
||||
|
||||
def update_db_column(self, model, column):
|
||||
super(Many2one, self).update_db_column(model, column)
|
||||
model.pool.post_init(self.update_db_foreign_key, model, column)
|
||||
|
||||
def update_db_foreign_key(self, model, column):
|
||||
comodel = model.env[self.comodel_name]
|
||||
# Note: ir_actions is inherited, so foreign key doesn't work on it
|
||||
if comodel._auto and comodel._table != 'ir_actions':
|
||||
if not column:
|
||||
model.pool.post_init(model._m2o_add_foreign_key_checked, self.name, comodel, self.ondelete)
|
||||
else:
|
||||
model.pool.post_init(model._m2o_fix_foreign_key, model._table, self.name, comodel, self.ondelete)
|
||||
# ir_actions is inherited, so foreign key doesn't work on it
|
||||
if not comodel._auto or comodel._table == 'ir_actions':
|
||||
return
|
||||
# create/update the foreign key, and reflect it in 'ir.model.constraint'
|
||||
process = sql.fix_foreign_key if column else sql.add_foreign_key
|
||||
new = process(model._cr, model._table, self.name, comodel._table, 'id', self.ondelete or 'set null')
|
||||
if new:
|
||||
conname = '%s_%s_fkey' % (model._table, self.name)
|
||||
model.env['ir.model.constraint']._reflect_constraint(model, conname, 'f', None, self._module)
|
||||
|
||||
def _update(self, records, value):
|
||||
""" Update the cached value of ``self`` for ``records`` with ``value``.
|
||||
@@ -2365,30 +2377,40 @@ class Many2many(_RelationalMulti):
|
||||
|
||||
def update_db(self, model, columns):
|
||||
cr = model._cr
|
||||
rel, id1, id2 = self.relation, self.column1, self.column2
|
||||
# do not create relations for custom fields as they do not belong to a module
|
||||
# they will be automatically removed when dropping the corresponding ir.model.field
|
||||
# table name for custom relation all starts with x_, see __init__
|
||||
if not rel.startswith('x_'):
|
||||
IMR = model.env['ir.model.relation']
|
||||
model.pool.post_init(IMR._reflect_relation, model, rel, self._module)
|
||||
if not sql.table_exists(cr, rel):
|
||||
if self.comodel_name not in model.env:
|
||||
raise UserError(_('Many2many comodel does not exist: %r') % (self.comodel_name,))
|
||||
# Do not reflect relations for custom fields, as they do not belong to a
|
||||
# module. They are automatically removed when dropping the corresponding
|
||||
# 'ir.model.field'.
|
||||
if not self.manual:
|
||||
model.pool.post_init(model.env['ir.model.relation']._reflect_relation,
|
||||
model, self.relation, self._module)
|
||||
if not sql.table_exists(cr, self.relation):
|
||||
comodel = model.env[self.comodel_name]
|
||||
cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s"))' % (rel, id1, id2, id1, id2))
|
||||
# create foreign key references with ondelete=cascade, unless the targets are SQL views
|
||||
if sql.table_kind(cr, comodel._table) != 'v':
|
||||
model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id2, comodel, 'cascade', self._module)
|
||||
if sql.table_kind(cr, model._table) != 'v':
|
||||
model.pool.post_init(model._m2o_add_foreign_key_unchecked, rel, id1, model, 'cascade', self._module)
|
||||
|
||||
cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id1))
|
||||
cr.execute('CREATE INDEX ON "%s" ("%s")' % (rel, id2))
|
||||
cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (rel, model._table, comodel._table))
|
||||
_schema.debug("Create table '%s': m2m relation between '%s' and '%s'", rel, model._table, comodel._table)
|
||||
query = """
|
||||
CREATE TABLE "{rel}" ("{id1}" INTEGER NOT NULL,
|
||||
"{id2}" INTEGER NOT NULL,
|
||||
UNIQUE("{id1}","{id2}"));
|
||||
COMMENT ON TABLE "{rel}" IS %s;
|
||||
CREATE INDEX ON "{rel}" ("{id1}");
|
||||
CREATE INDEX ON "{rel}" ("{id2}")
|
||||
""".format(rel=self.relation, id1=self.column1, id2=self.column2)
|
||||
cr.execute(query, ['RELATION BETWEEN %s AND %s' % (model._table, comodel._table)])
|
||||
_schema.debug("Create table %r: m2m relation between %r and %r", self.relation, model._table, comodel._table)
|
||||
model.pool.post_init(self.update_db_foreign_keys, model)
|
||||
return True
|
||||
|
||||
def update_db_foreign_keys(self, model):
|
||||
""" Add the foreign keys corresponding to the field's relation table. """
|
||||
cr = model._cr
|
||||
comodel = model.env[self.comodel_name]
|
||||
reflect = model.env['ir.model.constraint']._reflect_constraint
|
||||
# create foreign key references with ondelete=cascade, unless the targets are SQL views
|
||||
if sql.table_kind(cr, model._table) != 'v':
|
||||
sql.add_foreign_key(cr, self.relation, self.column1, model._table, 'id', 'cascade')
|
||||
reflect(model, '%s_%s_fkey' % (self.relation, self.column1), 'f', None, self._module)
|
||||
if sql.table_kind(cr, comodel._table) != 'v':
|
||||
sql.add_foreign_key(cr, self.relation, self.column2, comodel._table, 'id', 'cascade')
|
||||
reflect(model, '%s_%s_fkey' % (self.relation, self.column2), 'f', None, self._module)
|
||||
|
||||
def read(self, records):
|
||||
comodel = records.env[self.comodel_name]
|
||||
|
||||
|
||||
@@ -106,14 +106,6 @@ def check_method_name(name):
|
||||
if regex_private.match(name):
|
||||
raise AccessError(_('Private methods (such as %s) cannot be called remotely.') % (name,))
|
||||
|
||||
POSTGRES_CONFDELTYPES = {
|
||||
'RESTRICT': 'r',
|
||||
'NO ACTION': 'a',
|
||||
'CASCADE': 'c',
|
||||
'SET NULL': 'n',
|
||||
'SET DEFAULT': 'd',
|
||||
}
|
||||
|
||||
def intersect(la, lb):
|
||||
return filter(lambda x: x in lb, la)
|
||||
|
||||
@@ -2050,69 +2042,6 @@ class BaseModel(object):
|
||||
if row['attnotnull']:
|
||||
tools.drop_not_null(self._table, row['attname'])
|
||||
|
||||
# checked version: for direct m2o starting from ``self``
|
||||
def _m2o_add_foreign_key_checked(self, source_field, dest_model, ondelete):
|
||||
assert self.is_transient() or not dest_model.is_transient(), \
|
||||
'Many2One relationships from non-transient Model to TransientModel are forbidden'
|
||||
if self.is_transient() and not dest_model.is_transient():
|
||||
# TransientModel relationships to regular Models are annoying
|
||||
# usually because they could block deletion due to the FKs.
|
||||
# So unless stated otherwise we default them to ondelete=cascade.
|
||||
ondelete = ondelete or 'cascade'
|
||||
field = self._fields[source_field]
|
||||
self._m2o_add_foreign_key_unchecked(self._table, source_field, dest_model, ondelete, field._module)
|
||||
|
||||
# unchecked version: for custom cases, such as m2m relationships
|
||||
def _m2o_add_foreign_key_unchecked(self, source_table, source_field, dest_model, ondelete, module):
|
||||
cr = self._cr
|
||||
query = 'ALTER TABLE "%s" ADD FOREIGN KEY ("%s") REFERENCES "%s" ON DELETE %s'
|
||||
cr.execute(query % (source_table, source_field, dest_model._table, ondelete or 'set null'))
|
||||
_schema.debug("Table '%s': added foreign key '%s' with definition=REFERENCES \"%s\" ON DELETE %s",
|
||||
source_table, source_field, dest_model._table, ondelete or 'set null')
|
||||
conname = "%s_%s_fkey" % (source_table, source_field)
|
||||
self.env['ir.model.constraint']._reflect_constraint(self, conname, 'f', None, module)
|
||||
|
||||
@api.model_cr
|
||||
def _m2o_fix_foreign_key(self, source_table, source_field, dest_model, ondelete):
|
||||
# Find FK constraint(s) currently established for the m2o field,
|
||||
# and see whether they are stale or not
|
||||
query = """ SELECT confdeltype as ondelete_rule, conname as constraint_name,
|
||||
cl2.relname as foreign_table
|
||||
FROM pg_constraint as con, pg_class as cl1, pg_class as cl2,
|
||||
pg_attribute as att1, pg_attribute as att2
|
||||
WHERE con.conrelid = cl1.oid
|
||||
AND cl1.relname = %s
|
||||
AND con.confrelid = cl2.oid
|
||||
AND array_lower(con.conkey, 1) = 1
|
||||
AND con.conkey[1] = att1.attnum
|
||||
AND att1.attrelid = cl1.oid
|
||||
AND att1.attname = %s
|
||||
AND array_lower(con.confkey, 1) = 1
|
||||
AND con.confkey[1] = att2.attnum
|
||||
AND att2.attrelid = cl2.oid
|
||||
AND att2.attname = %s
|
||||
AND con.contype = 'f' """
|
||||
self._cr.execute(query, (source_table, source_field, 'id'))
|
||||
constraints = self._cr.dictfetchall()
|
||||
if constraints:
|
||||
if len(constraints) == 1:
|
||||
# Is it the right constraint?
|
||||
cons, = constraints
|
||||
if cons['ondelete_rule'] != POSTGRES_CONFDELTYPES.get((ondelete or 'set null').upper(), 'a')\
|
||||
or cons['foreign_table'] != dest_model._table:
|
||||
# Wrong FK: drop it and recreate
|
||||
tools.drop_constraint(self._cr, source_table, cons['constraint_name'])
|
||||
else:
|
||||
# it's all good, nothing to do!
|
||||
return
|
||||
else:
|
||||
# Multiple FKs found for the same field, drop them all, and re-create
|
||||
for cons in constraints:
|
||||
tools.drop_constraint(self._cr, source_table, cons['constraint_name'])
|
||||
|
||||
# (re-)create the FK
|
||||
self._m2o_add_foreign_key_checked(source_field, dest_model, ondelete)
|
||||
|
||||
@api.model_cr_context
|
||||
def _init_column(self, column_name):
|
||||
""" Initialize the value of the given column for existing rows. """
|
||||
|
||||
@@ -6,6 +6,14 @@ import psycopg2
|
||||
|
||||
_schema = logging.getLogger('odoo.schema')
|
||||
|
||||
_CONFDELTYPES = {
|
||||
'RESTRICT': 'r',
|
||||
'NO ACTION': 'a',
|
||||
'CASCADE': 'c',
|
||||
'SET NULL': 'n',
|
||||
'SET DEFAULT': 'd',
|
||||
}
|
||||
|
||||
def table_exists(cr, tablename):
|
||||
""" Return whether the given table exists. """
|
||||
query = "SELECT 1 FROM pg_class WHERE relkind IN ('r','v') AND relname=%s"
|
||||
@@ -121,6 +129,37 @@ def drop_constraint(cr, tablename, constraintname):
|
||||
except Exception:
|
||||
_schema.warning("Table %r: unable to drop constraint %r!", tablename, constraintname)
|
||||
|
||||
def add_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondelete):
|
||||
""" Create the given foreign key, and return ``True``. """
|
||||
query = 'ALTER TABLE "{}" ADD FOREIGN KEY ("{}") REFERENCES "{}"("{}") ON DELETE {}'
|
||||
cr.execute(query.format(tablename1, columnname1, tablename2, columnname2, ondelete))
|
||||
_schema.debug("Table %r: added foreign key %r references %r(%r) ON DELETE %s",
|
||||
tablename1, columnname1, tablename2, columnname2, ondelete)
|
||||
return True
|
||||
|
||||
def fix_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondelete):
|
||||
""" Update the foreign keys between tables to match the given one, and
|
||||
return ``True`` if the given foreign key has been recreated.
|
||||
"""
|
||||
deltype = _CONFDELTYPES.get(ondelete.upper(), 'a')
|
||||
query = """ SELECT con.conname, c2.relname, a2.attnum, con.confdeltype as deltype
|
||||
FROM pg_constraint as con, pg_class as c1, pg_class as c2,
|
||||
pg_attribute as a1, pg_attribute as a2
|
||||
WHERE con.contype='f' AND con.conrelid=c1.oid AND con.confrelid=c2.oid
|
||||
AND array_lower(con.conkey, 1)=1 AND con.conkey[1]=a1.attnum
|
||||
AND array_lower(con.confkey, 1)=1 AND con.confkey[1]=a2.attnum
|
||||
AND a1.attrelid=c1.oid AND a2.attrelid=c2.oid
|
||||
AND c1.relname=%s AND a1.attname=%s """
|
||||
cr.execute(query, (tablename1, columnname1))
|
||||
found = False
|
||||
for fk in cr.fetchall():
|
||||
if not found and fk[1:] == (tablename2, columnname2, deltype):
|
||||
found = True
|
||||
else:
|
||||
drop_constraint(cr, tablename1, fk[0])
|
||||
if not found:
|
||||
return add_foreign_key(cr, tablename1, columnname1, tablename2, columnname2, ondelete)
|
||||
|
||||
def index_exists(cr, indexname):
|
||||
""" Return whether the given index exists. """
|
||||
cr.execute("SELECT 1 FROM pg_indexes WHERE indexname=%s", (indexname,))
|
||||
|
||||
Reference in New Issue
Block a user