Руководство по SQLAlchemy в Flask

2000

Для работы понадобится python 3.6+, библиотеки SQLAlchemy и Flask. Код урока здесь.
Версии библиотек в файле requirements.txt

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

С его помощью можно будет добавлять новые книги, читать уже существующие, обновлять и удалять их. Эти операции — создание, чтение, обновление и удаление — также известны как «CRUD» и составляют основу почти всех веб-приложений. О них отдельно пойдет речь в статье.

Но прежде чем переходить к CRUD, разберемся с отдельными элементами приложения, начиная с SQLAlchemy.

Что такое SQLAlchemy?

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

Руководство по flask-sqlalchemy

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

При чтении этого определения в первую очередь возникает вопрос: а что же такое объектно-реляционное отображение? ORM — это техника, используемая для написания запросов к базам данных с помощью парадигм объектно-ориентированного программирования выбранного языка (Python в этом случае).

Если еще проще, ORM — это своеобразный переводчик, который переводит код с одного набора абстракций в другой. В этом случае — из Python в SQL.

Есть масса причин, почему стоит использовать ORM, а не вручную сооружать строки SQL. Вот некоторые из них:

  • Ускорение веб-разработки, ведь пропадает необходимость переключаться между написанием кода Python и SQL
  • Устранение повторяющегося кода
  • Оптимизация рабочего процесса и более эффективные запросы к базе данных
  • Абстрагирование системы базы данных, так что переключение между несколькими базами становится более плавным
  • Генерация шаблонного кода для основных операций CRUD
Python data course

Углубимся еще сильнее.

Зачем использовать ORM, когда можно писать сырой SQL? При написании запросов на сыром SQL, мы передаем их базе данных в виде строк. Следующий запрос написан на сыром SQL:

#импорт sqlite
import sqlite3

# подключаемся к базе данных коллекции книг
conn = sqlite3.connect('books-collection.db')

# создаем объект cursor, для работы с базой данных
c = conn.cursor()

# делаем запрос, который создает таблицу books с идентификатором и именем
c.execute('''
          CREATE TABLE books
          (id INTEGER PRIMARY KEY ASC,
	     name varchar(250) NOT NULL)
          ''' )

# выполняет запрос, который вставляет значения в таблицу
c.execute("INSERT INTO books VALUES(1, 'Чистый Python')")

# сохраняем работу
conn.commit()

# закрываем соединение
conn.close()

Нет ничего плохого в использовании чистого SQL для обращения к базам данных, только если вы не сделаете ошибку в запросе. Это может быть, например, опечатка в названии базы, к которой происходит обращение или неправильное название таблицы. Компилятор Python здесь ничем не поможет.

SQLAlchemy — один из множества ORM-инструментов для Python. При работе с маленькими приложения чистый SQL может сработать. Но если это большой сайт с массой данных, такой подход сильнее подвержен ошибкам и просто более сложен.

Создание базы данных с помощью SQLAlchemy

Создадим файл для настройки базы данных. Можете назвать его как угодно, но пусть это будет database_setup.py.

import sys  
# для настройки баз данных 
from sqlalchemy import Column, ForeignKey, Integer, String  
  
# для определения таблицы и модели 
from sqlalchemy.ext.declarative import declarative_base  
  
# для создания отношений между таблицами
from sqlalchemy.orm import relationship  
  
# для настроек
from sqlalchemy import create_engine  
  
# создание экземпляра declarative_base
Base = declarative_base()  
  
# здесь добавим классы 
  
# создает экземпляр create_engine в конце файла  
engine = create_engine('sqlite:///books-collection.db')  
  
Base.metadata.create_all(engine)

В верхней части файла импортируем все необходимые модули для настройки и создания баз данных. Для определения колонок в таблицах импортируем Column, ForeignKey, Integer и String.

Далее импортируем расширение declarative_base. Base = declarative_base() создает базовый класс для определения декларативного класса и присваивает его переменной Base.

Согласно документации declarative_base() возвращает новый базовый класс, который наследуют все связанные классы. Это таблица, mapper() и объекты класса в пределах его определения.

Далее создаем экземпляр класса create_engine, который указывает на базу данных с помощью engine = create_engine('sqlite:///books-collection.db'). Можно назвать базу данных как угодно, но здесь пусть будет books-collection.

Последний этап настройки — добавление Base.metadata.create_all(engine). Это добавит классы (напишем их чуть позже) в виде таблиц созданной базы данных.

После настройки базы данных создаем классы. В SQLAlchemy классы являются объектно-ориентированными или декларативными представлениями таблицы в базе данных.

# мы создаем класс Book наследуя его из класса Base.  
class Book(Base):  
    __tablename__ = 'book'  
    
    id = Column(Integer, primary_key=True)  
    title = Column(String(250), nullable=False)  
    author = Column(String(250), nullable=False)  
    genre = Column(String(250))

