Сообщить об ошибке.

Поддержка SQLAlchemy в приложение Flask.

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

Требования для работы расширения Flask-SQLAlchemy 2.x:

  • Python 3.7+;
  • Flask 0.12+;
  • SQLAlchemy 1.0.10+.

Установка модуля Flask-SQLAlchemy в виртуальное окружение.

# создаем виртуальное окружение, если нет
$ python3 -m venv .venv --prompt VirtualEnv
# активируем виртуальное окружение 
$ source .venv/bin/activate
# обновляем `pip`
(VirtualEnv):~$ python3 -m pip install -U pip
# ставим модуль `Flask-SQLAlchemy`
(VirtualEnv):~$ python3 -m pip install -U Flask-SQLAlchemy

Содержание:


Общий случай использования расширения Flask-SQLAlchemy.

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

После создания объект SQLAlchemy содержит все функции и помощники как из sqlalchemy, так и из sqlalchemy.orm. Кроме того, он предоставляет класс с именем Model, который является декларативной базой, которую можно использовать для объявления моделей:

# test_app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

# создаем приложение
app = Flask(__name__)
# загружаем конфигурацию
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
# передаем объект приложения `app` в `SQLAlchemy`
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

    def __repr__(self):
        return '<User %r>' % self.username

Чтобы создать исходную базу данных, необходимо импортировать объект db из интерактивной оболочки Python и запустить метод SQLAlchemy.create_all() для создания таблиц и базы данных:

>>> from test_app import db
>>> db.create_all()

Теперь, чтобы создать несколько пользователей:

>>> from test_app import User
>>> admin = User(username='admin', email='admin@example.com')
>>> guest = User(username='guest', email='guest@example.com')

Но их еще нет в базе. Записи необходимо добавить вызовом db.session.add(record), а затем зафиксировать командой db.session.commit():

>>> db.session.add(admin)
>>> db.session.add(guest)
>>> db.session.commit()

Доступ к данным в базе данных, также очень прост:

>>> User.query.all()
# [<User 'admin'>, <User 'guest'>]
>>> User.query.filter_by(username='admin').first()
# <User 'admin'>

Обратите внимание, что в классе User никогда не определяется метод __init__. Это связано с тем, что SQLAlchemy добавляет неявный конструктор ко всем классам моделей, который принимает ключевые аргументы для всех своих столбцов и отношений. При переопределении конструктора по какой-либо причине, чтобы сохранить поведение модели необходимо продолжать принимать **kwargs вызывав супер-конструктор с **kwargs:

class Foo(db.Model):

    def __init__(self, **kwargs):
        super(Foo, self).__init__(**kwargs)
        # далее делаем что-то нестандартное

Фабрика приложений и Flask-SQLAlchemy.

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

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)
    db.init_app(app)
    return app

Что он делает, так это подготавливает приложение app к работе с SQLAlchemy. При этом объект SQLAlchemy теперь не связан с приложением, так как может быть создано более одного приложения.

Как же SQLAlchemy узнает о приложении? Для этого нужно настроить контекст приложения. Если вы работаете внутри функции представления Flask или команды CLI, то это происходит автоматически. Если вы работаете внутри интерактивной оболочки, то придется сделать это самостоятельно (смотрите "Создание контекста приложения").

Если попытаться выполнить операции с базой данных вне контекста приложения, то можно увидеть следующую ошибку: "No application found. Either work inside a view function or push an application context."

В двух словах, сделайте что-то вроде этого:

>>> from yourapp import create_app
>>> app = create_app()
>>> app.app_context().push()

В качестве альтернативы используйте оператор with, чтобы позаботиться об очистке задействованных ресурсов:

def my_function():
    with app.app_context():
        user = db.User(...)
        db.session.add(user)
        db.session.commit()

Некоторые функции внутри модуля Flask-SQLAlchemy также могут опционально принимать приложение Flask для работы:

>>> from yourapp import db, create_app
>>> db.create_all(app=create_app())

Объявление моделей базы данных.

Обычно модуль Flask-SQLAlchemy ведет себя как правильно настроенная декларативная база данных. Для полного ознакомления по объявлению моделей БД рекомендуется прочитать документацию по SQLAlchemy. Наиболее распространенные варианты использования смотрите ниже.

