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

Django 2.2后台删用户触发IntegrityError:外键约束失败

Django 2.2删除自定义User模型时触发IntegrityError(FOREIGN KEY约束失败)

问题背景

我在使用Django 2.2时,在管理界面删除任意用户都会触发错误:IntegrityError at /admin/accounts/user/ FOREIGN KEY constraint failed。我使用的是继承自AbstractBaseUser的自定义User模型(网站用邮箱而非用户名标识用户),作为Django和关系数据库的初学者,暂时没找到简单的解决办法。

我的自定义User模型及相关代码如下:

# src/accounts.models.py
from django.conf import settings
from django.db import models
from django.db.models.signals import pre_save, post_save
from django.contrib.auth.models import (
    AbstractBaseUser,
    BaseUserManager
)
from django.core.mail import send_mail
from django.template.loader import get_template

class UserManager(BaseUserManager):
    def create_user(self, email, full_name=None, password=None, is_active=True, is_staff=False, is_admin=False):
        if not email:
            raise ValueError("Users must have an email address")
        if not password:
            raise ValueError("Users must have a password")
        user_obj = self.model(
            email = self.normalize_email(email),
            full_name=full_name
        )
        user_obj.set_password(password) # change user password
        user_obj.staff = is_staff
        user_obj.admin = is_admin
        user_obj.is_active = is_active
        user_obj.save(using=self._db)
        return user_obj

    def create_staffuser(self, email,full_name=None, password=None):
        user = self.create_user(
            email,
            full_name=full_name,
            password=password,
            is_staff=True
        )
        return user

    def create_superuser(self, email, full_name=None, password=None):
        user = self.create_user(
            email,
            full_name=full_name,
            password=password,
            is_staff=True,
            is_admin=True
        )
        return user

class User(AbstractBaseUser):
    email = models.EmailField(max_length=255, unique=True)
    full_name = models.CharField(max_length=255, blank=True, null=True)
    is_active = models.BooleanField(default=True) # can login
    staff = models.BooleanField(default=False) # staff user non superuser
    admin = models.BooleanField(default=False) # superuser
    timestamp = models.DateTimeField(auto_now_add=True)

    USERNAME_FIELD = 'email' #username
    # USERNAME_FIELD and password are required by default
    REQUIRED_FIELDS = [] #['full_name'] #python manage.py createsuperuser

    objects = UserManager()

    def __str__(self):
        return self.email

class EmailActivation(models.Model):
    user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE ) # required on Django 2.0+ deletes child objects on_delete
    #the error must be caused by this
    email = models.EmailField()
    key = models.CharField(max_length=120, blank=True, null=True)
    activated = models.BooleanField(default=False)
    forced_expired = models.BooleanField(default=False)
    expires = models.IntegerField(default=7) # 7 Days
    timestamp = models.DateTimeField(auto_now_add=True)
    update = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.email

    def regenerate(self):
        self.key = None
        self.save()
        if self.key is not None:
            return True
        return False

    def send_activation(self):
        if not self.activated and not self.forced_expired:
            if self.key:
                base_url = getattr(settings, 'BASE_URL', '<my-domain>')
                key_path = self.key # use reverse
                path = "{base}{path}".format(base=base_url, path=key_path)
                context = {
                    'path': path,
                    'email': self.email
                }
                txt_ = get_template("registration/emails/verify.txt").render(context)
                html_ = get_template("registration/emails/verify.html").render(context)
                subject = '1-Click Email Verification'
                from_email = settings.DEFAULT_FROM_EMAIL
                recipient_list = [self.email]
                sent_mail = send_mail(
                    subject,
                    txt_,
                    from_email,
                    recipient_list,
                    html_message=html_,
                    fail_silently=False,
                )
                return sent_mail
        return False

def pre_save_email_activation(sender, instance, *args, **kwargs):
    if not instance.activated and not instance.forced_expired:
        if not instance.key:
            instance.key = unique_key_generator(instance)
pre_save.connect(pre_save_email_activation, sender=EmailActivation)

def post_save_user_create_reciever(sender, instance, created, *args, **kwargs):
    if created:
        obj = EmailActivation.objects.create(user=instance, email=instance.email)
        obj.send_activation()
post_save.connect(post_save_user_create_reciever, sender=User)

class GuestEmail(models.Model):
    email = models.EmailField()
    active = models.BooleanField(default=True)
    update = models.DateTimeField(auto_now=True)
    timestamp = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.email

完整错误Traceback

Environment:

Request Method: POST
Request URL: http://127.0.0.1:8000/admin/accounts/user/
Django Version: 2.2.12
Python Version: 3.6.9
Installed Applications:
['django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'storages',
'accounts',
'addresses',
'analytics',
'billing',
'carts',
'marketing',
'orders',
'products',
'search',
'tags']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware']

