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

使用factory_boy与DjangoModelFactory时遇重复键奇怪错误

问题分析与解决方案

核心问题原因

  1. 主键序列与数据不一致:Django的TestCase默认通过事务回滚清理测试数据,回滚会删除测试创建的记录,但数据库的主键序列(如PostgreSQL的serial)不会因事务回滚重置。若User模型使用自增主键,序列值会持续递增,当某次插入尝试复用已被标记为"用过"的主键值时,就会触发主键冲突。
  2. django_get_or_create的潜在影响:Meta类中的django_get_or_create = ('phonenumber',)会让factory优先通过手机号查询用户,不存在才创建。虽手机号是Sequence生成的理论上不会重复,但事务环境下的查询/创建逻辑可能引发意外的主键占用问题。
  3. auto_now字段覆盖限制:build()+save()无法覆盖auto_now字段,因为save()会自动触发auto_now更新逻辑,直接覆盖手动设置的值。
  4. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 05:34:59