Перейти к содержанию

212 уроков, 15 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 7 из 20 Средний 40 мин 140 XP

Фильтрация: where и операторы

where превращает выборку всего подряд в точный ответ: сравнения, in_, like, цепочки условий как И и or_ как ИЛИ.

Редакция Питоники

Запрос из прошлого урока возвращал всё подряд — все задачи, все строки. Реальные вопросы всегда уже: какие дела ещё не сделаны? что с приоритетом повыше? какие задачи про покупки? Ответ даёт where — условие, которое база проверяет для каждой строки. И самое приятное: условие записывается почти как обычный Python, только вместо строк SQL — выражения над колонками.

Как и в разделе про SQL, договоримся об учебной базе: пять задач с флажками и приоритетами. Первый блок собирает её и печатает целиком — эталон, с которым будем сверять каждый фильтр.

учебная база: пять задач
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Task(Base):
    __tablename__ = "tasks"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String)
    done: Mapped[bool] = mapped_column(default=False)
    priority: Mapped[int] = mapped_column(default=1)

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add_all([
        Task(title="вынести мусор", priority=2),
        Task(title="купить хлеб", priority=1),
        Task(title="купить молоко", priority=1),
        Task(title="полить цветы", priority=3, done=True),
        Task(title="позвонить маме", priority=2),
    ])
    session.commit()

    for t in session.scalars(select(Task).order_by(Task.id)):
        print(t.id, t.title, t.done, t.priority)
Вывод
1 вынести мусор False 2
2 купить хлеб False 1
3 купить молоко False 1
4 полить цветы True 3
5 позвонить маме False 2

В остальных блоках урока подготовка та же самая — модель, движок, пять задач, — просто она опущена для краткости. Каждый блок самодостаточен: жми «Запустить» и меняй что угодно.

where: условие как Python-выражение

Фильтр навешивается на запрос методом .where(): передаёшь выражение над колонками — получаешь только строки, для которых оно истинно. Классика: активные задачи, то есть с флажком done равным ложь.

только незавершённые задачи
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Task(Base):
    __tablename__ = "tasks"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String)
    done: Mapped[bool] = mapped_column(default=False)
    priority: Mapped[int] = mapped_column(default=1)

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add_all([
        Task(title="вынести мусор", priority=2),
        Task(title="купить хлеб", priority=1),
        Task(title="купить молоко", priority=1),
        Task(title="полить цветы", priority=3, done=True),
        Task(title="позвонить маме", priority=2),
    ])
    session.commit()

    for t in session.scalars(
        select(Task).where(Task.done == False).order_by(Task.id)
    ):
        print(t.id, t.title)
Вывод
1 вынести мусор
2 купить хлеб
3 купить молоко
5 позвонить маме

Из пяти задач остались четыре — «полить цветы» отсеялся, у него done равен истина. Теперь про двойное равенство, на котором спотыкаются все. Условие в ORM — это Python-выражение над колонками: двойное равенство здесь не опечатка, а способ получить критерий сравнения. Task.done == False не сравнивает ничего прямо сейчас — оно строит объект-критерий, который база превратит в WHERE done = 0. Одиночное = в Python-выражении вообще не существует: это присваивание, и синтаксис его не пропустит.

ошибка: = вместо ==
select(Task).where(Task.done = False)
Вывод
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
Блок не запускается намеренно: одиночное = в выражении — синтаксическая ошибка Python. Интерпретатор сам подсказывает про двойное равенство.

Сравнения: знакомые операторы

Над числовыми колонками работают все привычные операторы: >, >=, <, <=, ==, !=. Отберём срочное — приоритет выше первого — и наоборот, самую простую очередь с приоритетом ровно один.

