You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django中如何为UniqueConstraint设置独立的触发与过滤条件

你之前的方案问题确实如你判断的一样:两个带条件的唯一约束仅会在各自匹配的行范围内校验唯一性,跨范围的重复(未关联customer的用户和已关联customer的用户邮箱相同)不会被拦截。

以下是完全在数据库层面实现的方案,优先适配Django原生支持的写法:


方案一:PostgreSQL 环境(Django 2.2+ 原生支持)

使用 UniqueConstraint + ExclusionConstraint 组合实现,完全符合所有校验规则,不需要额外编码:

from django.db import models
from django.db.models import Q, UniqueConstraint, ExclusionConstraint
from django.contrib.postgres.fields import RangeOperators

class Customer(models.Model):
    pass

class User(models.Model):
    email = models.EmailField(blank=True, default="")
    customer = models.ForeignKey(Customer, null=True, on_delete=models.CASCADE)

    class Meta:
        constraints = [
            # 约束1:已关联customer的用户,邮箱在所属customer范围内唯一
            UniqueConstraint(
                name="customer_scoped_email_unique",
                fields=["customer", "email"],
                condition=~Q(email="") & ~Q(email=None)
            ),
            # 约束2:未关联customer的用户的邮箱全局唯一,禁止任何其他用户使用相同邮箱
            ExclusionConstraint(
                name="global_email_unique_for_null_customer",
                expressions=[
                    ("email", RangeOperators.EQUAL),
                ],
                condition=~Q(email="") & ~Q(email=None) & Q(customer__isnull=True)
            )
        ]

校验效果验证:

  • 两个未关联customer的用户使用相同邮箱:触发约束2,拦截
  • 未关联customer的用户和已关联customer的用户使用相同邮箱:触发约束2,拦截
  • 同一个customer下的两个用户使用相同邮箱:触发约束1,拦截
  • 不同customer的用户使用相同邮箱:无约束触发,允许
    完全匹配你的所有规则要求。

方案二:MySQL 等不支持ExclusionConstraint的数据库

可通过数据库触发器实现同等校验逻辑,Django可通过迁移文件添加触发器:

# 新建迁移文件,添加如下内容
from django.db import migrations

class Migration(migrations.Migration):
    dependencies = [
        ("your_app_name", "000x_previous_migration"),
    ]

    operations = [
        # 新增时校验
        migrations.RunSQL(
            """
            CREATE TRIGGER check_email_unique_before_insert
            BEFORE INSERT ON your_app_name_user
            FOR EACH ROW
            BEGIN
                IF NEW.email IS NOT NULL AND NEW.email != '' THEN
                    IF NEW.customer_id IS NULL THEN
                        IF EXISTS (SELECT 1 FROM your_app_name_user WHERE email = NEW.email) THEN
                            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '该邮箱已被全局注册';
                        END IF;
                    ELSE
                        IF EXISTS (SELECT 1 FROM your_app_name_user WHERE email = NEW.email AND (customer_id = NEW.customer_id OR customer_id IS NULL)) THEN
                            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '该邮箱已在当前客户下使用或已被全局注册';
                        END IF;
                    END IF;
                END IF;
            END;
            """,
            reverse_sql="DROP TRIGGER IF EXISTS check_email_unique_before_insert;"
        ),
        # 更新时校验
        migrations.RunSQL(
            """
            CREATE TRIGGER check_email_unique_before_update
            BEFORE UPDATE ON your_app_name_user
            FOR EACH ROW
            BEGIN
                IF NEW.email IS NOT NULL AND NEW.email != '' THEN
                    IF NEW.customer_id IS NULL THEN
                        IF EXISTS (SELECT 1 FROM your_app_name_user WHERE email = NEW.email AND id != NEW.id) THEN
                            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '该邮箱已被全局注册';
                        END IF;
                    ELSE
                        IF EXISTS (SELECT 1 FROM your_app_name_user WHERE email = NEW.email AND id != NEW.id AND (customer_id = NEW.customer_id OR customer_id IS NULL)) THEN
                            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '该邮箱已在当前客户下使用或已被全局注册';
                        END IF;
                    END IF;
                END IF;
            END;
            """,
            reverse_sql="DROP TRIGGER IF EXISTS check_email_unique_before_update;"
        )
    ]

使用时将your_app_name替换为你的Django应用名称即可,触发器逻辑和方案一完全一致,同样在数据库层面保证数据一致性。


内容的提问来源于stack exchange,提问作者Kye

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 01:27:00