Перехват события при загрузке коллекции - PullRequest
0 голосов
/ 29 марта 2019

Допустим, у меня есть этот класс:

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_daughters или has_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 Ответ

1 голос
/ 30 марта 2019

У меня есть решение, которое я считаю эффективным и которое позволит вам легко создавать свойства только для чтения на основе комбинаций атрибутов Child, используя set в качестве отношения collection_class.

Вы можете использовать идиому Alternate Join Condition , чтобы создавать атрибуты отношений, которые являются подмножествами набора дочерних элементов Parent, например, вот отношение, которое вернет все женскиеchildren:

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

Я не буду объяснять это слишком сильно, поскольку оно очень похоже на документированные примеры, которые были связаны с выше, за исключением примечания к спецификации collection_class=set, важность которой будетстанет очевидным позже.

Я продолжал создавать похожие отношения для каждой возможной категоризации 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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...