приоритет выше первого и ровно первый
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Task(Base):
    __tablename__ = "tasks"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String)
    done: Mapped[bool] = mapped_column(default=False)
    priority: Mapped[int] = mapped_column(default=1)

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add_all([
        Task(title="вынести мусор", priority=2),
        Task(title="купить хлеб", priority=1),
        Task(title="купить молоко", priority=1),
        Task(title="полить цветы", priority=3, done=True),
        Task(title="позвонить маме", priority=2),
    ])
    session.commit()

    print("Приоритет выше первого:")
    for t in session.scalars(
        select(Task).where(Task.priority > 1).order_by(Task.id)
    ):
        print(" ", t.id, t.title)

    print("Приоритет ровно один:")
    for t in session.scalars(
        select(Task).where(Task.priority == 1).order_by(Task.id)
    ):
        print(" ", t.id, t.title)
Вывод
Приоритет выше первого:
  1 вынести мусор
  4 полить цветы
  5 позвонить маме
Приоритет ровно один:
  2 купить хлеб
  3 купить молоко

Три задачи с приоритетом 2 и 3 против двух задач с приоритетом 1 — сверься с эталонной распечаткой базы, числа сходятся. Сравнивать можно и строки с датами: Task.title > "н", Entry.created_at >= нужная_дата — база сравнит по своим правилам.

in_: значение из списка

Когда точных значений несколько, цепочка из равенств через or_ выглядит громоздко. Для этого есть in_: колонка должна совпасть с любым элементом списка.

задачи про покупки
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Task(Base):
    __tablename__ = "tasks"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String)
    done: Mapped[bool] = mapped_column(default=False)
    priority: Mapped[int] = mapped_column(default=1)

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add_all([
        Task(title="вынести мусор", priority=2),
        Task(title="купить хлеб", priority=1),
        Task(title="купить молоко", priority=1),
        Task(title="полить цветы", priority=3, done=True),
        Task(title="позвонить маме", priority=2),
    ])
    session.commit()

    for t in session.scalars(
        select(Task)
        .where(Task.title.in_(["купить хлеб", "купить молоко"]))
        .order_by(Task.id)
    ):
        print(t.id, t.title)
Вывод
2 купить хлеб
3 купить молоко

Список может прийти откуда угодно — из формы, из конфига, из другого запроса: фильтр собирается под данные на лету. Это та самая компонуемость, ради которой ORM и берут.

like: шаблон строки

Поиск по части строки делает like с шаблоном: знак процента заменяет любое количество символов, включая пустое. Шаблон "%купить%" найдёт строки, где слово «купить» стоит где угодно — в начале, середине или конце.

всё, что содержит «купить»
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Task(Base):
    __tablename__ = "tasks"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String)
    done: Mapped[bool] = mapped_column(default=False)
    priority: Mapped[int] = mapped_column(default=1)

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add_all([
        Task(title="вынести мусор", priority=2),
        Task(title="купить хлеб", priority=1),
        Task(title="купить молоко", priority=1),
        Task(title="полить цветы", priority=3, done=True),
        Task(title="позвонить маме", priority=2),
    ])
    session.commit()

    for t in session.scalars(
        select(Task).where(Task.title.like("%купить%")).order_by(Task.id)
    ):
        print(t.id, t.title)
Вывод
2 купить хлеб
3 купить молоко

Шаблоны как в SQL: %купить — только концовка, купить% — только начало, к__пить — подчёркивание вместо ровно одного символа. Все тонкости шаблонов разбирались в уроке про WHERE и LIKE, там же про регистр: для текста с заглавными буквами like в SQLite чувствителен к регистру, и об этом стоит помнить на настоящих данных.

Цепочка where — это И

Несколько условий навешиваются цепочкой: .where(...).where(...) — каждое добавляет своё требование, и строка должна удовлетворить всем сразу. Это логическое И, что бы ни подсказывала интуиция.

активные и срочные одновременно
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Task(Base):
    __tablename__ = "tasks"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String)
    done: Mapped[bool] = mapped_column(default=False)
    priority: Mapped[int] = mapped_column(default=1)

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add_all([
        Task(title="вынести мусор", priority=2),
        Task(title="купить хлеб", priority=1),
        Task(title="купить молоко", priority=1),
        Task(title="полить цветы", priority=3, done=True),
        Task(title="позвонить маме", priority=2),
    ])
    session.commit()

    for t in session.scalars(
        select(Task)
        .where(Task.done == False)
        .where(Task.priority >= 2)
        .order_by(Task.id)
    ):
        print(t.id, t.title)
