【问题标题】:How to use Factory boy when two foreign keys are pointing to the same table当两个外键指向同一个表时如何使用工厂男孩
【发布时间】:2018-06-10 09:23:53
【问题描述】:

我正在尝试使用 Factory Boy 链接两个 Django 模型,但我找不到解决此问题的简单解决方案。这些是带有相应工厂的模型:

class Currency(models.Model):
    id = models.CharField(max_length=3, primary_key=True)


class ConversionRate(models.Model):
    currency = models.ForeignKey(Currency, null=False, on_delete=models.CASCADE)
    quote = models.ForeignKey(Currency, null=False, on_delete=models.CASCADE)
    rate = models.DecimalField(max_digits=6, decimal_places=2)


class CurrencyFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Currency

    id = factory.Sequence(lambda n: ['EUR', 'USD'][n%2])
    conversion_rate = factory.RelatedFactory('my_app.factories.ConversionRateFactory', 'currency')


class ConversionRateFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = ConversionRate

    currency = factory.SubFactory(CurrencyFactory)
    quote = factory.SubFactory(CurrencyFactory, id='EUR')
    rate = 1.2

这是测试表格的默认内容:

+--------+   +--------------------------+ 
|Currency|   |       ConversionRate     |
+--------+   +----------+--------+------+ 
|   id   |   | currency |  quote | rate |
+--------+   +----------+--------+------+ 
|  EUR   |   |    USD   |  EUR   |  1.2 |
+--------+   +----------+--------+------+  
|  USD   |   |    EUR   |  EUR   |   1  |
+--------+   +----------+--------+------+  

当我尝试构建工厂时会抛出完整性错误:

CurrencyFactory.create()
# Error:  UNIQUE constraint failed: Currency.id

我也尝试在 CurrencyFactory“元”部分添加django_get_or_create = ('id',),但这会造成无限循环。

过去有人遇到过这样的问题吗?有什么建议吗?

这是使用django_get_or_create = ('id',)时的回溯:

    env/lib/python3.6/site-packages/factory/builder.py:272: in build
        step.resolve(pre)
    env/lib/python3.6/site-packages/factory/builder.py:221: in resolve
        self.attributes[field_name] = getattr(self.stub, field_name)
    env/lib/python3.6/site-packages/factory/builder.py:375: in __getattr__
        extra=context,
    env/lib/python3.6/site-packages/factory/declarations.py:324: in evaluate
        return self.generate(step, defaults)
    env/lib/python3.6/site-packages/factory/declarations.py:414: in generate
        return step.recurse(subfactory, params, force_sequence=force_sequence)
    env/lib/python3.6/site-packages/factory/builder.py:233: in recurse
        return builder.build(parent_step=self, force_sequence=force_sequence)
    env/lib/python3.6/site-packages/factory/builder.py:299: in build
        context=postgen_context,
    env/lib/python3.6/site-packages/factory/declarations.py:675: in call
        return step.recurse(factory, passed_kwargs)
    env/lib/python3.6/site-packages/factory/builder.py:233: in recurse
        return builder.build(parent_step=self, force_sequence=force_sequence)
    env/lib/python3.6/site-packages/factory/builder.py:272: in build
        step.resolve(pre)
    env/lib/python3.6/site-packages/factory/builder.py:221: in resolve
        self.attributes[field_name] = getattr(self.stub, field_name)
    env/lib/python3.6/site-packages/factory/builder.py:375: in __getattr__
        extra=context,
    env/lib/python3.6/site-packages/factory/declarations.py:324: in evaluate
        return self.generate(step, defaults)
    env/lib/python3.6/site-packages/factory/declarations.py:414: in generate
        return step.recurse(subfactory, params, force_sequence=force_sequence)
    env/lib/python3.6/site-packages/factory/builder.py:233: in recurse
        return builder.build(parent_step=self, force_sequence=force_sequence)
    env/lib/python3.6/site-packages/factory/builder.py:279: in build
        kwargs=kwargs,
    env/lib/python3.6/site-packages/factory/base.py:314: in instantiate
        return self.factory._create(model, *args, **kwargs)
    env/lib/python3.6/site-packages/factory/django.py:163: in _create
        return cls._get_or_create(model_class, *args, **kwargs)
    env/lib/python3.6/site-packages/factory/django.py:154: in _get_or_create
        instance, _created = manager.get_or_create(*args, **key_fields)
    env/lib/python3.6/site-packages/django/db/models/manager.py:82: in manager_method
        return getattr(self.get_queryset(), name)(*args, **kwargs)
    env/lib/python3.6/site-packages/django/db/models/query.py:487: in get_or_create
        return self.get(**lookup), False
    env/lib/python3.6/site-packages/django/db/models/query.py:394: in get
        clone = self.filter(*args, **kwargs)
    env/lib/python3.6/site-packages/django/db/models/query.py:836: in filter
        return self._filter_or_exclude(False, *args, **kwargs)
    env/lib/python3.6/site-packages/django/db/models/query.py:850: in _filter_or_exclude
        clone = self._chain()
    env/lib/python3.6/site-packages/django/db/models/query.py:1156: in _chain
        obj = self._clone()
    env/lib/python3.6/site-packages/django/db/models/query.py:1168: in _clone
        c = self.__class__(model=self.model, query=self.query.chain(), using=self._db, hints=self._hints)
    env/lib/python3.6/site-packages/django/db/models/sql/query.py:337: in chain
        obj = self.clone()
    env/lib/python3.6/site-packages/django/db/models/sql/query.py:300: in clone
        obj.where = self.where.clone()
    env/lib/python3.6/site-packages/django/db/models/sql/where.py:148: in clone
        children=[], connector=self.connector, negated=self.negated)

    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    cls = <class 'django.db.models.sql.where.WhereNode'>, children = [], connector = 'AND', negated = False

        @classmethod
        def _new_instance(cls, children=None, connector=None, negated=False):
            """
                Create a new instance of this class when new Nodes (or subclasses) are
                needed in the internal code in this class. Normally, it just shadows
                __init__(). However, subclasses with an __init__ signature that aren't
                an extension of Node.__init__ might need to implement this method to
                allow a Node to create a new instance of them (if they have any extra
                setting up to do).
                """
    >       obj = Node(children, connector, negated)
    E       RecursionError: maximum recursion depth exceeded while calling a Python object

