Учебное приложение
Книжный магазин 2.0
16
Авторские права
© Postgres Professional, 2017–2024
Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк
Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за
любые повреждения и убытки, включая потерю дохода, нанесенные
прямым или непрямым, специальным или случайным использованием
материалов курса. Компания Postgres Professional не предоставляет
каких-либо гарантий на материалы курса. Материалы курса
предоставляются на основе принципа «как есть» и компания Postgres
Professional не обязана предоставлять сопровождение, поддержку,
обновления, расширения и изменения.
2
Темы
Обзор приложения «Книжный магазин 2.0»
Схема данных
Интерфейс с клиентской частью
Разграничение доступа
3
Книжный магазин
Интернет-магазин для покупателей
поиск книг
детальная информация о выбранной книге
корзина для зарегистрированных пользователей
«Админка» для сотрудников
заказ книг у поставщика
установка розничной цены
фоновые задания: запуск, просмотр результатов
В этом курсе, как и в DEV1, мы будем использовать приложение
«Книжный магазин», но новой версии 2.0. Клиентская часть полностью
готова, а серверную мы будем улучшать по мере прохождения курса.
Как и раньше, клиентская часть состоит из интернет-магазина
и «админки». Однако со времен DEV1 магазин вырос и число
покупателей увеличилось.
Теперь, чтобы приобрести книги, покупатели должны регистрироваться
на сайте и входить под своим именем. В магазине теперь есть корзина,
а книги имеют цену.
Админка позволяет заказывать книги и устанавливать на них цену.
Добавление книг и авторов из админки исключено из приложения 2.0.
Зато добавилась возможность запускать фоновые задания (например,
отчеты) и просматривать их результаты после завершения.
Приложение предназначено исключительно для демонстрации
концепций, излагаемых в этом курсе. Оно умышленно сделано крайне
упрощенным и не может служить образцом проектирования
реальных систем.
4
Приложение
Приложение состоит из двух частей, представленных вкладками.
«Книжный магазин» — это интерфейс веб-пользователя, в котором
он может просматривать и покупать книги.
«Админка» — интерфейс сотрудников магазина, в котором они могут
заказывать книги и устанавливать их цену, а также выполнять
фоновые задания.
В учебных целях вся функциональность представлена на одной общей
веб-странице. Если какая-то часть функциональности недоступна из-за
того, что на сервере нет подходящего объекта, приложение сообщит
об этом. Также приложение выводит текст запросов, которые оно
посылает на сервер.
Приложение позволяет выбрать сервер для подключения. В основном
мы будем использовать значение по умолчанию.
Исходный код приложения не является темой курса, но может быть
получен в git-репозитории https://pubgit.postgrespro.ru/pub/dev2app.git
6
Основные сущности
Книга
название
формат издания
кол-во страниц
рейтинг
...
Автор
фамилия
имя
отчество
Операция
изменение кол-ва
цена
дата
Пользователь
имя
почта
Розничная
цена
цена
даты действия
авторство
порядок указания
книги в корзине
количество
Сеанс
токен
Основные сущности нашей базы данных практически не изменились
со времен курса DEV1. Это:
Книга. К книгам добавились атрибуты.
Автор. Книги и авторы связаны отношением многие-ко-многим
(авторство).
Операции с книгами: покупки в магазине и поступления на склад.
К атрибутам добавилась цена книги.
Новые сущности:
Розничная цена. Мы рассматриваем цену как отдельную сущность,
а не атрибут книги, поскольку она может меняться со временем,
то есть имеет диапазон дат действия.
Пользователь веб-магазина. Определяется именем (логином)
и имеет почтовый адрес.
Пользователь может класть книги в корзину, и поэтому связан
с книгами отношением многие-ко-многим. (Отдельной сущности
для корзины мы не предусматриваем.)
Сеанс работы пользователя с магазином. Сеанс связан с вопросами
аутентификации и разграничения доступа.
7
Основные таблицы
books
book_id
title
format
pages
rating
votes_up
votes_down
...
onhand_qty
authors
author_id
last_name
first_name
middle_name
operations
operation_id
book_id
qty
price
at
users
user_id
username
email
retail_prices
book_id
price
effective
authorships
author_id
book_id
seq_num
cart_items
user_id
book_id
qty
sessions
auth_token
user_id
Основные таблицы базы данных представлены на слайде.
В качестве идентификаторов используются суррогатные ключи,
генерируемые с помощью последовательностей.
Связи «многие ко многим» представлены дополнительными таблицами:
authorships — авторство;
cart_itemsкниги в корзине.
Отметим, что в таблице книг books имеется дополнительный столбец
onhand_qty, содержащий текущее количество книг на складе магазина,
и обновляемый триггером по таблице операций.
С расчетом рейтинга книг (rating) связаны еще два столбца: голоса
пользователей «за» (votes_up) и «против» (votes_down).
8
Интерфейс с клиентом
схема webapi
интерфейсные функции магазина
схема empapi
интерфейсные функции админки
схема public – таблицы и внутренние функции
register_user
login
logout
get_cart
checkout
get_catalog
get_image
cast_vote
add_to_cart
remove_from_cart
get_catalog
receipt
set_retail_price
get_tasks
get_programs
task_results
run_program
роль
web
роль
emp
Клиентский API серверной части полностью построен на функциях.
Интерфейсные функции веб-магазина доступны только пользователю
web и размещены в схеме webapi:
аутентификация и разграничение доступа;
работа с каталогом книг;
операции с корзиной.
Интерфейсные функции админки доступны только пользователю emp
и размещены в схеме empapi:
работа с каталогом книг;
фоновые задания.
Аутентификация в админке не предусмотрена.
Все таблицы, а также внутренние функции, не открытые напрямую для
клиента, размещены в схеме public.
Интерфейсные функции объявлены как SECURITY DEFINER, чтобы
иметь доступ к объектам базы данных. С помощью механизма
привилегий по умолчанию (ALTER DEFAULT PRIVILEGES) доступ
к функциям в схеме webapi автоматически выдается только
пользователю web, в схеме empapi пользователю emp, а доступ
к функциям в схеме public отбирается у роли public.
(Вопросы разграничения доступа рассматриваются в курсе DEV1.)
Сейчас мы рассмотрим интерфейс подробнее, но обратите внимание,
что информационная панель веб-клиента показывает, какие вызовы он
отправляет на сервер.
9
Аутентификация
users
user_id
username
email
sessions
auth_token
user_id
webapi.register_user
webapi.login public.check_auth
webapi.logout
токен
Аутентификация пользователей интернет-магазина происходит на
клиенте. С точки зрения базы данных клиент всегда представлен одной
ролью web.
Новый пользователь регистрируется вызовом register_user. Для
простоты мы не используем пароли (но если бы использовали, то хеш
пароля хранился бы в таблице users).
Чтобы иметь возможность совершать покупки, пользователь должен
войти в систему. Это выполняет функция login. Она создает сеанс
пользователя, который определяется токеном (UUID).
Токен возвращается клиенту и дальше клиент передает его как
параметр во все функции, связанные с покупками. Каждая такая
функция первым делом проверяет правильность токена с помощью
вызова check_auth, который определяет по токену имя пользователя.
Наконец, вызов logout завершает сеанс.
Функционал истечения срока сеанса не реализован, но может быть
легко добавлен.
11
Каталог книг
webapi.get_catalog
empapi.get_catalog
public.get_catalog
webapi.get_image
webapi.cast_vote
empapi.receipt
books
book_id
title
format
pages
rating
votes_up
votes_down
...
operations
operation_id
book_id
qty
price
at
retail_prices
book_id
price
effective
public.get_retail_price
public.get_catalog
empapi.set_retail_price
Список книгонечно, вместе с авторами, хотя эти таблицы не показаны
на слайде для краткости) нужен как магазину, так и админке — но
с разным набором полей. Всю возможную информацию выбирает
закрытая функция public.get_catalog, получая на вход параметры
поиска. Функции webapi.get_catalog и empapi.get_catalog реализованы
как обертки вокруг public.get_catalog.
Получение обложки книги выполняет функция webapi.get_image.
Обложки не входят в get_catalog, чтобы клиент как можно быстрее
отобразил результаты поиска, а обложки подгружал в фоновом режиме.
Голосование «за» или «против» книги выполняется функцией
webapi.cast_vote. Функция допускает голосование несколько раз,
но работает только для зарегистрированных пользователей.
Заказ книги у поставщика в админке выполняет функция
empapi.receipt. Она создает соответствующую операцию с книгой.
За установку розничной цены на книгу отвечает функция
empapi.set_retail_price. Текущую цену возвращает функция
public.get_retail_price, которую клиент никогда не вызывает напрямую,
но она используется во многих интерфейсных функциях.
13
Корзина
books
book_id
title
format
pages
rating
votes_up
votes_down
...
onhand_qty
operations
operation_id
book_id
qty
price
at
cart_items
user_id
book_id
qty
webapi.get_cart
webapi.remove_from_cart
webapi.add_to_cart
webapi.checkout
Все функции, относящиеся к покупке, работают от имени конкретного
пользователя магазина и поэтому требуют токена.
Функция webapi.add_to_cart добавляет в корзину одну книгу или
убирает из корзины одну книгу. Функция webapi.remove_from_cart
полностью удаляет всю позицию из корзины.
Функция webapi.get_cart возвращает содержимое корзины.
Функция webapi.checkout совершает покупку, убирая позиции
из корзины и создавая соответствующие операции с книгами.
Весь процесс взаимодействия с пользователем во время покупки книг
не может быть выполнен в рамках одной транзакции СУБД, поскольку
этот процесс занимает неизвестное (большое) время. Вместо этого мы
позволяем пользователю добавлять книги в корзину, не обращая
внимания на наличие необходимого количества книг на складе. Каждое
добавление происходит в отдельной (очень короткой) транзакции, так
что в базе данных не возникает никаких долговременных блокировок.
Фактическая проверка количества происходит только в транзакции при
покупке: если на складе будет недостаточно книг, ограничение
целостности в базе данных не позволит выполнить транзакцию.
(В реальной системе необходимо было бы предусмотреть
резервирование товара и фиксацию цены на время, отведенное для
оплаты онлайн. Конечно, и это действие тоже не должно приводить
к длинным транзакциям.)
15
Фоновые задания
programs
program_id
name
func
tasks
task_id
program_id
status
params
started
finished
result
...
empapi.get_programs
empapi.get_tasks
empapi.task_results
empapi.run_program
public.register_program
Для фоновых заданий используются еще две таблицы, не показанные
на основной схеме:
programs — программы, которые можно запускать.
У программы есть название и имя «исполняемой» функции.
tasksсобственно задания, то есть экземпляры программ.
Задание имеет статус (запланировано к запуску, работает и т. п.),
значения параметров, дату запуска и окончания работы, результат
выполнения и другие.
Программа регистрируется функцией public.register_program. Клиент
не вызывает эту функцию, поэтому она находится в схеме public.
Задание ставится на выполнение функцией empapi.run_program.
Несколько функций предназначены для получения информации:
о списке программ — empapi.get_programs;
о списке заданий — empapi.get_tasks;
о результате выполнения — empapi.task_results.
17
Итоги
Часть рассмотренного функционала еще только предстоит
реализовать
Прежде чем вносить изменения в приложение,
необходимо разобраться в том, как оно устроено
18
Практика
Здесь и далее все практические задания, связанные
с приложением, выполняются в базе данных bookstore2.
1. Сеанс удаляется, если пользователь выходит из системы,
но если просто закрыть вкладку браузера, сеансы будут
накапливаться. Реализуйте автоматическое удаление
прошлых сеансов пользователя при повторном входе.
2. Реализуйте отсутствующую интерфейсную функцию
webapi.add_to_cart. Функция должна работать только для
пользователей, вошедших в систему. Если книга
присутствует в корзине, количество ее экземпляров не может
быть меньше одного. Проверьте результат в приложении.
Какое обновление таблицы cart_items выполняется при
изменении количества книг — обычное или HOT?
База данных bookstore2 уже создана. При необходимости ее можно
пересоздать с помощью скрипта bookstore2.sql в домашнем каталоге.
1. Внесите изменение в функцию webapi.login: перед тем как создавать
новый сеанс, закройте существующие сеансы этого пользователя,
используя функцию webapi.logout.
Не забудьте выбрать подключение к основному серверу на порту 5432.
(Это значение не выбирается по умолчанию, поскольку в следующих
темах мы будет работать с пулом соединений.)
2. Функция webapi.add_to_cart присутствует в базе, но пуста. Сохраните
ее сигнатуру без изменений. Функция принимает параметры:
auth_token — токен;
book_id — идентификатор книги;
qty — количество (может быть +1 или −1, другие значения считаются
некорректными).
Если qty = +1, надо либо добавить экземпляр книги в корзину (если
такой книги еще нет в корзине), либо увеличить количество
экземпляров на единицу.
Если qty = −1, надо уменьшить количество экземпляров книги в корзине
на единицу. При этом количество не должно быть меньше 1 (чтобы
полностью удалить книгу из корзины, используется другая функция —
webapi.remove_from_cart).
Тип обновления можно узнать в таблице pg_stat_all_tables (вспомните
тему «Архитектура. Многоверсионность» ).
19
Практика+
1. Если для каждого пользователя магазина создать отдельную
роль, аутентификацию можно поручить базе данных.
Хорошая ли это идея и почему?
2. Вместо того, чтобы создавать функции в базе данных,
можно открыть доступ к таблицам и реализовать всю логику
в приложении. А генерацию запросов переложить на ORM.
Хорошая ли это идея и почему?
3. Приложение реализует собственный механизм фоновых
заданий. Вместо этого можно воспользоваться сторонним
решением для очередей сообщений.
Хорошая ли это идея и почему?