При объявление моделей нужно иметь в виду:

  • Базовый класс для всех моделей называется db.Model. Он хранится в экземпляре SQLAlchemy, который нужно создать.
  • Некоторые части, которые требует пакет SQLAlchemy, для модуля Flask-SQLAlchemy являются необязательными. Например, имя таблицы устанавливается автоматически, если оно не переопределено. Имя таблицы будет получено из имени класса, преобразованного в нижний регистр, а записанный в стиле "CamelCase" преобразован в "camel_case". Чтобы переопределить имя таблицы (назначить собственное имя таблицы), необходимо установить атрибут класса __tablename__.

Смотрим очень простой пример:

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

    def __repr__(self):
        return '<User %r>' % self.username

Для определения столбца необходимо использовать класс db.Column(). Имя переменной, которой присваивается экземпляр db.Column() будет именем столбца. Если нужно использовать в таблице БД другое имя, то можно указать необязательный первый аргумент, который представляет собой строку с желаемым именем столбца таблице БД. Столбец, который используются в качестве первичного ключа, помечается с помощью primary_key=True. В одной таблице могут быть помечены несколько столбцов в качестве первичных ключей, в этом случае они становятся составным первичным ключом.

Типы столбцов являются первым аргументом конструктора db.Column(). Можно предоставить их напрямую, либо указать длину. Наиболее распространены следующие виды:

  • Integer: хранит целое число;
  • String(size): строка максимальной длины, в MySQL - максимум 256 символов (необязательно в некоторых базах данных, например PostgreSQL);
  • Text: какой-то более длинный текст, в MySQL - максимум 64 Kb;
  • DateTime: дата и время, выраженные как объект datetime Python;
  • Float: хранит значения с плавающей запятой;
  • Boolean: хранит логическое значение;
  • PickleType: хранит pickle объект Python;
  • LargeBinary: хранит большие произвольные двоичные данные;

Отношений между таблицами базы данных.

Отношения "один-ко-многим".

Наиболее распространенными отношениями являются отношения "один-ко-многим". Отношения между таблицами объявляются до того, как они будут установлены. Для этого можно использовать строки для ссылки на классы, которые еще не созданы (например, если Person определяет отношение к Address, которое объявляется позже в файле).

Отношения выражаются с помощью функции db.relationship(), но внешний ключ должен быть объявлен отдельно с классом db.ForeignKey():

class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    addresses = db.relationship('Address', backref='person', lazy=True)

class Address(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(120), nullable=False)
    person_id = db.Column(db.Integer, db.ForeignKey('person.id'),
        nullable=False)

Функция db.relationship() возвращает новое свойство. В приведенном примере db.relationship() указали на класс Address с целью загружать из него несколько строк. SQLAlchemy угадывает полезное значение по умолчанию из переданных аргументов. Если необходимо иметь связь "один-к-одному", то в функцию db.relationship() нужно передать uselist=False.

Аргумент nullable=False указывает SQLAlchemy создать столбец как NOT NULL (т.к. человек без имени или адрес электронной почты без связанного адреса не имеет смысла. Такое значение аргумента nullable подразумевается для столбца с первичным ключом. Также этот аргумент рекомендуется указать для всех столбцов, для понимание того что столбец, действительно допускает значение NULL, а не то, что его просто забыли указать.

Аргумент backref - это простой способ объявить новое свойство в классе Address. Затем можно использовать my_address.person, чтобы получить данные человека из БД, соответствующие этому адресу.

Аргумент lazy определяет, когда SQLAlchemy будет загружать данные из базы данных:

  • 'select' или True: (значение по умолчанию) означает, что SQLAlchemy будет загружать данные по мере необходимости за один раз, используя стандартный оператор выбора.
  • 'joined' или False: указывает SQLAlchemy загрузить отношение в том же запросе, что и родитель, используя оператор JOIN.
  • 'subquery' работает как 'joined', но SQLAlchemy будет использовать подзапрос.
  • 'dynamic' является особенным и может быть полезен, если результат запроса к БД возвращает много элементов и нужно всегда применять к ним дополнительные фильтры SQL. В этом случае SQLAlchemy вернет другой объект запроса, который, перед загрузкой элементов можно отфильтровать. Обратите внимание, что такое поведение нельзя превратить в другую стратегию загрузки при запросе, поэтому часто рекомендуется избегать использования lazy='dynamic' в пользу lazy=True. Объект запроса, эквивалентный динамическому отношению user.addresses, может быть создан с помощью Address.query.with_parent(user), при этом при необходимости можно использовать ленивую или активную загрузку самого отношения.

Как определять "ленивый" статус для обратных ссылок? С помощью функции db.backref():

class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    addresses = db.relationship('Address', lazy='select',
            backref=db.backref('person', lazy='joined'))

Отношения "многие-ко-многим".

Если нужно использовать отношения "многие-ко-многим", то необходимо определить вспомогательную таблицу, которая будет использоваться для этого отношения. Для этой вспомогательной таблицы настоятельно рекомендуется использовать не модель, а реальную таблицу:

tags = db.Table('tags',
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True),
    db.Column('page_id', db.Integer, db.ForeignKey('page.id'), primary_key=True)
)