【问题讨论】:

  • 您能否提供您使用django_get_or_create 获得的无限循环的回溯?这似乎是处理您的问题的正确方法:/
  • 嗨@Xelnor,刚刚添加了回溯。似乎CurrencyFactory.conversion_rate 试图创建ConversionRateFactory 和ConversionRateFactory.quote 试图创建CurrencyFactory 创建和无限循环

标签: python django python-3.x django-models factory-boy


【解决方案1】:

正如您所指出的,问题来自 CurrencyFactory 创建一个 ConversionRateFactory,而后者又创建了 2 CurrencyFactory。

我建议使用factory.Trait 以递归方式禁用它:

  1. 在工厂class Params 部分定义特征:启用时(通过设置布尔标志),它将添加附加声明
  2. 为该特征设置默认值True:直接调用CurrencyFactory 将添加ConversionRate
  3. 在ConversionRateFactory 中,禁用特征 - 防止循环触发。

请看下面的代码:

class CurrencyFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.Currency
        django_get_or_create = ['id']

    class Params:
        with_conversion_rate = factory.Trait(
            conversion_rate=factory.RelatedFactory('my_app.factories.ConversionRateFactory', 'currency'),
        )

    # Small improvement: use a `factory.Iterator` to cycle between value
    id = factory.Iterator(['EUR', 'USD'])
    # By default, force each CurrencyFactory to create a ConversionRate.
    with_conversion_rate = True

class ConversionRateFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = models.ConversionRate

    rate = 1.2
    currency = factory.SubFactory(
        CurrencyFactory,
        with_conversion_rate=False,
    )
    quote = factory.SubFactory(
        CurrencyFactory,
        id='EUR',
        with_conversion_rate=False,
    )

【讨论】:

  • 我希望这个问题会通过factory-boy标签亲自联系到您,而且确实如此! :D 工作就像@Xelnor 的魅力,感谢您的调查。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-07-12
  • 1970-01-01
  • 2021-10-13
相关资源
最近更新 更多