В этом уроке мы рассмотрим, как взаимодействовать с базой данных во Flask. На сегодняшний день существуют две основные категории баз данных:

  1. Реляционные базы данных.
  2. Нереляционные или NoSQL базы данных.

Реляционные базы данных традиционно широко используются в веб-приложениях и остаются фаворитами многих ведущих компаний, таких как Facebook. Они структурируют данные в виде таблиц с колонками и связывают разные таблицы с помощью внешних ключей. Реляционные базы также предлагают поддержку транзакций, что означает выполнение SQL-операторов по принципу атомарности («всё или ничего»).

NoSQL базы данных за последние годы стали популярнее. Они отказываются от табличной модели данных, используя другие структуры, такие как документные хранилища или хранилища ключей и значений. В отличие от реляционных, NoSQL обычно не поддерживают транзакции, но предлагают преимущество в виде скорости.

Реляционные базы уже давно зарекомендовали себя как надёжные и безопасные, что и обуславливает их преимущественное использование в нашем дальнейшем рассмотрении взаимодействия с базами во Flask. Это не означает, что NoSQL не имеет применения — всё зависит от конкретных задач, но мы сосредоточимся на работе с реляционными базами данных.

SQLAlchemy и Flask-SQLAlchemy

SQLAlchemy представляет собой мощный инструмент для работы с реляционными базами данных в Python, созданный Майком Байером в 2005 году. SQLAlchemy поддерживает популярные базы данных, включая MySQL, PostgreSQL, Oracle, MS-SQL, SQLite и другие.

Этот инструмент включает мощный ORM (объектно-реляционное отображение), позволяющий использовать объектно-ориентированный код вместо традиционного SQL. Однако, если необходимо, можно применять и чистый SQL для большего контроля.

Flask-SQLAlchemy — это расширение, которое объединяет SQLAlchemy с Flask, предоставляя дополнительные возможности для упрощения интеграции. Для установки Flask-SQLAlchemy выполните команду:

(env) gvido@vm:~/flask_app$  pip install flask-sqlalchemy

Чтобы работать с Flask-SQLAlchemy, импортируйте класс SQLAlchemy из пакета flask_sqlalchemy и создайте его экземпляр, передав текущему приложению. Изменения в файле main2.py выглядят так:

#...
from forms import ContactForm
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'a really really really really long secret key'

manager = Manager(app)
db = SQLAlchemy(app)

class Faker(Command):
#...

Объект db дает доступ к функциям SQLAlchemy.

Теперь необходимо указать SQLAlchemy, где расположена база данных, предоставляя URI в следующем формате:

dialect+driver://username:password@host:port/database

Dialect — это название базы данных, такое как mysql, mssql, postgresql и так далее.

Driver указывает на DBAPI, используемый для соединения с базой. Без дополнительных драйверов SQLAlchemy поддерживает только SQLite. Для работы с другими нужно установить соответствующий DBAPI-совместимый драйвер.

Что представляет собой DBAPI?

DBAPI — это стандарт, описывающий Python API для взаимодействия с СУБД разных производителей.

Таблица ниже содержит список популярных баз данных и их соответствующих драйверов для DBAPI:

База данных Драйвер DBAPI
MySQL PyMysql
PostgreSQL Psycopg 2
MS-SQL pyodbc
Oracle cx_Oracle

Username и password вводятся при необходимости, а если указаны, их используют для авторизации в базе.

Host — расположение сервера базы данных.

Port — порт, использующийся сервером базы данных.

Database — название базы данных.

Примеры URL баз данных для наиболее распространенных типов:

# URL базы данных для MySQL с использованием драйвера PyMysql
'mysql+pymysql://root:pass@localhost/my_db'

# URL базы данных для PostgreSQL с использованием psycopg2
'postgresql+psycopg2://root:pass@localhost/my_db'

# URL базы данных для MS-SQL с использованием драйвера pyodbc
'mssql+pyodbc://root:pass@localhost/my_db'

# URL базы данных для Oracle с использованием драйвера cx_Oracle
'oracle+cx_oracle://root:pass@localhost/my_db'

