Проблема работы с базой данных напрямую
Мы уже работали с базами данных и использовали встроенную библиотеку для работы с движком баз данных sqlite.
И для того, чтобы совместить Python код и базу данных приходилось использовать что-то типа такого кода:
import sqlite3
DATABASE = "blog.db"
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
posts = conn.execute("SELECT * FROM post").fetchall()
for post in posts:
print(f"Заголовок {post['title']}\n\n{post['body']}")
Строка conn.row_factory = sqlite3.Row действительно спасает положение. Потому, что без нее объект post был бы просто кортежем с полями, к которым надо было обращаться по индексу post[0].
Но в целом код все еще достаточно примитивен. Хоть мы и получили словарь с полями, но если объект надо будет модифицировать или структура данных окажется сложнее чем просто одна единственная таблица, то начнутся очень большие проблемы по трансляции каждого шага в SQL код из объектов, которые находятся в памяти.
Python способен на большее. А что, если получится объединить возможности работы с объектами и сделать так, чтобы python объекты сами транслировались в SQL запросы?
Для этих целей были созданы ORM. Object-Relational Mapping, объектно-реляционное отображение. Это библиотека, которая позволяет связывать объекты и базы данных. ORM есть во многих языках программирования. Для Python'а было написано несколько таких библиотек, и одна из самых популярных называется SQLAlchemy.
Если вам придется плотно работать с базами данных из Python, то скорее всего документация этой библиотеки станет для вас самым посещаемым сайтом в интернете.
Помимо упрощения работы с данными из баз данных SQLAlchemy еще создает единый синтаксис работы с различными базами. То есть вы получаете единый интерфейс для работы с базой и не задумываетесь об особенностях диалектов конкретной СУБД, в которой хранятся данные.
На практике, конечно, этот вопрос не ставится в таком виде. Когда программист работает с реальным проектом, то он работает только с одной базой чтобы по максимуму использовать ее возможности. И практика, когда один и тот же код используется для подключения к разным СУБД, признана глупой и разрушительной. Но скорее всего вам попадутся проекты и команды, которые мучаются с ситуацией, когда production код работает с Postgres, а тесты запускаются с SQLite.
Недостатки ORM
В отличии от всех других ситуаций обучение использованию ORM лучше всего начинать с обсуждения его недостатков. До того, как вы в первый раз увидите код и даже получите представление о работе ORM.
Основные проблемы:
- Разрыв между реляционным представлением и объектно-ориентированным (термин для поиска Impedance Mismatch)
- Снижение производительности
- Перенос сложности из базы данных в код
Более подробно рассмотрим каждую из проблем.
Impedance Mismatch
Основная критика ORM в литературе всегда использует термин Impedance Mismatch. Этот термин идет из мира электротехники, но многие программисты в вузах изучают обе дисциплины вместе.
Реляционное представление данных использует свою собственную алгебру (ну или проще математическое представление) для работы с данными. Объектно-ориентированное программирование использует совершенное другое. И программистам, особенно новичкам сложно почувствовать эту разницу. Что приводит к проблемам фундаментального непонимания.
Базы данных используют очень точные и гранулярные типы данных для максимальной оптимизации работы базы. В языке программирования используются другие типы данных. Например, в базе данных Postgres строка обычно имеет указанную максимальную длину. А в Python такого ограничения нет. И программисту будет естественно присвоить переменной строковое значение. Но это может вызвать проблему при попытке сохранить данные в базу. Либо будет сообщение об ошибке, и программа упадет (это в целом хороший сценарий), либо строка обрежется, и при повторном чтении из базы будет выглядеть испорченной.
Такой же пример и с числами. Потому что в Postgres есть типы данных, которые позволяют хранить числа размером ограниченного размера. Вот разрешенные типы данных в Postgres: smallint, int, integer, int2, int4, int8, отдельный тип данных для больших чисел bigint. И еще может регулироваться наличие или отсутствие знака для хранения отрицательных или только положительных чисел. В добавок есть тип данных, который позволяет записывать только монотонно возрастающие значения serial.
В Python таких типов просто нет. А значит есть шанс присвоить объекту значение, которое не сможет записаться в базу.
Снижение производительности
Разница в подходах к изменению данных. В программе, чтобы изменить или удалить объект к нему надо обратиться напрямую. Например, если у вас есть список статей, и вы хотите их перенести из одной категории в другую, то скорее всего вы пройдете списком по статьям и присвоите свойству категория новое значение. Что-то типа:
for article in articles:
if article.category == 'Новость':
article.category = 'Архив'
При работе с базой данных вы пишете один запрос, который делает массовую модификацию данных. И в лучшем случае вернет количество строк которые он модифицировал. База не считывает каждый объект отдельно:
UPDATE articles SET category = 'Архив'
WHERE category = 'Новость'
Но при этом в коде новичков часто можно встретить что-то типа:
# метод .all() в этом случае транслируется в SQL =>
# SELECT * FROM articles;
for article in Article.all():
if article.category == 'Новость':
article.category = 'Архив'
# .save() => UPDATE article SET category = 'Архив' WHERE id = {article.id}
article.save()
В этом случае база данных будет использована неправильно. Базы умеют эффективно обновлять сразу большие массивы данных. Но вместо из базы будет прочитанная каждая запись и потом отдельным запросом будет отправлен запрос на модификацию каждой отдельной статьи.
Разница во времени исполнения кода в обоих случаях может составлять минуты или даже часы.
Перенос сложности из базы данных в код
Код для работы с данными приложения должен где-то храниться. До появления ORM в базах данных использовались хранимые процедуры. Они позволяли взять на себя логику работы с данными при этом позволяли сохранять целостность базы за счет использования отката транзакций. При использовании ORM код изменения данных может размазываться на множество слоев в кодовой базе Python приложения.
В целом добавление логики для работы с данными в код обычно не является проблемой. Но для этого надо изначально продумывать дизайн приложения и код программы может существенно увеличиваться.
Хранимые процедуры
Есть почти 100% вероятность, что вы услышите, что хранимые процедуры в базах данных — это что-то плохое и обязательно надо их избегать. Хранимые процедуры — это один из инструментов. Как молоток, скальпель или хирургический лазер. У хранимых процедур, как и у всех инструментов, есть область применения.
Хранимые процедуры имеют свои недостатки (например сложность версионирования), но в целом это очень хорошая и, главное, проверенная технология.