Файл проекта: 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, через который и осуществляется
работа с БД. В самом простом варианте это делается так:
Здесь при
создании класса передается ссылка на текущее приложение, с которым он будет
связан.
Создание таблиц во 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=False)
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=False указывает, что
оно должно обязательно содержать данные, т.е. пустым быть не может. Наконец,
последнее поле 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=False)
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">
<p><label>Имя: </label> <input type="text" name="name" value="" requied />
<p><label>Возраст: </label> <input type="text" name="old" value="" requied />
<p><label>Город: </label> <input type="text" name="city" value="" requied />
<p><label>Email: </label> <input type="text" name="email" value="" requied />
<p><label>Пароль: </label> <input type="password" name="psw" value="" requied />
<p><label>Повтор пароля: </label> <input type="password" name="psw2" value="" requied />
<p><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.