Flask-SQLAlchemy: установка, создание таблиц, добавление записей

Файл проекта: https://github.com/selfedu-rus/flsite_sqlalchemy-23

На всех наших занятиях этого курса по Flask мы с вами использовали БД SQLite: непосредственно подключались к ней и работали с таблицами через SQL-запросы. Но это не лучшее решение. Для реальных сайтов следует выбирать более функциональные СУБД, рассчитанные на большую нагрузку, когда множество пользователей одновременно могут записывать и читать данные из разных таблиц. Например:

MySQL, PostgreSQL, Oracle и другие.

Причем, разработчик сайта наперед может не знать с какой именно СУБД будет работать WSGI-приложение. Да и потом, уже в процессе работы, выбор может пасть на другой тип БД. Как в этом случае построить универсальную программу не привязанную к конкретной СУБД? В Python часто для этого используют пакет

SQLAlchemy

который является интерфейсом, построенным по технологии ORM (Object Role Model), позволяющий оперировать таблицами БД как объектами языка Python. Причем этот интерфейс универсален и на уровне WSGI-приложения не привязан к конкретной СУБД.

Так вот, для фреймворка Flask разработано специальное расширение:

Flask-SQLAlchemy

которое значительно упрощает настройку и использование самого SQLAlchemy. И о нем пойдет речь на этом занятии.

Вначале, как всегда, его нужно установить. Для этого достаточно выполнить команду:

pip install Flask-SQLAlchemy

и он будет готов к использованию. Далее, я создам новый проект, чтобы нам не разбираться в прежнем коде и не тратить на это время. И, к тому же, так будет проще воспринимать этот материал.

Настройка Flask-SQLAlchemy

Первым делом нужно подключить Flask-SQLAlchemy к нашей программе. Для этого выполним его импорт:

from flask_sqlalchemy import SQLAlchemy
from flask import Flask

и, затем, после создания экземпляра приложения, создадим в конфигурации специальную константу 'SQLALCHEMY_DATABASE_URI':

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
 
if __name__ == "__main__":
    app.run(debug=True)

Эта константа определяет вид используемой СУБД (в данном случае SQLite) и местоположение файла БД (в данном случае – корневой каталог программы). Именно определение этой константы связывает SQLAlchemy с той или иной СУБД. То есть, если в будущем потребуется другая БД, то мы можем просто переопределить эту константу, например, так:

  • postgresql://user:password@localhost/mydatabase
  • mysql://user:password@localhost/mydatabase
  • oracle://user:password@127.0.0.1:1521/mydatabase

и автоматически к программе подключится указанная СУБД. Как видите, все достаточно просто и удобно.

После настройки конфигурации необходимо создать экземпляр класса SQLAlchemy, через который и осуществляется работа с БД. В самом простом варианте это делается так:

db = SQLAlchemy(app)

Здесь при создании класса передается ссылка на текущее приложение, с которым он будет связан.

Создание таблиц во Flask-SQLAlchemy

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

Так вот, концепция SQLAlchemy заключается в отображении таблиц с помощью Python-классов. То есть, нам достаточно в программе прописать два класса с именами

Users и Profiles

которые бы расширяли базовый класс модели таблиц:

SQLAlchemy.Model

Сначала определим класс Users (порядок не имеет значения, просто иду по рисунку):

class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(50), unique=True)
    psw = db.Column(db.String(500), nullable=True)
    date = db.Column(db.DateTime, default=datetime.utcnow)
 
    def __repr__(self):
        return f"<users {self.id}>"

Смотрите, мы здесь указываем базовый класс Model, который и превращает класс Users в модель таблицы для SQLAlchemy. А поля таблицы прописываются как обычные переменные, которые ссылаются на специальный класс Column. Этот класс как раз и указывает SQLAlchemy воспринимать эти переменные как поля таблицы. Причем, имя таблицы будет соответствовать имени класса, записанное малыми буквами, а имена переменных – именам полей в этой таблице. Далее, в конструкторе класса Column мы указываем тип поля и дополнительные параметры. Например, поле id будет создаваться как целочисленное и представлять собой главный ключ (то есть, оно будет принимать уникальное значение для каждой записи). Далее, тип String(50) – это строка максимальной длиной в 50 символов, а параметр unique=True означает, что оно должно быть уникальным среди всех записей данной таблицы. Следующее поле psw также является строкой и параметр nullable=True указывает, что оно должно обязательно содержать данные, т.е. пустым быть не может. Наконец, последнее поле date имеет тип datetime для хранения даты и по умолчанию ему присваивается текущая дата, взятая с помощью функции datetime.utcnow. Выше в программе мы ее импортируем:

from datetime import datetime

В конце описания класса идет «магическая» функция __repr__, которая определяет способ отображения класса в консоли. С ее помощью мы будем выводить класс в виде строки формата:

<users идентификатор>

Она здесь прописана для удобства и к функционированию таблиц не имеет отношения.

По аналогии создается вторая таблица Profiles:

class Profiles(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=True)
    old = db.Column(db.Integer)
    city = db.Column(db.String(100))
 
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
 
    def __repr__(self):
        return f"<profiles {self.id}>"

