Выражение sqlalchemy hybrid_attribute - PullRequest
0 голосов
/ 11 апреля 2019

Предполагается, что следующие модели:

class Worker(Model):
    __tablename__ = 'workers'
    ...
    jobs = relationship('Job',
                        back_populates='worker',
                        order_by='desc(Job.started)',
                        lazy='dynamic')

    @hybrid_property
    def latest_job(self):
        return self.jobs.first()  # jobs already ordered descending

    @latest_job.expression
    def latest_job(cls):
        Job = db.Model._decl_class_registry.get('Job')
        return select([func.max(Job.started)]).where(cls.id == Job.worker_id).as_scalar()

class Job(Model):
    ...
    started = db.Column(db.DateTime, default=datetime.utcnow)
    worker_id = db.Column(db.Integer, db.ForeignKey('workers.id'))
    worker = db.relationship('Worker', back_populates='jobs')

Хотя этот запрос дает правильные результаты:

db.session.query(Worker).join(Job.started).filter(Job.started >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).distinct().count()

Я предполагал, что могу запросить это поле напрямую, но этот запрос не выполняется:

db.session.query(Worker).join(Job).filter(Worker.latest_job.started >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).count()

с этой ошибкой:

AttributeError: Neither 'hybrid_property' object nor 'ExprComparator' object associated with Worker.latest_job has an attribute 'started'

Как я могу напрямую запросить это свойство?Что мне здесь не хватает?

РЕДАКТИРОВАТЬ 1: Следуя совету @Ilja из его ответа, я попытался:

db.session.query(Worker).\
    join(Job).\
    filter(Worker.latest_job >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).\
    count()

, но получил эту ошибку:

TypeError: '>=' not supported between instances of 'Select' and 'datetime.datetime'

1 Ответ

1 голос
/ 11 апреля 2019

Вы возвращаете скалярный подзапрос из своего гибридного свойства при использовании в контексте SQL (класса), поэтому просто используйте его, как если бы вы использовали выражение значения:

db.session.query(Worker).\
    filter(Worker.latest_job >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).\
    count()

Требуется само гибридное свойстводля явной обработки корреляции в этом случае:

@latest_job.expression
def latest_job(cls):
    Job = db.Model._decl_class_registry.get('Job')
    return select([func.max(Job.started)]).\
        where(cls.id == Job.worker_id).\
        correlate(cls).\
        as_scalar()

Обратите внимание, что существует некоторая асимметрия между стороной Python гибридного свойства и стороной SQL.Он генерирует последний объект Job при обращении к экземпляру по сравнению с созданием коррелированного скалярного подзапроса max(started) в SQL.Если вы хотите, чтобы он также возвращал строку Job в SQL, вы должны сделать что-то вроде

@latest_job.expression
def latest_job(cls):
    Job = db.Model._decl_class_registry.get('Job')
    return select([Job]).\
        where(cls.id == Job.worker_id).\
        order_by(Job.started.desc()).\
        limit(1).\
        correlate(cls).\
        subquery()

, но на самом деле это менее полезно, потому что обычно - но не всегда - такого родакоррелированный подзапрос будет медленнее, чем объединение с подзапросом.Например, чтобы получить работников с последними заданиями, которые соответствуют исходным критериям:

job_alias = db.aliased(Job)
# This reads as: find worker_id and started of jobs that have no matching
# jobs with the same worker_id and greater started, or in other words the
# worker_id, started of the latest jobs.
latest_jobs = db.session.query(Job.worker_id, Job.started).\
    outerjoin(job_alias, and_(Job.worker_id == job_alias.worker_id,
                              Job.started < job_alias.started)).\
    filter(job_alias.id == None).\
    subquery()

db.session.query(Worker).\
    join(latest_jobs, Worker.id == latest_jobs.c.worker_id).\
    filter(latest_jobs.c.started >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).\
    count()

и, конечно, если вы просто хотите подсчитать, то объединение вообще не нужно:

job_alias = db.aliased(Job)
db.session.query(func.count()).\
    outerjoin(job_alias, and_(Job.worker_id == job_alias.worker_id,
                              Job.started < job_alias.started)).\
    filter(job_alias.id == None,
           Job.started >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)).\
    scalar()

Обратите внимание, что вызов Query.scalar() не совпадает с Query.as_scalar(), но просто возвращает первое значение первой строки.

...