简体   繁体   English

与工厂男孩一对多的关系

[英]One to many relation with Factory Boy

I have a many-to-one relationship in my SQLAlchemy models. 我的SQLAlchemy模型中存在多对一关系。 One report has many samples (simplified for brevity): 一份报告包含许多示例(为简便起见,已简化):

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')

Now in my tests, I want to be able to generate a Sample instance, or a Report instance, and fill in the missing relationships. 现在在测试中,我希望能够生成一个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)

When I go to create an instance of these, the factories get stuck in an infinite loop: 当我创建这些实例时,工厂陷入无限循环:

RecursionError: maximum recursion depth exceeded in comparison

However, if I try to use SelfAttribute s to stop the infinite loop, I end up with a report without any samples: 但是,如果尝试使用SelfAttribute停止无限循环,则最终得到的报告将不包含任何样本:

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

However, if I generate a Sample with SampleFactory() , it correctly has a Report object. 但是,如果我用SampleFactory()生成一个Sample ,则它正确地具有一个Report对象。

How should I correctly design my factories such that SampleFactory() will generate a Sample with associated Report , and ReportFactory() will generate a Report with 2 associated Samples , without infinite loops? 如何正确设计工厂,使SampleFactory()生成带有关联ReportSample ,而ReportFactory()生成带有2个关联SamplesReport ,没有无限循环?

The RelatedFactory declaration is evaluated once the instance has been created: 创建实例后, RelatedFactory声明进行评估:

  1. The Report is instantiated Report已实例化
  2. 3 calls to SampleFactory are performed 进行了3次对SampleFactory调用
  3. The Report instantiated in step 1 is returned 返回在步骤1中实例化的Report

In order to populate the field on the Report instances, you have to link the Sample instances to the Report at step 2. 为了填充Report实例上的字段,您必须在步骤2将Sample实例链接Report

A possible implementation would be: 可能的实现方式是:

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)

With that code, when you call ReportFactory , you: 使用该代码,当您调用ReportFactory ,您:

  1. Generate a Report without any samples 生成没有任何样本的Report
  2. Generate 3 samples, passing them a reference to the just-generated report 生成3个样本,并将它们传递给刚刚生成的报告的参考
  3. Upon creation, those Sample instances attach themselves to Report.samples 创建后,这些Sample实例将自己附加到Report.samples

My final solution was actually a lot simpler than I thought: 我的最终解决方案实际上比我想象的要简单得多:

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=[])

The key thing was using the second argument to RelatedFactoryList , which has to correspond to the parent link on the child , in this case 'report' . 关键是使用RelatedFactoryList的第二个参数,该参数必须与子级上的父级链接相对应 ,在本例中为'report' In addition, I used SubFactory(ReportFactory, samples=[]) , which ensures that no extra samples are created on the parent if I construct a single sample. 另外,我使用了SubFactory(ReportFactory, samples=[]) ,这可以确保在构造单个样本时不会在父项上创建任何额外的样本。

With this setup, I can construct a sample that will have a Report associated with it, and that report only has 1 child Sample . 通过此设置,我可以构造一个样本,该样本将具有与之关联的Report ,并且该报告只有1个子Sample Conversely, I can construct a Report that will automatically be populated with 3 child Samples. 相反,我可以构建一个Report ,该Report将自动填充3个子样本。

I don't think there's any need to generate the actual model IDs, because SQLAlchemy will do that automatically once the models are actually inserted into the database. 我认为不需要生成实际的模型ID,因为一旦将模型实际插入数据库中,SQLAlchemy就会自动执行。 However, if you want to do that without using the database, I think @Xelnor's solution of report_id = factory.SelfAttribute('report.id') will work. 但是,如果您想在不使用数据库的情况下执行此操作,我认为@Xelnor的report_id = factory.SelfAttribute('report.id')解决方案将起作用。

The only outstanding issue I have is with overriding the list of samples on the Report (eg ReportFactory(samples = [SampleFactory()]) ), but I've opened an issue documenting this bug: https://github.com/FactoryBoy/factory_boy/issues/636 我唯一的未解决的问题是覆盖Report上的样本列表(例如ReportFactory(samples = [SampleFactory()]) ),但是我打开了一个记录此bug的问题: https : //github.com/FactoryBoy / factory_boy /问题/ 636

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM