收集负载时捕获事件

时间:2019-03-29 15:07:18

标签: python sqlalchemy

让我们说我上了这个课:

class Parent(Base):
    children = relationship(Child)

class Child(Base):
    parent_id = Column(Integer, ForeignKey('parent.id'))
    sex = Column(String)
    is_married = Column(Boolean)
    is_working = Column(Boolean)

我想要一些要计算的只读属性,例如has_single_daughtershas_working_sons。我可以这样写:

class Parent(Base):
    children = relationship(Child)

    def has_working_sons(self):
        for child in children:
            if child.is_working:
                return True
        return False

但是,如果我们查询很多父母,每个父母都有几个孩子,那么想象一下性能会受到影响。我想缓存一次计算它的每个属性,就像这样:

class Parent(Base):
    children = relationship(Child)

    def on_load_or_update_children_collection(self):
        for child in self.children:
            if child.is_working:
                if child.sex == 'M':
                    self.has_working_sons = True
            # ... and so on and so forth

在这种情况下,on_load_children应该链接到收集事件,但是我没有发现任何有效的方法。我热切地在某些查询中使用joinedload来加载相关属性。

我可以使用任何事件或其他方法吗?

1 个答案:

答案 0 :(得分:1)

我有一个解决方案,我相信它是有效的,并且可以通过使用Child作为关系的collection_class来基于set属性的组合轻松创建只读属性。

您可以使用Alternate Join Condition惯用语来创建作为Parent子集的子集的关系属性,例如,下面的关系将返回所有女子: / p>

female_children = sa.orm.relationship(
    'Child', collection_class=set,
    primaryjoin='and_(Parent.id == Child.parent_id, Child.sex == "female")'
)

除了要注意collection_class=set的规范之外,我将不做过多解释,因为它与上面链接的已记录示例非常相似。 / p>

我继续为Child(所有collection_class=set)的每种可能分类创建类似的关系:

class Parent(Base):
    id = sa.Column(sa.Integer, primary_key=True)

    children = sa.orm.relationship('Child', collection_class=set)
    male_children = sa.orm.relationship(
        'Child', collection_class=set,
        primaryjoin='and_(Parent.id == Child.parent_id, Child.sex == "male")'
    )
    female_children = sa.orm.relationship(
        'Child', collection_class=set,
        primaryjoin='and_(Parent.id == Child.parent_id, Child.sex == "female")'
    )
    working_children = sa.orm.relationship(
        'Child', collection_class=set,
        primaryjoin='and_(Parent.id == Child.parent_id, Child.is_working == True)'
    )
    married_children = sa.orm.relationship(
        'Child', collection_class=set,
        primaryjoin='and_(Parent.id == Child.parent_id, Child.is_married == True)'
    )

子模型是您定义的(我正在使用MySQL的字符串长度除外):

class Child(Base):
    id = sa.Column(sa.Integer, primary_key=True)
    parent_id = sa.Column(sa.Integer, sa.ForeignKey('parent.id'))
    sex = sa.Column(sa.String(6))
    is_married = sa.Column(sa.Boolean)
    is_working = sa.Column(sa.Boolean)

以下是一些测试数据:

s = Session()
parent = Parent()
s.add(parent)
parent.children.update([
    Child(sex='male', is_married=True, is_working=False),
    Child(sex='female', is_married=True, is_working=True),
    Child(sex='male', is_married=False, is_working=True)
])
s.commit()

查询Parent时,joinedload的所有关系都使您只需要一次往返数据库(如您所说的效率问题,尽管这并非绝对必要,延迟加载也可以正常工作):

parent = s.query(Parent).options(sa.orm.joinedload('*')).first()
print(parent.children)
# InstrumentedSet({Child(is_married=False, id=3, is_working=True, sex=male, parent_id=1), Child(is_married=True, id=2, is_working=True, sex=female, parent_id=1), Child(is_married=True, id=1, is_working=False, sex=male, parent_id=1)})
print(parent.female_children)
# InstrumentedSet({Child(is_married=True, id=2, is_working=True, sex=female, parent_id=1)})
print(parent.male_children)
# InstrumentedSet({Child(is_married=False, id=3, is_working=True, sex=male, parent_id=1), Child(is_married=True, id=1, is_working=False, sex=male, parent_id=1)})
print(parent.married_children)
# InstrumentedSet({Child(is_married=True, id=2, is_working=True, sex=female, parent_id=1), Child(is_married=True, id=1, is_working=False, sex=male, parent_id=1)})

由于Parent上的关系属性均为sets,因此可以对其进行非常有效的操作。例如,这里是一个正在工作的男孩的隔离:

print(parent.working_children & parent.male_children)
# {Child(is_married=False, id=3, is_working=True, sex=male, parent_id=1)}

然后,您可以在Parent模型上定义所需的任何只读属性,下面是一些示例:

class Parent(Base):

    ### excluded all of the columns and relationships for brevity ###

    @property
    def working_sons(self):
        return self.working_children & self.male_children

    @property
    def working_daughters(self):
        return self.working_children & self.female_children

    @property
    def unmarried_children(self):
        return self.children - self.married_children

    @property
    def married_unemployed_sons(self):
        return (
            self.male_children - self.working_children & self.married_children)

print(parent.working_sons)
# {Child(is_married=False, id=3, is_working=True, sex=male, parent_id=1)}
print(parent.working_daughters)
# {Child(is_married=True, id=2, is_working=True, sex=female, parent_id=1)}
print(parent.unmarried_children)
# {Child(is_married=False, id=3, is_working=True, sex=male, parent_id=1)}
print(parent.married_unemployed_sons)
# {Child(is_married=True, id=1, is_working=False, sex=male, parent_id=1)}

由于这些都是已设置的操作,因此它们非常有效,但是如果需要,您可以在首次访问时将其结果缓存。

您的问题专门引用了bool属性has_working_sons,因此在测试Parent.working_sons是否有工作儿子时,您可以使用Parent属性的真实性(例如if parent.working_sons: ...),或者如果确实需要将其用作bool(Parent.working_sons) / True,则使用False