class Page(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    tags = db.relationship('Tag', secondary=tags, lazy='subquery',
        backref=db.backref('pages', lazy=True))

class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True)

Здесь настраивается загрузка тегов Page.tags сразу после загрузки страницы, но с использованием отдельного запроса. При извлечении одной страницы, такая схема всегда приводит к двум запросам, но при запросе нескольких страниц, дополнительных запросов не будет.

С другой стороны, список страниц для тега нужен редко. Например, при получении тегов для конкретной страницы, этот список не понадобится. Поэтому обратная ссылка настроена на ленивую загрузку. Если нужно применить дополнительные параметры запроса в этом списке, можно либо переключиться на "динамическую" стратегию (lazy='dynamic'), с упомянутыми выше недостатками, либо получить объект запроса, используя Page.query.with_parent(some_tag), а затем использовать его точно так, как будет с объектом запроса из динамической связи.

Пример использование отношений между таблицами.

SQLAlchemy подключается к реляционным базам данных, а реляционные базы данных хороши своими отношениями между таблицами. Рассмотрим пример приложения, в котором используются две таблицы, связанные друг с другом:

from datetime import datetime

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(80), nullable=False)
    body = db.Column(db.Text, nullable=False)
    pub_date = db.Column(db.DateTime, nullable=False,
        default=datetime.utcnow)

    category_id = db.Column(db.Integer, db.ForeignKey('category.id'),
        nullable=False)
    category = db.relationship('Category',
        backref=db.backref('posts', lazy=True))

    def __repr__(self):
        return '<Post %r>' % self.title

class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)

    def __repr__(self):
        return '<Category %r>' % self.name

Сначала создадим несколько объектов:

>>> py = Category(name='Python')
>>> Post(title='Hello Python!', body='Python is pretty cool', category=py)
>>> p = Post(title='Snakes', body='Ssssssss')
>>> py.posts.append(p)
>>> db.session.add(py)

Как видите, нет необходимости добавлять объекты Post в сеанс базы данных session. Категория является частью сеанса, все объекты, связанные с ней отношениями, также будут добавлены. Неважно, вызывается ли db.session.add() до или после создания этих объектов. Связь также может быть выполнена с любой стороны отношений, так что сообщение Post может быть создано с категорией Category или добавлено в список сообщений категории.

Смотрим посты Post. Доступ к ним приведет к загрузке их из базы данных. Загрузка данных по отношениям в таблицах происходит "лениво", при этом это не заметно, т.к. загрузка списка происходит довольно быстро:

>>> py.posts
# [<Post 'Hello Python!'>, <Post 'Snakes'>]

Хотя "ленивая" загрузка связи выполняется быстро, она может легко стать серьезным узким местом, если инициировать дополнительные запросы в цикле для нескольких объектов. В этом случае SQLAlchemy позволяет переопределить стратегию загрузки на уровне запроса. Если необходимо, чтобы один запрос загрузил все категории и их сообщения, то можно сделать это следующим образом:

>>> from sqlalchemy.orm import joinedload
>>> query = Category.query.options(joinedload('posts'))
>>> for category in query:
...     print category, category.posts
# <Category 'Python'> [<Post 'Hello Python!'>, <Post 'Snakes'>]

Если нужно получить объект запроса для этой связи, то можно сделать это с помощью метода .with_parent(). Например, исключим этот пост о 'Snakes':

>>> Post.query.with_parent(py).filter(Post.title != 'Snakes').all()
# [<Post 'Hello Python!'>]

Значения конфигурации Flask-SQLAlchemy.

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

