One-to-many relationships in factory_boy

7.3k views Asked by At

I use SQLalchemy as my ORM and am trying to port my test fixtures to factory_boy. My schema includes two objects in a one-to-many relation. I.e. instances of one model have list like structures with instances of the other. Example:

class Person(...):
  id = Column(Integer, primary_key=True)
  name = Column(Text)
  [...]

class Address(...):
  id = Column(Integer, primary_key=True)
  city = Column(Text)
  [...]
  person_id = Column(Integer, ForeignKey('person.id'))
  person = relationship("Person", backref="addresses")

Now I am trying to create a factory which creates persons with a couple of addresses. Factory_boy has the SubFactory. But I only see how you can use that in a one-to-one relationship. I know I can create the addresses with a separate factory and then attach them, but I would like to do something like person =PersonFactory.create(num_addresses=4)`.

Does anyone know if this is currently possible in factory_boy?

I use factory_boy 2.4.1.

5

There are 5 answers

2
Xelnor On

You could use the solution described here: http://factoryboy.readthedocs.org/en/latest/recipes.html#reverse-dependencies-reverse-foreignkey

Basically, just declare a few RelatedFactory on your PersonFactory:

class PersonFactory(factory.alchemy.SQLAlchemyFactory):
    class Meta:
        model = Person

    address_1 = factory.RelatedFactory(AddressFactory, 'person')
    address_2 = factory.RelatedFactory(AddressFactory, 'person')
0
The Aelfinn On

Currently, there is no way to implement a "many-to-one RelatedFactory" such that it is "baked into your factory"...

That said, this behavior can be implemented with a bit of hackery when instantiating your PersonFactory.

The following recipe will get you what you are looking for:

from sqlalchemy import create_engine, Integer, Text, ForeignKey, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, scoped_session, sessionmaker
import factory
from factory.alchemy import SQLAlchemyModelFactory as sqla_factory
import random

engine = create_engine("sqlite:////tmp/factory_boy.sql")
session = scoped_session(sessionmaker(bind=engine))
Base = declarative_base()

class Person(Base):
    id = Column(Integer, primary_key=True)
    name = Column(Text)
    addresses = relationship("Address", backref="person")

class Address(Base):
    id = Column(Integer, primary_key=True)
    street = Column(Text)
    street_number = Column(Integer)
    person_id = Column(Integer, ForeignKey('person.id'))

class AddressFactory(sqla_factory):
    class Meta:
        model = Address
        sqlalchemy_session = session
    street_number = random.randint(0, 10000)
    street = "Commonwealth Ave"

class PersonFactory(sqla_factory):
    class Meta:
        model = Person
        sqlalchemy_session = session
    name = "John Doe"

Base.metadata.create_all(engine)
for i in range(100):
    person = PersonFactory(addresses=AddressFactory.create_batch(3))
2
Kristen On

I had this exact question and was disappointed in the lack of good answers here. Turns out it is possible! Leaving this here for those who have the same question.

First, your model needs to define the relationship on the opposite model from the ForeignKey, so it should look like:


class Person(...):
    id = Column(Integer, primary_key=True)
    name = Column(Text)
    addresses = relationship("Person", backref="person")
    [...]

class Address(...): id = Column(Integer, primary_key=True) city = Column(Text) [...] person_id = Column(Integer, ForeignKey('person.id'))

Then, on your PersonFactory, you can add a post_generation hook like this:


class PersonFactory(BaseFactory):
    [...attributes...]

    @factory.post_generation
    def addresses(self, create, extracted, **kwargs):
        return AddressFactory.create_batch(4)

and replace the '4' with whatever number you want. Obviously, you need to define the AddressFactory as well.

0
Danil On

@Kristen pointed to the right direction, but AdderssFactory didn't related to Person. In Django we can use post_generation decorator like this.

class PersonFactory(BaseFactory):
    @factory.post_generation
    def addresses(self, create, extracted, **kwargs):
        self.addresses_set.add(AddressFactory(person=self))
1
Sardorbek Imomaliev On

I am using this pattern in my project. Assuming you already have AddressFactory.

https://factoryboy.readthedocs.io/en/latest/reference.html?highlight=post_generation#factory.post_generation

class PersonFactory(factory.alchemy.SQLAlchemyFactory):
    class Meta:
        model = Person
    
    @factory.post_generation
    def addresses(obj, create, extracted, **kwargs):
        if not create:
            return

        if extracted:
            assert isinstance(extracted, int)
            AddressFactory.create_batch(size=extracted, person_id=obj.id, **kwargs)

Usage

PersonFactory(addresses=4)

This will create Person with 4 Addresses

Also this can accept kwargs

PersonFactory(addresses=2, addresses__city='London')

This will create Person with 2 Addresses which have city field set to 'London'

Here is blog post which may help https://simpleit.rocks/python/django/setting-up-a-factory-for-one-to-many-relationships-in-factoryboy/