Перевод подготовлен в рамках набора учащихся на курс "Kotlin Backend Developer".Также приглашаем всех желающих на демо-урок «Объектно-ориентированное программирование в Kotlin». Цели занятия:- узнать про элементы объектной модели Kotlin;- создавать различные классы и объекты;- выполнять наследование и делегирование;- пользоваться геттерами и сеттерами.   В этой статье мы рассмотрим, как направлять запросы к реляционной базе данных с помощью Exposed. Exposed — это открытая библиотека, разработанная компанией JetBrains. Она распространяется по лицензии Apache и позволяет использовать идиоматический API Kotlin для реализации некоторых реляционных баз данных от различных поставщиков. Exposed можно использовать как в качестве высокоуровневого языка DSL в SQL, так и в качестве облегченной технологии ORM (объектно-реляционного отображения). В этом руководстве мы рассмотрим оба варианта использования. Фреймворк Exposed еще не опубликован в Maven Central, потому нам придется использовать отдельный репозиторий: Теперь можно подключить библиотеку: Ниже мы приведем несколько примеров использования базы данных H2 в памяти: Последняя версия Exposed доступна на Bintray, а последняя версия H2 — на Maven Central. Для того чтобы установить соединение с базой данных, будем использовать класс Database: Мы можем также указать пользователя (user) и пароль (password) в качестве именованных параметров: Обратите внимание: вызов метода connect не устанавливает соединения с БД сразу. Соединение будет установлено позже с использованием сохраненных параметров. Для того чтобы задать другие параметры соединения, мы будем использовать перегруженный метод connect, который позволит нам полностью контролировать подключение: В этом случае нам придется использовать замыкание. Exposed вызывает замыкание при необходимости установления нового соединения с базой данных.  Если мы будем подключаться к базе данных с использованием DataSource (именно этот подход обычно используется в корпоративных приложениях, например, чтобы иметь преимущества пула подключений), нам потребуется соответствующий перегруженный метод connect: Все операции с базами данных в Exposed выполняются только при наличии активных транзакций. Метод transaction принимает замыкание и вызывает его в активной транзакции. Метод transaction возвращает значение, которое вернуло замыкание. После выполнения блока Exposed автоматически закрывает транзакцию. После успешного выполнения блока с помощью метода transaction Exposed подтверждает транзакцию. Если же замыкание генерирует исключение, фреймворк откатывает транзакцию. Подтверждение и откат транзакции можно выполнить в ручном режиме. В Kotlin замыкание, которое мы передали в методе transaction, фактически является экземпляром класса Transaction. Таким образом, мы можем использовать метод commit или rollback: При изучении фреймворка или отладке кода не лишним будет отследить инструкции и запросы SQL, которые Exposed отправляет в базу данных. Для этого добавим регистратор к активной транзакции: Как правило, Exposed не используется для работы с неформатированными строками и именами SQL. Мы определяем таблицы, столбцы, ключи, связи и т. д. с помощью высокоуровневого DSL. AD Для представления каждой таблицы будем использовать экземпляр класса Table: Exposed автоматически присваивает таблице имя на основании имени класса, но мы можем задать имя самостоятельно: Без столбцов таблица работать не будет. Определим столбцы как свойства класса Table: Для краткости мы не указали типы — Kotlin определит их автоматически. В любом случае каждая колонка относится к классу Column<T>, у нее есть имя, тип и, возможно, параметры типа. В предыдущем разделе мы рассмотрели пример, где индексы и первичные ключи определяются с помощью текучего API. Однако, если в таблице в качестве первичного ключа используется целое число, мы можем использовать встроенные в Exposed классы IntIdTable и LongIdTable для определения ключей: Есть также класс UUIDTable, а еще мы можем определить собственные варианты, выделив подклассы в классе IdTable. Добавить внешний ключ очень просто. Мы можем использовать статическую типизацию, поскольку мы всегда обращаемся к свойствам, которые были известны в момент создания таблицы. Предположим, что нам нужно узнать имена актеров, которые снимались в каждом фильме: Для того чтобы не прописывать тип столбца (в этом примере — integer), который можно получить из связанного столбца, воспользуемся методом reference: Если мы ссылаемся на первичный ключ, имя столбца можно не указывать: Таблицы можно создавать программно, как указано выше: Таблицу можно создать, только если она не существует. Однако миграция баз данных не поддерживается. Определив классы таблиц, как показано выше, мы можем направлять запросы к базе данных с использованием функций расширения, встроенных в фреймворк. Для того чтобы извлечь данные из базы, будем использовать объекты Query, созданные на основе классов таблиц. Самый простой запрос будет возвращать все строки заданной таблицы: Запрос является итерируемым и поддерживает циклы forEach: Параметр замыкания, которому в нашем примере присвоено имя it, — это экземпляр класса ResultRow. В результате столбцам присваиваются ключи. Мы можем также выбрать подмножество столбцов таблицы, то есть выполнить проекцию, с помощью метода slice: Этот метод позволяет применить функцию к столбцу: Часто при использовании агрегатных функций, например count и avg, направляя запрос, мы используем группировку по оператору. О группах мы поговорим в разделе 6.5. В Exposed для выражений where, которые используются для фильтрации запросов и других типов инструкций, используется специальный DSL. В основе этого мини-языка лежат свойства столбцов, с которыми мы познакомились ранее, и серия логических операторов. Вот пример выражения where: Оно относится к комплексному типу и является подклассом SqlExpressionBuilder, который определяет такие операторы, как like, eq, and. Как видим, это последовательность операций сравнения, соединенных операторами and и or. Мы можем передать такое выражение в метод select, который вернет очередной запрос: Поскольку тип может быть выведен из контекста, нам не обязательно указывать тип complex для выражения where, когда оно передается непосредственно в метод select, как в рассмотренном примере. В Kotlin выражения с where являются объектами, поэтому специальных параметров для запросов нет. Мы используем переменные: Существует несколько методов для уточнения запросов, которые возвращает метод select и его эквиваленты. Например, можно удалить повторяющиеся строки: Или вернуть только подмножество строк, например в случае нумерации страниц с результатами при работе над пользовательским интерфейсом: Эти методы будут возвращать новые объекты Query, поэтому мы можем выстроить их вызовы в цепочку. Метод Query.orderBy принимает список столбцов, связанных со значением SortOrder, которое задает тип сортировки элементов — по возрастанию или по убыванию: Группировка по одному или нескольким столбцам будет особенно полезна при использовании агрегатной функции (см. раздел 6.2). Для этого воспользуемся методом groupBy: Соединения — это, пожалуй, одна из самых важных характеристик реляционной базы данных. Вот самый простой пример. Если мы знаем внешний ключ и у нас нет условий соединения, мы можем воспользоваться встроенными операторами соединения: В этом примере мы использовали оператор innerJoin, но по этому же принципу можно использовать операторы LEFT JOIN, RIGHT JOIN и CROSS JOIN. Затем можно добавить условия соединения, используя выражение where; например, если у нас нет внешнего ключа, придется указать явную операцию соединения: В общем случае операция соединения полностью записывается так: Благодаря тому что имена столбцов связаны со свойствами, при типичном соединении нам не придется присваивать им псевдонимы, даже если у некоторых столбцов имена совпадают: На самом деле в этом примере StarWarsFilms.sequelId и Players.sequelId — это разные столбцы. Однако, если в запросе одна и та же таблица появляется несколько раз, можно присвоить ей псевдоним. Для этого воспользуемся функцией alias: Псевдоним можно указывать в качестве названия таблицы: В этом примере sequel — это таблица, которая участвует в операции соединения. Чтобы обратиться к столбцу, в качестве ключа будем использовать столбец таблицы, представленной псевдонимом: Мы рассмотрели, как выполнять запросы к базе данных. Теперь разберемся с DML-инструкциями. Для того чтобы вставить данные, вызовем функцию, эквивалентную функции insert. Все они принимают замыкание: В этом замыкании используются два объекта: this (само замыкание) — это экземпляр класса StarWarsFilms; именно этот объект позволяет нам обращаться к столбцам, которые являются свойствами, по неуточненному имени; it (параметр замыкания) — это InsertStatement; это структура, аналогичная коллекции ключ/значение, в которой есть слоты для вставки столбцов. Если у нас есть инструкция insert с автоматически генерируемыми столбцами (обычно это автоматическое увеличение индекса или последовательности), мы можем извлечь сгенерированные значения. В типичном сценарии есть только одно сгенерированное значение. Воспользуемся методом insertAndGetId: Если у нас несколько сгенерированных значений, их можно считывать по имени: Мы научились выполнять запросы и вставлять данные. Теперь перейдем к обновлению данных, которые содержатся в базе. Самый простой способ обновления похож на комбинацию методов select и insert: В этом примере выражение where используется вместе с замыканием UpdateStatement. UpdateStatement и InsertStatement — это потомки класса UpdateBuilder, поэтому в них используется один и тот же API и одна и та же логика. Родительский класс позволяет задать значение столбца с помощью квадратных скобок. Если для обновления столбца нам нужно вычислять новое значение из старого, воспользуемся SqlExpressionBuilder: Этот объект позволяет использовать инфиксный оператор (например, plus, minus и т. д.) для создания инструкции обновления. И наконец, мы можем удалить данные с помощью метода deleteWhere: Мы использовали Exposed для того, чтобы связать операции над объектами Kotlin с запросами и инструкциями SQL напрямую. Такие методы, как insert, update, select и т. д., немедленно отправляют строку SQL в базу данных. Однако в Exposed есть высокоуровневый API DAO, который представляет собой простую технологию ORM. Давайте рассмотрим его подробнее. В рассмотренных выше примерах мы использовали классы для представления таблиц базы данных и описания операций над ними с использованием статических методов. Теперь мы можем определить сущности на основе этих классов таблиц, где каждый экземпляр сущности представляет собой строку базы данных: Давайте подробно проанализируем это определение. Из первой строки видно, что сущность — это класс, расширяющий Entity. У нее есть ID специфического типа, в нашем случае — Int. Затем определяем объект-компаньон. Объект-компаньон — это класс сущности, то есть статические метаданные, которые определяют сущность и операции, которые мы можем выполнять над ней. Объявляя объект-компаньон, мы соединяем сущность с именем StarWarsFilm (в единственном числе), которая представляет собой одну строку, с таблицей с именем StarWarsFilms (во множественном числе), которая представляет собой коллекцию всех строк. Наконец, мы задаем свойства с помощью делегатов свойств для соответствующих столбцов таблицы. Обратите внимание, что раньше мы объявляли столбцы с помощью val, поскольку они являются неизменяемыми метаданными. Теперь же мы объявляем свойства сущности с помощью var, поскольку они являются изменяемыми слотами в строке базы данных. Чтобы вставить строку в таблицу, нам нужно просто создать экземпляр класса сущности с использованием статического фабричного метода new в транзакции: Обратите внимание: с базой данных выполняются отложенные операции, которые запускаются только после выполнения warm cache. В Hibernate, например, теплый кэш привязан к сессии (session). Операция выполняется автоматически. Например, когда мы в первый раз считываем сгенерированный идентификатор, Exposed выполняет инструкцию insert: Сравните это поведение с методом insert, который мы рассматривали в разделе 7.1, — в этом примере метод сразу же выполняет инструкцию в базе данных. Здесь же мы работаем на более высоком уровне абстракции. Для обновления строк нужно просто задать их свойства: Для удаления объекта вызовем метод delete этого объекта: Так же, как при использовании метода new, обновление и операции выполняются в отложенном режиме. Обновить и удалить можно только ранее загруженный объект. В этом фреймворке нет API для обновления и удаления нескольких объектов, и нам придется использовать API более низкого уровня (см. раздел 7). Тем не менее в одной транзакции можно одновременно использовать и тот и другой API. API DAO позволяет выполнять три типа запросов. Для загрузки всех объектов, для которых не заданы условия, будем использовать статический метод all: Для загрузки одного объекта по ID воспользуемся методом findById: Если объекта с таким ID нет, findById вернет значение null. В самом общем случае мы можем использовать метод find с выражением where: В ORM соотнесение соединений со ссылками так же важно, как соединения в реляционных базах данных. Посмотрим, какие возможности предлагает Exposed. Предположим, что нам нужно узнать пользовательский рейтинг каждого фильма. Сначала определим две дополнительные таблицы: Затем создадим соответствующие сущности. Опустим сущность User (это очевидно) и перейдем сразу к классу UserRating: Обратите внимание: инфиксный метод referencedOn вызывает свойства, которые представляют собой связи. Модель следующая: объявляем переменную var через сущность (by) со ссылкой на соответствующий столбец (referencedOn). Свойства, объявленные таким образом, ведут себя как обычные свойства, но их значением является связанный объект: Рассмотренные выше связи являются обязательными — мы должны всегда указывать значение. Чтобы установить дополнительные связи, нам нужно сначала разрешить столбцу таблицы принимать значение null: Вместо метода referencedOn будем использовать optionalReferencedOn: Таким образом, свойство user сможет принимать значение null. Мы можем создать обратную связь с помощью внешнего ключа. Будем использовать его для моделирования рейтинга фильма в базе данных; у фильма будет несколько рейтингов. Для отображения рейтингов нужно добавить свойство к той стороне связи, где используется «один» объект. В нашем случае это сущность film: Модель похожа на модель связи «многие к одному», но сейчас мы используем метод referrersOn. Свойство, определенное таким образом, является итерируемым, поэтому мы можем выполнить обход с помощью цикла forEach: В отличие от обычных свойств, здесь мы определили ratings через val. Свойство неизменяемо, поэтому мы можем только считывать его. У значения свойства тоже нет API для изменения. Поэтому для добавления нового рейтинга нам нужно создать его, указав ссылку на фильм: Теперь новый рейтинг появится в перечне рейтингов для этого фильма. Иногда требуется установить связь «многие ко многим». Предположим, что нам нужно связать класс StarWarsFilm с таблицей Actors: После того как мы определим таблицу и сущность, нужно будет создать другую таблицу для установления связи: В этой таблице два столбца, каждый из которых является внешним ключом, а вместе они представляют собой сложный первичный ключ. Теперь можно подключить таблицу связей к сущности StarWarsFilm: На момент написания статьи создать сущность с генерируемым идентификатором и использовать ее в связи «многие ко многим» в одной транзакции нельзя. Нам приходится выполнять несколько транзакций: В этом примере мы для удобства выполнили три транзакции, хотя двух было бы достаточно. В этой статье мы подробно рассмотрели фреймворк Kotlin Exposed. Дополнительную информацию и примеры можно найти в вики-учебнике по Exposed. Варианты реализации рассмотренных примеров и фрагменты кода можно найти на GitHub. Узнать подробнее о курсе "Kotlin Backend Developer". Смотреть вебинар «Объектно-ориентированное программирование в Kotlin».  Редактор