Здесь все достаточно очевидно, только дополнительно прописано целочисленное поле user_id, определенное как внешний ключ. Как раз через него можно будет связывать эти две таблицы, то есть, конкретного пользователя с его профайлом.

Вообще, наиболее распространенными типами данными в SQLAlchemy, являются следующие:

  • Integer – целочисленный;
  • String(size) – строка максимальной длиной size;
  • Text – текст (в формате Unicode);
  • DateTime – дата и время представленные в формате объекта datetime;
  • Float – число с плавающей точкой (вещественное);
  • Boolean – логическое значение;
  • LargeBinary – для больших произвольных бинарных данных (например, изображений).

Итак, таблицы определены и теперь можно их создать в БД. Для этого файл (или файлы) БД нужно перенести в какое-либо другое место (или удалить) и, затем, в консоли Python выполнить следующие команды:

from app import db

db.create_all()

Здесь app – это название файла, в котором инициализирована переменная db. А метод create_all как раз и создает таблицы, используя классы Users и Profiles.

Добавление записей

Пришло время создать в нашей программе первую функцию представления для регистрации пользователей, где мы могли бы добавлять новые записи в таблицы БД. Вначале пропишем обработчик:

@app.route("/register", methods=("POST", "GET"))
def register():
    return render_template("register.html", title="Регистрация")

И определим шаблон register.html на основе базового шаблона layout.html:

- layout.html:

<!DOCTYPE html>
<html>
<head>
         <link type="text/css" href="{{ url_for('static', filename='css/styles.css')}}" rel="stylesheet" />
         <title>{{title}}</title>
</head>
<body>
<ul class="mainmenu">
<li><a href="{{ url_for('index') }}">Главная</a></li>
<li><a href="{{ url_for('register') }}">Регистрация</a></li>
</ul>
 
<div class="content">
{% block content %}
{% endblock %}
</div>
</body>
</html>

- register.html:

{% extends 'layout.html' %}
 
{% block content %}
{% for cat, msg in get_flashed_messages(True) %}
<div class="flash {{cat}}">{{msg}}</div>
{% endfor %}
<form action="" method="post" class="form-contact">
<label>Имя: </label> <input type="text" name="name" value="" requied />
<label>Возраст: </label> <input type="text" name="old" value="" requied />
<label>Город: </label> <input type="text" name="city" value="" requied />
<label>Email: </label> <input type="text" name="email" value="" requied />
<label>Пароль: </label> <input type="password" name="psw" value="" requied />
<label>Повтор пароля: </label> <input type="password" name="psw2" value="" requied />
<input type="submit" value="Регистрация" />
</form>
{% endblock %}

Здесь, в принципе, должно быть все вам знакомо: мы описали простую форму с набором полей и кнопкой «Регистрация». Осталось в самом обработчике выполнить добавление записей на основе данных формы:

    if request.method == "POST":
        # здесь должна быть проверка корректности введенных данных
        try:
            hash = generate_password_hash(request.form['psw'])
            u = Users(email=request.form['email'], psw=hash)
            db.session.add(u)
            db.session.flush()
 
            p = Profiles(name=request.form['name'], old=request.form['old'],
                         city=request.form['city'], user_id = u.id)
            db.session.add(p)
            db.session.commit()
        except:
            db.session.rollback()
            print("Ошибка добавления в БД")
 
        return redirect(url_for('index'))

Сначала, конечно, идет проверка, что данные переданы методом POST, затем, по идее должна быть проверка корректности заполнения полей формы. Но мы пропустим этот шаг, чтобы не перегружать занятие лишней информацией. И после этого, в блоке try осуществляем работу с БД.

В первую очередь мы создаем экземпляр класса Users и через именованные параметры передаем данные из формы. Причем, именованные параметры должны совпадать с именами переменных, объявленных нами ранее. В результате создается объект с данными по email и паролю, который и представляет собой будущую запись в таблице users.

Далее, чтобы ее добавить, происходит обращение к специальному объекту session – сессии БД и в нее добавляется запись с помощью метода add. В качестве параметра этот метод принимает ссылку на объект класса Users. Но эта запись пока еще хранится в сессии – памяти устройства. Физически в таблицу она не занесена. Поэтому, далее, выполняется метод flush, который из сессии перемещает запись в таблицу. Но пока все изменения происходят в памяти устройства и, физически, файл БД остается прежним. Это необходимо, на случай возникновения исключений (ошибок) и «отката» состояния БД к исходному состоянию с помощью метода rollback.

Если ошибок не возникает, то формируется следующий экземпляр класса Profiles с набором данных из формы и дополнительно берется значение u.id, которое сформировалось после метода flush при помещении записи в таблицу users. Вот именно поэтому мы его и вызывали. Далее, запись помещается в сессию и вызывается метод commit, который уже физически меняет файлы БД и сохраняет изменения в таблицах.

В итоге всех этих действий, мы добавили в таблицы users и profiles по одной записи для нового зарегистрированного пользователя, причем, внешний ключ user_id будет ссылаться на id записи из таблицы users.

Видео по теме