Редактирование данных. Пути приложения #

Васильев Андрей Михайлович, 2024

Версии презентации

Архитектура приложения #

diagram

Жёлтым обозначены физические (виртуальные) машины, зелёным — составляющие веб-приложения, синим — важные компоненты системы

  • Веб-сервер обслуживает множество клиентов
  • Клиентам необходимо множество документов, для получения которых выполняется несколько HTTP-запросов
    • HTML-документ с данными для отображения
    • CSS-документы с описанием классов для стилизации HTML
    • JS-документы для выполнения динамических действий на стороне клиента
  • Для формирования документов серверу недостаточно данных, которые хранятся в его оперативной памяти
    • Необходимо обратиться к хранилищу данных (СУБД или другая система хранения) за динамическими данными: список товаров, их количество, данные пользователя
    • Необходимо обратиться к внутренним ресурсам для получения статической информации: CSS/JS-документы, шаблоны страниц
  • Клиентам необходимо обратиться за документами, которые были загружены или сформированы приложением: изображения, архивы, PDF-документы

Максимальное использование локальных ресурсов #

diagram

  • Современные системы предоставляют множество процессоров, способных независимо обрабатывать информацию
  • HTTP-запросы хорошо масштабируются, т.к. каждый запрос не зависит от других запросов
  • HTTP-сервера обычно запускают множество процессов (и потоков), которые используют разные процессоры
  • Потоки имеют доступ к общим данным:
    • Файловая система
    • Ресурсы приложения
    • Данные в оперативной памяти
  • Для изменяемых данных необходимо аккуратно подходить к задаче редактирования общих данных: данные в оперативной памяти и на файловой системе
  • Библиотека http4k по умолчанию запускает несколько процессов

Балансировка запросов между компьютерами #

diagram

  • Для обработки большого числа запросов от клиентов мощностей одного компьютера может не хватать
  • Можно организовать обработку запросов с помощью множества одинаковых приложений, запущенных на нескольких компьютерах
  • Выводы из данной архитектуры:
    • Файловая система не может служить подходящим средством для хранения общих бинарных данных
    • Все данные приложения должны находится в единых системах хранения: кластере СУБД-серверов, серверах хранения больших данных и т.д.
    • Приложение должно считывать конфигурацию из сетевой службы.
    • Невозможно организовать доступ к общим данным в оперативной памяти, необходимо использовать специализированные инструменты
  • При разработке локального приложения стоит изначально рассматривать сложности перехода к запуску нескольких приложений на нескольких серверах

Локальные данные приложения #

Рассмотрим следующие источники данных, доступные JVM-приложениям:

  • Ресурсы приложения
  • Файловая система

Общие свойства #

  • Предоставляют данные в виде файлов
  • Предоставляют данные в иерархической структуре каталогов
  • Для построения путей к файлам можно использовать абсолютные и относительные пути

Отличия #

  • Данные внутри ресурсов доступны только на чтение
  • Ресурсы поставляются вместе с исполняемым кодом приложения
  • Файлы поставляются отдельно
  • Абсолютные пути внутри ресурсов будут работать на любом компьютере для файлов стоит использовать относительные

Работа с данными без СУБД #

СУБД решает много вопросов при разработке веб-приложений: проблемы надёжного хранения данных, проблемы совместного доступа, проблемы работы с большим объёмом данных

Без СУБД будем считывать и записывать все данные приложения с файловой системы

  1. При старте приложение считывает информацию из файловой системы
  2. Во время работы приложение изменяет данные в оперативной памяти
  3. При завершении работы приложение сохраняет данные на файловую систему

Ключевые проблемы:

  • Возникновение коллизий в данных в оперативной памяти в случае редактирования в нескольких потоках
  • Надёжное сохранение данных в случае завершения работы приложения (приложению могут не дать корректно завершить работу)

Жизненный цикл JVM-приложения #

  1. Создаётся процесс внутри операционной системы, в рамках которой запускается первый JVM-процесс
  2. Запускается функция main(), порождающая множество JVM-процессов
  3. По завершении работы последнего JVM-процесса выполняется вызов обработчиков завершения работы
  4. После выполнения всех задач по обработке запроса основной процесс ОС завершает свою работу

Жизненный цикл http4k-приложения #

  • Функция start() создаёт столько JVM-процессов, сколько есть процессоров в системе
  • Во время инициализации приложения функции по созданию фильтров вызываются 1 раз на каждый процессор
  • Есть функция stop(), которая завершит работу всех потоков

Написание обработчика завершения работы приложения #

public void addShutdownHook(Thread hook)

Обработчик завершения работы JVM-приложения — это интерфейс Thread, описывающий отдельный поток выполнения

Kotlin предоставляет удобную функцию для создания объектов, реализующих поток:

fun thread(
    start: Boolean = true,
    ...
    block: () -> Unit
): Thread
  • Аргумент start указывает надо ли сразу запускать данный поток на исполнение
  • Аргумент block содержит код, который надо выполнять в рамках процесса

Обработчики для http4k-приложений не должны запускаться автоматически, а в блоке должна быть логика по сохранению данных

