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

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

Начать обучение
Урок 1 из 20 Начальный 35 мин 100 XP

Что такое SQLAlchemy и ORM: первый движок

SQLAlchemy переводит классы Python в таблицы SQL и обратно. Создаём первый движок и выполняем первые запросы — всё прямо в браузере, без установки.

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

В разделе про SQLite ты уже работал с базой честно: каждый запрос — текстовая строка вроде "SELECT id, body FROM notes". Строка не проверяется никем и ничем. Опечатка в имени колонки всплывёт только при запуске, значение для условия вклеивается в текст руками, а тестировать такой код на настоящей базе страшновато. Сегодня начинаем раздел про SQLAlchemy — библиотеку, которая берёт эту рутину на себя: таблицы описываются классами Python, запросы собираются из Python-выражений, а SQL генерируется сам.

В первом уроке разберёмся, что такое ORM и зачем он нужен, создадим первый движок и выполним первые запросы — сначала сырым SQL, чтобы увидеть фундамент, на котором всё стоит.

Что такое SQLAlchemy и ORM: переводчик между классами и SQL

SQLAlchemy — самая популярная библиотека Python для работы с базами данных. ORM (Object-Relational Mapping, «объектно-реляционное отображение») — подход, который она реализует: таблицы базы отображаются на классы, строки — на объекты, колонки — на атрибуты. ORM — переводчик между двумя языками: ты говоришь на Python, база понимает SQL, и оба остаются при своём.

Опечатка в имени колонки при работе с ORM всплывёт при написании кода, а не на проде — Python-выражение просто не соберётся. Это первый из трёх подарков, ради которых ORM и ставят в проект:

  • типобезопасность: запрос — это Python-выражение из классов и атрибутов, ошибка в имени колонки видна сразу, а не в логах в три часа ночи;
  • компонуемость: условия собираются из кусков под переменные — фильтр по категории, дате или автору добавляется одной строкой, а не переписыванием строки SQL;
  • тестируемость: вместо боевой базы в тест подставляется копия в памяти — тесты быстрые и не трогают настоящие данные.

Внутри SQLAlchemy два этажа. Core — нижний: движки, соединения, SQL-выражения — всё, что работает близко к базе. ORM — верхний: классы, сессии и объекты, построенные поверх Core. Начнём с общего фундамента обоих этажей — движка.

Как запускать код из этого урока

У каждого код-блока на странице есть кнопка «Запустить». Код выполняется в настоящем интерпретаторе Python внутри браузера (проект Pyodide), а SQLAlchemy ставится в эту песочницу автоматически при первом же импорте — ничего устанавливать не нужно. Пишешь from sqlalchemy import create_engine — библиотека уже на месте.

первый запрос
from sqlalchemy import create_engine, text

engine = create_engine("sqlite:///:memory:")

with engine.connect() as conn:
    one = conn.execute(text("SELECT 1")).scalar()
    print(one)
Вывод
1

Четыре новые сущности — разберём каждую. create_engine("sqlite:///:memory:") создаёт движок: он не подключается к базе сразу, а только настраивает, как подключаться, — фабрика соединений. Строка sqlite:///:memory: — адрес базы: диал sqlite, а :memory: означает «держи базу в памяти процесса». text() оборачивает строку в SQL-выражение — объект, который понимает метод execute. engine.connect() открывает соединение, а scalar() выполняет запрос и возвращает первое значение первой колонки результата.

Внутри песочницы при этом работает настоящая SQLite — со всем своим SQL-арсеналом, от подзапросов до оконных функций. Движок лишь доставляет запросы до неё.

запрос умеет считать
from sqlalchemy import create_engine, text

engine = create_engine("sqlite:///:memory:")

with engine.connect() as conn:
    calc = conn.execute(text("SELECT 40 + 2")).scalar()
    print(calc)
Вывод
42

Сырой SQL через SQLAlchemy: знакомый путь

Прежде чем описывать классы, посмотрим на старый способ ещё раз — так работали в модуле sqlite3 из стандартной библиотеки. База, таблица, одна заметка:

