Мапить в программировании что это
Практичные способы маппинга данных в Kotlin
Маппинг данных – один из способов для разделения кода приложения на слои. Маппинг широко используется в Android приложениях. Популярный пример архитектуры мобильного приложения Android-CleanArchitecture использует маппинг как в оригинальной версии (пример маппера из CleanArchitecture), так и в новой Kotlin версии (пример маппера).
Маппинг позволяет развязать слои приложения (например, отвязаться от API), упростить и сделать код более наглядным.
Пример полезного маппинга изображен на схеме:
Для примера модели упрощены. Person содержит Salary в обоих слоях приложения.
В настоящем коде, если у вас одинаковые модели, возможно, стоит пересмотреть слои приложения и не использовать маппинг.
Метод №1: Методы-мапперы
Самый быстрый и простой метод. Именно он используется в CleanArchitecture Kotlin (пример маппинга).
Такой код быстрее писать и проще модифицировать – объявления полей и их использование находятся в одном месте. Не надо бегать по проекту и модифицировать разные файлы при изменении полей класса.
Еще проблема может возникнуть если по требованиям архитектуры слои приложения не могут знать друг о друге: т.е. в классе Src слоя нельзя работать со слоем Dst и наоборот. В этом случае такой вариант маппинга использовать не получится.
В рассмотренном примере слой Src зависим от слоя Dst и может создавать классы этого слоя. Для обратной ситуации (когда Dst зависим от Src ) подойдет вариант со статическими методами-фабриками:
Маппинг находится внутри классов Dst слоя, значит эти классы не раскрывают все свои свойства и структуру использующему их коду.
Если в приложении один слой зависим от другого и осуществляется передача данных между слоями приложения в обоих направлениях, статические методы-фабрики логично использовать вместе с методами-мапперами.
Резюме метода маппинга:
+ Быстро писать код, маппинг всегда под рукой
+ Легкая модификация
+ Низкая связность кода
— Затруднено Unit-тестирование (нужны моки)
— Не всегда позволено архитектурой
Метод №2: функции-мапперы
Размещение маппера и классов, с которыми он работает, в разных местах проекта не всегда удобно. При частой модификации класса придётся искать и изменять разные файлы в разных местах.
Резюме метода маппинга:
+ Простое Unit-тестирование
— Затруднена модификация
— Требуются открытые поля у классов с данными
Метод № 3: Функции-расширения
При этом стоит учесть, что функции расширения могут приводить к неожиданному поведению из-за своей статической природы: https://kotlinlang.org/docs/reference/extensions.html#extensions-are-resolved-statically
Резюме метода маппинга:
+ Простое Unit-тестирование
— Затруднена модификация
— Требуются открытые поля у классов с данными
Метод №4: Классы-мапперы с интерфейсом
Относительно маппинга в функции у этого примера только один недостаток – необходимость писать немного больше кода.
Резюме метода маппинга:
+ Лучше типизация
— Больше кода
Как и функции-мапперы:
+ Простое Unit-тестирование
— Затруднена модификация
— требует открытые поля у классов с данными
Метод 5: Рефлексия
Метод черной магии. Рассмотрим этот метод на других моделях.
В данном примере EmployeeSrc и EmployeeDst хранят имя в разных форматах. Мапперу нужно только составить имя для новой модели. Остальные поля обработаются автоматически, без написания кода (вариант else в when ).
Метод может быть полезен, например, если у вас большие модели с кучей полей и поля в основном совпадают у одних и тех же моделей из разных слоев.
Большая проблема возникнет, например, если вы добавите обязательные поля в Dst и его случайно не окажется в Src или в маппере: cлучится IllegalArgumentException в runtime. Также рефлексия имеет проблемы с производительностью.
Резюме метода маппинга:
+ меньше кода
+ простое Unit-тестирование
— опасен
— может негативно сказаться на производительности
Выводы
Такие выводы можно сделать из нашего рассмотрения:
Методы-мапперы — наглядный код, быстрее писать и поддерживать
Функции-мапперы и функции расширения – просто тестировать маппинг.
Классы мапперы с интерфейсом — просто тестировать маппинг и яснее код.
Рефлексия – подходит для нестандартных ситуаций.
Часть 2. Создание классов, маппингов и заполнение БД
В предыдущей части были рассмотрены виды связей (один-к-одному, один-ко-многим, многие-ко-многим), а также один класс Book и его маппинг-класс BookMap. Во второй части обновим класс Book, создадим остальные классы и связи между ними, как это было изображено в предыдущей главе в Диаграмме баз данных, расположившейся над подзаголовком 1.3.1 Связи.
Небольшое объяснение
public virtual ISet Genres < get; set; >
public virtual ISet Authors
Отношение многие-к-одному, один-ко-многим.
Book | Series |
---|---|
References(x => x.Series).Cascade.SaveUpdate(); | HasMany(x => x.Books) .Inverse(); |
Метод References применяется на стороне «Многие-к-одному», на другой стороне «Один-ко-многим» будет метод HasMany.
Если сейчас запустить проект и посмотреть БД Bibilioteca, то появятся новые таблицы с уже сформированными связями.
Далее заполним созданные таблицы данными…
Для этого создадим тестовое приложение, которое будет сохранять данные в БД, обновлять и удалять их, изменив HomeController следующим образом (Ненужные участки кода комментируем):
Изменим представление следующим образом:
Маппинг для классов, у которых есть наследование.
А как маппить классы у которых есть наследование? Допустим, имеем такой пример:
В принципе, ничего сложного в этом маппинге нет, мы просто создадим один маппинг для производного класса, то есть таблицы Triangle.
После запуска приложения, в БД Biblioteca появится следующая (пустая) таблица
Elasticsearch — Урок 3.1 Mapping: схема документов
Маппинг (сопоставление) — это процесс определения схемы или структуры документов. Он описывает свойства полей в документе. Свойства поля включают тип данных (например, string, integer и т.д.) и метаданные.
Подобно тому, как вы определяете схему таблицы в SQL, важно рассказать про схему документов, прежде чем индексировать любые данные. Как мы обсуждали ранее, тип в Elasticsearch похож на таблицу SQL, который группирует документы аналогичного характера (например тип для пользователей, тип для заказов). Каждый тип имеет схему. Наличие разных схем данных также может быть мотивацией для определения нового типа.
Apache Lucene, которая хранит ваши данные под капотом, не имеет понятия типов. Информация о типе хранится в метаданных и обрабатывается Elasticsearch.
Прежде чем говорить о различных типах данных, поддерживаемых Elasticsearch, давайте взглянем на документы и как определим их схему. Предположим, мы хотим индексировать документы, показанные ниже:
Динамическое отображение
Когда вы индексируете документ без указания сопоставления, Elasticsearch автоматически определяет типы данных для полей в документе. Чтобы понять, как работает динамическое сопоставление, давайте попробуем проиндексировать документ человека, как показано ниже:
Теперь давайте проверим сопоставление типа человека, которое устанавливается автоматически:
Вы можете видеть из предыдущего ответа, что Elasticsearch определил тип данных age как числовое, date_of_birth как дату и name как строковое. Когда встречается новое поле, он пытается определить, является ли поле логическим, числовым, текстовым или датой.
Сопоставление числовых и логических полей данных является простым, но для сопоставления полей даты сначала проверяется значение строки, чтобы увидеть, соответствует ли оно любым известным шаблонам дат.
По умолчанию строковое значение проверяется в отношении трех форматов дат, показанных ниже:
Хотя Elasticsearch может определить тип данных, вы должны установить отображение во все известные поля, чтобы избежать любых неожиданностей. По умолчанию для любых полей, не входящих в сопоставление, тип данных определяется на основе первого найденного значения поля. Чтобы избежать неожиданного сопоставления, вы можете отключить динамическое сопоставление, как показано ниже:
Динамическая настройка принимает три разных значения:
Иногда автоматическое определение даты может вызвать проблемы. Если начало строкового поля случайно совпадает с форматом даты по умолчанию, тип данных поля может быть установлен на дата вместо текста. Чтобы этого избежать, вы можете отключить автоматическое определение даты, как показано ниже:
Создать индекс с отображением
Чтобы определить или добавить схему, мы должны использовать API маппинга.
API маппинга позволит вам сделать следующее:
Обратите внимание, что метод HTTP, используемый для запроса — PUT.
Если все прошло успешно, вы должны увидеть ответ, как показано ниже:
Добавление нового типа / поля
В предыдущем разделе мы обсудили, как создать индекс со схемой маппинга. В этом разделе мы обсудим, как добавить новый тип и новые поля. Давайте добавим новый тип, назовем его history с целью хранить там историю заходов пользователя.
Обратите внимание на _mapping в конце URL-адреса. Вы можете добавить имя типа history в example3 индекс, как показано ниже:
Установив ip_address поле как тип ip, мы можем выполнить запросы диапазона и агрегации по IP-адресу. Мы обсудим тип данных ip в скором будущем.
Получение существующей схемы
API маппинга также используется для извлечения существующей схемы. Вы можете проверить схему существующего индекса или типа, как показано ниже:
Обратите внимание, что метод HTTP, используемый для запроса — GET.
Следующий ответ содержит отображение всех типов в example3 :
Вы также можете получить сопоставление одного типа, как показано ниже:
Изменение существующей схемы
Схема существующих полей не может быть изменена. Когда документы индексируются, они сохраняются в инвертированном индексе в соответствии с типом данных. Если вы попытаетесь обновить тип данных существующего поля, вы получите исключение, как показано ниже:
Вы всегда можете добавлять новые поля или использовать несколько полей для индексации одного и того же поля с использованием нескольких типов данных, но вы не можете обновлять существующее сопоставление. Если вы хотите изменить отображение, вам нужно заново создать индекс или использовать Reindex API. Мы обсудим переиндексацию в 5 уроке. Если вам не нужны уже индексированные данные, вы можете добавить новое поле с правильным типом данных.
Тип данных
В традиционном мире SQL тип данных может быть только простым, таким как integer, boolean и т. д. Поскольку данные в Elasticsearch представлены как JSON, он поддерживает типы данных, которые являются более сложными объекты, массивы и т. д.
Поддерживаются разные типы данных:
Основные типы данных:
Сложные типы данных:
Типы данных Geo:
Специализированные типы данных:
Прежде чем мы перейдем к каждому типу данных, давайте поговорим о мета-полях, которые содержатся в каждом документе.
Мета-поля
Каждый индекс, который мы индексируем, имеет следующие мета-поля, также известные как мета-поля идентификации, поскольку они используются для однозначного определения документа:
Как обрабатывать нулевые значения
Когда Elasticsearch встречает нулевое значение JSON в документе, оно пропускает это поле, поскольку оно не может быть проиндексировано. Но если вы хотите искать все документы, содержащие нулевое значение, вы можете сказать Elasticsearch заменить значение null значением по умолчанию.
Обратите внимание, что он null_value должен иметь тот же тип данных, что и поле. Целочисленное поле не может иметь нулевое значение, которое является строкой.
Хранение оригинального документа
Поиск по всем полем в документе
Когда предыдущий документ индексируется вместе с полями документа, Elasticsearch индексирует все значения полей в одну большую строку ( «john jake iphone 2017-02-08» ) в качестве _all поля. Запрос для всех заказов, которые содержат, item_title как iphone показано ниже:
По умолчанию поле _all включено. Если вы не планируете использовать его можно отключить, как показано ниже. Отключение его приведет к уменьшению размера индекса на диске:
В следующем части урока мы обсудим анализаторы и почему отображение необходимо для получения правильных результатов поиска.
Маппинг данных из реляционной БД
Иногда возникают ситуации, когда решение задачи выборки данных из реляционной БД не укладывается в возможности используемой в проекте ОРМ, например, либо из-за недостаточной скорости работы самой ОРМ, либо не совсем оптимальных SQL запросов генерируемых ею. В таком случае обычно приходится писать запросы вручную.
Проблема в том, что данные из БД (в т.ч. в ответ на JOIN запрос) возвращаются в виде “плоского” двухмерного массива никак не отражающего сложную “древовидную” структуру данных приложения. Работать с таким массивом дальше крайне неудобно, поэтому требуется более-менее универсальное решение, позволяющее привести этот массив в более подходящий вид по заданному шаблону.
Решение было найдено, удобное и достаточно быстрое.
На сколько быстрое
Для оценки скорости работы библиотеки я собрал небольшой испытательный стенд на котором скорость работы моей библиотеки сравнивается со скоростью работы Eloquent. Для замеров использовался пакет phpbench.
Для того чтобы развернуть стенд у себя:
Здесь я использовал инструмент описанный в моей предыдущей статье.
Затем в меню выбираем: 1 Develop, затем: 1 Build, затем 2 Deploy and Up;
Затем запускаем тесты 5. Run tests
В базе 3000 книг. Результаты получились следующие:
benchEloquent — вытаскивает все книги с авторами с использованием Eloquent
benchEloquentId — вытаскивает определенную книгу с авторами с использованием Eloquent (10 раз)
benchProc — вытаскивает все книги с авторами с использованием библиотеки
benchProcId — вытаскивает определенную книгу с авторами с использованием библиотеки (10 раз)
Возможно приведенные тесты недостаточно репрезентативны, но разница заметна, как по времени выполнения, так и по расходованию памяти.
Как это работает
Далее, для примера (крайне простого), представим, что у нас имеется БД книг и авторов со следующей структурой.
Задача — вытащить все книги с их авторами.
Запрос будет выглядеть как-то так:
В ответ мы получим примерно такой массив данных.
book.id | book.name | author.id | author.name |
1 | book1 | 2 | author2 |
1 | book1 | 4 | author4 |
1 | book1 | 6 | author6 |
2 | book2 | 2 | author2 |
2 | book2 | 3 | author3 |
2 | book2 | 6 | author6 |
2 | book2 | 7 | author7 |
Для этого немного изменим наш запрос:
Здесь мы в секции SELECT задали алиасы: для полей с данными о книгах алиасы с префиксом ‘book_’, а для полей с информацией об авторах с префиксом ‘author’.
Далее преобразуем ответ БД
$rows — ответ БД в виде массива объектов /stdClass()
$config — ассоциативный массив отражающий структуру данных итогового массива
Маппинг данных из реляционной БД
Иногда возникают ситуации, когда решение задачи выборки данных из реляционной БД не укладывается в возможности используемой в проекте ОРМ, например, либо из-за недостаточной скорости работы самой ОРМ, либо не совсем оптимальных SQL запросов генерируемых ею. В таком случае обычно приходится писать запросы вручную.
Проблема в том, что данные из БД (в т.ч. в ответ на JOIN запрос) возвращаются в виде “плоского” двухмерного массива никак не отражающего сложную “древовидную” структуру данных приложения. Работать с таким массивом дальше крайне неудобно, поэтому требуется более-менее универсальное решение, позволяющее привести этот массив в более подходящий вид по заданному шаблону.
Решение было найдено, удобное и достаточно быстрое.
На сколько быстрое
Для оценки скорости работы библиотеки я собрал небольшой испытательный стенд на котором скорость работы моей библиотеки сравнивается со скоростью работы Eloquent. Для замеров использовался пакет phpbench.
Для того чтобы развернуть стенд у себя:
Здесь я использовал инструмент описанный в моей предыдущей статье.
Затем в меню выбираем: 1 Develop, затем: 1 Build, затем 2 Deploy and Up;
Затем запускаем тесты 5. Run tests
В базе 3000 книг. Результаты получились следующие:
benchEloquent — вытаскивает все книги с авторами с использованием Eloquent
benchEloquentId — вытаскивает определенную книгу с авторами с использованием Eloquent (10 раз)
benchProc — вытаскивает все книги с авторами с использованием библиотеки
benchProcId — вытаскивает определенную книгу с авторами с использованием библиотеки (10 раз)
Возможно приведенные тесты недостаточно репрезентативны, но разница заметна, как по времени выполнения, так и по расходованию памяти.
Как это работает
Далее, для примера (крайне простого), представим, что у нас имеется БД книг и авторов со следующей структурой.
Задача — вытащить все книги с их авторами.
Запрос будет выглядеть как-то так:
В ответ мы получим примерно такой массив данных.
book.id | book.name | author.id | author.name |
1 | book1 | 2 | author2 |
1 | book1 | 4 | author4 |
1 | book1 | 6 | author6 |
2 | book2 | 2 | author2 |
2 | book2 | 3 | author3 |
2 | book2 | 6 | author6 |
2 | book2 | 7 | author7 |
Для этого немного изменим наш запрос:
Здесь мы в секции SELECT задали алиасы: для полей с данными о книгах алиасы с префиксом ‘book_’, а для полей с информацией об авторах с префиксом ‘author’.
Далее преобразуем ответ БД
$rows — ответ БД в виде массива объектов /stdClass()
$config — ассоциативный массив отражающий структуру данных итогового массива