[REF] models: refactoring of foreign keys

This commit is contained in:
Raphael Collet
2017-02-22 15:24:08 +01:00
parent dc82ff59ac
commit f8e573db23
3 changed files with 90 additions and 100 deletions
+51 -29
View File
@@ -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]
-71
View File
@@ -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. """
+39
View File
@@ -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,))