使用factory_boy与DjangoModelFactory时遇重复键奇怪错误
问题分析与解决方案
核心问题原因
- 主键序列与数据不一致:Django的
TestCase默认通过事务回滚清理测试数据,回滚会删除测试创建的记录,但数据库的主键序列(如PostgreSQL的serial)不会因事务回滚重置。若User模型使用自增主键,序列值会持续递增,当某次插入尝试复用已被标记为"用过"的主键值时,就会触发主键冲突。 django_get_or_create的潜在影响:Meta类中的django_get_or_create = ('phonenumber',)会让factory优先通过手机号查询用户,不存在才创建。虽手机号是Sequence生成的理论上不会重复,但事务环境下的查询/创建逻辑可能引发意外的主键占用问题。auto_now字段覆盖限制:build()+save()无法覆盖auto_now字段,因为save()会自动触发auto_now更新逻辑,直接覆盖手动设置的值。in_atomic_block返回True是正常行为:Django的TestCase会在每个测试方法外层自动包裹原子事务,因此在setUp和测试方法内,当前连接始终处于原子块中,测试结束后事务会被回滚。
具体解决方案
1. 修复主键冲突问题
方案A:确保主键为自增字段
检查User模型,确认主键字段是Django默认的自增AutoField:
# models.py class User(models.Model): # 若未手动定义主键,Django会自动添加 id = models.AutoField(primary_key=True) phonenumber = models.CharField(unique=True, max_length=11) # 其他字段...
如果手动定义了主键,必须确保它是自增类型,否则需要手动维护主键唯一性。
方案B:手动重置主键序列
若坚持使用TestCase,可在测试的tearDown中手动重置主键序列,避免序列持续递增:
from django.db import connection from django.test import TestCase class ApiV1TestCase(TestCase): def setUp(self): self.user = UserFactory.create() def tearDown(self): super().tearDown() # 替换为你的实际序列名,格式通常为 appname_modelname_id_seq with connection.cursor() as cursor: cursor.execute("ALTER SEQUENCE accounts_user_id_seq RESTART WITH 1;")
方案C:移除django_get_or_create(若无需复用用户)
如果测试仅需创建全新用户,不需要通过手机号复用现有记录,直接移除Meta类中的django_get_or_create配置:
class UserFactory(factory.django.DjangoModelFactory): class Meta: model = models.User # 移除 django_get_or_create 配置 uuid = factory.Faker('uuid4') email = factory.Faker('email') phonenumber = factory.Sequence(generate_phone_number)
2. 解决auto_now字段覆盖需求
方案A:通过Factory的_create方法手动保存
自定义factory的创建逻辑,跳过auto_now字段的自动更新:
import datetime import factory from . import models class UserFactory(factory.django.DjangoModelFactory): class Meta: model = models.User uuid = factory.Faker('uuid4') email = factory.Faker('email') phonenumber = factory.Sequence(generate_phone_number) # 自定义auto_now字段的默认值(可选) created_at = factory.LazyFunction(datetime.datetime.now) @classmethod def _create(cls, model_class, *args, **kwargs): # 提取要保留的字段值,排除auto_now字段 auto_now_fields = [f.name for f in model_class._meta.fields if f.auto_now or f.auto_now_add] save_fields = [k for k in kwargs if k not in auto_now_fields] obj = model_class(*args, **kwargs) # 只保存非auto_now字段,避免自动更新 obj.save(update_fields=save_fields) return obj
方案B:创建后手动更新字段
在测试中创建用户后,直接更新auto_now字段并指定仅更新该字段:
import datetime class ApiV1TestCase(TestCase): def setUp(self): self.user = UserFactory.create() # 覆盖auto_now字段(例如created_at) self.user.created_at = datetime.datetime(2023, 1, 1, 0, 0, 0) self.user.save(update_fields=['created_at'])
内容的提问来源于stack exchange,提问作者Dash Winterson
相关产品推荐
相关产品推荐

