Хранение данных

Хранение данных #

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

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


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

  • Веб-приложение предназначено для обработки запросов от пользователя
  • Веб-приложение управляет некоторым набором данных
  • Веб-приложение предоставляет логичный интерфейс для управления данными
flowchart TB user("Пользователь") view["Слой представления"] business["Слой предметной области"] storage["Слой хранения данных"] fs[("Файловая система")] db[("Базы данных")] dataSources(["Другие источники данных"]) user --> view view --> business business --> storage storage --> fs storage --> db storage --> dataSources

Особенности хранилищ данных #

  • Данные хранятся вне приложения в специальных хранилищах
  • Веб-приложение не должно сохранять любые данные внутри себя
    • Это позволяет масштабировать обработку запросов
    • Это позволяет сохранять и восстанавливать состояние приложения
  • Хранилища предоставляют интерфейс для работы с данными:
    • Быстрый поиск данных
    • Добавление элементов
    • Обновление и обновление элементов
    • Работа с очень большими объёмами данных
    • Распределённое хранение данных
    • Надёжное сохранение данных

Схема работы с хранилищем данных #

  1. При старте приложение:
    1. Считывает конфигурацию, в частности о местоположении хранилищ
    2. Устанавливает соединение со всеми хранилищами данных
  2. При получении запроса от пользователя
    1. Формируется запрос в хранилище
    2. Используется одно из соединений к хранилищу
    3. Данные от хранилища преобразуются в объекты предметной области
    4. Объекты обрабатываются
    5. Подготовленные данные передаются в слой представления

Для работы с хранилищами необходимо изучить специальные API или языки программирования, например язык SQL


Собственное хранилище #

В рамках курса будем использовать собственное хранилище данных

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

Минусы такого подхода:

  • Данные могут быть потеряны в случае непреднамеренного завершения приложения
  • Данные потенциально могут быть испорчены ввиду конкуретного доступа и одновременного изменения
  • Можно нарушить шаблоны доступа к данным, что приведёт к деградации архитектуры приложения

Данный подход к хранению данных можно применять для непрофессиональных или небольших приложений


Архитектура элементов хранилища #

flowchart TB fs("Файловая система") store("Хранилище") repoOne("Репозиторий бабочек") butterfly("Бабочка") repoTwo("Репозиторий коней") horse("Конь") queryOneBlue("Запрос на голубые бабочки") queryOneBeautiful("Запрос на 5 красивых") queryTwo("Запрос на красных коней") handlerMain("HTTP-обработчик стартовой страницы") handlerButterfly("HTTP-обработчик бабочек") handlerMain --> queryOneBeautiful handlerMain --> queryTwo handlerButterfly --> queryOneBlue queryOneBlue --> repoOne queryOneBeautiful --> repoOne queryTwo --> repoTwo repoOne --> store repoOne --> butterfly repoTwo --> store repoTwo --> horse store --> fs

Назначение элементов #

Рассмотрим ключевые особенности каждого элемента архитектуры

Хранилище #

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

В обычных веб-приложениях реализуется внешним приложением (СУБД) или классами специализированной библиотеки

Репозиторий #

  • Обеспечивает конкретное управление списком объектов при добавлении, обновлении и удалении элементов
  • Предоставляет доступ к извлечению объектов данного репозитория
  • Хранит список объектов во время работы приложения

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


Запросы #

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

Слой предметной области #

  • Описывает детали работы с данными предметной области
  • Формируется в виде выделенных элементов обычно для приложений среднего и большого размера, оперирующих большими объёмами данных
  • В случае приложений курса данные сущности появляются в случае необходимости работы с несколькими списками данных

Задача сохранения данных на жёстком диске #

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

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

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

Существует множество отрытых форматов хранения данных: 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-значений рекомендуется описывать возможные состояния с помощью:


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

flowchart TB user("Пользователь") view["Слой представления"] business["Слой предметной области"] fs[("Файловая система")] db[("Базы данных")] dataSources(["Источники данных"]) user --> view view --> business business --> fs business --> db business --> dataSources

Слой предметной области #

Задачи слоя предметной области:

  • Выполнение любых операций над данными: добавление, модификация, удаление
  • При выполнении операций контролируется корректность структуры данных
  • Операции выполняют сложные манипуляции над наборами данных

Организация слоя в формате запросов #

flowchart LR handler["HTTP-обработчик"] query("Запрос") repoOne["Репозиторий #1"] repoTwo["Репозиторий #2"] handler -- вызывает --> query query -- использует --> repoOne query -- использует --> repoTwo
  • Запросу необходим доступ к репозиториям для работы с информацией
  • Запросу передаются параметры:
    • Какое количество информации необходимо получить
    • По каким критериям необходимо выбирать данные
    • Какие данные необходимо добавить
  • Запрос возвращает результат

Предназначение пакетов приложения #

  • domain
    • operations
    • tables
  • web
    • filters
    • handlers
    • models
    • public

© A. M. Васильев, 2022, CC BY-SA 4.0, andrey@crafted.su