factory_boy icon indicating copy to clipboard operation
factory_boy copied to clipboard

Pass sequence to SubFactory

Open mademovic opened this issue 2 years ago • 6 comments

Description

When copying a sequence to a SubFactory, the value of the field on the child factory is always 0.

To Reproduce

Model / Factory code
class PositionFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = models.Position
        sqlalchemy_session_persistence = "commit"

    position_number = factory.Sequence(lambda n: n)
    position_account = factory.SubFactory(
        "factories.PositionAccountFactory",
        position_number=factory.SelfAttribute("..position_number"),
    )


class PositionAccountFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = models.PositionAccount
        sqlalchemy_session_persistence = "commit"

    payment_amount = 150.0
    position_number = None
The issue

The sequence counter is ignored and the PositionAccount.position_number is always 0.

mademovic avatar Jul 04 '22 11:07 mademovic

Could you show how you call the PositionFactory()? Do you have specific settings or hooks on the Position model?

I can't reproduce this issue with simple factories 🤔

rbarrois avatar Jul 04 '22 12:07 rbarrois

I just call it like that: PositionFactory() I use the primaryjoin kw argument for the relationships between the tables, because the underlying tables have no foreign keys set up. Other than that, there are no specific settings.

from sqlalchemy import Column
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.mysql import SMALLINT, DECIMAL, INTEGER

class Position(Base):
    id = Column(INTEGER(11), primary_key=True)
    position_number = Column(SMALLINT(6))
    position_account = relationship(
        "PositionAccount",
        primaryjoin="foreign(Position.position_number) == PositionAccount.position_number",
    )

class PositionAccount(Base):
    id = Column(INTEGER(11), primary_key=True)
    payment_amount = Column(DECIMAL(10, 2), nullable=False)

mademovic avatar Jul 05 '22 14:07 mademovic

This should work.

class PositionFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = models.Position
        sqlalchemy_session_persistence = "commit"
        sqlalchemy_session = models.session

    position_number = factory.Sequence(lambda n: n)
    position_account = factory.SubFactory(
        PositionAccountFactory,
        position_number=factory.Sequence(lambda n: n)
    )

@rbarrois can expand on this more/ or just correct me. But I think since your relationship between Position and PositionAccount is position_number, I don't think factory.SelfAttribute(..) would ever work for you. Again, @rbarrois will give you better information.

^ Take this with a grain of salt


Changes I made, and tests:

class PositionAccount(Base):
    id = Column(INTEGER(11), primary_key=True)
    payment_amount = Column(DECIMAL(10, 2), nullable=False)
+   position_number = Column(SMALLINT(6))  # this was missing from your example, but you're referencing it

(Everything else from your example code)

class TestCase(TransactionTestCase):
    def test_sequence(self):
        position = PositionFactory()
        self.assertEqual(position.position_number, 0)
        self.assertEqual(position.position_account.position_number, 0)
        self.assertEqual(position.position_account.payment_amount, 150)

        position = PositionFactory()
        self.assertEqual(position.position_number, 1)
        self.assertEqual(position.position_account.position_number, 1)
        self.assertEqual(position.position_account.payment_amount, 150)

kingbuzzman avatar Jul 05 '22 16:07 kingbuzzman

@kingbuzzman your example will only work as long as one never calls the PositionAccountFactory() or any factory's reset_sequence, as it relies on the fact that both sequences stay in sync.

When I write the code without SQLAlchemy, I see no issue:

from dataclasses import dataclass
from decimal import Decimal

import factory

@dataclass
class PositionAccount:
  position_number: int = None
  payment_amount: Decimal = Decimal()

@dataclass
class Position:
  position_number: int
  position_account: PositionAccount

class PositionAccountFactory(factory.Factory):
  position_number = None
  payment_amount = Decimal("150.0")
  class Meta:
    model = PositionAccount

class PositionFactory(factory.Factory):
  position_number = factory.Sequence(lambda n: n)
  position_account = factory.SubFactory(
    PositionAccountFactory,
    position_number=factory.SelfAttribute("..position_number"),
  )
  class Meta:
    model = Position

With that, if I perform the following:

p1, p2 = PositionFactory.create_batch(2)
pa3 = PositionAccountFactory()  # Shift the sequence on the position account
p4 = PositionFactory(position_number=24)
p5 = PositionFactory()
assert p1.position_number == 0
assert p2.position_number == 1
assert pa3.position_number is None
assert p4.position_number == 24
assert p5.position_number == 3  # One step of the sequence was consumed by p4

for p in [p1, p2, p4, p5]:
  assert p.position_number == p.position_account.position_number

rbarrois avatar Jul 06 '22 16:07 rbarrois

Ah I see what you mean, each Factory has its own sequence and I'm incrementing them in parallel, the second they fall behind for whatever reason, this whole thing falls apart -- right? That's what you're illustrating correct? How do you make it more robust? How can you use the sequence of the parent always?

kingbuzzman avatar Jul 07 '22 13:07 kingbuzzman

Mhm, is a mimimal reproductible case available? This does work with no problems

class A(DictFactory):
    x = 0

Class B(DictFactory):
    y = Sequence(int)
    a = SubFactory(A, x=SelfAttribute("..y"))

B.build()
B.build()

is there a chance the issue is in those sqla models @mademovic ? Did you ever try the sqlalchemy_session_persistence = "flush" meta parameter?

n1ngu avatar Sep 17 '22 10:09 n1ngu