15

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.

NiklasMM
  • 2,895
  • 2
  • 22
  • 26

5 Answers5

12

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/

Sardorbek Imomaliev
  • 14,861
  • 2
  • 51
  • 63
1

@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))
Sardorbek Imomaliev
  • 14,861
  • 2
  • 51
  • 63
Danil
  • 4,781
  • 1
  • 35
  • 50
0

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.
Kristen
  • 377
  • 1
  • 3
  • 9
  • 2
    This simply does not work. Could you edit the answer with your *full* `AddressFactory` and `PersonFactory` classes if you believe that it works? – The Aelfinn May 09 '17 at 15:05
  • 1
    Why do you downvote this post? it works. @Kristen point to the right direction. This goal can be achieved with a `post_generation` decorator https://factoryboy.readthedocs.io/en/latest/reference.html?highlight=post_generation#factory.post_generation – Danil Jul 03 '18 at 08:00
0

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))
The Aelfinn
  • 13,649
  • 2
  • 54
  • 45
-2

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')
Xelnor
  • 3,194
  • 12
  • 14
  • But this offers no flexibility right? All persons will have two addresses (or however many I define), correct? – NiklasMM Nov 25 '14 at 14:02
  • 1
    @Xelnor this missing the point of a many-to-one relation. The above answer does not scale to support "n" addresses per person. – The Aelfinn May 09 '17 at 15:55