Формат URL для SQLite несколько отличается, так как это файловая база данных и она не требует логина и пароля, требуется лишь путь к файлу базы данных:

# Для Unix / Mac мы используем 4 слеша
sqlite:////absolute/path/to/my_db.db

# Для Windows мы используем 3 слеша
sqlite:///c:/absolute/path/to/mysql.db

Flask-SQLAlchemy использует конфигурационный параметр SQLALCHEMY_DATABASE_URI для указания URI базы данных Откройте main2.py и добавьте параметр SQLALCHEMY_DATABASE_URI:

#...
app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'a really really really really long secret key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:pass@localhost/flask_app_db'

manager = Manager(app)
db = SQLAlchemy(app)
#...

В этом курсе предполагается использование базы данных MySQL. Убедитесь, что MySQL работает на вашем компьютере перед переходом к следующему этапу.

Создание моделей

Модель в контексте SQLAlchemy представляет собой класс Python, который отражает структуру таблицы базы данных. Атрибуты класса сопоставляются со столбцами таблицы, наследована от db.Model и определяет колонки как экземпляры db.Column. В файле main2.py добавьте следующий класс перед функцией updating_session():

#...
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

#...

class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer(), primary_key=True)
    title = db.Column(db.String(255), nullable=False)
    slug = db.Column(db.String(255), nullable=False)
    content = db.Column(db.Text(), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)

    def __repr__(self):
        return "<{}:{}>".format(self.id,  self.title[:10])

Здесь создается модель Post с пятью атрибутами. Каждый из них, кроме __tablename__, представлен как экземпляр db.Column. Атрибут __tablename__ обычно определяет имя таблицы в базе, однако, если нужно отклониться от соглашения SQLAlchemy (использовать множественное число в качестве имени таблицы), можно вручную задать его, используя __tablename__.

Первый аргумент в db.Column() определяет тип создаваемой колонки. SQLAlchemy предлагает множество встроенных типов, можно создавать и пользовательские. Таблица ниже иллюстрирует основные типы колонок и их соответствия в Python и SQL.

SQLAlchemy Python SQL
BigInteger int BIGINT
Boolean bool BOOLEAN или SMALLINT
Date datetime.date DATE
DateTime datetime.date DATETIME
Integer int INTEGER
Float float FLOAT или REAL
Numeric decimal.Decimal NUMERIC
Text str TEXT

Также можно задавать дополнительные ограничения для колонок через ключевые аргументы для db.Column. Таблица ниже содержит часто используемые ограничения:

Ограничение Описание
nullable Когда значение False, колонка обязательна. Значение по умолчанию — True.
default Устанавливает значение по умолчанию для колонки.
index Создает индексированную колонку, если True.
onupdate Устанавливает значение по умолчанию при обновлении записи.
primary_key Если True, делает колонку первичным ключом таблицы.
unique Обеспечивает уникальность значений в колонке, если True.

В 16-17 строках определен метод __repr__(), который возвращает строковое представление объекта. Это необязательное, но полезное дополнение.

Обратите внимание, что значениями по умолчанию для created_on и updated_on указаны методы datetime.utcnow, а не их вызовы с круглыми скобками (datetime.utcnow()). Это связано с тем, что вызов метода должен осуществляться в момент добавления или обновления записи.

Определение отношений (связей)

Ранее была создана модель Post с несколькими полями. Однако реальность такова, что модели редко существуют обособленно. Чаще всего они связаны с другими моделями через различные типы отношений: один-к-одному, один-ко-многим и многие-ко-многим.

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

Внесем модели Category и Tag в файл main2.py:

#...
def updating_session():
    #...
    return res

class Category(db.Model):
    __tablename__ = 'categories'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    slug = db.Column(db.String(255), nullable=False)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)

    def __repr__(self):
    return "<{}:{}>".format(id, self.name)

class Posts(db.Model):
    # ...

class  Tag(db.Model):
    __tablename__ = 'tags'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    slug = db.Column(db.String(255), nullable=False)
    created_on  =  db.Column(db.DateTime(), default=datetime.utcnow)

    def __repr__(self):
        return "<{}:{}>".format(id, self.name)