Список ключей конфигурации, которые в настоящее время понимает расширение:

  • SQLALCHEMY_DATABASE_URI: URI базы данных, который должен использоваться для подключения. Примеры:
    • sqlite:////tmp/test.db,
    • mysql://username:password@server/db
  • SQLALCHEMY_BINDS: Словарь, который сопоставляет ключи привязки с URI соединения SQLAlchemy. Дополнительные сведения о привязках смотрите в подразделе "Использования нескольких баз данных".
  • SQLALCHEMY_ECHO: Если установлено значение True, то SQLAlchemy будет регистрировать все инструкции, выданные stderr, что может быть полезно для отладки.
  • SQLALCHEMY_RECORD_QUERIES: Может использоваться для явного отключения или включения записи запросов. Запись запроса автоматически происходит в режиме отладки или тестирования.
  • SQLALCHEMY_TRACK_MODIFICATIONS: Если установлено значение True, то Flask-SQLAlchemy будет отслеживать модификации объектов и выдавать сигналы. Значение по умолчанию None, включает отслеживание, но выдает предупреждение о том, что в будущем оно будет отключено по умолчанию. Это требует дополнительной памяти и должно быть отключено, если не требуется.
  • SQLALCHEMY_ENGINE_OPTIONS: Словарь ключевых аргументов для отправки в функцию sqlalchemy.create_engine() пакета SQLAlchemy.
  • SQLALCHEMY_POOL_RECYCLE: Количество секунд, по истечении которых соединение автоматически перезапускается. Это необходимо для MySQL, который по умолчанию удаляет соединения после 8 часов простоя. Обратите внимание, что если используется MySQL, то Flask-SQLAlchemy автоматически устанавливает это значение равным 2 часам. Некоторые серверные части могут использовать другое значение времени ожидания по умолчанию. Этот ключ конфигурации устарел с версии 2.4 и будет удалено в версии 3.0.

Формат URI для подключения к БД.

Полный список URI подключения описан в документации пакета SQLAlchemy в разделе "Поддерживаемые базы данных". Здесь разобраны некоторые общие случаи подключения.

Пакет SQLAlchemy указывает источник механизма в виде URI в сочетании с необязательными ключевыми аргументами для указания параметров механизма. Форма URI:

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

Многие части строки являются необязательными. Если драйвер driver БД не указан, то выбирается драйвер по умолчанию (в этом случае не нужно указывать +).

Примеры подключения к популярным базам данных:

  • Postgres:

    postgresql://scott:tiger@localhost/mydatabase
    
  • MySQL:

    mysql://scott:tiger@localhost/mydatabase
    
  • SQLite (обратите внимание, что применяются соглашения о пути к платформе):

    # Unix/Mac (обратите внимание на 
    # четыре ведущие косые черты)
    sqlite:////absolute/path/to/foo.db
    # Windows (3 передние косые черты и 
    # экранирование обратной косой черты)
    sqlite:///C:\\absolute\\path\\to\\foo.db
    # Windows (альтернатива с использованием 
    # необработанной строки)
    r'sqlite:///C:\absolute\path\to\foo.db'
    
  • Oracle:

    oracle://scott:tiger@127.0.0.1:1521/sidname
    

Проблема тайм-аутов баз данных.

Некоторые сервера базы данных могут налагать разные тайм-ауты неактивных соединений, что мешает пулу соединений Flask-SQLAlchemy.

По умолчанию BD MariaDB настроена на тайм-аут 600 секунд. Этот тайм-аут часто трудно правильно выставить. Если что-то не так, то выдается исключение только для среды разработки - "2013: Lost connection to MySQL server during query". На "боевом" сервере, при неправильно выставленном тайм-ауте приложение будет выдавать HTTP-ошибку 500 server error.

Если используется серверная часть (или предварительно настроенная база данных как услуга) с более низким временем ожидания соединения, то рекомендуется установить для SQLALCHEMY_POOL_RECYCLE значение, меньшее, чем время ожидания вашей серверной части.

Примеры Select, Insert, Delete.

В примерах будем использовать определения модели из главы "Общий случай использования расширения Flask-SQLAlchemy".

Вставка записей в БД.

Прежде чем что-то запросить из БД, нужно будет вставить некоторые данные. Все модели должны иметь конструктор, поэтому обязательно добавьте его, если забыли. Конструкторы используются только пользователями, а не пакетом SQLAlchemy, поэтому только от пользователя зависит, как он их определяет.