Ручная корректная остановка работы приложения #

Ввиду того, что обработчик завершения JVM-процесса может не всегда сработать (например при остановке приложения из IDEA в Windows), можно реализовать остановку приложения по отправке команды из командного интерфейса

  • При старте приложения выполняется считывание данных в оперативную память
  • Выполняется создание обработчиков HTTP-запроса
  • Создание обработчика остановки JVM-процесса, чтобы можно было обработать остановку JVM-процесса
  • Запуск HTTP-сервера
  • Блокировка основного потока приложения на считывание данных из стандартного потока ввода
  • Остановка сервера (данный шаг выполнится, если пользователь ввёл данные и нажал Enter)
  • Автоматическое выполнение обработчика остановки приложения

Работа со сложными структурами данных #

При работе со сложными вложенными структурами приходится решать вопрос по получению и изменению свойств вложенных элементов

Рассмотрим следующие структуры данных

data class Class(val id: Int, val name: String, val teacher: String)
data class Course(val id: Int, val name: String, val clasess: List<Class>)
data class Speciality(val id: Int, val courses: List<Course>)
val speciality: Speciality = ...

diagram

Модификация структур данных #

Для получения информации по предмету необходимо найти курс в списке, найти предмет, обратиться к полям класса для получения:

val class = speciality.courses[1].classes[5]

Для редактирования потребуется выполнить несколько копирований:

val newClass = speciality.courses[1].classes[5]
    .copy(name = "Безопасность жизнедеятельности")
val newClasses = speciality.courses[1].classes.toMutableList()
    .apply { set(5, newClass) }
val newCourse = speciality.courses[1].copy(classes = newClasses)
val newCourses = speciality.courses.toMutableList()
    .apply { set(1, newCourse) }
val newSpeciality = speciality.copy(courses = newCourses)

Можно создать новую версию неизменяемых данных с нужным для нас состоянием

Сложные структуры сложны #

Сильная вложенность структур данных несёт следующие проблемы:

  • Надо корректно указать путь к данным несколько раз
    • Необходимо написать много кода
    • Возможны технические ошибки
  • Код, которому нужен доступ только к конечным данным, начинает зависеть от всех промежуточных объектов, что усложняет изменение структур данных в приложении

Решение с помощью функций #

Мы можем ввести функции, позволяющие решить данные задачи:

fun getName(speciality: Speciality, courseId: Int, classId: Int): String
fun setName(name: String, speciality: Speciality,
            courseId: Int, classId: Int): Speciality

Код будет зависеть только от класса Speciality и одной из указанных функций

Моделирование связных списков #

В рамках приложения данные зачастую представлены в виде связных списков:

  • Список групп
  • Список студентов
  • Список предметов

При моделировании связей между объектами в приложении кажется удобным подход с хранением ссылок на связные объекты:

class Group(
  val id: Int,
  val name: Sting,
  val students: List<Student>
)

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

Подход к моделированию списка #

  • Каждый элемент хранится в своём хранилище
  • Для связи элемента одного списка с элементом другого списка надо сохранить соответствующий идентификатор

В примере выше:

  • У сущности студент добавляется идентификатор группы, в которой он состоит
  • У предмета добавляется идентификатор группы, на котором он преподаётся

Для выполнения операций над несколькими списками вводите операции, которые обращаются с несколькими хранилищами

HTML-формы #

Для передачи данных от пользователя к веб-приложению используются HTML-формы

diagram

GET и POST-запросы #

POST-запросы обычно используется для создания нового элемента в данных сервера или для изменения существующих элементов

  • GET-запросы должны быть безопасными, не должны менять видимое состояние
  • Безопасные запросы можно повторять несколько раз, можно кешировать
  • POST-запросы не являются безопасными
  • Результат работы POST-запроса не кешируется на прокси-серверах

Если POST-запрос приводит к изменению данных на стороне сервера запрещено возвращать HTML-документ пользователю, т.к. он легко сможет повторить такой запрос

Правильное решение — перенаправить пользователя на адрес, где он с помощью GET-запроса сможет просмотреть новое состояние сервера

Форма на добавление данных #

Формы на редактирование данных не отличаются от форм для поиска и фильтрации

<form method="POST">
  <div class="mb-3">
    <label for="email" class="form-label">Адрес электронной почты</label>
    <input type="email" class="form-control" id="email" name="email" required>
  </div>
  <div class="mb-3">
    <label for="password" class="form-label">Пароль</label>
    <input type="password" class="form-control" id="password"
        name="password" required>
  </div>
  <div class="mb-3 form-check">
    <input type="checkbox" class="form-check-input" id="check" name="check">
    <label class="form-check-label" for="check">Проверить меня</label>
  </div>
  <button type="submit" class="btn btn-primary">Отправить</button>
</form>

Единственное отличие — передача данных осуществляется POST-запросом

Кодирование данных формой #

POST-запрос может содержать тело, а HTML-формы могут туда помещать данные с использованием следующих кодировок:

  • application/x-www-form-urlencoded — данные кодируются в форме данных URL-запроса, но в отличие от последнего нет ограничения на длину
  • multipart/form-data — данные передаются в бинарном виде, подходит для отправки файлов на сервер, требуют отдельной обработки в http4k
  • text/plain — подходит для низкоуровневой отладки, почти не используется

