【问题标题】:One to many relation with Factory Boy与工厂男孩的一对多关系
【发布时间】:2019-08-15 06:47:00
【问题描述】:

我的 SQLAlchemy 模型中有一个多对一的关系。一份报告有许多样本(为简洁起见):

class Sample(db.Model, CRUDMixin):
    sample_id = Column(Integer, primary_key=True)
    report_id = Column(Integer, ForeignKey('report.report_id', ondelete='CASCADE'), index=True, nullable=False)
    report = relationship('Report', back_populates='samples')

class Report(db.Model, CRUDMixin):
    report_id = Column(Integer, primary_key=True)
    samples = relationship('Sample', back_populates='report')

现在在我的测试中,我希望能够生成Sample 实例或Report 实例,并填写缺失的关系。

class ReportFactory(BaseFactory):
    class Meta:
        model = models.Report
    report_id = Faker('pyint')
    samples = RelatedFactoryList('tests.factories.SampleFactory', size=3)

class SampleFactory(BaseFactory):
    class Meta:
        model = models.Sample
    sample_id = Faker('pyint')
    report = SubFactory(ReportFactory)

当我去创建这些的实例时,工厂陷入了无限循环:

RecursionError: maximum recursion depth exceeded in comparison

但是,如果我尝试使用SelfAttributes 来停止无限循环,我最终会得到一个没有任何样本的报告:

class ReportFactory(BaseFactory):
    samples = RelatedFactoryList('tests.factories.SampleFactory', size=3, report_id=SelfAttribute('..report_id'))

class SampleFactory(BaseFactory):
    report = SubFactory(ReportFactory, samples=[])
report = factories.ReportFactory()
l = len(report.samples) # 0

但是,如果我用SampleFactory() 生成Sample,它正确地有一个Report 对象。

我应该如何正确设计我的工厂,使SampleFactory() 将生成一个Sample 与关联的Report,而ReportFactory() 将生成一个Report 与2 个关联的Samples,没有无限循环?

【问题讨论】:

    标签: python sqlalchemy factory-boy


    【解决方案1】:

    我的最终解决方案实际上比我想象的要简单得多:

    class ReportFactory(BaseFactory):
        class Meta:
            model = models.Report
    
        samples = RelatedFactoryList('tests.factories.SampleFactory', 'report', size=3)
    
    
    class SampleFactory(BaseFactory):
        class Meta:
            model = models.Sample
    
        report = SubFactory(ReportFactory, samples=[])
    

    关键是使用RelatedFactoryList 的第二个参数,它必须对应于子节点上的父链接,在本例中为'report'。另外,我使用了SubFactory(ReportFactory, samples=[]),这样可以确保在我构建单个样本时不会在父节点上创建额外的样本。

    通过此设置,我可以构建一个与Report 关联的样本,并且该报告只有一个子Sample。相反,我可以构造一个Report,它将自动填充 3 个子样本。

    我认为没有必要生成实际的模型 ID,因为一旦模型实际插入数据库,SQLAlchemy 就会自动生成。但是,如果您想在不使用数据库的情况下这样做,我认为@Xelnor 的report_id = factory.SelfAttribute('report.id') 解决方案将起作用。

    我遇到的唯一未解决的问题是覆盖报告上的样本列表(例如ReportFactory(samples = [SampleFactory()])),但我已经打开了一个记录此错误的问题:https://github.com/FactoryBoy/factory_boy/issues/636

    【讨论】:

    • 这也行得通,但这只是因为你的 ORM 的底层特性:当你阅读 report.samples 时,SQLAlchemy 将动态获取数据库(或会话)中指向的 Sample 对象列表那个特定的Report。如果您不使用 ORM,则必须手动链接它们。
    • 感谢您的澄清。不过,我确实在问题中提到了 SQLAlchemy。
    【解决方案2】:

    创建实例后评估RelatedFactory 声明:

    1. Report 已实例化
    2. 执行了 3 次对 SampleFactory 的调用
    3. 返回步骤 1 中实例化的 Report

    为了填充 Report 实例上的字段,您必须在第 2 步将Sample 实例链接Report

    一个可能的实现是:

    class SampleFactory(BaseFactory):
        class Meta:
            model = Sample
    
        @classmethod
        def _after_postgeneration(cls, instance, create, results=None):
            if instance.report is not None and instance not in instance.report.samples:
                instance.report.samples.append(instance)
    
        id = factory.Faker('pyint')
        # Enfore `post_samples = None` to prevent creating additional samples
        report = factory.SubFactory('example.ReportFactory', samples=[], post_samples=None)
        report_id = factory.SelfAttribute('report.id')
    
    class ReportFactory(factory.Factory):
        class Meta:
            model = Report
    
        id = factory.Faker('pyint')
        # Set samples = [] if needed by `Report.__init__`
        samples = []
        # Named `post_samples` to mark that they are instantiated
        # *after* the `Report` is ready (and never passed to the `samples` kwarg)
        post_samples = factory.RelatedFactoryList(SampleFactory, 'report', size=3)
    

    使用该代码,当您致电 ReportFactory 时,您:

    1. 在没有任何样本的情况下生成Report
    2. 生成 3 个样本,将参考传递给刚刚生成的报告
    3. 在创建时,那些Sample 实例将自己附加到Report.samples

    【讨论】:

    • 但是既然你把RelatedFactoryList命名为post_samples,那生成的Report不会没有Samples吗?
    • 否,因为我们在 _after_postgeneration 挂钩中手动将每个 Sample 附加到其 Report
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多