#...
Отношение один-ко-многим

Чтобы установить отношение один-ко-многим, обычно требуется добавить внешний ключ в таблицу, соответствующую дочернему классу. Это наиболее распространенный тип связей. Для реализации в SQLAlchemy необходимо:

  1. В дочернем классе объявить столбец с ограничением db.ForeignKey.
  2. В родительском классе добавить свойство с помощью db.relationship, для доступа к связанным объектам.

Внесем изменения в модели Post и Category в main2.py:

#...
class Category(db.Model):
    # ...
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    posts = db.relationship('Post', backref='category')

class Post(db.Model):
    # ...
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)
    category_id = db.Column(db.Integer(), db.ForeignKey('categories.id'))
#...

Новые атрибуты posts и category_id добавляются соответственно к моделям Category и Post.

db.ForeignKey() принимает имя столбца с внешним ключом. В примере указано categories.id, что означает, что category_id в Post ссылается на id в categories.

В Category также имеется атрибут posts, который определен с использованием db.relationship(). Этот метод не добавляет реальную колонку, а создает атрибут для доступа к связанным объектам. Он требует как минимум одного позиционного аргумента — название класса на другой стороне отношений.

class Category(db.Model):
    # ...
    posts = db.relationship('Post')

Например, у объекта Category (скажем c) можно получить все посты используя c.posts. Если нужно получить категорию для объекта Post, используется backref. Так, в коде:

posts = db.relationship('Post', backref='category')

добавляет атрибут category к объекту Post, поэтому p.category предоставляет доступ к категории.

Атрибуты category и posts упрощают работу с объектами Post и Category, но не являются реальными колонками в базе данных.

Важно отметить, db.relationship() можно определять с любой стороны, в отличие от внешнего ключа, который должен принадлежать стороне «много».

Отношение один-к-одному

Связь один-к-одному в SQLAlchemy похожа на один-ко-многим, но с дополнительным аргументом uselist=False в db.relationship(). Пример:

class  Employee(db.Model):
    __tablename__ = 'employees'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    designation = db.Column(db.String(255), nullable=False)
    doj = db.Column(db.Date(), nullable=False)
    dl = db.relationship('DriverLicense', backref='employee', uselist=False)

class DriverLicense(db.Model):
    __tablename__ = 'driverlicense'
    id = db.Column(db.Integer(), primary_key=True)
    license_number = db.Column(db.String(255), nullable=False)
    renewed_on = db.Column(db.Date(), nullable=False)
    expiry_date = db.Column(db.Date(), nullable=False)
    employee_id = db.Column(db.Integer(), db.ForeignKey('employees.id')) # Foreign key

Примечание: у сотрудника есть только одно водительское удостоверение, поэтому отношения между классами Employee и DriverLicense является один-к-одному.

Имея объект Employee, можно использовать e.dl для получения объекта DriverLicense. Если не использовать аргумент uselist=False в db.relationship(), отношение будет один-ко-многим, и e.dl будет вести список объектов DriverLicense. Однако, атрибут employee всегда вернёт один объект.

Отношение многие-ко-многим

Отношение многие-ко-многим реализуется с использованием вспомогательной таблицы. Например, в блоге это может быть система тегов для постов.

Один пост может иметь несколько тегов, а один тег может относиться к нескольким постам. Для решения этой задачи необходимо создать ассоциативную таблицу с двумя внешними ключами, где первый ссылается на post.id, а второй — на tag.id.

Отношение многие-ко-многим

Как показано на схеме, отношение многие-ко-многим между постом и тегом достигается благодаря двум связям один-ко-многим: между posts и post_tags и между tags и post_tags. Приведённый далее код демонстрирует создание такой структуры в SQLAlchemy. В main2.py добавьте следующий код:

# ...
class Category(db.Model):
    # ...
    def __repr__(self):
        return "<{}:{}>".format(id, self.name)

post_tags = db.Table('post_tags',
    db.Column('post_id', db.Integer, db.ForeignKey('posts.id')),
    db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'))
)

class Post(db.Model):
    # ...

class Tag(db.Model):
    # ...
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    posts = db.relationship('Post', secondary=post_tags, backref='tags')
#...

