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

433

Для работы понадобится 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

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

Зачем использовать 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

Какая функция удаляет объект из списка?
Что выведет этот код?
Какой код выведет строку — "C:\Common\testString.doc" ?
Что выведет этот код?
Что делает код ниже?