Предполагая следующие модели:
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'
@IljaEverilä после того, как покопался, я вижу, что ты пытался мне показать. Исходя из Django, я ожидал определенного поведения, которое не соответствует SQL, как это делает SA. Ваш ответ был полезен, и я хотел бы выбрать его, но он был удален. Пожалуйста, сделайте репост.
@IljaEverilä - это скалярные значения, которые можно возвращать только в подзапросах?
Нет, вы можете, например, сравнить сконструированную строку с результатом подзапроса или с результатами при использовании предиката IN
. Также довольно часто используются подзапросы в предложении FROM
для создания производных таблиц.
Вы возвращаете скалярный подзапрос из своего гибридного свойства при использовании в контексте 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.
Кроме того, разве мне не пришлось бы делать filter(Worker.latest_job.started >= datetime.datetime(2017, 5, 10, 0, 2, 45, 932983))
, иначе как БД узнает, как сравнивать их по полю started
?
Вам бы не пришлось. Определение скалярного подзапроса, используемого в качестве скалярного значения, состоит в том, что это таблица из 1 строки и 1 столбца. В этом смысле SQL забавен, и я бы хотел, чтобы он не был таким уж «волшебным». Что касается проблемы корреляции, просто добавьте correlate(cls)
к конструкции select()
перед as_scalar()
.
Например, это верно: select 1 = (select 1);
. В SQL (почти) все является таблицей. На самом деле сравнение =
определяется с точки зрения строк, и когда вы говорите 1 = 1
, это на самом деле означает (1) = (1)
, или, другими словами, сравнение этой строки из 1 столбца с этой другой строкой из 1 столбца.
<Предикат сравнения> определяется в стандарте как <comparison predicate> ::= <row value predicand> <comparison predicate part 2>
, где <comparison predicate part 2> ::= <comp op> <row value predicand>
.
возможно, я неправильно понял метод as_scalar
, я должен прочитать, что именно он делает в этом запросе. Учитывая то, что я пытаюсь сделать (запрос атрибутов модели Job
через гибридное свойство latest_job
в модели Worker
), необходимо ли использовать этот метод? И в любом случае, как бы вы порекомендовали мне подойти к этому? Также приветствуются любые ссылки на определения и правильное использование as_scalar
и скалярных подзапросов.
после повторного прочтения вашего первого комментария, как SA узнает, какой столбец использовать для представления модели Job
в виде скалярного значения в этом запросе?
если предположить, что мне нужна только дата latest_job_started
, как будет выглядеть этот запрос с псевдонимом? Кроме того, при использовании «коррелированного скалярного подзапроса max(started)», почему, когда я делаю session.query(Worker.latest_job_started).all()
, он создает только одно значение, а не столбец? У меня сложилось впечатление, что эти выражения должны были возвращать столбцы.
Технически session.query(Worker.latest_job_started).all()
создает список кортежей результатов. Причина, по которой это не 1-кортеж столбца ..., заключается в том, что SQL специально обрабатывает скалярный подзапрос, как если бы это было значение.
ваш второй пример last_job.expression, возвращающий строку Job (с использованием метода select и .subquery()
в цепочке), вызывает исключение: AttributeError: 'Select' object has no attribute 'subquery'
. Каков правильный способ вернуть строку, которую я могу затем запросить атрибуты, такие как отношение внешнего ключа?
Обычно вы должны использовать подходящее соединение. Исправлена отмеченная вами мешанина стиля ORM и Core.
Если вы получаете
TypeError: '>=' not supported between instances of 'Select' and 'datetime.datetime'
, вы удалили вызовas_scalar()
.