Таблица ассоциаций создается с помощью db.Table() в 7-10 строках. Первый аргумент — название таблицы, последующие параметры — колонки. Возможно, синтаксис создания таблицы ассоциации покажется необычным по сравнению с созданием класса модели, так как для этого используется SQLAlchemy Core — отдельный компонент.

Следует сообщить классу модели, какая таблица ассоциации будет использоваться. Это достигается аргументом secondary. На 18 строке db.relationship() вызывается с аргументом secondary равным post_tags. Хотя реализация проведена в модели Tag, её можно было бы определить и в Post.

При наличии объекта класса Post, например, p, все его теги доступны через p.tags. Для объекта Tag (t), посты доступны через t.posts.

Теперь, когда структура моделей определена, можно перейти к созданию базы данных и таблиц в ней.

Создание таблиц

Для успешного выполнения этапов этого урока, убедитесь в наличии MySQL на вашем устройстве.

Отметим, что если не установлены дополнительные драйверы, SQLAlchemy работает только с SQLite. Для MySQL установите драйвер PyMySql:

(env) gvido@vm:~/flask_app$ pip install pymysql

После этого следует авторизоваться в сервере MySQL и с использованием команды, создать базу данных flask_app_db:

(env) gvido@vm:~/flask_app$ mysql -u root -p
mysql>
mysql> CREATE DATABASE flask_app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Query OK, 1 row affected (0.26 sec)
mysql> \q
Bye
(env) gvido@vm:~/flask_app$

Эта команда создаёт базу с полным набором символов Unicode, обеспечивая корректное хранение информации на любом языке.

Необходимые таблицы создаются вызовом метода create_all() класса SQLAlchemydb. Затем выполните следующую команду через оболочку Python:

(env) gvido@vm:~/flask_app$ python main2.py shell
>>>
>>> from main2 import db
>>>
>>> db.create_all()
>>>

Метод create_all() создаёт таблицы, если они ещё не существуют и проигнорирует изменения в структурах моделей в случае их уже существования. Таким образом, запускать метод можно многократно, но для изменения схем таблиц после первого вызова понадобятся специализированные инструменты, такие как Alembic.

Чтобы убедиться в правильности создания таблиц, авторизуйтесь в сервер MySQL и выполните следующую команду:

mysql>
mysql> use flask_app_db
Database changed
mysql>
mysql> show tables;
+------------------------+
| Tables_in_flask_app_db |
+------------------------+
| categories 		 |
| post_tags 		 |
| posts  			 |
| tags 			 |
+------------------------+
4 rows in set (0.02 sec)

mysql>

Альтернативный способ проверить состояние таблиц — использовать админку вроде HeidiSQL. HeidiSQL представляет собой кроссплатформенный инструмент с открытым исходным кодом для управления MySQL, MS-SQL и PostgreSQL. Используя его, можно просматривать данные, редактировать их, а также управлять схемами, без необходимости написания SQL-запросов. Скачать HeidiSQL можно отсюда.

Подключившись к flask_app_db через HeidiSQL, в списке таблиц вы увидите что-то наподобие этого снимка экрана:
управление базами данных MySQL, MS-SQL и PostgreSQL

База flask_app_db теперь имеет четыре таблицы: categories, posts, tags и вспомогательную post_tags, которая связывает модели Post и Tag.

Метод drop_all() в классе SQLAlchemy удаляет все таблицы в базе, игнорируя условия наличия данных. Этот метод полезен при необходимости очистки, но требует осторожности чтобы избежать потери данных.

Таблицы подготовлены — следующий шаг заключается в добавление данных в базу.

Вопросы для самопроверки:

  1. Что делает Flask-SQLAlchemy?
  2. то такое DBAPI и для чего оно используется?
  3. Приведи пример URI для подключения к база данных MySQL с использованием драйвера PyMysql.
  4. Что представляет собой модель в контексте SQLAlchemy и как она создается?
  5. Как создается отношение один-ко-многим в SQLAlchemy?
  6. Как реализовать отношение многие-ко-многим с использованием db.Table() в SQLAlchemy?