Что такое 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 из стандартной библиотеки. База, таблица, одна заметка:
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-строкой из пользовательского ввода:
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.
pip install sqlalchemy
python -c "import sqlalchemy; print(sqlalchemy.__version__)"
Successfully installed sqlalchemy-2.0.43
Что дальше
Ты собрал фундамент: движок как фабрика соединений, адрес базы 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)
1. Что делает create_engine("sqlite:///:memory:")?
2. Зачем строку запроса оборачивают в text()?
3. Как безопасно подставить значение пользователя в text-запрос?
4. Что вернёт метод scalar() у результата запроса?
5. Что произойдёт с in-memory базой после завершения программы?
Через SQLAlchemy и text() создай в памяти таблицу goods с колонками id (INTEGER PRIMARY KEY), name (TEXT) и price (INTEGER). Вставь товар «линейка» с ценой 45 — значения подставь параметрами. Выведи имя товара, найдя его по цене 45, затем выведи удвоенную цену запросом SELECT :p * 2.
Что такое 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
SQLAlchemy · Урок 2
Подключение к SQLite: create_engine
Строка подключения — это адрес базы. Разбираем sqlite:///:memory: против файла, ленивый движок и журнал echo, который показывает настоящий SQL.
SQLAlchemy · Урок 3
Модели: декларативные таблицы
Таблица как класс Python: Base, Mapped-аннотации и mapped_column. Модель — это и код, и схема, и документация одновременно.
sqlite3 · Урок 1
SQL с нуля через Python: первая база SQLite и таблица
Первая настоящая база данных без установки серверов: import sqlite3, курсор, CREATE TABLE и первые строки — всё исполняется прямо на странице.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
NumPy · Урок 1
Что такое NumPy и как установить через pip: первый массив ndarray
Первый массив ndarray: создаём, сравниваем со списком, разбираем dtype и shape — и ускоряем сумму миллиона чисел примерно в сто раз.
numpy для начинающихчто такое numpy
FastAPI · Урок 7
Подключаем базу данных SQLite к FastAPI
Словарь задач из урока 5 умирает при перезапуске. Ставим на его место SQLite через SQLAlchemy: движок, сессии, Depends и CRUD, который переживает uvicorn --reload.
sqlalchemy fastapifastapi sqlite
SQLAlchemy · Урок 14
Сырой SQL: text() внутри ORM
SQL, написанный руками, живёт внутри ORM: text() исполняет его в обычной сессии — с параметрами, без инъекций и без отказа от моделей.
sqlalchemy textsqlalchemy сырой sql