Вставка данных в базу данных представляет собой трехэтапный процесс:

  • Создается объект Python.
  • Созданный объект добавляется в сессию.
  • Затем сессия фиксируется.

Здесь сессия - это не сессия Flask, а сессия Flask-SQLAlchemy. По сути, это усиленная версия транзакции базы данных. Вот как это работает:

# импортируем созданную модель таблицы 
>>> from yourapp import User
# создаем объект новой записи 
>>> me = User('admin', 'admin@example.com')
# добавляем объект новой записи в сессию БД
>>> db.session.add(me)
# фиксируем новую запись 
>>> db.session.commit()

Прежде чем добавить объект в сеанс, SQLAlchemy обычно не добавляет его сразу физически в базу данных ( не осуществляет транзакцию). Это общепринятое поведение, так как можно отказаться от внесения изменений в БД. Затем, вызов функции db.session.add() добавляет объект. Он составляет запрос к БД на SQL с оператором INSERT, но транзакцию еще не фиксирует. И наконец последний вызов db.session.commit() физически вставляет запись в БД с присвоением пользователю идентификатора id:

>>> me.id
# 1

Удаление записей из БД.

Удаление записей очень похоже, просто вместо add() нужно использовать delete():

>>> db.session.delete(me)
>>> db.session.commit()

Запрос/выбор записей в БД.

Модуль Flask-SQLAlchemy предоставляет атрибут .query в классе db.Model. При доступе к нему получим новый объект запроса по всем записям. Прежде чем запускать выбор .all() или .first(), для фильтрации/отбора записей можно использовать метод .filter(). Если необходимо использовать первичный ключ, то также можно использовать .get().

Выполняемые ниже запросы предполагают наличие следующих записей в базе данных:

idusernameemail
1adminadmin@example.com
2peterpeter@example.org
3guestguest@example.com

Получить пользователя по имени пользователя:

>>> peter = User.query.filter_by(username='peter').first()
>>> peter.id
# 2
>>> peter.email
# 'peter@example.org'

То же, что и выше, но для несуществующего имени пользователя дает None:

>>> missing = User.query.filter_by(username='missing').first()
>>> missing is None
# True

Выбор группы пользователей по более сложному выражению:

>>> User.query.filter(User.email.endswith('@example.com')).all()
# [<User 'admin'>, <User 'guest'>]

Упорядочивание пользователей по чему-то:

>>> User.query.order_by(User.username).all()
# [<User 'admin'>, <User 'guest'>, <User 'peter'>]

Ограничение количества возвращаемых записей пользователей:

>>> User.query.limit(1).all()
# [<User 'admin'>]

Получение пользователя по первичному ключу:

>>> User.query.get(1)
# <User 'admin'>

Запросы в представлениях Flask.

Если пишете функцию представления Flask, очень удобно возвращать ошибку 404 для отсутствующих записей. Так как это очень распространенная идиома, то модуль Flask-SQLAlchemy предоставляет помощник именно для этой цели. Вместо метода .get() можно использовать .get_or_404() и вместо метода .first() - .first_or_404(). Это приведет к ошибке 404 вместо возврата None:

@app.route('/user/<username>')
def show_user(username):
    user = User.query.filter_by(username=username).first_or_404()
    return render_template('show_user.html', user=user)

Кроме того, если нужно добавить описание возникшей ошибки в функцию flask.abort(), то описание ошибки можно использовать в качестве значения аргумента description.

>>> User.query.filter_by(username=username).first_or_404(description=f'There is no data with {username}')

Подключение одного приложения к разным БД.

Начиная с 0.12, Flask-SQLAlchemy может легко подключаться к нескольким базам данных. Для этого он предварительно настраивает пакет SQLAlchemy для поддержки нескольких "привязок" (bind).

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

Если для модели не указан ключ привязки, вместо него используется соединение по умолчанию (как в SQLALCHEMY_DATABASE_URI).

Пример конфигурации с несколькими подключениями.

Следующая конфигурация объявляет три подключения к базе данных. Специальный по умолчанию, а также два других именованных пользователя users и appmeta (который подключается к базе данных sqlite для доступа только для чтения к некоторым данным, которые приложение предоставляет внутри):

SQLALCHEMY_DATABASE_URI = 'postgres://localhost/main'
SQLALCHEMY_BINDS = {
    'users':        'mysqldb://localhost/users',
    'appmeta':      'sqlite:////path/to/appmeta.db'
}

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