Для этого руководства достаточно одной таблицы: Book. Она будет содержать 4 колонки: id, title, author и genre. Integer и String используются для определения типа значений, которые будут храниться в колонках. Колонка с названием, именем автора и жанром — это строки, а id — число.

Есть много атрибутов класса, которые используются для определения колонок, но рассмотрим уже использованные:

  1. primary_key: при значении True указывает на значение, используемое для идентификации каждой уникальной строки таблицы.
  2. String(250): String — тип значения, а значение в скобках — максимальная длина строки.
  3. Integer: указывает тип значения (целое число).
  4. nullable: если False, это значит, что для создания строки обязательно должно быть значение .

На этом процесс настройки заканчивается. Если сейчас использовать команду python database_setup.py в командной строке, будет создана пустая база данных books-collection.db. Теперь можно наполнять ее данными и пробовать обращаться.

CRUD с SQLAlchemy на примерах

В начале кратко была затронута тема операций CRUD. Пришло время их использовать.

Создадим еще один файл и назовем его populate.py (или любым другим именем).

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# импортируем классы Book и Base из файла database_setup.py
from database_setup import Book, Base

engine = create_engine('sqlite:///books-collection.db')
# Свяжим engine с метаданными класса Base,
# чтобы декларативы могли получить доступ через экземпляр DBSession
Base.metadata.bind = engine

DBSession = sessionmaker(bind=engine)
# Экземпляр DBSession() отвечает за все обращения к базе данных
# и представляет «промежуточную зону» для всех объектов, 
# загруженных в объект сессии базы данных.
session = DBSession()

В первую очередь импортируем все зависимости и некоторые классы из файла database_setup.py.

Затем сообщим программе, с какой базой данных хотим взаимодействовать. Это делается с помощью функции create_engine.

Что бы создать соединение между определениями класса и таблицами в базе данных, используем команду Base.metadata.bind.

Для создания, удаления, чтения или обновления записей в базе данных SQLAlchemy предоставляет интерфейс под названием Session. Для выполнения запросов необходимо добавлять и фиксировать (делать комит) запроса. Используем метод flush(). Он переносит изменения из памяти в буфер транзакции базы данных без фиксации изменения.

CREATE:

Стандартный процесс создания записи следующий:

entryName = ClassName(property="value", property="value" ... )

# Чтобы сохранить наш объект ClassName, мы добавляем его в наш сессию:
session.add(entryName)

'''
Чтобы сохранить изменения в нашу базу данных и зафиксировать 
транзакцию, мы используем commit(). Любое изменение, 
внесенное для объектов в сессии, не будет сохранено 
в базу данных, пока вы не вызовете session.commit().
'''

session.commit()

Создать первую книгу можно с помощью следующей команды:

bookOne = Book(title="Чистый Python", author="Дэн Бейде", genre="компьютерная литература") 
session.add(bookOne) 
session.commit()

READ:

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

session.query(Book).all() — вернет список всех книг

session.query(Book).first() — вернет первый результат или None, если строки нет

UPDATE:

Для обновления записей в базе данных, нужно проделать следующее:

  1. Найти запись
  2. Сбросить значения
  3. Добавить новую запись
  4. Зафиксировать сессию в базе данных (сделать комит)

Если еще не заметили, в записи bookOne есть ошибка. Книгу «Чистый Python» написал Дэн Бейдер, а не «Дэн Бейде». Обновим имя автора с помощью 4 описанных шагов.

Для поиска записи используется filter(), который фильтрует запросы на основе атрибутов записей. Следующий запрос выдаст книгу с id=1 (то есть, «Чистый Python»)

editedBook = session.query(Book).filter_by(id=1).one()

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

editedBook.author = "Дэн Бейдер" 
session.add(editedBook) 
session.commit()

Можно использовать all(), one() или first() для поиска записи в зависимости от ожидаемого результата. Но есть несколько нюансов, о которых важно помнить.

  1. all() — возвращает результаты запроса в виде списка
  2. one() — возвращает один результат или вызывает исключение. Вызовет исключение sqlaclhemy.orm.exc.NoResultFoud, если результат не найден или sqlaclhemy.orm.exc.NoResultFoud, если были возвращены несколько результатов
  3. first() — вернет первый результат запроса или None, если он не содержит строк, но без исключения

DELETE:

Удаление значений из базы данных — это почти то же самое, что и обновление:

  1. Находим запись
  2. Удаляем запись
  3. Фиксируем сессию
bookToDelete = session.query(Book).filter_by(title='Чистый Python').one() 
session.delete(bookToDelete) 
session.commit()

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

Создадим новый файл app.py в той же папке, что и database_setup.py и populate.py. Затем импортируем необходимые зависимости.

	from flask import Flask, render_template, request, redirect, url_for  
