Создание баз данных во Flask



В этом уроке речь пойдет о взаимодействии с базой данных. Сегодня существуют две конкурирующих системы баз данных:

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

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

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

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

SQLAlchemy и Flask-SQLAchemy

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

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

Flask-SQLAlchemy – это расширение, которое интегрирует SQLAlchemy во фреймворк Flask. Он также предлагает дополнительные методы, благодаря которым работать с SQLAlchemy становится немного проще. Установить Flask-SQLAlchemy вместе с дополнительными модулями можно с помощью следующей команды:

(env) [email protected]:~/flask_app$  pip install flask-sqlalchemy

Для использования Flask-SQLAlchemy нужно импортировать класс SQLAlchemy из пакета flask_sqlalchemy и создать экземпляр объекта 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.

Дальше нужно сообщить SQLAlchemy местоположение базы данных в виде URI. Формат URI базы данных следующий:

dialect+driver://username:[email protected]:port/database

dialect ссылается на имя базы данных, такое как mysql, mssql, postgresql и так далее.

driver ссылается на DBAPI, который он использует, чтобы соединяться с базой данных. По умолчанию SQLAlchemy работает только с SQLite без дополнительных драйверов. Чтобы работать с другими базами данных, нужно установить конкретный драйвер для базы данных, совместимый с DBAPI.

Что такое DBAPI?

DBAPI – это всего лишь стандарт, определяющий API Python для доступа к базам данных от разных производителей.

Следующая таблица содержит некоторые базы данных и драйвера для них, совместимые с 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:[email protected]/my_db'

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

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

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

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

# Для 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:[email protected]/flask_app_db'

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

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

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

Модель — это класс в Python, который представляет собой таблицу базы данных. Ее атрибуты сопоставляются со столбцами таблицы. Класс модели наследуется из db.Mobel и определяет колонки как экземпляры класса 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 с 5 переменными класса. Каждая переменная класса, кроме __tablename__, — это экземпляр класса db.Column. __tablename__ — это специальная переменная класса, используемая для определения имени таблицы базы данных. По умолчанию SQLAlchemy не следует соглашению о создании имен во множественном числе, поэтому название таблицы здесь — это название модели. Если на хочется опираться на такое поведение, следует использовать переменную __tablename__, чтобы явно указать название таблицы.

Первый аргумент конструктора db.Column() — это тип колонки, которая создается. SQLAlchemy предлагает большое количество типов колонок, а если их недостаточно, то можно создать свои. Следующая таблица описывает основные типы колонок в 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()). Так сделано, потому что при исполнении кода вызывать метод datetime.utcnow() нет необходимости. Вместо этого его стоит вызывать, когда запись добавляется или обновляется.

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

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

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

Откроем main2.py, чтобы добавить модели Category и Tag:

#...
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.Column с помощью ограничения db.ForeignKey в дочернем классе.
  2. Определить новое свойство с помощью инструкции db.relationship в родительском классе. Это свойство будет использоваться для получения доступа к связанным объектам.

Откроем main2.py, чтобы изменить модели Post и Catеgory:

#...
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'))
#...

Здесь для модели Post в Category были добавлены два новых атрибута: posts и category_id.

db.ForeignerKey() принимает имя столбца, внешний ключ которого используется. Здесь значение categories.id передается исключению db.ForeignKey(). Это значит, что атрибут category_id у Post может принимать значение только у колонки id таблицы categories.

Далее в модели Catagory имеется атрибут posts, определенный инструкцией db.relationship(). db.relationship() используется для добавления двунаправленной связи. Другими словами, она добавляет атрибут классу модели для доступа к связанным объектам. Простыми словами, она принимает как минимум один позиционный аргумент, который является именем класса на другой стороне отношений.

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

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

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

добавляет атрибут category объекту Post. Это значит, что если есть объект Post (например, p), тогда доступ к категории можно получать с помощью p.category.

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

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

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

Создание отношения один-к-одному в SQLAlchemy – это почти то же самое, что и отношение один-ко-многим. Единственное отличие — то, что инструкции db.relationship() передается дополнительный аргумент uselist=False. Например:

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) не может быть большого одного водительского удостоверения (driver license). Поэтому отношения между сотрудником и правами — один-к-одному.

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

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

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

Пост в блоге обычно имеет один или несколько тегов. Аналогичным образом один тег может ассоциироваться с одним или несколькими постами. Так образовывается отношение между posts и tags. Недостаточно добавить внешний ключ, ссылающийся на id постов, потому что у тега может быть один или несколько постов.

В качестве решения нужно создать новую таблицу ассоциаций, определив 2 внешних ключа, ссылающихся на колонки 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')
#...

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

Дальше нужно сообщить классу модели о таблице ассоциаций, которая будет использоваться. За это отвечает аргумент-ключевое слово secondary. На 18 строке db.relationship() вызывается с аргументом secondary, значение которого — post_tags. Хотя отношение было определено в модели Tag, его можно так же просто определить в модели Post.

Если есть, например, объект p класса Post, тогда доступ ко всем его тегам можно получить с помощью p.tags. С помощью объекта класса Tag (t), доступ к постам можно получить командой t.posts.

Пришло время создать базу данных и таблицы.

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

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

Стоит напомнить, что по умолчанию SQLAlchemy работает только с базой данных SQLite. Для работы с другими базами данных нужно установить драйвер, совместимый с DBAPI. Для использования MySQL подойдет драйвер PyMySql.

(env) [email protected]:~/flask_app$ pip install pymysql

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

(env) [email protected]:~/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) [email protected]:~/flask_app$

Эта команда создает базу данных flask_app_db с полной поддержкой Unicode.

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

(env) [email protected]:~/flask_app$ python main2.py shell
>>>
>>> from main2 import db
>>>
>>> db.create_all()
>>>

Метод create_all() создает таблицы только в том случае, если их нет в базе данных. Поэтому запускать его можно несколько раз. Также этот метод не берет во внимание изменения моделей при создании таблиц. Это значит, что если запустить метод create_all() после изменения его метода, когда таблица уже создана, то он не поменяет схему таблицы. Чтобы сделать это, нужно воспользоваться инструментом переноса Alembic. О том, как переносить базы данных с помощью Alembic, будет рассказано в отдельном уроке «Перенос базы данных с помощью 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 можно отсюда.

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

База данных flask_app_db имеет 4 таблицы. Таблицы с названиями categories, posts и tags созданы прямо из моделей, а post_tags — это таблица ассоциаций, которая представляет собой отношение многие-ко-многим между моделями Post и Tag.

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

Все таблицы на месте. Пора добавить в них какие-то данные.