Методы db..create_all() и db..drop_all() по умолчанию работают со всеми объявленными подключениями, включая привязку по умолчанию. Это поведение можно настроить, указав параметр привязки. Требуется либо одно имя привязки, '__all__' для ссылки на все привязки, либо список привязок. Связывание по умолчанию (SQLALCHEMY_DATABASE_URI) называется None:

>>> db.create_all()
>>> db.create_all(bind=['users'])
>>> db.create_all(bind='appmeta')
>>> db.drop_all(bind=None)

Указание подключений к БД в моделях.

При объявлении модели можно указать подключение к определенной БД, использовав атрибут __bind_key__:

class User(db.Model):
    __bind_key__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)

Внутренне, ключ соединения хранится в словаре таблицы info как "bind_key". Это важно знать, так как при создании табличного объекта напрямую, необходимо поместить туда и ключ соединения:

user_favorites = db.Table('user_favorites',
    db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
    db.Column('message_id', db.Integer, db.ForeignKey('message.id')),
    info={'bind_key': 'users'}
)

Если атрибут __bind_key__ указаy во всех моделях, то их можно использовать как обычно и не думать о подключениях. Модель сама подключается к указанному соединению с базой данных.

Пользовательская настройка моделей и взаимодействия с ними.

Модуль Flask-SQLAlchemy определяет разумные значения по умолчанию, но иногда требуется дополнительная настройка. Существуют различные способы настройки определения моделей и взаимодействия с ними.

Эти настройки применяются при создании объекта SQLAlchemy и распространяются на все модели, производные от его класса db.Model.

Класс модели.

Все модели SQLAlchemy наследуются от декларативного базового класса. В Flask-SQLAlchemy это отображается как db.Model, который расширяет все модели. Его можно настроить, создав подкласс по умолчанию и передав пользовательский класс model_class.

В следующем примере каждой модели присваивается целочисленный первичный ключ или внешний ключ для наследования объединенной таблицы.

Примечание. Целочисленные первичные ключи для всего не обязательно являются лучшим дизайном базы данных (это зависит от требований конкретного проекта), это только пример.

from flask_sqlalchemy import Model, SQLAlchemy
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declared_attr, has_inherited_table

class IdModel(Model):
    @declared_attr
    def id(cls):
        for base in cls.__mro__[1:-1]:
            if getattr(base, '__table__', None) is not None:
                type = sa.ForeignKey(base.id)
                break
        else:
            type = sa.Integer

        return sa.Column(type, primary_key=True)

db = SQLAlchemy(model_class=IdModel)

class User(db.Model):
    name = db.Column(db.String)

class Employee(User):
    title = db.Column(db.String)

Миксины модели.

Если поведение требуется не для всех моделей, то для настройки конкретных моделей необходимо использовать классы миксинов/примесей. Например, если некоторые модели должны отслеживать время их создания или обновления:

from datetime import datetime

class TimestampMixin(object):
    created = db.Column(
        db.DateTime, nullable=False, default=datetime.utcnow)
    updated = db.Column(db.DateTime, onupdate=datetime.utcnow)

class Author(db.Model):
    ...

class Post(TimestampMixin, db.Model):
    ...

Класс запросов.

Также можно настроить то, что доступно для использования в специальном свойстве запроса моделей. Например, предоставив метод .get_or():

from flask_sqlalchemy import BaseQuery, SQLAlchemy

class GetOrQuery(BaseQuery):
    def get_or(self, ident, default=None):
        return self.get(ident) or default

db = SQLAlchemy(query_class=GetOrQuery)

# получаем пользователя по идентификатору 
# или возвращаем экземпляр анонимного пользователя
user = User.query.get_or(user_id, anonymous_user)

и теперь все запросы, выполняемые из специального свойства запроса в моделях Flask-SQLAlchemy, могут использовать метод .get_or() как часть своих запросов. Все отношения, определенные с помощью db.relationship() (но не sqlalchemy.orm.relationship()), также будут снабжены этой функциональностью.

Также можно определить пользовательский класс запроса для отдельных отношений, указав ключевое слово query_class в определении. Это работает как с db.relationship(), так и с sqlalchemy.relationship():

class MyModel(db.Model):
    cousin = db.relationship('OtherModel', query_class=GetOrQuery)

Примечание. Если класс запроса определен для отношения, то он будет иметь приоритет над классом запроса, присоединенным к соответствующей модели.