from sqlalchemy import create_engine  
from sqlalchemy.orm import sessionmaker  
from database_setup import Base, Book  
  
app = Flask(__name__)  
  
# Подключаемся и создаем сессию базы данных  
engine = create_engine('sqlite:///books-collection.db?check_same_thread=False')  
Base.metadata.bind = engine  
  
DBSession = sessionmaker(bind=engine)  
session = DBSession()  
  
  
# страница, которая будет отображать все книги в базе данных  
# Эта функция работает в режиме чтения.  
@app.route('/')  
@app.route('/books')  
def showBooks():  
    books = session.query(Book).all()  
    return render_template("books.html", books=books)  
  
  
# Эта функция позволит создать новую книгу и сохранить ее в базе данных.  
@app.route('/books/new/', methods=['GET', 'POST'])  
def newBook():  
    if request.method == 'POST':  
        newBook = Book(title=request.form['name'], author=request.form['author'], genre=request.form['genre'])  
        session.add(newBook)  
        session.commit()  
        return redirect(url_for('showBooks'))  
    else:  
        return render_template('newBook.html')  
  
  
# Эта функция позволит нам обновить книги и сохранить их в базе данных.  
@app.route("/books/<int:book_id>/edit/", methods=['GET', 'POST'])  
def editBook(book_id):  
    editedBook = session.query(Book).filter_by(id=book_id).one()  
    if request.method == 'POST':  
        if request.form['name']:  
            editedBook.title = request.form['name']  
            return redirect(url_for('showBooks'))  
    else:  
        return render_template('editBook.html', book=editedBook)  
  
  
# Эта функция для удаления книг  
@app.route('/books/<int:book_id>/delete/', methods=['GET', 'POST'])  
def deleteBook(book_id):  
    bookToDelete = session.query(Book).filter_by(id=book_id).one()  
    if request.method == 'POST':  
        session.delete(bookToDelete)  
        session.commit()  
        return redirect(url_for('showBooks', book_id=book_id))  
    else:  
        return render_template('deleteBook.html', book=bookToDelete)  
  
  
if __name__ == '__main__':  
    app.debug = True  
    app.run(port=4996)

Наконец, нужно создать шаблоны: books.html, newBook.html, editBook.html и deleteBook.html. Для этого создадим папку с шаблонами во Flask templates на том же уровне, где находится файл app.py. Внутри него создадим четыре файла.

books.html

<html>  
<body>  
 <h1>Books</h1>  
 <a href="{{url_for('newBook')}}">  
   <button>Добавить книгу</button>  
 </a> 
 <ol>  
   {% for book in books %}  
    <li> {{book.title}} by {{book.author}} </li>  
    <a href="{{url_for('editBook', book_id = book.id )}}">  
      Изменить  
    </a>  
    <a href="{{url_for('deleteBook', book_id = book.id )}}" style="margin-left: 10px;">  
      Удалить  
    </a>  
    <br> <br>  
  {% endfor %}  
 </ol>  
</body>  
</html>

Теперь newBook.html.

<h1>Add a Book</h1>  
<form action="#" method="post">  
  <div class="form-group">  
    <label for="name">Название:</label>  
    <input type="text" maxlength="100" name="name" placeholder="Название книги">  
  
    <label for="author">Автор:</label>  
    <input maxlength="100" name="author" placeholder="Автор книги">  
  
    <label for="genre">Жанр:</label>  
    <input maxlength="100" name="genre" placeholder="Жанр книги">  
  
    <button type="submit">Добавить</button>  
   </div>
</form>

Дальше editBook.html.

<form action="{{ url_for('editBook',book_id = book.id)}}" method="post">  
  <div class="form-group">  
    <label for="name">Название:</label>  
    <input type="text" class="form-control" name="name" value="{{book.title }}">  
    <button type="submit"> Обновить</button>  
    <a href='{{ url_for('showBooks') }}'>  
      <button>Отменить</button>  
    </a> 
  </div>
</form>

И deleteBook.html.

<h2>Вы уверены, что хотите удалить {{book.title}}?</h2>  
<form action="#" method='post'>  
  <button type="submit"> Удалить</button>  
  <a href='{{url_for('showBooks')}}'>  
    <button> Отменить</button>  
  </a>
</form>

Если запустить приложение app.py и перейти в браузере на страницу https://localhost:4996/books, отобразится список книг. Добавьте несколько и если все работает, это выглядит вот так:

приложение, которое ведет учет книг.

Расширение приложения и выводы

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

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

Тест на знание python

Какой цикл `for` выведет такой результат?
Какой будет результат выполнения кода в python 3 — print(3/5) ?
Что выведет этот код?
Как нельзя назвать функцию?
Какой будет результат выполнения кода — print('Monty' + 'Python') ?
Александр
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ. Пишу на популярные темы: веб-разработка, работа с базами данных, data sciense и другие...