Сохранение объекта factory-boy RelatedFactory на родительской фабрике

У меня есть две модели Django (Customer и CustomerAddress), которые содержат ForeignKey друг к другу. Я использую factory-boy для управления созданием этих моделей и не могу сохранить экземпляр дочерней фабрики в родительской фабрике (используя отношения, определенные с помощью класса RelatedFactory).

Две мои модели:

class ExampleCustomerAddress(models.Model):
    # Every customer mailing address is assigned to a single Customer,
    # though Customers may have multiple addresses.
    customer = models.ForeignKey('ExampleCustomer', on_delete=models.CASCADE)

class ExampleCustomer(models.Model):
    # Each customer has a single (optional) default billing address:
    default_billto = models.ForeignKey(
        'ExampleCustomerAddress',
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='+')

У меня две фабрики, по одной на каждую модель:

class ExampleCustomerAddressFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = ExampleCustomerAddress

    customer = factory.SubFactory(
        'ExampleCustomerFactory',
        default_billto=None)  # Set to None to prevent recursive address creation.

class ExampleCustomerFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = ExampleCustomer

    default_billto = factory.RelatedFactory(ExampleCustomerAddressFactory,
                                            'customer')

При создании ExampleCustomerFactory default_billto имеет значение Нет, хотя ExampleCustomerAddress уже создан:

In [14]: ec = ExampleCustomerFactory.build()

In [15]: ec.default_billto is None
Out[15]: True

(При использовании create() в базе данных существует новый ExampleCustomerAddress. Здесь я использую build() для упрощения примера).

Создание ExampleCustomerAddress работает, как и ожидалось, при этом Customer создается автоматически:

In [22]: eca = ExampleCustomerAddressFactory.build()

In [23]: eca.customer
Out[23]: <ExampleCustomer: ExampleCustomer object>

In [24]: eca.customer.default_billto is None
Out[24]: True  <-- I was expecting this to be set to an `ExampleCustomerAddress!`.

Я чувствую, что схожу с ума здесь, упуская что-то очень простое. У меня складывается впечатление, что я сталкиваюсь с этой ошибкой из-за того, что обе модели содержат ForeignKeys друг к другу.


person Taj Morton    schedule 11.04.2020    source источник


Ответы (1)


Во-первых, простое эмпирическое правило: когда вы следуете ForeignKey, всегда предпочитайте SubFactory; RelatedFactory предназначен для обратной связи.

Возьмем каждый завод по очереди.

ExampleCustomerAddressFactory

Когда мы вызываем эту фабрику без клиента, мы хотим получить адрес, связанный с клиентом и используемый в качестве адреса по умолчанию для этого клиента.

Однако, когда мы вызываем его с клиентом, не изменяйте его.

Следующее будет работать:

class ExampleCustomerAddressFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = ExampleCustomerAddress

    # Fill the Customer unless provided
    customer = factory.SubFactory(
        ExampleCustomerFactory,
        # We can't provide ourself there, since we aren't saved to the database yet.
        default_billto=None,
    )

    @factory.post_declaration
    def set_customer_billto(obj, create, *args, **kwargs):
        """Set the default billto of the customer to ourselves if empty"""
        if obj.customer.default_billto is None:
            obj.customer.default_billto = obj
            if create:
                obj.customer.save()

Здесь мы установим для вновь созданного клиента значение «нас»; обратите внимание, что эту логику также можно переместить в ExampleCustomerAddress.save().

ExampleCustomerFactory

Для этой фабрики правила проще: при создании клиента создайте платежный адрес по умолчанию (если не указано значение).

class ExampleCustomerFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = ExampleCustomer

    # We can't use a SubFactory here, since that would be evaluated before
    # the Customer has been saved.
    default_billto = factory.RelatedFactory(
        ExampleCustomerAddressFactory,
        'customer',
    )

Эта фабрика будет работать следующим образом:

  1. Создайте экземпляр ExampleCustomer с default_billto=None;
  2. Позвоните ExampleCustomerAddressFactory(customer=obj) с вновь созданным клиентом;
  3. Эта фабрика создаст ExampleCustomerAddress с этим клиентом;
  4. Затем хук постгенерации на этой фабрике обнаружит, что у клиента нет default_billto, и переопределит его.

Примечания

  • Я не проверял это, поэтому могут быть опечатки или мелкие ошибки;
  • Вам решать, какая фабрика объявляется первой, используя путь к целевой фабрике вместо прямой ссылки;
  • Как указано выше, логика для установки платежного адреса клиента по умолчанию, когда он пуст и к этому клиенту добавляется адрес, может быть перенесена в метод .save() вашей модели.
person Xelnor    schedule 12.04.2020
comment
Большое спасибо! Это сработало отлично. По какой-то причине я застрял, добавляя хук постгенерации в файл CustomerFactory. Мне никогда не приходило в голову поместить его на CustomerAddressFactory. Одно быстрое редактирование вашего ответа: декоратор @factory.post_declaration должен быть @factory.post_generation. - person Taj Morton; 12.04.2020