Custom unique_together key name Custom unique_together key name django django

Custom unique_together key name


Its not well documented, but depending on if you are using Django 1.6 or 1.7 there are two ways you can do this:

In Django 1.6 you can override the unique_error_message, like so:

class MyModel(models.Model):    clid = models.AutoField(primary_key=True, db_column='CLID')    csid = models.IntegerField(db_column='CSID')    cid = models.IntegerField(db_column='CID')    # ....def unique_error_message(self, model_class, unique_check):    if model_class == type(self) and unique_check == ("csid", "cid", "uuid"):        return _('Your custom error')    else:        return super(MyModel, self).unique_error_message(model_class, unique_check)

Or in Django 1.7:

class MyModel(models.Model):    clid = models.AutoField(primary_key=True, db_column='CLID')    csid = models.IntegerField(db_column='CSID')    cid = models.IntegerField(db_column='CID')    uuid = models.CharField(max_length=96, db_column='UUID', blank=True)    class Meta(models.Meta):        unique_together = [            ["csid", "cid", "uuid"],        ]        error_messages = {            NON_FIELD_ERRORS: {                'unique_together': "%(model_name)s's %(field_labels)s are not unique.",            }        }


Integrity error is raised from database but from django:

create table t ( a int, b int , c int);alter table t add constraint u unique ( a,b,c);   <-- 'u'    insert into t values ( 1,2,3);insert into t values ( 1,2,3);Duplicate entry '1-2-3' for key 'u'   <---- 'u'

That means that you need to create constraint with desired name in database. But is django in migrations who names constraint. Look into _create_unique_sql :

def _create_unique_sql(self, model, columns):    return self.sql_create_unique % {        "table": self.quote_name(model._meta.db_table),        "name": self.quote_name(self._create_index_name(model, columns, suffix="_uniq")),        "columns": ", ".join(self.quote_name(column) for column in columns),    }

Is _create_index_name who has the algorithm to names constraints:

def _create_index_name(self, model, column_names, suffix=""):    """    Generates a unique name for an index/unique constraint.    """    # If there is just one column in the index, use a default algorithm from Django    if len(column_names) == 1 and not suffix:        return truncate_name(            '%s_%s' % (model._meta.db_table, self._digest(column_names[0])),            self.connection.ops.max_name_length()        )    # Else generate the name for the index using a different algorithm    table_name = model._meta.db_table.replace('"', '').replace('.', '_')    index_unique_name = '_%x' % abs(hash((table_name, ','.join(column_names))))    max_length = self.connection.ops.max_name_length() or 200    # If the index name is too long, truncate it    index_name = ('%s_%s%s%s' % (        table_name, column_names[0], index_unique_name, suffix,    )).replace('"', '').replace('.', '_')    if len(index_name) > max_length:        part = ('_%s%s%s' % (column_names[0], index_unique_name, suffix))        index_name = '%s%s' % (table_name[:(max_length - len(part))], part)    # It shouldn't start with an underscore (Oracle hates this)    if index_name[0] == "_":        index_name = index_name[1:]    # If it's STILL too long, just hash it down    if len(index_name) > max_length:        index_name = hashlib.md5(force_bytes(index_name)).hexdigest()[:max_length]    # It can't start with a number on Oracle, so prepend D if we need to    if index_name[0].isdigit():        index_name = "D%s" % index_name[:-1]    return index_name

For the current django version (1.7) the constraint name for a composite unique constraint looks like:

>>> _create_index_name( 'people', [ 'c1', 'c2', 'c3'], '_uniq' )'myapp_people_c1_d22a1efbe4793fd_uniq'

You should overwrite _create_index_name in some way to change algorithm. A way, maybe, writing your own db backend inhering from mysql and overwriting _create_index_name in your DatabaseSchemaEditor on your schema.py (not tested)


Changing index name in ./manage.py sqlall output.

You could run ./manage.py sqlall yourself and add in the constraint name yourself and apply manually instead of syncdb.

$ ./manage.py sqlall testBEGIN;CREATE TABLE `test_mymodel` (    `CLID` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,    `CSID` integer NOT NULL,    `CID` integer NOT NULL,    `UUID` varchar(96) NOT NULL,    UNIQUE (`CSID`, `CID`, `UUID`));COMMIT;

e.g.

$ ./manage.py sqlall testBEGIN;CREATE TABLE `test_mymodel` (    `CLID` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,    `CSID` integer NOT NULL,    `CID` integer NOT NULL,    `UUID` varchar(96) NOT NULL,    UNIQUE constraint_name (`CSID`, `CID`, `UUID`));COMMIT;

Overriding BaseDatabaseSchemaEditor._create_index_name

The solution pointed out by @danihp is incomplete, it only works for field updates (BaseDatabaseSchemaEditor._alter_field)

The sql I get by overriding _create_index_name is:

BEGIN;CREATE TABLE "testapp_mymodel" (    "CLID" integer NOT NULL PRIMARY KEY AUTOINCREMENT,    "CSID" integer NOT NULL,    "CID" integer NOT NULL,    "UUID" varchar(96) NOT NULL,    UNIQUE ("CSID", "CID", "UUID"));COMMIT;

Overriding BaseDatabaseSchemaEditor.create_model

based on https://github.com/django/django/blob/master/django/db/backends/schema.py

class BaseDatabaseSchemaEditor(object):    # Overrideable SQL templates    sql_create_table_unique = "UNIQUE (%(columns)s)"    sql_create_unique = "ALTER TABLE %(table)s ADD CONSTRAINT %(name)s UNIQUE (%(columns)s)"    sql_delete_unique = "ALTER TABLE %(table)s DROP CONSTRAINT %(name)s"

and this is the piece in create_model that is of interest:

    # Add any unique_togethers    for fields in model._meta.unique_together:        columns = [model._meta.get_field_by_name(field)[0].column for field in fields]        column_sqls.append(self.sql_create_table_unique % {            "columns": ", ".join(self.quote_name(column) for column in columns),        })

Conclusion

You could:

  • override create_model to use _create_index_name for unique_together contraints.
  • modify sql_create_table_unique template to include a name parameter.

You may also be able to check a possible fix on this ticket:

https://code.djangoproject.com/ticket/24102