original_query
- это просто объект API запроса SQLAlchemy , к нему можно применить дополнительные фильтры и критерии.API запроса генеративный ;каждая Query()
операция экземпляра возвращает новый (неизменяемый) экземпляр, и ваша начальная точка (original_query
) не изменяется.
Это включает использование Query.distinct()
для добавления предложения DISTINCT()
, Query.with_entities()
для изменения того, какие столбцы являются частью запроса, и Query.values()
, чтобы выполнить ваш запрос, но вернуть только определенные значения в одном столбце.
Используйте либо .distinct(<column>).with_entities(<column>)
для создания нового объекта запроса (который может быть использован повторно):
another_query = original_query.distinct(SomeTable.column).with_entities(SomeTable.column)
или просто используйте .distinct(<column>).values(<column>)
, чтобы сразу получить итератор (column_value,)
результатов кортежа:
distinct_values = original_query.distinct(SomeTable.column).values(SomeTable.column)
Обратите внимание, что .values()
выполняет запрос немедленно, как .all()
, а.with_entities()
возвращает вам новый Query
объект только с одним столбцом (и тогда .all()
или итерация или нарезка будут выполнять и возвращать результаты).
Демонстрация с использованием надуманной Foo
модели(выполняется для sqlite, чтобы упростить демонстрацию):
>>> from sqlalchemy import *
>>> from sqlalchemy.ext.declarative import declarative_base
>>> from sqlalchemy.orm import sessionmaker
>>> Base = declarative_base()
>>> class Foo(Base):
... __tablename__ = "foo"
... id = Column(Integer, primary_key=True)
... bar = Column(String)
... spam = Column(String)
...
>>> engine = create_engine('sqlite:///:memory:', echo=True)
>>> session = sessionmaker(bind=engine)()
>>> Base.metadata.create_all(engine)
2019-06-10 13:10:43,910 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("foo")
2019-06-10 13:10:43,910 INFO sqlalchemy.engine.base.Engine ()
2019-06-10 13:10:43,911 INFO sqlalchemy.engine.base.Engine
CREATE TABLE foo (
id INTEGER NOT NULL,
bar VARCHAR,
spam VARCHAR,
PRIMARY KEY (id)
)
2019-06-10 13:10:43,911 INFO sqlalchemy.engine.base.Engine ()
2019-06-10 13:10:43,913 INFO sqlalchemy.engine.base.Engine COMMIT
>>> original_query = session.query(Foo).filter(Foo.id.between(17, 42))
>>> print(original_query) # show what SQL would be executed for this query
SELECT foo.id AS foo_id, foo.bar AS foo_bar, foo.spam AS foo_spam
FROM foo
WHERE foo.id BETWEEN ? AND ?
>>> another_query = original_query.distinct(Foo.bar).with_entities(Foo.bar)
>>> print(another_query) # print the SQL again, don't execute
SELECT DISTINCT foo.bar AS foo_bar
FROM foo
WHERE foo.id BETWEEN ? AND ?
>>> distinct_values = original_query.distinct(Foo.bar).values(Foo.bar) # executes!
2019-06-10 13:10:48,470 INFO sqlalchemy.engine.base.Engine SELECT DISTINCT foo.bar AS foo_bar
FROM foo
WHERE foo.id BETWEEN ? AND ?
2019-06-10 13:10:48,470 INFO sqlalchemy.engine.base.Engine (17, 42)
В вышеприведенной демонстрации исходный запрос выбрал бы определенные Foo
экземпляра с фильтром BETWEEN
, но добавив .distinct(Foo.bar).values(Foo.bar)
, затемвыполняет запрос для просто столбец DISTINCT foo.bar
, но с тем же BETWEEN
фильтр на месте.Точно так же, используя .with_entities()
, мы получили новый объект запроса только для этого одного столбца, но фильтр все еще является частью этого нового запроса.
Ваш добавленный пример работает точно так же;вам на самом деле не нужно иметь дополнительный выбор там, так как тот же запрос может быть выражен как:
SELECT sum(tab.value)
FROM tab
WHERE tab.product_id IN (1, 2) AND tab_key = 'length';
, что можно сделать, просто добавив дополнительные фильтры, а затем используйте .with_entities()
для заменыстолбцы, выбранные с помощью SUM()
:
summed_query = (
original_query
.filter(Tab.key == 'length') # add a filter
.with_entities(func.sum(Tab.value)
или, с точки зрения вышеприведенной демонстрации Foo
:
>>> print(original_query.filter(Foo.spam == 42).with_entities(func.sum(Foo.bar)))
SELECT sum(foo.bar) AS sum_1
FROM foo
WHERE foo.id BETWEEN ? AND ? AND foo.spam = ?
Существуют варианты использования для подзапросов (например,ограничение результатов из определенной таблицы в объединении), но это не один из них.
Если вам нужен подзапрос, тогда API запроса имеет Query.from_self()
(дляболее простые случаи) и Query.subselect()
.
Например, если вам нужно было выбрать только агрегированные строки из исходного запроса и отфильтровать агрегированные значения с помощью HAVING
, а затем присоединиться крезультаты с другой таблицей для самого высокого идентификатора строки для каждой группы и некоторой дальнейшей фильтрации, а затем вам нужен подзапрос:
summed_col = func.sum(SomeTable.some_column)
max_id = func.max(SomeTable.primary_key)
summed_results_by_eggs = (
original_query
.with_entities(max_id, summed_col) # only select highest id and the sum
.group_by(SomeTable.other_column) # per group
.having(summed_col > 10) # where the sum is high enough
.from_self(summed_col) # give us the summed value as a subselect
.join( # join these rows with another table
OtherTable,
OtherTable.foreign_key == max_id # using the highest id
)
.filter(OtherTable.some_column < 1000) # and filter some more
)
Приведенное выше выберет только суммированные значения SomeTable.some_column
, где это значение больше 10и где самое высокое значение SomeTable.id
в каждой группе.Этот запрос имеет для использования подзапроса, потому что вы хотите ограничить допустимые строки SomeTable
перед объединением с другой таблицей.
Чтобы продемонстрировать это, я добавил вторую таблицу Eggs
:
>>> from sqlalchemy.orm import relationship
>>> class Eggs(Base):
... __tablename__ = "eggs"
... id = Column(Integer, primary_key=True)
... foo_id = Column(Integer, ForeignKey(Foo.id))
... foo = relationship(Foo, backref="eggs")
...
>>> summed_col = func.sum(Foo.bar)
>>> max_id = func.max(Foo.id)
>>> print(
... original_query
... .with_entities(max_id, summed_col)
... .group_by(Foo.spam)
... .having(summed_col > 10)
... .from_self(summed_col)
... .join(Eggs, Eggs.foo_id==max_id)
... .filter(Eggs.id < 1000)
... )
SELECT anon_1.sum_2 AS sum_1
FROM (SELECT max(foo.id) AS max_1, sum(foo.bar) AS sum_2
FROM foo
WHERE foo.id BETWEEN ? AND ? GROUP BY foo.spam
HAVING sum(foo.bar) > ?) AS anon_1 JOIN eggs ON eggs.foo_id = anon_1.max_1
WHERE eggs.id < ?
Метод Query.from_self()
принимает новые сущности для использования во внешнем запросе, если вы их опускаете, все столбцы извлекаются.Выше я вытащил суммированное значение столбца;без этого аргумента будет также выбран столбец MAX(Foo.id)
.