Тип кодировки формы задаётся с помощью атрибута формы enctype

В рамках данной лекции рассмотрим обработку первого типа кодировок данных

Обработка данных от формы #

От клиента к серверу все данные передаются в виде пар ключ-значение, схожим образом с параметрами URI-запроса: может быть несколько одинаковых ключей

Класс Request предоставляет следующие методы для получения данных из тела формы:

  • fun Request.form(name: String): String? — получить значение поля по названию ключа, при каждом вызове происходит разбор всей строки (не эффективный)
  • fun Request.form(): Form — получить весь набор параметров
  • fun Request.form(name: String, value: String): Request — указать новое значение для поля формы внутри объекта-запроса, подходит для тестирования
  • fun Request.formAsMap(): Map<String, List<String?>> — получить список значений в форме словаря

Особенности указанных функций

  • Тип данных Form является псевдонимом типа Parameters: typealias Form = Parameters, т.е. поддерживает все соответствующие функции
  • Для работы со словарём предоставляется метод getFirst(key: String), позволяющий извлечь первый элемент из списка значений ключа

Перенаправление HTTP-клиента #

HTML-формы зачастую направлены на изменение данных на стороне сервера:

  • Добавление новых элементов
  • Редактирование существующих элементов коллекции
  • Удаление существующих элементов коллекций

Для этих случаев используются не-GET-запросы, GET-запрос не должен менять состояние сервера

В случае успеха POST-запроса, в случае изменения данных, HTTP-сервер должен вернуть ответ с указанием адреса, куда следует сделать GET-запрос для получения HTML-документа с результатом

Параметры ответа:

  • Статус ответа — 302, FOUND
  • Заголовок Location с ссылкой на страницу для просмотра результата

Принципиальная схема работы #

val strings =  mutableListOf<String>()

val formHandler: HttpHandler = { request ->
    val form = request.form()
    val newString = form.findSingle("text").orEmpty()
    strings.add(newString)
    Response(FOUND).header("Location", "/strings")
}

Данный обработчик не проверяет переданные данные, это неверный способ решения задачи!

Обработка HTML-форм #

Принципиальная схема не удовлетворяет требованиям по обработке данных от пользователя:

  • Работает только в случае передачи корректных данных
  • Падает с сообщением внутренних проблем при получении данных
  • Теряет данные, которые были введены пользователем

В случае возникновения проблемы POST-запрос должен вернуть HTML-документ с формой, на которой:

  • Показаны все данные, которые ввёл пользователь
  • Показаны сообщения о проблемах с теми данными, которые указал пользователь

Полный сценарий обработки формы #

diagram

Пример формы #

Форма для отправки обратной связи

<form method="POST">
  <div class="mb-3">
    <label for="age" class="form-label">Возраст</label>
    <input type="number" class="form-control" id="age" name="age" required>
  </div>
  <div class="mb-3">
    <label for="name" class="form-label">Имя</label>
    <input type="password" class="form-control" id="password"
        name="password" required>
  </div>
  <div class="mb-3 form-check">
    <label for="feedback" class="form-label">Комментарии</label>
    <textarea id="feedback" name="feedback" rows="10"></textarea>
  </div>
  <button type="submit" class="btn btn-primary">Отправить</button>
</form>

Отображение результатов проверки в форме #

  • При первом показе форма должна отображать пустые поля ввода
  • В случае ошибки форма должна отображать данные, введённые пользователем и сообщения об ошибках

Для описания полей формы можно воспользоваться либо:

  • Специальным объектом, в котором предоставлены все поля
    • Удобно для написания кода (везде есть подсказки от компилятора)
    • Полями объекта должны быть значения String?, т.к. данные пользователя надо сохранить
  • Списком параметров
    • Усложняется вытаскивание данных для

Редактирование данных в приложении #

Сценарий схож с добавлением, отличается только начальное состояние - данные на странице должны быть заполнены

Логичным подходом к сокращению своих издержек является выделение формы в отдельный Pebble-файл, который подключать в шаблонах форм страниц на добавление и редактирование данных

Шаг извлечения данных из шаблонов формы можно тоже объединить

Удаление данных в приложении #

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

Формирование путей к ресурсам, REST #

Проблема - количество путей начинает возрастать, как с этим можно выживать в приложении

REST как подход к решению задачи

Передача JSON между приложениями #

При формировании JSON-запросов учитываем:

  • Метод
  • Путь
  • Данные

Можно всё запихнуть в данные, но исследовать работу с таким интерфейсом будет достаточно сложно

REST #

Idemponent requests & other things

  • Ноль, один и много

Описание путей для работы с одним элементом #

  • GET /element
  • PUT /element

Описание путей для работы со множеством элементов #

  • GET /elements
  • PUT /elements
  • GET /elements/1
  • POST /elements/new
  • PUT /elements/1
  • DELETE /elements/1

Отправка дополнительных типов запросов через формы в HTML #