Также можно определить конкретный класс запросов для отдельных моделей, переопределив атрибут класса query_class в модели:

class MyModel(db.Model):
    query_class = GetOrQuery

В этом случае метод .get_or() будет доступен только для запросов, исходящих из MyModel.query().

Пагинация страниц, класс flask_sqlalchemy.Pagination().

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

@app.route('/user/<username>')
def user(username):
    # получаем данные пользователя
    user = User.query.filter_by(username=username).first_or_404()
    # получаем номер текущей страницы 
    # пагинации из параметра URL `?page=`
    page = request.args.get('page', 1, type=int)
    # получаем сообщения для текущей страницы, 
    # отсортированные по дате. 
    # `POSTS_PER_PAGE` - сколько отображать 
    # сообщений на странице
    posts = user.posts.order_by(Post.timestamp.desc()).paginate(
        page, app.config['POSTS_PER_PAGE'], False)
    # получаем данные следующей страницы (если есть)
    next_url = url_for('user', username=user.username, page=posts.next_num) \
        if posts.has_next else None
    # получаем данные предыдущей страницы (если есть)
    prev_url = url_for('user', username=user.username, page=posts.prev_num) \
        if posts.has_prev else None
    # Это форма для отправки сообщений пользователю
    form = EmptyForm()
    return render_template('user.html', user=user, posts=posts.items,
                           next_url=next_url, prev_url=prev_url, form=form)

И шаблон представления:

# шаблон `user.html`
    ...
    {% for post in posts %}
        {% include '_post.html' %}
    {% endfor %}
    {% if prev_url %}
    <a href="{{ prev_url }}">Новые сообщения</a>
    {% endif %}
    {% if next_url %}
    <a href="{{ next_url }}">Старые сообщения</a>
    {% endif %}

Класс flask_sqlalchemy.Pagination(query, page, per_page, total, items) - это внутренний вспомогательный класс, возвращаемый BaseQuery.paginate(). В качестве объекта запроса query можно передать None, и в этом случае методы Pagination.prev() и Pagination.next() больше не будут работать.

Объект Pagination можно получить из любого объекте query в качестве его метода .paginate(). Члены этого объекта содержат список элементов запрошенной страницы. Другими словами, метод .paginate() вызывается на любом объекте query. Он принимает три аргумента:

  • Номер текущей страницы, отсчет начиная с 1;
  • количество элементов/записей отображаемых на странице,
  • флаг, отвечающий за "что делать", когда происходит превышение количества страниц пагинации. Если флаг выставлен в True, то при превышении клиенту возвращается ошибка 404. В противном случает будет возвращен пустой список элементов/записей вместо ошибки.

Свойства и методы объекта Pagination.

  • Pagination.has_next: возвращает True, если существует следующая страница.
  • Pagination.has_prev: возвращает True, если существует предыдущая страница.
  • Pagination.items = None: элементы для текущей страницы.
  • Pagination.iter_pages(left_edge=2, left_current=2, right_current=5, right_edge=2): перебирает номера страниц в разбивке на страницы. Четыре параметра определяют пороговые значения того, сколько чисел должно быть получено с обеих сторон. Пропущенные номера страниц отображаются как None. Вот как вы могли бы отобразить такую разбивку на страницы в шаблонах:

    {% macro render_pagination(pagination, endpoint) %}
    <div class=pagination>
    {%- for page in pagination.iter_pages() %}
      {% if page %}
        {% if page != pagination.page %}
          <a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
        {% else %}
          <strong>{{ page }}</strong>
        {% endif %}
      {% else %}
        <span class=ellipsis>…</span>
      {% endif %}
    {%- endfor %}
    </div>
    {% endmacro %}
    
  • Pagination.next(error_out=False): возвращает объект разбивки на страницы для следующей страницы.

  • Pagination.next_num: номер следующей страницы

  • Pagination.page = None: текущий номер страницы (начинается с 1)

  • Pagination.pages: общее количество страниц

  • Pagination.per_page = None: количество элементов, отображаемых на странице.

  • Pagination.prev(error_out=False): возвращает объект разбивки на страницы для предыдущей страницы.

  • Pagination.prev_num: номер предыдущей страницы.

  • Pagination.query = None: объект запроса, который использовался для создания этого объекта разбивки на страницы.

  • Pagination.total = None: общее количество элементов, соответствующих запросу.