factory_boy
factory_boy copied to clipboard
Pass sequence to SubFactory
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.
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 🤔
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)
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 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
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?
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?