Module 2.2

Данные имеют ценность — сохраним их!

Веб-приложения, которые мы создавали в предыдущей части курса, обрабатывали и хранили данные внутри приложения. Это означает, что при перезапуске веб-сервера или приложения данные теряются.

Почти каждому веб-приложению нужно хранить и получать данные. Данные могут сохраняться приложением в файловой системе как файлы, или ответственность за хранение данных можно передать отдельному приложению, например системе управления базами данных. Системы управления базами данных создаются специально для надежного и эффективного хранения и получения данных; они могут находиться как на том же сервере, что и веб-приложение, так и где-то еще в интернете.

Хотя между терминами "система управления базами данных" и "база данных" есть значительные различия, мы будем использовать термин "база данных" для обоих случаев. Аналогично, хотя существует много типов систем баз данных, в основном мы сосредоточимся на реляционных базах данных.

Python и SQLite

Мы будем использовать SQLite с Python, поскольку это чрезвычайно просто. SQLite — это простой движок SQL-базы данных, который очень легко использовать: например, не нужно заботиться о настройке прав пользователей базы данных, поскольку пользователей нет. Хотя SQLite является идеальным движком для изучения SQL, более серьезные проекты должны использовать более развитые движки баз данных, такие как MySQL или PostgreSQL. Обратите внимание, что все они поддерживают обычные SQL-команды; различия связаны с производительностью, расширениями SQL, а также правами доступа.

SQLite хранит свою базу данных в файле, например db.sqlite. Доступ к файлу в Python может выглядеть следующим образом.

import sqlite3

conn = sqlite3.connect('db.sqlite')
cursor = conn.cursor()

После создания курсора можно использовать cursor.execute() для выполнения одной SQL-команды или cursor.executescript() для выполнения нескольких SQL-команд. Если мы изменяем базу данных, изменения нужно сохранить с помощью conn.commit(). Подробнее смотрите в библиотеке SQLite для Python.

Loading
Loading

Объекты и базы данных

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

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

Модели Django

По соглашению постоянные структуры данных, которые приложение хочет хранить в базе данных, определяются в models.py. Структуры данных должны быть определены как подкласс models.model.

# models.py

from django.db import models

class Person(models.Model):
    name = models.TextField()
    age = models.IntegerField()

Класс позволяет нам управлять базой данных. Мы можем добавить нового Person с помощью

bob = Person.objects.create(name='Bob', age=42)
alice = Person.objects.create(name='Alice', age=37)

Мы можем получить всех persons с помощью

persons = Person.objects.all()

Мы можем выполнять запросы к базе данных с помощью

bobs = Person.objects.filter(name='Bob')
middleaged = Person.objects.filter(age__gte=40)
alice = Person.objects.get(name='Alice') # Works only if there is a unique entry

О нотации запросов смотрите документацию.

Мы можем обновлять записи (если сохраняем их)

bob.age = 45
bob.save()

Модели сохраняются в базе данных. База данных по умолчанию — SQLite, хранимая в файле db.sqlite. Та же база данных также используется для хранения пользовательских сессий, а также зарегистрированных пользователей и администраторов (это Django предоставляет как встроенный сервис).

Каждый раз, когда спецификации моделей изменяются, Django нужно сообщить, что следует обновить схему в db.sqlite. Требуются две команды:

python3 manage.py makemigrations
python3 manage.py migrate

Первая команда создаст Python-файл миграции, расположенный в pages/migrations и начинающийся с числа, а вторая команда обновит базу данных. На раннем этапе разработки может оказаться, что мигрировать базу данных слишком неудобно с существующей базой. Если в db.sqlite нет ценной информации, допустимой временной стратегией будет удалить файлы миграций и базу данных, а затем синхронизировать базу данных с нуля. Естественно, это плохая идея, если в базе данных есть какая-либо ценная информация.

Loading

Транзакции базы данных

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

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

def transfer(sender, receiver, amount):
    acc1 = Account.objects.get(iban=sender)
    acc2 = Account.objects.get(iban=receiver)

    acc1.balance -= amount
    acc2.balance += amount

    acc1.save()
    acc2.save()

Рассмотрим два потока A и B, вызывающих transfer одновременно со следующей последовательностью:

  • Поток A получает счета
  • Поток B получает счета
  • Поток B обновляет и сохраняет счета
  • Поток A обновляет и сохраняет счета

Если отправитель и получатель в обоих потоках были одними и теми же, итоговый эффект этой операции состоит в том, что действие потока B будет перезаписано потоком A. Хотя такая последовательность событий маловероятна, очевидно, что нам следует предотвратить ее заранее.

Для этого Django предоставляет transaction.atomic либо как декоратор функции

from django.db import transaction

@transaction.atomic
def transfer(sender, receiver, amount):
    acc1 = Account.objects.get(iban=sender)
    acc2 = Account.objects.get(iban=receiver)

    acc1.balance -= amount
    acc2.balance += amount

    acc1.save()
    acc2.save()

либо как менеджер контекста внутри функции

from django.db import transaction

def transfer(sender, receiver, amount):
	with transaction.atomic():
		acc1 = Account.objects.get(iban=sender)
		acc2 = Account.objects.get(iban=receiver)

		acc1.balance -= amount
		acc2.balance += amount

		acc1.save()
		acc2.save()

В фоновом режиме происходит следующее. Когда выполнение доходит до transaction.atomic, Django отправляет SQLite команду BEGIN TRANSACTION (на самом деле отправляется SAVEPOINT, но это почти то же самое). Когда функция завершается, Django отправляет SQLite COMMIT, что завершает транзакцию.

SQLite не допускает никаких записей, пока открыта (вторая) транзакция. Поэтому в нашем предыдущем примере оба потока A и B завершатся с ошибкой, выбросив исключение. Что важнее, ошибка произойдет только во время commit. То есть локальные объекты acc1 и acc2, которые во время вызова transfer находятся в памяти, обновлены и имеют разные значения, но сама база данных не обновлена.

SQLite блокирует всю базу данных, то есть пока транзакция открыта, никакие записи невозможны. Чтобы увидеть этот эффект на практике, можно открыть соединение с базой данных и открыть транзакцию, не закрывая ее.

$ sqlite3 src/db.sqlite3 
SQLite version 3.30.1 2019-10-10 20:19:45
Enter ".help" for usage hints.
sqlite> BEGIN TRANSACTION;
sqlite> 

Пока это соединение открыто, никакое другое соединение (ручное или Django) не может записывать в базу данных. Это очень консервативный подход (и один из недостатков SQLite). Более продвинутые движки баз данных не блокируют всю базу, и, вероятно, понадобится select for update, чтобы заблокировать объекты, которые собираются изменить.

Loading

Обработка связей между объектами

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

from django.db import models

from django.contrib.auth.models import User

class Account(models.Model):
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    iban = models.TextField()

Здесь User — встроенная модель Django для пользователей.

Приведенное выше выражение позволяет пользователям иметь несколько счетов. Если для пользователя разрешен только один счет, можно использовать OneToOneField.

Мы можем получить объект User из объекта Account с помощью

acc = Account.objects.get(pk=0)
user = acc.owner

Также можно выполнять перекрестный поиск счетов с использованием информации владельца.

accounts_owned_by_johns = Account.objects.filter(owner__first_name='John')
Loading
Вы дошли до конца этого раздела! Перейти к следующему разделу:

Не забудьте проверить свои баллы в индикаторе в правом нижнем углу материала!