как в разделе про sqlite3
import sqlite3

conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)")
conn.execute("INSERT INTO notes VALUES (1, 'так писали до ORM')")
conn.commit()

for row in conn.execute("SELECT id, body FROM notes"):
    print(row)

conn.close()
Вывод
(1, 'так писали до ORM')

Тот же путь через SQLAlchemy — это уровень Core: SQL всё ещё пишешь руками, но появляются движок, пул соединений и единый интерфейс к любой СУБД.

то же самое через движок
from sqlalchemy import create_engine, text

engine = create_engine("sqlite:///:memory:")

with engine.connect() as conn:
    conn.execute(text("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)"))
    conn.execute(text("INSERT INTO notes VALUES (1, 'первая заметка')"))
    conn.commit()

    for row in conn.execute(text("SELECT id, body FROM notes")):
        print(row)
Вывод
(1, 'первая заметка')

Строки результата — кортежи: row[0] даст id, row[1] — текст. conn.commit() здесь обязателен: без него вставка не закрепится. И заметь, что классов пока нет вообще — таблица описана текстом. Именно это и поменяет ORM в уроке 3: таблица станет классом, а её описание Python проверит за тебя.

Параметры: значения подставляет SQLAlchemy

Первое правило веб-разработчика: значения никогда не вклеиваются в текст запроса. Посмотри, что получается, если слепить запрос f-строкой из пользовательского ввода:

f-строка и SQL — плохая пара
user_input = "'; DROP TABLE notes; --"

query = f"SELECT body FROM notes WHERE body = '{user_input}'"
print(query)
Вывод
SELECT body FROM notes WHERE body = ''; DROP TABLE notes; --'

Это классическая SQL-инъекция: ввод пользователя закрыл кавычку поиска, дописал вторую команду — уничтожение таблицы — и закомментировал хвост. База выполнит обе команды, потому что для неё это просто текст. Выполню такой запрос на настоящей таблице — и данные уйдут.

параметры: :имя и словарь
from sqlalchemy import create_engine, text

engine = create_engine("sqlite:///:memory:")

with engine.connect() as conn:
    conn.execute(text("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)"))
    conn.execute(text("INSERT INTO notes (body) VALUES (:body)"), {"body": "заметка про параметры"})
    conn.commit()

    note = conn.execute(
        text("SELECT body FROM notes WHERE id = :i"), {"i": 1}
    ).scalar()
    print(note)

    calc = conn.execute(text("SELECT :a + :b"), {"a": 40, "b": 2}).scalar()
    print(calc)
Вывод
заметка про параметры
42

Имена с двоеточием (:body, :i, :a) — места подстановки, словарь — значения. SQLAlchemy передаёт их в базу отдельно от текста, так что даже ввод вида '; DROP TABLE останется просто строкой для поиска. В ORM-выражениях, к которым мы перейдём со следующего урока, параметры появятся сами собой — там не будет ни одного ручного словаря.

Где живёт база: строка подключения

Строка sqlite:///:memory: — это адрес. У всех СУБД он строится одинаково: диал+драйвер://пользователь:пароль@хост/имя_базы. Для SQLite хоста нет — только путь к файлу, а :memory: — особый путь, означающий «в памяти». База живёт, пока жив процесс: завершился скрипт — база исчезла вместе со всеми данными.

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

таблица видна в служебном каталоге
from sqlalchemy import create_engine, text

engine = create_engine("sqlite:///:memory:")

with engine.connect() as conn:
    conn.execute(text("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)"))
    conn.commit()

    tables = conn.execute(
        text("SELECT name FROM sqlite_master WHERE type = 'table'")
    ).all()
    print(tables)
Вывод
[('notes',)]

Страница таблицы notes появилась в каталоге, хотя мы не писали ни одного класса. В уроке 3 эту CREATE-команду будет составлять SQLAlchemy по описанию класса, а проверять таблицы удобнее через инспектор — ему посвящён урок 4.

Установка на своём компьютере

