Django自定义用户模型测试触发IntegrityError问题排查:重复键值违反唯一约束
你遇到的这个问题确实是因为重复的空username字段导致的,我来给你拆解清楚原因和可行的解决办法:
问题根源
你继承了AbstractUser这个基类,它自带一个username字段,并且默认是unique=True(唯一约束)。而你把USERNAME_FIELD改成了email,同时REQUIRED_FIELDS设为空数组——这意味着调用create_user时,如果不主动传入username参数,Django会自动把它设为空字符串。
在你的test_user_email_normalized测试用例里,循环创建用户时完全没给username赋值,所以每个测试用户的username都是空字符串。第一次创建没问题,但第二次创建时,数据库就会抛出duplicate key错误——因为空字符串已经存在于有唯一约束的username字段里了。
你添加user.delete()后测试通过,本质是每次创建完就删掉用户,数据库里不会留下重复的空username记录,下一次创建自然就不会冲突了。
解决办法(按推荐程度排序)
1. 彻底移除username字段(最推荐)
既然你已经用email作为用户的唯一标识,完全可以抛弃AbstractUser自带的username字段,改用AbstractBaseUser重构模型,从根源避免这类冲突:
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager class UserManager(BaseUserManager): """用户管理器""" def create_user(self, email, password=None, **extra_fields): user = self.model(email=self.normalize_email(email), **extra_fields) user.set_password(password) user.save(using=self._db) return user class User(AbstractBaseUser, PermissionsMixin): """用户模型""" email = models.EmailField(max_length=255, unique=True) name = models.CharField(max_length=255) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) # 如果需要AbstractUser自带的last_login等字段,可以手动添加 # last_login = models.DateTimeField(null=True, blank=True) objects = UserManager() REQUIRED_FIELDS = [] USERNAME_FIELD = 'email'
这种方式完全贴合你用email作为唯一标识的设计,后续不会再出现username相关的隐性问题。
2. 自动生成唯一的username值
如果不想替换AbstractUser,可以在UserManager里自动给username生成唯一值,避免空字符串重复:
import uuid from django.contrib.auth.models import AbstractUser, PermissionsMixin, BaseUserManager class UserManager(BaseUserManager): """用户管理器""" def create_user(self, email, password=None, **extra_fields): # 自动生成唯一的username(截断到符合CharField的默认长度) if not extra_fields.get('username'): extra_fields['username'] = str(uuid.uuid4())[:30] user = self.model(email=self.normalize_email(email), **extra_fields) user.set_password(password) user.save(using=self._db) return user
这样每次创建用户时,username都会是随机唯一的字符串,不会出现重复空值的问题。
3. 测试用例中显式传入唯一username
如果只想临时解决测试里的问题,可以在测试循环中给每个用户指定唯一的username:
class ModelTest(TestCase): def test_user_email_normalized(self): """测试邮箱规范化功能""" test_emails = [ ['test1@EXAMPLE.Com', 'test1@example.com'], ['TesT2@exaMple.com', 'TesT2@example.com'], ] for idx, (email, expected) in enumerate(test_emails): user = get_user_model().objects.create_user( email, 'password123', username=f'test_user_{idx}' ) self.assertEqual(user.email, expected)
不过这种方法只解决测试场景的问题,生产环境如果创建用户时没传username,还是会触发同样的错误。
内容的提问来源于stack exchange,提问作者Darek Napierski