Вывод
1 вынести мусор
5 позвонить маме

Флажок «не готово» оставлял четыре задачи, приоритет от двух — три. Пересечение — всего две: задачи 1 и 5. Каждое условие сузило список, как и положено И. Короткая запись того же смысла — функция and_: where(and_(Task.done == False, Task.priority >= 2)) — удобно, когда условия собираются в список динамически.

or_: хотя бы одно условие

Логическое ИЛИ строится функцией or_ из sqlalchemy: строка попадёт в выдачу, если истинно хотя бы одно из условий. Спросим: что выполнено или что срочное?

выполнено ИЛИ срочно
from sqlalchemy import create_engine, String, select, or_
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Task(Base):
    __tablename__ = "tasks"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String)
    done: Mapped[bool] = mapped_column(default=False)
    priority: Mapped[int] = mapped_column(default=1)

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add_all([
        Task(title="вынести мусор", priority=2),
        Task(title="купить хлеб", priority=1),
        Task(title="купить молоко", priority=1),
        Task(title="полить цветы", priority=3, done=True),
        Task(title="позвонить маме", priority=2),
    ])
    session.commit()

    for t in session.scalars(
        select(Task)
        .where(or_(Task.done == True, Task.priority == 3))
        .order_by(Task.id)
    ):
        print(t.id, t.title, t.done, t.priority)
Вывод
4 полить цветы True 3

Условия пересеклись на одной задаче: «полить цветы» и выполнена, и срочная. Остальные не подошли ни под один критерий — вот и весь ответ. Импорт у or_ и and_ общий с остальным инструментарием: from sqlalchemy import or_, and_.

И и ИЛИ рядом на одной базе
from sqlalchemy import create_engine, String, select, or_
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Task(Base):
    __tablename__ = "tasks"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String)
    done: Mapped[bool] = mapped_column(default=False)
    priority: Mapped[int] = mapped_column(default=1)

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add_all([
        Task(title="вынести мусор", priority=2),
        Task(title="купить хлеб", priority=1),
        Task(title="купить молоко", priority=1),
        Task(title="полить цветы", priority=3, done=True),
        Task(title="позвонить маме", priority=2),
    ])
    session.commit()

    print("Два where - это И:")
    for t in session.scalars(
        select(Task)
        .where(Task.done == False)
        .where(Task.priority == 3)
        .order_by(Task.id)
    ):
        print(" ", t.id, t.title)

    print("or_ - это ИЛИ:")
    for t in session.scalars(
        select(Task)
        .where(or_(Task.done == False, Task.priority == 3))
        .order_by(Task.id)
    ):
        print(" ", t.id, t.title)
Вывод
Два where - это И:
or_ - это ИЛИ:
  1 вынести мусор
  2 купить хлеб
  3 купить молоко
  4 полить цветы
  5 позвонить маме

Первая программа: умный фильтр

Соберём приёмы в одну выборку: активные задачи с приоритетом не ниже двух, отсортированные по срочности, с подсчётом найденного. Три условия, сортировка и счётчик — типовой запрос настоящего списка дел.

срочные незавершённые дела
from sqlalchemy import create_engine, String, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session

class Base(DeclarativeBase):
    pass

class Task(Base):
    __tablename__ = "tasks"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String)
    done: Mapped[bool] = mapped_column(default=False)
    priority: Mapped[int] = mapped_column(default=1)

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add_all([
        Task(title="вынести мусор", priority=2),
        Task(title="купить хлеб", priority=1),
        Task(title="купить молоко", priority=1),
        Task(title="полить цветы", priority=3, done=True),
        Task(title="позвонить маме", priority=2),
    ])
    session.commit()

    rows = session.scalars(
        select(Task)
        .where(Task.done == False)
        .where(Task.priority >= 2)
        .order_by(Task.priority)
    ).all()

    for t in rows:
        print(t.title, "- приоритет", t.priority)
    print("Найдено:", len(rows))
