FactoryBoy / factory_boy

A test fixtures replacement for Python
https://factoryboy.readthedocs.io/
MIT License
3.48k stars 392 forks source link

Question: Appending child in post_generation doesn't create many to many relationship record in association table. #885

Open mockingjet opened 2 years ago

mockingjet commented 2 years ago
# models.py
assoc_managers_roles = sa.Table(
    "assoc_managers_roles",
    Base.metadata,
    sa.Column(
        "manager_id",
        sa.Integer,
        sa.ForeignKey("managers.id", ondelete="CASCADE"),
        primary_key=True,
    ),
    sa.Column(
        "role_id",
        sa.Integer,
        sa.ForeignKey("roles.id", ondelete="CASCADE"),
        primary_key=True,
    ),
)

class Manager(Base, TimestampMixin):
    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)

    username = sa.Column(sa.String, nullable=False, unique=True)
    hashed_password = sa.Column(sa.String, nullable=False)
    enabled = sa.Column(sa.Boolean, default=True)
    roles = sa.orm.relationship(
        "Role",
        secondary=assoc_managers_roles,
        back_populates="managers",
    )

class Role(Base, TimestampMixin):
    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)

    name = sa.Column(sa.String, nullable=False, unique=True)
    enabled = sa.Column(sa.Boolean, default=True)
    managers = sa.orm.relationship(
        "Manager",
        secondary=assoc_managers_roles,
        back_populates="roles",
    )
# seeder.py

class BaseFactory(factory.alchemy.SQLAlchemyModelFactory):
    """Base Factory."""

    class Meta:
        sqlalchemy_session = sync_session()
        sqlalchemy_session_persistence = "commit"

class RoleFactory(BaseFactory):
    class Meta:
        model = Role
        sqlalchemy_get_or_create = {"name"}

    name: str

class ManagerFactory(BaseFactory):
    class Meta:
        model = Manager
        sqlalchemy_get_or_create = {"username"}

    username = settings.SUPER_USER_NAME
    hashed_password = hash_password(settings.SUPER_USER_PASSWORD)

    @factory.post_generation
    def roles(self, create, extracted, **kwargs):
        if create and extracted:
            for role in extracted:
                self.roles.append(role)

def create_first_user():
    superuser = RoleFactory.create(name="superuser")
    first_manager = ManagerFactory.create(
        username=settings.SUPER_USER_NAME,
        hashed_password=hash_password(settings.SUPER_USER_PASSWORD),
        roles=[superuser],
    )

    print(superuser.managers[0] is not None)  # True
    print(first_manager.roles[0] is not None)  # True

Although both object has relationship in orm, no real record created in table "assoc_managers_roles" How to create real many to many relationship with factory_boy ?

mockingjet commented 2 years ago

I found that although I set sqlalchemy_session_persistence = "commit" in my BaseFactory, factory_boy doesn't commit changes after executing post_generation wrapped method. I have to commit changes in the end by myself.

@rbarrois Is not committing changes in @post_generaion intended?

I inspect the code and see that factory_boy working with django has this

# factory/django.py 
# line: 168

@classmethod
def _after_postgeneration(cls, instance, create, results=None):
    """Save again the instance if creating and at least one hook ran."""
    if create and results:
        # Some post-generation hooks ran, and may have modified us.
        instance.save()

On the other hand, I don't see the similar implementation in factory/alchemy.py.

mockingjet commented 2 days ago

Hello @rbarrois. after 3 years I still find this occur. Is it intentional to let developers explicitly commit postgeneration by themselves?