【问题标题】:Custom unique_together key name自定义 unique_together 键名
【发布时间】:2015-02-26 01:45:09
【问题描述】:

我有一个模型,其中定义了 unique_together 3 个字段一起唯一:

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"],
        ]

现在,如果我尝试使用现有 csid+cid+uuid 组合保存 MyModel 实例,我会得到:

IntegrityError: (1062, "Duplicate entry '1-1-1' for key 'CSID'")

这是正确的。但是,有没有办法自定义那个键名? (在这种情况下为CSID)

换句话说,我可以为unique_together 中列出的约束提供一个名称吗?

据我了解,这不在文档中。

【问题讨论】:

  • 只是好奇更改索引名称的用例是什么?除了美观之外,我看不出它有什么重要意义,除非您直接与数据库打交道。
  • @user193130 这是一个好点。我有一个相当奇怪的 Django 用例,其中我有一个没有任何外键关系的遗留 mysql 数据库,并且在整个应用程序中我主要通过原始查询来操作数据库(这让我很生气,顺便说一句,不要问)。不过在测试中,我使用 django ORM,models.py 尽可能接近真实的遗留数据库。在这里,我希望将唯一键约束命名为真实数据库中的名称。希望你能理解我目前的所有奇怪之处:)
  • 啊,是的,这实际上很有意义。处理遗留数据库听起来很麻烦,但我假设您正在使models.py 尽可能接近真实数据库,以便准备将其不仅用于测试,而且用于实际应用程序代码,所以你不要'不必再处理原始查询了。希望你一切顺利
  • 猜猜所有更改错误消息的答案对你不起作用,哈哈
  • @danihp 请取消删除它 - 您花了时间并付出了巨大的努力来研究这个主题。不要担心赏金 - 我很确定会有另一个赏金:)

标签: python mysql django django-models unique-constraint


【解决方案1】:

没有很好的文档记录,但取决于您使用的是 Django 1.6 还是 1.7,有两种方法可以做到这一点:

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.",
            }
        }

【讨论】:

  • 有趣的做法,设置错误信息而不是设置约束名!谢谢!
  • 我喜欢这个解决方案不涉及 SQL。这是一种直接的 python 式方法。
【解决方案2】:

在./manage.py sqlall 输出中更改索引名称。

您可以自己运行./manage.py sqlall 并自己添加约束名称并手动应用而不是syncdb。

$ ./manage.py sqlall test
BEGIN;
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;

例如

$ ./manage.py sqlall test
BEGIN;
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;

覆盖BaseDatabaseSchemaEditor._create_index_name

@danihp 指出的解决方案不完整,仅适用于字段更新 (BaseDatabaseSchemaEditor._alter_field)

我通过覆盖_create_index_name得到的sql是:

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;

覆盖BaseDatabaseSchemaEditor.create_model

基于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"

这是create_model 中有趣的部分:

    # 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),
        })

结论

你可以:

  • 覆盖create_model 以将_create_index_name 用于unique_together 约束。
  • 修改sql_create_table_unique 模板以包含name 参数。

您还可以检查此票证的可能修复方法:

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

【讨论】:

【解决方案3】:

从数据库引发完整性错误,但从 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'

这意味着您需要在数据库中创建具有所需名称的约束。但是迁移中的 django 是命名约束的。查看 _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),
    }

是_create_index_name谁拥有命名约束的算法:

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

对于当前的 django 版本 (1.7),复合唯一约束的约束名称如下所示:

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

您应该以某种方式覆盖_create_index_name 以更改算法。一种方法,也许是编写你自己的数据库后端,继承自 mysql 并在 DatabaseSchemaEditor 中覆盖 _create_index_name schema.py(未测试)

【讨论】:

  • 这更接近我期望得到的答案。我会试一试,然后回来找你。谢谢!
  • 只能用于字段更新,_create_index_name 不用于create_model 中的unique_together。
  • 感谢@dnozay,我们有一个真正的解决方案(+django ticket)。不过,我不得不说我们仍然在 django 1.6.9 上(现在卡在 python2.6 上)。这意味着自定义唯一约束名称 (source) 要困难得多。无论如何,很好的答案。谢谢!
【解决方案4】:

我相信你必须在你的数据库中这样做;

MySQL:

ALTER TABLE `votes` ADD UNIQUE `unique_index`(`user`, `email`, `address`);

我相信然后会说 ... for key 'unique_index'

【讨论】:

  • 感谢您的参与。这是一个选项。
【解决方案5】:

一种解决方案是您可以在 save() 中捕获 IntegrityError,然后根据需要制作自定义错误消息,如下所示。

try:
    obj = MyModel()
    obj.csid=1
    obj.cid=1
    obj.uuid=1
    obj.save()

except IntegrityError:
    message = "IntegrityError: Duplicate entry '1-1-1' for key 'CSID', 'cid', 'uuid' "

现在您可以使用此消息显示为错误消息。

【讨论】:

    猜你喜欢
    • 2017-05-14
    • 1970-01-01
    • 2020-11-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多