Вывод
вынести мусор - приоритет 2
позвонить маме - приоритет 2
Найдено: 2

Обрати внимание: фильтрация и сортировка живут в одном запросе-цепочке, а подсчёт делает уже Python — список-то маленький. На больших таблицах счётчик переедет в базу через func.count(), о чём будет отдельный урок. Условия можно навешивать и на связанные таблицы через join — это тема урока 11.

Что дальше

Ты фильтруешь базу как на родном языке: сравнения через знакомые операторы, списки через in_, шаблоны через like, комбинирование через цепочки where, and_ и or_. База пока только читается и пополняется — в следующем уроке данные научатся обновляться и удаляться, и CRUD-квадрат замкнётся: создавать, читать, изменять, удалять.

Каждое .where() в цепочке сужает выдачу — это И. Для «хотя бы одно» существует только or_ с его явным списком условий.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

# в базе задачи с приоритетами:
# 1 вынести мусор (2), 2 купить хлеб (1),
# 3 полить цветы (3, done=True)

rows = session.scalars(
    select(Task).where(Task.priority > 1).order_by(Task.id)
).all()
for t in rows:
    print(t.id, t.title)
rows = session.scalars(
    select(Task).where(Task.title.like("купить%")).order_by(Task.id)
).all()
print(len(rows))
# в базе: 1 дело А (done=False), 2 дело Б (done=True),
# 3 дело В (done=False)

rows = session.scalars(
    select(Task).where(or_(Task.done == True, Task.id == 1)).order_by(Task.id)
).all()
print(len(rows))
Проверь себя
0 / 5

1. Как в where записать условие «done равен False»?

2. Что делает цепочка .where(A).where(B)?

3. Как выбрать задачи, у которых title — «хлеб» или «молоко»?

4. Что найдёт шаблон like("%купить%")?

5. Запрос с where(Task.done == False).where(Task.priority == 3) вернул пустой список, хотя подходящие «или-или» строки есть. Почему?

Карточки терминов
Запомнено: 0 / 6
Практика

Собери «умный фильтр» по учебной базе пяти задач: выбери активные задачи (done равен False) с приоритетом не ниже 2, отсортируй по возрастанию id и выведи каждую в формате «id название», а последней строкой — «Найдено: N». Для сверки: должны остаться задачи 1 и 5.

practice.py
Вопросы и ответы по уроку

Как написать несколько условий в where SQLAlchemy?

Цепочкой: select(Task).where(A).where(B) — условия складываются как логическое И, строка должна пройти все. Для динамического списка условий есть and_(A, B). Логическое ИЛИ выражается только функцией or_(A, B) из sqlalchemy.

Почему в where нужно двойное равенство?

Потому что условие — это Python-выражение: Task.done == False вычисляет критерий сравнения и передаёт его базе. Одиночное = в выражении — синтаксическая ошибка Python ещё до запуска. Не путай с текстом SQL-запроса, где в WHERE пишется одиночное =: там это язык SQL, здесь — Python.

Как найти строки, содержащие часть слова?

Через like с процентом: Task.title.like("%купить%") найдёт строки, где «купить» стоит в любом месте. Процент заменяет любое количество символов, подчёркивание — ровно один. Учти, что в SQLite like чувствителен к регистру для текста со смешанным регистром.

Чем in_ лучше цепочки or_?

Для проверки колонки против списка значений in_ короче и быстрее читается: Task.title.in_(список) вместо or_(Task.title == x, Task.title == y, ...). Плюс список для in_ можно построить во время работы программы — из формы, конфига или другого запроса, а or_ пришлось бы собирать вручную.

Понравился урок? Сошлитесь на него

«Условие в ORM — это Python-выражение над колонками: двойное равенство здесь не опечатка, а способ получить критерий сравнения.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.