factory_boy icon indicating copy to clipboard operation
factory_boy copied to clipboard

post_generation method not auto-committing in SQLAlchemy

Open colton-flyhomes opened this issue 2 years ago • 1 comments

Description

I am using the post_generation method to set a many-to-many relationship. I have sqlalchemy_session_persistence = 'commit', however the many-to-many relationship is not being saved to the DB.

To Reproduce

Set sqlalchemy_session_persistence = 'commit' Set up many-to-many relationship between two models Create factory for both models Create post_generation method to set many-to-many Call factory.create()

Model / Factory code
class TaskTypeFactory(BaseCustomFactory):
    class Meta:
        model = models.TaskType

    cms_slug = factory.Faker('word')
    section = factory.LazyAttribute(lambda _: random.choice(list(constants.TaskTypeSections)).value)

    @factory.post_generation
    def journey_versions(self, create, extracted, **kwargs):
        """
        Creates a JourneyVersion and associates it with the TaskType object after creation, or add a list of
        JourneyVersion objects that are passed into the factory declaration with the `journey_versions` attribute.
        Ex. TaskTypeFactory.create(journey_versions=(v1, v2, v3))
        Ref: https://factoryboy.readthedocs.io/en/stable/recipes.html#simple-many-to-many-relationship

        @param create: a boolean indicating which strategy was used -- .build() or .create()
        @type create: bool
        @param extracted: extracted is None unless a value was passed in for the PostGeneration declaration at Factory
        declaration time
        @type extracted: Any
        @param kwargs: any extra parameters passed as attr__key=value when calling the Factory
        @type kwargs: Any
        """
        if not create:
            # Simple build, do nothing.
            return

        if extracted:
            if isinstance(extracted, Iterable):
                # A list of JourneyVersion were passed in
                for journey_version in extracted:
                    self.journey_versions.append(journey_version)
            elif isinstance(extracted, models.JourneyVersion):
                # A single JourneyVersion was passed in
                self.journey_versions.append(extracted)
        else:
            # Nothing was passed in so we will just create a new JourneyVersion
            journey_version = JourneyVersionFactory.create()
            self.journey_versions.append(journey_version)

class TaskType(BaseIntPKModel):
    __tablename__ = "task_types"

    # Properties
    cms_slug = Column(String, nullable=False)
    section = Column(Enum(constants.TaskTypeSections), nullable=False, index=True)

    # Relationships
    journey_versions = relationship(
        "JourneyVersion", secondary=journey_version_task_types_table, cascade="all", back_populates="task_types",
        passive_deletes=True
    )
    triggers = relationship("Trigger", back_populates="task_type", cascade="all, delete-orphan", passive_deletes=True)
    tasks = relationship("Task", back_populates="task_type", cascade="all, delete-orphan", passive_deletes=True)

class JourneyVersion(BaseIntPKModel):
    __tablename__ = "journey_versions"

    # Properties
    name = Column(String, unique=True, nullable=False)
    deal_type = Column(Enum(constants.DealTypes), nullable=False)
    description = Column(String, nullable=True)
    language = Column(Enum(constants.Languages), default=constants.Languages.en.value, nullable=False)
    current = Column(Boolean, default=False, nullable=False)

    # Relationships
    journeys = relationship(
        "Journey", back_populates="journey_version", cascade="all, delete-orphan", passive_deletes=True
    )
    task_types = relationship(
        "TaskType", secondary=journey_version_task_types_table, cascade="all", back_populates="journey_versions",
        passive_deletes=True
    )
journey_version_task_types_table = Table('journey_version_task_types', Base.metadata,
                                         Column('journey_version_id',
                                                ForeignKey('journey_versions.id', ondelete="CASCADE"),
                                                primary_key=True),
                                         Column('task_type_id', ForeignKey('task_types.id', ondelete="CASCADE"),
                                                primary_key=True)
                                         )
The issue

Once the create method is called, it should save the model objects to the DB along with the many-to-many relationship. However, after running create, there is no DB entry in the association table between the two newly created objects.

factories.TaskTypeFactory.create()  # after this executes, the association table entry is missing
session.commit()  # after this executes, the association table entry is present

colton-flyhomes avatar May 27 '22 00:05 colton-flyhomes

I think repeating the

if session_persistence == SESSION_PERSISTENCE_FLUSH:
    session.flush()
elif session_persistence == SESSION_PERSISTENCE_COMMIT:
    session.commit()

block in the use_postgeneration_results/_after_postgeneration functions would fix this, but the challenge is being aware of whether the instance was actually created or just fetched via the _get_or_create feature. I doubt this would be possible at this height of the execution flow, but maybe unconditionally issuing the commit/flush would not be actually harmful? Do post-generation declarations run if the instance was fetched and not created? What is the outcome after a manual flush/commit in that case?

n1ngu avatar Sep 17 '22 10:09 n1ngu