Traceback:

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/backends/utils.py" in _execute
84. return self.cursor.execute(sql, params)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py" in execute
383. return Database.Cursor.execute(self, query, params)

The above exception (FOREIGN KEY constraint failed) was the direct
cause of the following exception:

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/core/handlers/exception.py" in inner
34. response = get_response(request)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
115. response = self.process_exception_by_middleware(e, request)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
113. response = wrapped_callback(request, *args, **callback_kwargs)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/contrib/admin/options.py" in wrapper
606. return self.admin_site.admin_view(view)(*args, **kwargs)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/utils/decorators.py" in _wrapped_view
142. response = view_func(request, *args, **kwargs)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/views/decorators/cache.py" in _wrapped_view_func
44. response = view_func(request, *args, **kwargs)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/contrib/admin/sites.py" in inner
223. return view(request, *args, **kwargs)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/utils/decorators.py" in _wrapper
45. return bound_method(*args, **kwargs)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/utils/decorators.py" in _wrapped_view
142. response = view_func(request, *args, **kwargs)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/contrib/admin/options.py" in changelist_view
1727. response = self.response_action(request, queryset=cl.get_queryset(request))

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/contrib/admin/options.py" in response_action
1397. response = func(self, request, queryset)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/contrib/admin/actions.py" in delete_selected
40. modeladmin.delete_queryset(request, queryset)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/contrib/admin/options.py" in delete_queryset
1098. queryset.delete()

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/models/query.py" in delete
711. deleted, _rows_count = collector.delete()

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/models/deletion.py" in delete
312. count = query.delete_batch(pk_list, self.using)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/models/sql/subqueries.py" in delete_batch
41. num_deleted += self.do_query(self.get_meta().db_table, self.where, using=using)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/models/sql/subqueries.py" in do_query
24. cursor = self.get_compiler(using).execute_sql(CURSOR)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/models/sql/compiler.py" in execute_sql
1140. cursor.execute(sql, params)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/backends/utils.py" in execute
99. return super().execute(sql, params)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/backends/utils.py" in execute
67. return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/backends/utils.py" in _execute_with_wrappers
76. return executor(sql, params, many, context)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/backends/utils.py" in _execute
84. return self.cursor.execute(sql, params)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/utils.py" in exit
89. raise dj_exc_value.with_traceback(traceback) from exc_value

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/backends/utils.py" in _execute
84. return self.cursor.execute(sql, params)

File "/home/ray/Dev/rxvenv/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py" in execute
383. return Database.Cursor.execute(self, query, params)

Exception Type: IntegrityError at /admin/accounts/user/
Exception Value: FOREIGN KEY constraint failed


问题分析

你猜的完全没错!问题确实出在EmailActivation模型上。虽然你设置了on_delete=models.CASCADE,但这里有个Django批量删除的隐藏坑:当你在Admin里用批量删除选中多个用户时,Django会直接生成批量删除SQL语句,先尝试删除User表的记录,而数据库的外键约束会立刻阻止这个操作——因为此时EmailActivation表中还有依赖该User的记录存在,所以触发了FOREIGN KEY约束失败。

而如果是单个进入用户详情页删除,Django会先处理关联的EmailActivation记录,再删除User,所以不会报错。

解决方案

这里有两种简单有效的解决办法,你可以根据自己的需求选择:

方法1:自定义Admin批量删除动作

在accounts/admin.py中,给User模型的Admin类替换默认的批量删除逻辑,让它先清理关联的EmailActivation记录,再删除用户:

# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import User, EmailActivation

class UserAdmin(BaseUserAdmin):
    # 保留你已有的配置,比如list_display、fieldsets等
    actions = ['delete_selected_users']

    def delete_selected_users(self, request, queryset):
        # 先逐个删除每个用户对应的激活记录
        for user in queryset:
            EmailActivation.objects.filter(user=user).delete()
        # 再删除用户本身
        queryset.delete()
    delete_selected_users.short_description = "删除选中的用户"

admin.site.register(User, UserAdmin)

方法2:重写User模型的delete方法(更彻底)

直接在User模型中添加自定义的delete逻辑,确保无论单个删除还是批量删除(需要配合Admin动作),都先清理关联记录:

class User(AbstractBaseUser):
    # ... 保留原有字段和方法 ...

    def delete(self, *args, **kwargs):
        # 先删除当前用户对应的激活记录
        EmailActivation.objects.filter(user=self).delete()
        # 再调用父类的删除方法
        super().delete(*args, **kwargs)

注意:queryset.delete()不会触发模型的delete()方法,所以如果用这个方法,还是需要配合方法1的Admin自定义动作,或者让批量删除走单个删除的逻辑。

额外检查步骤

最后,确保你的数据库迁移已经正确应用了外键的级联删除设置,可以运行以下命令重新生成并应用迁移:

python manage.py makemigrations accounts
python manage.py migrate accounts

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 16:17:44