🐍🗄️ Управление данными с помощью Python, SQLite и SQLAlchemy
На одном примере сравниваются три модели управления данными: csv-файлы, SQL-запросы к простой базе данных на SQLite и контроль информации в виде объектов Python с SQLAlchemy. В конце пример веб-приложения на Flask с использованием SQLAlchemy.
Эта публикация – выполненный с небольшими сокращениями перевод статьи Дуга Фаррелла Data Management With Python, SQLite, and SQLAlchemy. Полный программный код пособия доступен в GitHub-репозитории. Все упоминаемые примеры подпрограмм отсылают к соответствующим файлам.
Все программы в той или иной форме занимаются обработкой информации. Многим из них необходимо сохранять эти данные и впоследствии извлекать. В небольших проектах хранить информацию можно в одном файле без необходимости использования сервера баз данных. С такими задачами вполне можно справиться средствами Python, SQLite и SQLAlchemy.
Аналогичных результатов можно добиться, используя форматы CSV, JSON, XML или даже кастомные решения. Преимуществом таких текстовых хранилищ является легкий для восприятия формат данных – обычно структура понятна с первого взгляда. Ниже мы сравним такой способ хранения и обработки данных с применением реляционных баз данных и языка SQL, а также разберемся, какой метод лучше подходит для вашей программы. В общих чертах мы рассмотрим следующие вопросы:
- как использовать плоские файлы и SQLite для хранения данных;
- как SQL позволяет облегчить доступ к данным;
- как применять SQLAlchemy для работы с данными в форме объектов Python.
В практическом отношении основной акцент в статье делается на SQLAlchemy.
🧾 Плоские базы данных
Под плоской базой данных (плоской таблицей) будем понимать файл, содержащий данные без внутренней иерархии и ссылок на внешние файлы. Это объясняет ряд особенностей: в таких базах данных нет необходимости использовать фиксированную ширину полей, а csv-файлы (англ. comma separated values) представляют собой строки простого текста, в которых элементы данных разделены запятыми. Каждая строка текста представляет собой строку данных, а каждое значение в строке, отделенное от остальных запятой соответствует одному из полей таблицы.
В первой строке представленного примера находится список полей – имена столбцов данных. Каждая следующая строка соответствует одной записи.
➕ Преимущества плоских баз данных
Небольшие плоские базы данных легко создавать и корректировать с помощью текстового редактора, не составляет трудностей найти несоответствие или иную проблему. Многие приложения умеют их импортировать и экспортировать. В Excel можно превратить csv-файл в электронную таблицу и обратно.
Ещё одно преимущество плоских файлов – они автономны, данными в такой форме легко поделиться. Практически в любом языке программирования есть инструменты и библиотеки для работы с csv-файлами. Python имеет встроенный модуль csv и мощную стороннюю библиотеку pandas.
➖ Недостатки плоских баз данных
Преимущества плоских баз данных меркнут по мере увеличения объема информации. Файлы остаются читаемыми, но редактирование и поиск отдельных записей вызывают трудности. И не только для человека – файл разрастается, и его обработка требует вычислительных ресурсов.
Другая трудность, связанная с применением плоских файлов – нужно явно создавать и поддерживать отношения между компонентами данных в рамках одного файла. Чтобы описать новые отношения, приходится писать дополнительный текст, создавать новые поля.
Ещё одна сложность – люди, с которыми мы хотим поделиться файлом данных, также должны знать о структурах и отношениях, которые в этих данных отразили. Чтобы получить доступ к информации, пользователи нередко должны понимать не только структуру данных, но и сопровождающие инструменты программирования.
📇 Пример работы с плоской базой данных
В качестве примера рассмотрим описанный выше файл author_book_publisher.csv
, содержащий список авторов, опубликованных ими книг и издателей. Для простоты будем считать, что все авторы, книги и издательства уникальны, и все книги пишутся автором без соавторов.
Рассмотрим функцию main()
программы, находящейся в упомянутом репозитории по относительному адресу examples/example_1/main.py.
При запуске программы для обновленного csv-файла выводится следующий результат:
Для выполнения основной части работы main()
вызывает другие функции, с которыми можно ознакомиться в файле. Само приложение работает корректно и демонстрирует возможности библиотеки pandas.
Мы показали этот пример, чтобы далее создать программу с идентичной функциональностью, используя базу данных SQLite и библиотеку SQLAlchemy для манипуляции этими данными.
🦋 Использование SQLite для хранения данных
Как мы упомянули выше в примечании, в файле author_book_publisher.csv
в некоторых данных есть повторы. Например, роман Перл Бак The Good Earth
опубликовали два разных издателя.
Представьте, что было бы, если файл содержал больше связанных данных, например, сведения об авторе, дату публикации, ISBN книги, адрес и телефонный номер издательства. Подобные сведения будут дублироваться для каждого корневого элемента: автора, книги, издательства. Такое построение не только избыточно, но и усложняет процедуры 1) изменения полей, связанных с отдельным объектом и 2) добавления новых свойств.
Перечисленные причины обуславливают использование баз данных, учитывающих отношения между скрытыми в них структурами – реляционных баз данных. Важной темой в этом плане является нормализация базы данных – приведение структуры к виду, обеспечивающему минимальную логическую избыточность. Когда структура базы данных расширяется новыми типами данных, предварительная нормализация сводит к минимуму изменения существующей структуры.
SQLite является системой управления реляционными базами данных, доступной в стандартной библиотеке Python. Для работы с ней не требуется отдельный сервер, формат файла является кросс-платформенным и доступен в других языках программирования, поддерживающих SQLite.
🏗️ Создаем структуру базы данных
Реляционные базы данных позволяют хранить структурированные данные в виде связанных друг с другом таблиц. В качестве основного способа взаимодействия с данными используется язык запросов SQL.
SQL – это декларативный язык, используемый для создания, управления и поиска данных, содержащихся в базе. Декларативный язык описывает, что должно быть выполнено, а не то, как это нужно сделать. Мы увидим примеры операторов SQL позже, когда перейдем к созданию таблиц базы данных.
Чтобы воспользоваться преимуществами SQL, нам нужно провести нормализацию базы данных из файла author_book_publisher.csv
. Для этого мы перенесем авторов, книги и издателей в отдельные таблицы базы данных.
Данные в этом формате хранятся в виде двумерных табличных структур. Данные, содержащиеся в полях таблицы, относятся к заранее определенным типам: текст, целые числа, числа с плавающей запятой и т. д. Это отличает базы данных от csv-файлов, где все поля исходно являются текстовыми и для распознавания типа должны быть проанализированы программно.
Каждая запись в таблице имеет первичный ключ (англ. primary key), определенный для присвоения записи уникального идентификатора. Первичный ключ похож на ключ в словаре Python. Движок базы данных обычно сам генерирует целочисленный первичный ключ, увеличивая значение на единицу для каждой новой записи. Если данные, хранящиеся в поле, уникальны среди всех других данных в этом поле, это значение также может использоваться, как первичный ключ. Например, в таблице, содержащей данные о книгах, в качестве первичного ключа может использоваться уникальный по своей природе ISBN.
🏢 Создание таблиц базы данных
Ниже представлен пример, как с помощью операторов SQL можно создать три таблицы, представляющие авторов, книги и издательства:
Обратите внимание, что здесь нет ни файловых операций, ни переменных, ни структур для их хранения. Описывается только желаемый результат: создание таблицы с определенными атрибутами. Механизм базы данных определяет, как это сделать.
Представим, что далее мы сделали SQL-запросы, чтобы заполнить таблицы базы данных информацией. Следующий оператор использует знак *
, чтобы получить и вывести все данные в таблице авторов:
Вы можете использовать инструмент командной строки sqlite3
для взаимодействия с файлом базы данных author_book_publisher.db:
Ниже показан результат работы указанной команды SQL и ее вывод, за которой следует команда .q
для выхода из программы:
Обратите внимание, что в отличие от csv-файла каждый автор присутствует в таблице только один раз.
🕹️ Манипуляции данными с помощью SQL
SQL предоставляет различные способы работы с базами данных и таблицами, добавление новых данных, обновление и удаление уже существующих. Пример оператора SQL для вставки нового автора в таблицу author
:
Оператор INSERT
вставляет строковые значения Paul
и Mendez
в соответствующие столбцы first_name
и last_name
таблицы author
.
Обратите внимание, что столбец author_id
не указан. Поскольку этот столбец является первичным ключом, механизм базы данных сам генерирует и добавляет значение.
Обновление записей в таблице базы данных – также несложный процесс. Например, предположим, что Стивен Кинг хотел, чтобы его знали под псевдонимом Ричард Бахман:
Оператор SQL находит запись с помощью условного оператора WHERE
, а затем обновляет поля first_name
и last_name
свежими значениями. Знак равенства (=
) в SQL используется и для сравнения, и для присваивания.
Пример оператора SQL для удаления записи из таблицы авторов:
first_name = 'Paul'
, то были бы удалены все авторы с именем Paul.🤝 Строим отношения: «один ко многим»
Данные в файле author_book_publisher.csv
отображают данные и связи путем дублирования данных. База данных SQLite разбивает данные на три таблицы (author
, book
, and publisher)
и устанавливает между ними отношения.
Связь «один ко многим» похоже на связь покупателя с заказываемыми товарами в Интернете. У одного покупателя может быть много заказов, но каждый заказ принадлежит одному покупателю. В базе данных author_book_publisher.db
связь «один ко многим» представлена отношением авторов и книг. Каждый автор может написать много книг, но каждая художественная книга написана одним автором (в рамках рассматриваемого примера).
Как реализовать связь «один ко многим» между двумя таблицами? Каждая таблица в базе данных имеет поле первичного ключа, обычно названное по шаблону <имя таблицы>_id
.
Кроме того, таблица book
содержит поле author_id
, которое ссылается на таблицу author
(см. выше SQL-запрос для создания таблиц). Таким образом, поле author_id
устанавливает связь «один ко многим» между авторами и книгами, которая выглядит следующим образом.
Приведенная выше диаграмма представляет собой простую диаграмму отношений сущностей (ERD), созданную с помощью приложения JetBrains DataGrip. Два графических элемента добавляют информацию о связях:
- Значки ключей обозначают первичный (желтый цвет) и внешний (голубой цвет) ключи.
- Стрелка указывает на связь между таблицами на основе внешнего ключа
author_id
.
Чтобы вывести две таблицы вместе, используем SQL-оператор JOIN
:
Приведенный SQL-запрос собирает информацию из author
и book
, используя установленную между ними связь. Конкатенация строк SQL позволяет присвоить author_name
полное имя автора. Данные, собранные запросом, сортируются в порядке возрастания по полю last_name
.
Создав отдельные таблицы для авторов и книг и установив между ними связь, мы уменьшили избыточность данных. Теперь данные об авторе редактируются в одном месте, данные о книгах – в другом.
🌉 Добавляем связи: «многие ко многим»
Автор может работать со многими издателями, а издатель – со многими авторами. Книга может быть опубликована в нескольких издательствах, а издатель может опубликовать множество разных книг. То есть в базе данных author_book_publisher.db
отношение «многие ко многим» существует между авторами и издателями, а также между издателями и книгами.
Такой тип связи являются двусторонним и создается с помощью свящующей таблицы. Такая таблица содержит как минимум два поля внешних ключей, которые являются первичными ключами для каждой из двух связанных таблиц:
В этом примере создается новая таблица author_publisher
, которая ссылается на первичные ключи уже существующих таблиц author
и publisher
. То есть таблица author_publisher
устанавливает отношения между автором и издателем. Аналогично создается таблица book_publisher
.
Поскольку связь устанавливается между двумя первичными ключами, нет необходимости создавать первичный ключ для самой таблицы связей. Комбинация двух связанных ключей создает уникальный идентификатор.
Объединить таблицы можно с помощью оператора JOIN
, но в случае связи многие-ко-многим требуется использовать оператор дважды:
- Соединяя таблицы
author
иauthor_publisher
- Соединяя таблицы
author_publisher
иpublisher
Пример SQL-запроса, возвращающего список авторов и издателей, публикующих их книги:
Приведем пример еще одного запроса, отражающего некоторые возможности SQL:
Этот запрос возвращает список авторов и количество написанных ими книг. Список сортируется сначала по количеству книг в порядке убывания, а затем по имени автора в алфавитном порядке.
То есть здесь мы используем SQL одновременно для агрегирования и сортировки результатов. Выполнение вычислений в базе данных на основе встроенных возможностей систем управления базами данных обычно происходит быстрее, чем выполнение тех же вычислений с необработанными наборами данных в Python.
🧰 Работа с SQLAlchemy и объектами Python
SQLAlchemy – это мощный набор Python-инструментов для доступа к базам данных. Когда мы программируем на объектно-ориентированном языке, полезно мыслить в категориях объектов. Но между объектно-ориентированной и реляционной моделями существует концептуальный разрыв. Этот зазор покрывает объектно-реляционное отображение (ORM), предоставляемое SQLAlchemy. Между базой данных и программой Python появляется модель, преобразующая информацию из потока базы данных в объекты Python и обратно. Таким образом, SQLAlchemy позволяет мыслить в терминах объектов, при этом сохраняя мощные механизмы базы данных.
Модель. Для подключения SQLAlchemy к базе данных создается модель – класс Python, унаследованный от класса SQLAlchemy Base
, определяющий отображение данных между объектами Python, возвращаемыми в результате запроса, и самими таблицами базы данных.
Представленный ниже файл models.py
создает модели для представления базы данных author_book_publisher.db
:
В приведенных в коде комментариях модели сопоставлены пяти показанным выше таблицам базы данных author_book_publisher.db
.
Класс Table
служит для создания связующей таблицы, то есть для создания отношений многие-ко-многим. Первый параметр – имя таблицы, определенное в базе данных, второй (Base.metadata
) обеспечивает связь между функциональностью SQLAlchemy и механизмами базы данных. Остальные параметры являются экземплярами класса Column
, определяющего поле таблицы по имени, типу, а также соответствие внешнему ключу.
Класс ForeignKey
определяет взаимосвязь между двумя полями Column
в разных таблицах. Например, следующая строка в определении таблицы author_publisher
сообщает SQLAlchemy, что в таблице author_publisher
есть поле с именем author_id
, тип этого поля – Integer
, и он является внешним ключом, связанным с первичным ключом в таблице author
.
Функция relationship
определяет отношение один-ко-многим.
Первый параметр функции relationship
– имя класса Book
(не путайте с именем таблицы book
) – это класс, к которому относится атрибут books
. Это отношение сообщает SQLAlchemy, что существует связь между классами Author
и Book
. SQLAlchemy найдет связь в определении класса Book
:
SQLAlchemy распознает, что это точка соединения ForeignKey
между двумя классами.
Параметр backref
создает атрибут author
для каждого экземпляра Book
. Этот атрибут ссылается на Author
, с которым связан экземпляр Book
.
Например, если выполнить следующий код Python, то в результате запроса будет возвращен экземпляр Book
, имеющий атрибуты, которые можно использовать для вывода информации о книге:
Наличие атрибута author
в book
связано с определением backref
. Обратная ссылка очень удобна, когда нам нужно обратиться к родительскому объекту, а все, что у нас есть, – это дочерний экземпляр.
Другая связь у Author
с классом Publisher
:
Как и книги, атрибут publishers
обозначает совокупность издателей, связанных с автором. Первый параметр, "Publisher"
, сообщает SQLAlchemy, что это за класс. Второй параметр secondary
сообщает SQLAlchemy, что связь с классом Publisher
осуществляется через «вторичную» таблицу – таблицу author_publisher
. Третий параметр back_populate
сообщает SQLAlchemy о наличии дополнительной коллекции в классе Publisher
, называемой authors
.
💬 Запросы к базе данных
Стандартный запрос наподобие SELECT * FROM author
в SQLAlchemy можно сделать вот так:
Здесь session
– объект SQLAlchemy, используемый для связи с SQLite в программах Python. Здесь мы сообщаем, что хотим выполнить запрос к модели Author
и вернуть все записи.
На этом этапе преимущества использования SQLAlchemy вместо простого SQL могут быть неочевидны, особенно с учетом необходимости создания моделей. Однако оказывается, что вместо списка скалярных данных, теперь мы получаем список экземпляров объектов Author
с атрибутами, соответствующими заданным именам столбцов.
Коллекции книг и издателей, поддерживаемая SQLAlchemy, создает иерархический список авторов и написанных ими книг, а также издателей, которые эти книги опубликовали. За кулисами SQLAlchemy превращает вызовы объектов и методов в операторы SQL для выполнения в системе управления базами данных SQLite. И наоборот, SQLAlchemy преобразует данные, возвращаемые запросами SQL, в объекты Python.
С помощью SQLAlchemy мы можем выполнить запрос агрегирования, показанный ранее для списка авторов и количества написанных книг, следующим образом:
Запрос session.query
возвращает имя и фамилию автора, а также количество книг, написанных автором. Агрегирующий счетчик в инструкции group_by
производит подсчет по фамилии автора. Результаты сортируются в порядке убывания на основе расчетной переменной с псевдонимом book_total
.
👨💻 Пример программы
Пример программы examples/example_2/main.py содержит те же функции, что и первый программный пример, но использует SQLAlchemy исключительно для взаимодействия с базой данных SQLite author_book_publisher.db
. Приведем здесь функцию main()
:
В сравнении с изначальным кодом теперь весь код в файле выражает, что нужно получить или сделать, а не то, как это нужно сделать. И в отличие от второй части нашего рассказа теперь вместо использования SQL мы применяем объекты и методы Python.
👥 Предоставление доступа к данным нескольким пользователям
К этому моменту мы узнали, как для доступа к одним и тем же данным могут использоваться pandas, SQLite и SQLAlchemy. Одним из решающих факторов при выборе между использованием плоского файла или базы данных является объем данных и количество связей между различными структурами данных. Еще один фактор, который следует учитывать, – с каким количеством пользователей мы делимся данными и вопрос критичности их рассинхронизации.
Проблема обеспечения единообразия данных обычно возникает, когда множество пользователей взаимодействуют с данными удаленно через сеть. Предоставление данных через серверное приложение и пользовательский интерфейс решает проблему целостности данных. В этом случае сервер – единственное приложение, имеющее доступ к базе данных на уровне файлов.
Последний пример – законченное веб-приложение и пользовательский интерфейс для образца существенно более крупной учебной базы данных SQLite Chinook, содержащей информацию о музыкальных исполнителях, альбомах, треках, жанрах и т. п. (11 связанных таблиц).
🌐 Использование Flask с SQLite и SQLAlchemy
Программа examples/example_3/chinook_server.py создает приложение Flask, с которым можно взаимодействовать с помощью браузера. В приложении используются следующие технологии:
- Flask Blueprint – часть Flask для делегирования задач отдельным модулям с заранее определенной функциональностью;
- Flask SQLAlchemy – расширение Flask, добавляющее в веб-приложения поддержку SQLAlchemy;
- Flask_Bootstrap4 упаковывает набор интерфейсных инструментов Bootstrap, интегрируя его с веб-приложениями Flask;
- Flask_WTF расширяет Flask с помощью WTForms, предоставляя вашим веб-приложениям удобный способ создания и проверки веб-форм;
- python_dotenv – модуль Python, используемый приложением для чтения переменных среды из файла и сохранения конфиденциальной информации за пределами программного кода.
Пример приложения довольно большой, и лишь некоторая его часть имеет отношение к руководству. По этой причине изучение кода оставлено в качестве упражнения для читателя. Тем не менее вы можете посмотреть представленный ниже скринкаст, за которой следует HTML-код шаблона домашней страницы и Python-скрипт, который динамически предоставляет данные.
HTML-шаблон Jinja2, который создает домашнюю страницу приложения:
Файл Python, ответственный за отображение страницы:
Заключение
Разумно задаться вопросом: является ли SQLite правильным выбором в качестве серверной части базы данных для веб-приложения. На веб-сайте SQLite указано, что SQLite – хороший выбор для сайтов, обслуживающих около 100 тыс. обращений в день. Если на вашем сайт посещений больше, в первую очередь вас стоит поздравить 🎉
Если вы реализовали веб-сайт с помощью SQLAlchemy, то можно перенести данные из SQLite в другую базу данных, такую как MySQL или PostgreSQL. Описательные модели данных SQLAlchemy останутся прежними.