Хранение данных #
Васильев Андрей Михайлович, 2022
Версии презентации
Архитектура веб-приложения #
- Веб-приложение предназначено для обработки запросов от пользователя
- Веб-приложение управляет некоторым набором данных
- Веб-приложение предоставляет логичный интерфейс для управления данными
Особенности хранилищ данных #
- Данные хранятся вне приложения в специальных хранилищах
- Веб-приложение не должно сохранять любые данные внутри себя
- Это позволяет масштабировать обработку запросов
- Это позволяет сохранять и восстанавливать состояние приложения
- Хранилища предоставляют интерфейс для работы с данными:
- Быстрый поиск данных
- Добавление элементов
- Обновление и обновление элементов
- Работа с очень большими объёмами данных
- Распределённое хранение данных
- Надёжное сохранение данных
Схема работы с хранилищем данных #
- При старте приложение:
- Считывает конфигурацию, в частности о местоположении хранилищ
- Устанавливает соединение со всеми хранилищами данных
- При получении запроса от пользователя
- Формируется запрос в хранилище
- Используется одно из соединений к хранилищу
- Данные от хранилища преобразуются в объекты предметной области
- Объекты обрабатываются
- Подготовленные данные передаются в слой представления
Для работы с хранилищами необходимо изучить специальные API или языки программирования, например язык SQL
Собственное хранилище #
В рамках курса будем использовать собственное хранилище данных
- Не требует изучение отдельного языка программирования
- Позволяет познакомиться с техниками структурированного хранения данных
Минусы такого подхода:
- Данные могут быть потеряны в случае непреднамеренного завершения приложения
- Данные потенциально могут быть испорчены ввиду конкуретного доступа и одновременного изменения
- Можно нарушить шаблоны доступа к данным, что приведёт к деградации архитектуры приложения
Данный подход к хранению данных можно применять для непрофессиональных или небольших приложений
Архитектура элементов хранилища #
Назначение элементов #
Рассмотрим ключевые особенности каждого элемента архитектуры
Хранилище #
- Обеспечить сохранение данных приложения между перезапусками
- Обеспечить доступ к объектам-репозиториев для слоя уровня предметной области
В обычных веб-приложениях реализуется внешним приложением (СУБД) или классами специализированной библиотеки
Репозиторий #
- Обеспечивает конкретное управление списком объектов при добавлении, обновлении и удалении элементов
- Предоставляет доступ к извлечению объектов данного репозитория
- Хранит список объектов во время работы приложения
В обычных веб-приложениях использует подключение к СУБД для доступа к данным
Запросы #
- Описывает порядок извлечения данных из хранилища
- В результате запроса может быть извлечён как один объект, так и список
- Запросы могут быть повторно использованы в рамках множества обработчиков
- Запросы могут быть оформлены как функции репозитория или как внешние объекты, последняя схема характерна для больших приложений
Слой предметной области #
- Описывает детали работы с данными предметной области
- Формируется в виде выделенных элементов обычно для приложений среднего и большого размера, оперирующих большими объёмами данных
- В случае приложений курса данные сущности появляются в случае необходимости работы с несколькими списками данных
Задача сохранения данных на жёстком диске #
При перезапуске веб-приложения данные должны сохраняться в надёжном хранилище. При использовании ручного хранения таким хранилищем выступает файловая система
- Необходимо определить способ сохранения данных на жёсткий диск
- Необходимо реализовать схему сериализации данных
- Необходимо реализовать схему десериализации
- Необходимо вызвать методы сериализации и десериализации при запуске приложения и при его остановке
Сериализация — процесс преобразования структуры данных в последовательность байтов. Формат хранения может быть как отрытым, так и закрытым
Существует множество отрытых форматов хранения данных: XML, JSON, CSV, BSON, YAML, TOML и т.д.
JSON: JavaScript Object Notation #
Простой формат обмена данными, удобный для чтения и написания как человеком, так и компьютером
Формат поддерживает следующие типы данных:
- Число
- Строка
- Логическое значение
- Null
Для объединения этих элементов используется:
- Массивы, упорядоченный набор значений
- Объекты, коллекция пар ключ-значение
Описание формата доступно на официальном сайте https://www.json.org/json-ru.html
Пример JSON-документа #
1[
2 {
3 "title": "Война и мир",
4 "author": "Лев Толстой",
5 "year": 1869
6 },
7 {
8 "title": "Бесы",
9 "author": "Федор Достоевский",
10 "year": 1872
11 },
12 {
13 "title": "Чайка",
14 "author": "Антон Чехов",
15 "year": 1896
16 }
17]
- В корне документа находится массив, описываемый символами
[
и]
на 1 и 17 строках соответственно - Элементы массива разделяются запятыми на 6 и 11 строках соответственно
- Внутри массива находится описание трёх объектов
- Каждый объект описывается с помощью пары
{
и}
- Внутри объекта каждая пара ключ-значение описывается строкой-ключом и её значением
Работа с JSON-документами #
В рамках экосистемы JVM существует множество библиотек, позволяющих обрабатывать JSON-документы
Библиотека http4k предоставляет единый интерфейс для работы с JSON-документами
В рамках данной лекции рассмотрим использование библиотеки Jackson, которая является одной из лучших в своём классе
В рамках библиотеки http4k разработан ряд методов, которые позволяют удобно взаимодействовать с данными документами. Методы описаны в пакете http4k / org.http4k.format / Json
Формирование JSON-объектов #
Для описания JSON-объекта можно воспользоваться следующим кодом:
val objectUsingExtensionFunctions: JsonNode =
listOf(
"thisIsAString" to "stringValue".asJsonValue(),
"thisIsANumber" to 12345.asJsonValue(),
"thisIsAList" to listOf(true.asJsonValue()).asJsonArray()
).asJsonObject()
Для преобразования объекта в строку можно воспользоваться методами asCompactJsonString
и asPrettyJsonString
. Первый позволяет сохранить данные в наиболее компактной форме, а последний — в форме удобной для человека
Метод parse позволяет выполнить обратное преобразование из строки в JSON-объект
Преобразование объектов в JSON #
Существует несколько стратегий для реализации сериализации:
- Реализация всей логики внутри хранилища
- Только хранилище знает о форме представления данных на жёстком диске
- Каждый объект должен предоставлять доступ к внутреннему состоянию, которое нужно сохранить
- При изменении внутренней структуры сохраняемых объектов необходимо изменять методы сериализации
- Реализация логики в хранилище и его объектах
- Каждый объект должен реализовать единый интерфейс для сохранения и восстановления состояния
- Не возникает проблемы раскрытия внутреннего состояния
- Реализация обобщённого подхода к хранению данных
- Используются технологии метапрограммирования для решения задачи
- Для сложных типов данных необходимо описать стратегии сериализации и десериализации
- При работе со сложными данными необходимо дополнительно помечать данные как необходимые для сохранения и ненужные
Сохранение и восстановление состояния #
Хранилище должно реализовывать надёжное сохранение и восстановление состояния
- При старте приложение серализованные данные должны быть считаны с файловой системы
- Сохранение данных может быть реализовано:
- При изменении данных, по окончании транзакции
- При выходе из приложения
Будем выполнять сохранение состояния при завершении работы приложения
- Структура JSON-документов не предназначена для внесения изменений в произвольные места
- Процесс сохранения части данных требует аккуратной реализации
- Возможна потеря данных ввиду разных обстоятельств
Отслеживание завершения приложения #
JVM-процесс может завершить свою работу в следующих случаях:
- Все основные потоки внутри процесса завершают свою работу
- Частный пример: завершает работу основной поток
- Программа получает сигнал завершения работы
При завершении работы могут быть запущены потоки-обработчики данной ситуации
Для регистрации потока используется метод Platform.addShutdownHook(Thread thread)
Для описания потока в Kotlin можно воспользоваться thread
val hook = thread(start = false) {
println("Работаю во время выключения")
}
Runtime.getRuntime().addShutdownHook(hook)
Ключевые задачи репозитория #
- Поддержание корректного состояния объектов под управлением репозитория
- Реализация операций по получению элементов из репозитория
- Реализация операций по изменению операций репозитория:
- Удаление элементов
- Обновление элементов
interface Repository<T> {
fun fetch(id: UUID): T
fun list(): Iterable<T>
fun query(queryParams: QueryParams): Iterable<T>
fun add(entity: T): UUID
fun delete(entity: T)
fun update(entity: T)
}
Репозиторий не должен реализовывать все перечисленные методы, если они не нужны для работы приложения
Проблема идентификации элементов #
Каждый конкретный объект в приложении должен быть точно идентифицирован:
- Не должен изменяться в результате добавления или удаления других объектов
- Стабильность идентификатора позволяет использовать его для связи объектов как внутри системы, так и вне её (для сохранения ссылок пользователями)
Для решения данной задачи порядковый номер в массиве, индекс, не подходит. Вместо этого необходимо каждый объект внутри системы расширить идентификатором
Проблема формирования идентификатора #
- Специальный генератор соответствующих элементов внутри хранилища
- Случайное значение из достаточно большого объёма данных
Удобным способом для формирования идентификаторов является использование UUID
Неизменяемость данных #
Хранилище должно являться единственным источником правдивых данных для всего приложения. Для этого хранилище должно выполнять функции по контролю над данными, которыми оно оперирует. Наиболее эффективный способ достижения этой цели — сделать данные неизменяемыми
data-классы в Kotlin будут неизменяемыми, если:
- все их поля являются неизменяемыми,
val
- все значения полей являются неизменяемыми
В рамках JVM-экосистемы большинство примитивных типов являются неизменяемыми: числа, строки, классы даты и времени (LocalDate, LocalTime и т.д.)
Обычные хранилища, базы данных, хранят данные вне процесса веб-приложения, поэтому с точки зрения приложения они являются неизменяемыми
Процедура обновления #
data-классы в Kotlin предоставляют возможность создания нового объекта с на основе существующего с помощью функции копирования copy
data class BankNote(val currency: String, val denomination: Int)
val baseNote = BankNote("RUB", 500)
val newNote = baseNote.copy(currency="CHY")
Функция copy
создаёт новый объект с изменёнными свойствами, заменяются только поля, указанные в рамках аргумента функции copy
Новый объект, с новыми свойствами, можно использовать для сохранения нового объекта в хранилище или изменении базового объекта в хранилище
Обработка null
-состояний
#
Для описания специальных состояний можно прибегнуть к null
-значению, однако данный подход несёт ряд проблем:
null
-значения ведут себя не как обычные объекты данного класса- Автоматический вывод типов в Kotlin, к сожалению, позволяет работу с null-типами
Вместо null
-значений рекомендуется описывать возможные состояния с помощью:
Архитектура веб-приложения #
Слой предметной области #
Задачи слоя предметной области:
- Выполнение любых операций над данными: добавление, модификация, удаление
- При выполнении операций контролируется корректность структуры данных
- Операции выполняют сложные манипуляции над наборами данных
Организация слоя в формате запросов #
- Запросу необходим доступ к репозиториям для работы с информацией
- Запросу передаются параметры:
- Какое количество информации необходимо получить
- По каким критериям необходимо выбирать данные
- Какие данные необходимо добавить
- Запрос возвращает результат
Предназначение пакетов приложения #
domain
operations
tables
web
filters
handlers
models
public