Выражение гибридного_атрибута sqlalchemy

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

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'

Если вы получаете TypeError: '>=' not supported between instances of 'Select' and 'datetime.datetime', вы удалили вызов as_scalar().

Ilja Everilä 11.04.2019 21:33

@IljaEverilä после того, как покопался, я вижу, что ты пытался мне показать. Исходя из Django, я ожидал определенного поведения, которое не соответствует SQL, как это делает SA. Ваш ответ был полезен, и я хотел бы выбрать его, но он был удален. Пожалуйста, сделайте репост.

Verbal_Kint 11.04.2019 21:37

@IljaEverilä - это скалярные значения, которые можно возвращать только в подзапросах?

Verbal_Kint 11.04.2019 21:39

Нет, вы можете, например, сравнить сконструированную строку с результатом подзапроса или с результатами при использовании предиката IN. Также довольно часто используются подзапросы в предложении FROM для создания производных таблиц.

Ilja Everilä 11.04.2019 21:43
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
4
1 050
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы возвращаете скалярный подзапрос из своего гибридного свойства при использовании в контексте 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 Job.query.\
        filter(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(), а просто возвращает первое значение первой строки.

Выдает ошибку: InvalidRequestError: Select statement 'SELECT max(jobs.started) AS max_1 FROM jobs, workers WHERE workers.id = jobs.worker_id' returned no FROM clauses due to auto-correlation; specify correlate(<tables>) to control correlation manually.

Verbal_Kint 11.04.2019 15:02

Кроме того, разве мне не пришлось бы делать filter(Worker.latest_job.started >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983)), иначе как БД узнает, как сравнивать их по полю started?

Verbal_Kint 11.04.2019 15:04

Вам бы не пришлось. Определение скалярного подзапроса, используемого в качестве скалярного значения, состоит в том, что это таблица из 1 строки и 1 столбца. В этом смысле SQL забавен, и я бы хотел, чтобы он не был таким уж «волшебным». Что касается проблемы корреляции, просто добавьте correlate(cls) к конструкции select() перед as_scalar().

Ilja Everilä 11.04.2019 15:39

Например, это верно: select 1 = (select 1);. В SQL (почти) все является таблицей. На самом деле сравнение = определяется с точки зрения строк, и когда вы говорите 1 = 1, это на самом деле означает (1) = (1), или, другими словами, сравнение этой строки из 1 столбца с этой другой строкой из 1 столбца.

Ilja Everilä 11.04.2019 15:50

<Предикат сравнения> определяется в стандарте как <comparison predicate> ::= <row value predicand> <comparison predicate part 2>, где <comparison predicate part 2> ::= <comp op> <row value predicand>.

Ilja Everilä 11.04.2019 15:54

возможно, я неправильно понял метод as_scalar, я должен прочитать, что именно он делает в этом запросе. Учитывая то, что я пытаюсь сделать (запрос атрибутов модели Job через гибридное свойство latest_job в модели Worker), необходимо ли использовать этот метод? И в любом случае, как бы вы порекомендовали мне подойти к этому? Также приветствуются любые ссылки на определения и правильное использование as_scalar и скалярных подзапросов.

Verbal_Kint 11.04.2019 19:30

после повторного прочтения вашего первого комментария, как SA узнает, какой столбец использовать для представления модели Job в виде скалярного значения в этом запросе?

Verbal_Kint 11.04.2019 19:53

если предположить, что мне нужна только дата latest_job_started, как будет выглядеть этот запрос с псевдонимом? Кроме того, при использовании «коррелированного скалярного подзапроса max(started)», почему, когда я делаю session.query(Worker.latest_job_started).all(), он создает только одно значение, а не столбец? У меня сложилось впечатление, что эти выражения должны были возвращать столбцы.

Verbal_Kint 20.04.2019 23:01

Технически session.query(Worker.latest_job_started).all() создает список кортежей результатов. Причина, по которой это не 1-кортеж столбца ..., заключается в том, что SQL специально обрабатывает скалярный подзапрос, как если бы это было значение.

Ilja Everilä 21.04.2019 09:04

ваш второй пример last_job.expression, возвращающий строку Job (с использованием метода select и .subquery() в цепочке), вызывает исключение: AttributeError: 'Select' object has no attribute 'subquery'. Каков правильный способ вернуть строку, которую я могу затем запросить атрибуты, такие как отношение внешнего ключа?

Verbal_Kint 14.10.2019 16:18

Обычно вы должны использовать подходящее соединение. Исправлена ​​отмеченная вами мешанина стиля ORM и Core.

Ilja Everilä 14.10.2019 16:45

Другие вопросы по теме