На этой странице SQLAlchemy уже установлен. Дома — стандартный путь через pip, команды выполняются в терминале, а не внутри Python.

терминал, не Python
pip install sqlalchemy
python -c "import sqlalchemy; print(sqlalchemy.__version__)"
Вывод
Successfully installed sqlalchemy-2.0.43
Это команды терминала, поэтому блок не запускается в браузере. Версия у тебя может быть новее — это нормально: код курса рассчитан на SQLAlchemy 2.0 и старше.

Что дальше

Ты собрал фундамент: движок как фабрика соединений, адрес базы sqlite:///:memory:, сырой SQL через text() и безопасные параметры. Дальше по курсу: подключение — файловые базы, журнал SQL под капотом; модели — таблицы как классы. С урока 3 сырые строки начнут исчезать из кода, и к середине курса ты будешь собирать запросы, которые руками записывал бы на пол-экрана.

ORM — переводчик: ты описываешь таблицы классами Python, а SQLAlchemy сама составляет из них SQL и раздаёт строки обратно объектами.

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

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

from sqlalchemy import create_engine, text

engine = create_engine("sqlite:///:memory:")

with engine.connect() as conn:
    result = conn.execute(text("SELECT :a * :b"), {"a": 6, "b": 7}).scalar()
    print(result)
from sqlalchemy import create_engine, text

engine = create_engine("sqlite:///:memory:")

with engine.connect() as conn:
    conn.execute(text("CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)"))
    conn.execute(text("INSERT INTO notes (body) VALUES (:b)"), {"b": "дело один"})
    conn.commit()

    rows = conn.execute(text("SELECT id, body FROM notes")).all()
    print(rows)
user_input = "кот"

query = f"SELECT * FROM notes WHERE body = '{user_input}'"
print(query)
Проверь себя
0 / 5

1. Что делает create_engine("sqlite:///:memory:")?

2. Зачем строку запроса оборачивают в text()?

3. Как безопасно подставить значение пользователя в text-запрос?

4. Что вернёт метод scalar() у результата запроса?

5. Что произойдёт с in-memory базой после завершения программы?

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

Через SQLAlchemy и text() создай в памяти таблицу goods с колонками id (INTEGER PRIMARY KEY), name (TEXT) и price (INTEGER). Вставь товар «линейка» с ценой 45 — значения подставь параметрами. Выведи имя товара, найдя его по цене 45, затем выведи удвоенную цену запросом SELECT :p * 2.

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

Что такое SQLAlchemy простыми словами?

Это библиотека Python для работы с базами данных: ты описываешь таблицы классами, а SQLAlchemy сама составляет SQL, доставляет его в базу и возвращает строки объектами. Сырой SQL тоже доступен — через text(), поэтому переход с sqlite3 получается постепенным.

Зачем ORM, если я уже умею писать SQL?

ORM добавляет к SQL три вещи: типобезопасность (запрос — Python-выражение, опечатка видна сразу), компонуемость (условия собираются из кусков под переменные) и тестируемость (боевую базу можно заменить копией в памяти). SQL при этом не выбрасывается: SQLAlchemy выполняет его же, просто собирает за тебя.

Что означает sqlite:///:memory:?

Это строка подключения: диал sqlite, база — в памяти процесса (:memory:). Такая база живёт, пока жив скрипт, и исчезает после его завершения, поэтому каждый запуск кода получает чистую базу. Для постоянного хранения вместо :memory: пишется путь к файлу, например sqlite:///tasks.db.

Как подставить значения в SQL-запрос без риска инъекции?

Через параметры: в тексте запроса ставится :имя, а значения передаются словарём вторым аргументом execute — conn.execute(text("... WHERE id = :i"), {"i": 2}). SQLAlchemy передаёт их в базу отдельно от текста и экранирует сама. Вклейка значений f-строкой или конкатенацией — это SQL-инъекция.

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

«Опечатка в имени колонки при работе с ORM всплывёт при написании кода, а не на проде — Python-выражение просто не соберётся.»

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

TelegramVK

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

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