- Типобезопасный SQL на Kotlin
- Немного теории
- Перенос на Kotlin
- Управление данными
- Описание данных
- Работа с данными в Kotlin с использованием JDBC
- Пример проекта
- Добавление коннектора базы данных MySQL
- Создание подключения
- Создание таблиц
- Внесение счетов
- Поиск счетов
- Одна из проблем с JDBC
- Заключение
- Руководство по работе с фреймворком Kotlin Exposed
- 1. Введение
- 2. Установка
- 3. Соединение с базой данных
- 3.1. Дополнительные параметры
- 3.2. Подключение через DataSource
- 4. Открытие транзакции
- 4.1. Подтверждение и откат транзакций
- 4.2. Запись инструкций в журнал
- 5. Определение таблиц
- 5.1. Столбцы
- 5.2. Первичные ключи
- 5.3. Внешние ключи
- 5.4. Создание таблиц
- 6. Запросы
- 6.1. Выбор всех объектов
- 6.2. Выбор подмножества столбцов
- 6.3. Фильтрация с помощью выражения where
- 6.4. Дополнительная фильтрация
- 6.5. Методы orderBy и groupBy
- 6.6. Соединения
- 6.7. Псевдонимы
- 7. Инструкции
- 7.1. Вставка данных
- 7.2. Извлечение автоинкрементного значения столбцов
- 7.3. Обновление данных
- 7.4. Удаление данных
- 8. API DAO, облегченная технология ORM
- 8.1. Сущности
- 8.2. Вставка данных
- 8.3. Обновление и удаление объектов
- 8.4. Запросы
- 8.5. Связь «многие к одному»
- 8.6. Дополнительные связи
- 8.7. Связь «один ко многим»
- 8.8. Связь «многие ко многим»
- 9. Заключение
Типобезопасный SQL на Kotlin
Экспрессивность — интересное свойство языков программирования. С помощью простого комбинирования выражений можно добиться впечатляющих результатов. Некоторые языки осмысленно отвергают идеи выразительности, но Kotlin точно не является таким языком.
С помощью базовых конструкций языка и небольшого количества сахара мы попытаемся воссоздать SQL в синтаксисе Kotlin настолько близко, насколько это возможно.
Нашей целью будет помочь программисту отловить определенное подмножество ошибок на этапе компиляции. Kotlin, являясь строготипизованным языком, поможет нам уйти от невалидных выражений в структуре SQL запроса. Как бонус, мы получим еще защиту от опечаток и помощь от IDE в написании запросов. Исправить недостатки SQL полностью не получится, но устранить некоторые проблемные места вполне возможно.
Данная статья расскажет про библиотеку на Kotlin, которая позволяет писать SQL запросы в синтаксисе Kotlin. Также, мы немного посмотрим на внутренности библиотеки, чтобы понять как это работает.
Немного теории
SQL расшифровывается как Structured Query Language, т.е. структура у запросов присутствует, хотя синтаксис оставляет желать лучшего — язык создавался, чтобы им мог воспользоваться любой пользователь, даже не имеющий навыков программирования.
Однако, под SQL скрывается довольно мощный фундамент в виде теории реляционных баз данных — там всё очень логично. Чтобы понять структуру запросов, обратимся к простой выборке:
Что важно понять: запрос состоит из трех последовательных частей. Каждая из этих частей во-первых — зависит от предыдущей, во-вторых — подразумевает ограниченный набор выражений для продолжения запроса. На самом деле даже не совсем так: выражение FROM тут явно является первичным по отношению к SELECT, т.к. то, какой набор полей мы можем выбрать, напрямую зависит от таблицы, из которой производится выборка, но никак не наоборот.
Перенос на Kotlin
Итак, FROM первичен по отношению к любым другим конструкциям языка запросов. Именно из этого выражения возникают все возможные варианты продолжения запроса. В Kotlin мы отразим это через функцию from(T), которая будет принимать на вход объект, представляющий из себя таблицу, у которой есть набор колонок.
Функция вернет объект, который содержит в себе методы, отражающие возможное продолжение запроса. Конструкция from всегда идет самой первой, перед любыми другими выражениями, поэтому она предполагает большое количество продожений, включая завершающий SELECT (в противоположность SQL, где SELECT всегда идет перед FROM). Код, эквивалентный SQL-запросу выше будет выглядеть следующим образом:
Интересно, что таким образом мы можем предотвратить невалидный SQL еще во время компиляции. Каждое выражение, каждый вызов метода в цепочке предполагает ограниченное число продолжений. Мы можем контролировать валидность запроса средствами языка Kotlin. Как пример — выражение where не предполагает после себя продолжения в виде еще одного where и, тем более, from, а вот конструкции groupBy, having, orderBy, limit, offset и завершающий select все являются валидными.
Лямбды, переданные в качестве агрументов операторам where и select призваны сконструировать предикат и проекцию соответственно (мы уже упоминали их ранее). На вход лямбде передается таблица, чтобы можно было обращаться к колонкам. Важно, что типобезопасность сохраняется и на этом уровне — с помощью перегрузки операторов мы можем добиться того, что предикат в конечном итоге будет представлять из себя псевдобулевое выражение, которое не скомпилируется при наличии синтаксической ошибки или ошибки, связанной с типами. То же самое касается и проекции.
Реляционные базы данных позволяют работать с множеством таблиц и связями между ними. Было бы хорошо дать возможность разработчику работать с JOIN и в нашей библиотеке. Благо, реляционная модель хорошо ложится на всё, что было описанно ранее — нужно лишь добавить метод join, который добавит вторую таблицу в наше выражение.
JOIN, в данном случае, будет иметь методы, аналогичные тем, что предоставляет выражение FROM, с тем лишь отличием, что лямбды проекции и предикатов будут принимать по два параметра для возможности обращения к колонкам обеих таблиц.
Управление данными
Data manipulation language — средство языка SQL, которое позволяет помимо запросов к таблицам осуществлять вставку, модификацию и удаление данных. Эти конструкции хорошо вписываются в нашу модель. Для поддержки update и delete нам понадобится всего-лишь дополнить выражения from и where вариантом с вызовом соответствующих методов. Для поддержки insert, введем дополнительную функцию into.
Описание данных
SQL работает со структурированными данными в виде таблиц. Таблицы требуют описания перед началом работы с ними. Эта часть языка называется Data definition language.
Операторы CREATE TABLE и DROP TABLE реализованы аналогично — функция over будет служить стартовой точкой.
Источник
Работа с данными в Kotlin с использованием JDBC
В этой статье мы узнаем, как сохранять данные в Kotlin с помощью JDBC (Java Database Connectivity).
Пример проекта
В качестве примера я буду использовать Bytebank, проект, имитирующий цифровой банк, для регистрации счетов. Для регистрации счета требуется номер счета, имя клиента и баланс:
В классе Account мы представляем наш счет, который может быть создан следующим образом:
И к этому результату мы пришли, запустив программу:
После представления проекта и создания учетной записи давайте приступим к настройке JDBC.
Добавление коннектора базы данных MySQL
В качестве первого шага нам необходимо создать соединение между нашим приложением и базой данных.
В этом примере я буду использовать MySQL, но можно установить такое же соединение и с другими распространенными базами данных на рынке, поскольку каждая из них предлагает коннектор, способный осуществлять связь.
MySQL, например, имеет специальную страницу со всеми доступными драйверами, даже из других приложений.
На странице MySQL у нас есть возможность загрузить коннектор, однако, учитывая использование инструмента сборки (Gradle), мы можем добавить зависимость в файл сборки, build.gradle.kts:
Далее нам просто нужно синхронизировать проект, чтобы Gradle загрузил и сделал драйвер доступным для использования. Затем мы можем приступить к настройке соединения.
Создание подключения
Когда драйвер доступен, мы можем создать соединение с MySQL следующим образом:
У нас здесь много кода! Не пугайтесь, потому что мы будем разбираться, что означает каждая строчка кода:
- Class.forName(): настраивает коннектор, который мы будем использовать в соединении с JDBC. Если бы вы использовали другой драйвер, вам пришлось бы поместить класс, соответствующий используемому драйверу, которым в данном случае является драйвер MySQL (“com.mysql.cj.jdbc.Driver”).
- DriverManager.getConnection(): пытается установить соединение с базой данных через JDBC, в качестве аргумента получает адрес соединения, пользователя и пароль.
В этом вызове нам нужно обратить внимание в основном на адрес! Например, у нас по умолчанию стоит jdbc:mysql, что указывает на то, что мы будем устанавливать соединение JDBC с базой данных MySQL, т.е. если вы делаете конфигурацию для другой базы данных, то значение будет другим!
Примечание: еще один момент, который следует отметить, заключается в том, что я использую базу данных на своем компьютере, на порту 3306 (порт MySQL по умолчанию), поэтому достаточно localhost. Однако при интеграции с внешней базой данных адрес будет другим!
Обратите также внимание, что мы указываем базу данных, к которой хотим получить доступ, и в данном случае я создал банк bytebank. Не стесняйтесь использовать этот же пример или другой по вашему выбору.
Затем, в качестве второго и третьего аргумента, нам нужно отправить пользователя и пароль. Во время этого теста я буду использовать пользователя root с пустым паролем.
“Красота! Я понял конфигурацию коннектора и то, как мы создаем соединение, но почему мы используем try catch?”.
Перехват try необходим для того, чтобы наше приложение могло определить общие проблемы во время попытки соединения, например, недопустимый адрес, сбой аутентификации или любая другая проблема. Поэтому мы представляем трассировку стека исключения и сообщение ниже, указывающее на то, что мы не смогли подключиться к базе данных.
После ввода каждой строки кода, при запуске программы мы получаем следующий результат:
Вот и все! Мы можем начать выполнение операторов в MySQL.
Создание таблиц
Из всех возможностей, нашим первым действием с MySQL будет создание таблицы, в которой будет храниться информация о счете! Для этого рассмотрим следующий оператор SQL:
Чтобы создать запрос, нам нужно обернуть весь оператор в строку (мы можем использовать строковый литерал или необработанную строку).
Затем нам нужно подготовить наш запрос из метода prepareStatement() соединения и выполнить его с помощью execute():
При запуске программы мы получаем следующий результат в консоли:
Сообщение об успехе! При проверке базы данных из ведомости DESC счетов:
У нас есть наша таблица! Теперь мы можем вносить счета в базу данных!
Внесение счетов
Чтобы вставить счет в таблицу, сначала мы выполняем шаги, аналогичные тем, что мы делали для создания таблицы, объявляем и подготавливаем запрос, а затем выполняем:
Большая разница заключается в том, что нам нужно выполнить процесс связывания, чтобы связать данные нашего объекта с запросом. Для этого мы используем сеттеры PrepareStatement:
“Но почему бы нам просто не конкатенировать информацию об объекте непосредственно в операторе SQL?”.
Довольно часто возникают подобные сомнения, чтобы увидеть это решение, когда мы используем технику связывания данных, чтобы избежать проблем безопасности, таких как SQL Injection. Обратите внимание, что в этой технике мы используем значение 1 для функции setString(), которая получает клиента в качестве аргумента, и значение 2 для функции setDouble(), которая получает баланс.
Это означает, что значение 1 представляет первую колонку (клиент), а 2 – вторую колонку (баланс), в случае большего количества колонок, просто добавьте последовательно другие числа, как 3, 4 ….
После выполнения процесса связывания мы можем запустить программу, однако важно закомментировать или удалить код создания таблицы, поскольку в противном случае мы получим следующее исключение:
Обратите внимание, что это java.sql.SQLSyntaxErrorException, указывающий на то, что таблица счетов уже была создана… Мы можем использовать некоторые приемы, чтобы избежать этой проблемы, например, использовать if в операторе создания таблицы:
При повторном тестировании программы наш счет вставляется в таблицу:
Мы даже можем проверить результат непосредственно в MySQL:
Теперь, когда мы научились сохранять учетные записи, мы можем приступить к реализации поиска учетных записей.
Поиск счетов
Для получения счетов мы выполняем ту же процедуру, но разница в том, что теперь мы используем функцию executeQuery(), которая возвращает ResultSet, представляющий собой таблицу базы данных в соответствии с выполненным запросом.
В данном запросе, в частности, у нас есть доступ ко всем зарегистрированным счетам!
Чтобы получить каждый счет, нам нужно просмотреть каждую строку ResultSet, мы можем сделать это с помощью метода next(), который переходит к следующей строке ResultSet и возвращает true, если есть данные, или false, если нет:
Таким образом, для каждой строки мы можем получить значение столбцов из геттеров, например, в первом столбце, который представляет id типа Int, мы используем getInt() с аргументом 1, указывающим на первый столбец, во втором мы используем getString() с аргументом 2, чтобы получить второй столбец, который является клиентом, и так далее…
Перед запуском программы мы можем даже сохранить новую учетную запись, если результат показывает более одной учетной записи:
Обратите внимание, что даже при номере счета 1, на счете Владимир он был записан со значением 2. Это происходит из-за того, что в процессе привязки не передается номер счета и сохраняется настройка автоматического увеличения в таблице.
Одна из проблем с JDBC
Особенностью этого решения является то, что нам необходимо точно знать тип значения, которое мы хотим получить для каждого столбца, потому что если мы выполним getInt(), а значением столбца будет текст (строка), у нас возникнет исключение при преобразовании:
Вы можете сделать эту имитацию, попытавшись получить столбец customer, как если бы это было целое число val test = result.getInt(2).
Это одна из проблем JDBC. Обратите внимание, что он показывает исключение NumberFormatException, указывающее на строковую запись со значением Алексей.
Заключение
Как представлено в этой статье, мы можем использовать те же решения на Java с помощью Kotlin. Если вы имели дело с JDBC, вы, вероятно, не заметили большой разницы с реализацией в Java, потому что в Kotlin мы можем изучить всю концепцию взаимодействия с Java, что позволяет использовать библиотеки Java в Kotlin! Это означает, что вы можете пойти дальше и даже использовать JPA, например, с Hibernate.
Источник
Руководство по работе с фреймворком Kotlin Exposed
Также приглашаем всех желающих на демо-урок «Объектно-ориентированное программирование в Kotlin». Цели занятия:
— узнать про элементы объектной модели Kotlin;
— создавать различные классы и объекты;
— выполнять наследование и делегирование;
— пользоваться геттерами и сеттерами.
1. Введение
В этой статье мы рассмотрим, как направлять запросы к реляционной базе данных с помощью Exposed.
Exposed — это открытая библиотека, разработанная компанией JetBrains. Она распространяется по лицензии Apache и позволяет использовать идиоматический API Kotlin для реализации некоторых реляционных баз данных от различных поставщиков.
Exposed можно использовать как в качестве высокоуровневого языка DSL в SQL, так и в качестве облегченной технологии ORM (объектно-реляционного отображения). В этом руководстве мы рассмотрим оба варианта использования.
2. Установка
Фреймворк Exposed еще не опубликован в Maven Central, потому нам придется использовать отдельный репозиторий:
Теперь можно подключить библиотеку:
Ниже мы приведем несколько примеров использования базы данных H2 в памяти:
Последняя версия Exposed доступна на Bintray, а последняя версия H2 — на Maven Central.
3. Соединение с базой данных
Для того чтобы установить соединение с базой данных, будем использовать класс Database :
Мы можем также указать пользователя (user) и пароль (password) в качестве именованных параметров:
Обратите внимание: вызов метода connect не устанавливает соединения с БД сразу. Соединение будет установлено позже с использованием сохраненных параметров.
3.1. Дополнительные параметры
Для того чтобы задать другие параметры соединения, мы будем использовать перегруженный метод connect , который позволит нам полностью контролировать подключение:
В этом случае нам придется использовать замыкание. Exposed вызывает замыкание при необходимости установления нового соединения с базой данных.
3.2. Подключение через DataSource
Если мы будем подключаться к базе данных с использованием DataSource (именно этот подход обычно используется в корпоративных приложениях, например, чтобы иметь преимущества пула подключений), нам потребуется соответствующий перегруженный метод connect :
4. Открытие транзакции
Все операции с базами данных в Exposed выполняются только при наличии активных транзакций.
Метод transaction принимает замыкание и вызывает его в активной транзакции.
Метод transaction возвращает значение, которое вернуло замыкание. После выполнения блока Exposed автоматически закрывает транзакцию.
4.1. Подтверждение и откат транзакций
После успешного выполнения блока с помощью метода transaction Exposed подтверждает транзакцию. Если же замыкание генерирует исключение, фреймворк откатывает транзакцию.
Подтверждение и откат транзакции можно выполнить в ручном режиме. В Kotlin замыкание, которое мы передали в методе transaction , фактически является экземпляром класса Transaction .
Таким образом, мы можем использовать метод commit или rollback :
4.2. Запись инструкций в журнал
При изучении фреймворка или отладке кода не лишним будет отследить инструкции и запросы SQL, которые Exposed отправляет в базу данных.
Для этого добавим регистратор к активной транзакции:
5. Определение таблиц
Как правило, Exposed не используется для работы с неформатированными строками и именами SQL. Мы определяем таблицы, столбцы, ключи, связи и т. д. с помощью высокоуровневого DSL.
AD
Для представления каждой таблицы будем использовать экземпляр класса Table :
Exposed автоматически присваивает таблице имя на основании имени класса, но мы можем задать имя самостоятельно:
5.1. Столбцы
Без столбцов таблица работать не будет. Определим столбцы как свойства класса Table :
Для краткости мы не указали типы — Kotlin определит их автоматически. В любом случае каждая колонка относится к классу Column , у нее есть имя, тип и, возможно, параметры типа.
5.2. Первичные ключи
В предыдущем разделе мы рассмотрели пример, где индексы и первичные ключи определяются с помощью текучего API.
Однако, если в таблице в качестве первичного ключа используется целое число, мы можем использовать встроенные в Exposed классы IntIdTable и LongIdTable для определения ключей:
Есть также класс UUIDTable , а еще мы можем определить собственные варианты, выделив подклассы в классе IdTable .
5.3. Внешние ключи
Добавить внешний ключ очень просто. Мы можем использовать статическую типизацию, поскольку мы всегда обращаемся к свойствам, которые были известны в момент создания таблицы.
Предположим, что нам нужно узнать имена актеров, которые снимались в каждом фильме:
Для того чтобы не прописывать тип столбца (в этом примере — integer), который можно получить из связанного столбца, воспользуемся методом reference :
Если мы ссылаемся на первичный ключ, имя столбца можно не указывать:
5.4. Создание таблиц
Таблицы можно создавать программно, как указано выше:
Таблицу можно создать, только если она не существует. Однако миграция баз данных не поддерживается.
6. Запросы
Определив классы таблиц, как показано выше, мы можем направлять запросы к базе данных с использованием функций расширения, встроенных в фреймворк.
6.1. Выбор всех объектов
Для того чтобы извлечь данные из базы, будем использовать объекты Query , созданные на основе классов таблиц. Самый простой запрос будет возвращать все строки заданной таблицы:
Запрос является итерируемым и поддерживает циклы forEach :
Параметр замыкания, которому в нашем примере присвоено имя it , — это экземпляр класса ResultRow . В результате столбцам присваиваются ключи.
6.2. Выбор подмножества столбцов
Мы можем также выбрать подмножество столбцов таблицы, то есть выполнить проекцию, с помощью метода slice :
Этот метод позволяет применить функцию к столбцу:
Часто при использовании агрегатных функций, например count и avg , направляя запрос, мы используем группировку по оператору. О группах мы поговорим в разделе 6.5.
6.3. Фильтрация с помощью выражения where
В Exposed для выражений where, которые используются для фильтрации запросов и других типов инструкций, используется специальный DSL. В основе этого мини-языка лежат свойства столбцов, с которыми мы познакомились ранее, и серия логических операторов.
Вот пример выражения where :
Оно относится к комплексному типу и является подклассом SqlExpressionBuilder , который определяет такие операторы, как like , eq , and . Как видим, это последовательность операций сравнения, соединенных операторами and и or .
Мы можем передать такое выражение в метод select , который вернет очередной запрос:
Поскольку тип может быть выведен из контекста, нам не обязательно указывать тип complex для выражения where , когда оно передается непосредственно в метод select , как в рассмотренном примере.
В Kotlin выражения с where являются объектами, поэтому специальных параметров для запросов нет. Мы используем переменные:
6.4. Дополнительная фильтрация
Существует несколько методов для уточнения запросов, которые возвращает метод select и его эквиваленты.
Например, можно удалить повторяющиеся строки:
Или вернуть только подмножество строк, например в случае нумерации страниц с результатами при работе над пользовательским интерфейсом:
Эти методы будут возвращать новые объекты Query, поэтому мы можем выстроить их вызовы в цепочку.
6.5. Методы orderBy и groupBy
Метод Query.orderBy принимает список столбцов, связанных со значением SortOrder , которое задает тип сортировки элементов — по возрастанию или по убыванию:
Группировка по одному или нескольким столбцам будет особенно полезна при использовании агрегатной функции (см. раздел 6.2). Для этого воспользуемся методом groupBy :
6.6. Соединения
Соединения — это, пожалуй, одна из самых важных характеристик реляционной базы данных. Вот самый простой пример. Если мы знаем внешний ключ и у нас нет условий соединения, мы можем воспользоваться встроенными операторами соединения:
В этом примере мы использовали оператор innerJoin , но по этому же принципу можно использовать операторы LEFT JOIN , RIGHT JOIN и CROSS JOIN .
Затем можно добавить условия соединения, используя выражение where ; например, если у нас нет внешнего ключа, придется указать явную операцию соединения:
В общем случае операция соединения полностью записывается так:
6.7. Псевдонимы
Благодаря тому что имена столбцов связаны со свойствами, при типичном соединении нам не придется присваивать им псевдонимы, даже если у некоторых столбцов имена совпадают:
На самом деле в этом примере StarWarsFilms.sequelId и Players.sequelId — это разные столбцы.
Однако, если в запросе одна и та же таблица появляется несколько раз, можно присвоить ей псевдоним. Для этого воспользуемся функцией alias :
Псевдоним можно указывать в качестве названия таблицы:
В этом примере sequel — это таблица, которая участвует в операции соединения. Чтобы обратиться к столбцу, в качестве ключа будем использовать столбец таблицы, представленной псевдонимом:
7. Инструкции
Мы рассмотрели, как выполнять запросы к базе данных. Теперь разберемся с DML-инструкциями.
7.1. Вставка данных
Для того чтобы вставить данные, вызовем функцию, эквивалентную функции insert. Все они принимают замыкание:
В этом замыкании используются два объекта:
this (само замыкание) — это экземпляр класса StarWarsFilms ; именно этот объект позволяет нам обращаться к столбцам, которые являются свойствами, по неуточненному имени;
it (параметр замыкания) — это InsertStatement ; это структура, аналогичная коллекции ключ/значение, в которой есть слоты для вставки столбцов.
7.2. Извлечение автоинкрементного значения столбцов
Если у нас есть инструкция insert с автоматически генерируемыми столбцами (обычно это автоматическое увеличение индекса или последовательности), мы можем извлечь сгенерированные значения.
В типичном сценарии есть только одно сгенерированное значение. Воспользуемся методом insertAndGetId :
Если у нас несколько сгенерированных значений, их можно считывать по имени:
7.3. Обновление данных
Мы научились выполнять запросы и вставлять данные. Теперь перейдем к обновлению данных, которые содержатся в базе. Самый простой способ обновления похож на комбинацию методов select и insert:
В этом примере выражение where используется вместе с замыканием UpdateStatement . UpdateStatement и InsertStatement — это потомки класса UpdateBuilder , поэтому в них используется один и тот же API и одна и та же логика. Родительский класс позволяет задать значение столбца с помощью квадратных скобок.
Если для обновления столбца нам нужно вычислять новое значение из старого, воспользуемся SqlExpressionBuilder :
Этот объект позволяет использовать инфиксный оператор (например, plus, minus и т. д.) для создания инструкции обновления.
7.4. Удаление данных
И наконец, мы можем удалить данные с помощью метода deleteWhere :
8. API DAO, облегченная технология ORM
Мы использовали Exposed для того, чтобы связать операции над объектами Kotlin с запросами и инструкциями SQL напрямую. Такие методы, как insert , update , select и т. д., немедленно отправляют строку SQL в базу данных.
Однако в Exposed есть высокоуровневый API DAO, который представляет собой простую технологию ORM. Давайте рассмотрим его подробнее.
8.1. Сущности
В рассмотренных выше примерах мы использовали классы для представления таблиц базы данных и описания операций над ними с использованием статических методов.
Теперь мы можем определить сущности на основе этих классов таблиц, где каждый экземпляр сущности представляет собой строку базы данных:
Давайте подробно проанализируем это определение.
Из первой строки видно, что сущность — это класс, расширяющий Entity . У нее есть ID специфического типа, в нашем случае — Int .
Затем определяем объект-компаньон. Объект-компаньон — это класс сущности, то есть статические метаданные, которые определяют сущность и операции, которые мы можем выполнять над ней.
Объявляя объект-компаньон, мы соединяем сущность с именем StarWarsFilm (в единственном числе), которая представляет собой одну строку, с таблицей с именем StarWarsFilms (во множественном числе), которая представляет собой коллекцию всех строк.
Наконец, мы задаем свойства с помощью делегатов свойств для соответствующих столбцов таблицы.
Обратите внимание, что раньше мы объявляли столбцы с помощью val, поскольку они являются неизменяемыми метаданными. Теперь же мы объявляем свойства сущности с помощью var, поскольку они являются изменяемыми слотами в строке базы данных.
8.2. Вставка данных
Чтобы вставить строку в таблицу, нам нужно просто создать экземпляр класса сущности с использованием статического фабричного метода new в транзакции:
Обратите внимание: с базой данных выполняются отложенные операции, которые запускаются только после выполнения warm cache. В Hibernate, например, теплый кэш привязан к сессии (session).
Операция выполняется автоматически. Например, когда мы в первый раз считываем сгенерированный идентификатор, Exposed выполняет инструкцию insert :
Сравните это поведение с методом insert , который мы рассматривали в разделе 7.1, — в этом примере метод сразу же выполняет инструкцию в базе данных. Здесь же мы работаем на более высоком уровне абстракции.
8.3. Обновление и удаление объектов
Для обновления строк нужно просто задать их свойства:
Для удаления объекта вызовем метод delete этого объекта:
Так же, как при использовании метода new , обновление и операции выполняются в отложенном режиме.
Обновить и удалить можно только ранее загруженный объект. В этом фреймворке нет API для обновления и удаления нескольких объектов, и нам придется использовать API более низкого уровня (см. раздел 7). Тем не менее в одной транзакции можно одновременно использовать и тот и другой API.
8.4. Запросы
API DAO позволяет выполнять три типа запросов.
Для загрузки всех объектов, для которых не заданы условия, будем использовать статический метод all :
Для загрузки одного объекта по ID воспользуемся методом findById :
Если объекта с таким ID нет, findById вернет значение null .
В самом общем случае мы можем использовать метод find с выражением where:
8.5. Связь «многие к одному»
В ORM соотнесение соединений со ссылками так же важно, как соединения в реляционных базах данных. Посмотрим, какие возможности предлагает Exposed.
Предположим, что нам нужно узнать пользовательский рейтинг каждого фильма. Сначала определим две дополнительные таблицы:
Затем создадим соответствующие сущности. Опустим сущность User (это очевидно) и перейдем сразу к классу UserRating :
Обратите внимание: инфиксный метод referencedOn вызывает свойства, которые представляют собой связи. Модель следующая: объявляем переменную var через сущность ( by ) со ссылкой на соответствующий столбец ( referencedOn ).
Свойства, объявленные таким образом, ведут себя как обычные свойства, но их значением является связанный объект:
8.6. Дополнительные связи
Рассмотренные выше связи являются обязательными — мы должны всегда указывать значение.
Чтобы установить дополнительные связи, нам нужно сначала разрешить столбцу таблицы принимать значение null :
Вместо метода referencedOn будем использовать optionalReferencedOn :
Таким образом, свойство user сможет принимать значение null .
8.7. Связь «один ко многим»
Мы можем создать обратную связь с помощью внешнего ключа. Будем использовать его для моделирования рейтинга фильма в базе данных; у фильма будет несколько рейтингов.
Для отображения рейтингов нужно добавить свойство к той стороне связи, где используется «один» объект. В нашем случае это сущность film :
Модель похожа на модель связи «многие к одному», но сейчас мы используем метод referrersOn . Свойство, определенное таким образом, является итерируемым, поэтому мы можем выполнить обход с помощью цикла forEach :
В отличие от обычных свойств, здесь мы определили ratings через val . Свойство неизменяемо, поэтому мы можем только считывать его.
У значения свойства тоже нет API для изменения. Поэтому для добавления нового рейтинга нам нужно создать его, указав ссылку на фильм:
Теперь новый рейтинг появится в перечне рейтингов для этого фильма.
8.8. Связь «многие ко многим»
Иногда требуется установить связь «многие ко многим». Предположим, что нам нужно связать класс StarWarsFilm с таблицей Actors :
После того как мы определим таблицу и сущность, нужно будет создать другую таблицу для установления связи:
В этой таблице два столбца, каждый из которых является внешним ключом, а вместе они представляют собой сложный первичный ключ.
Теперь можно подключить таблицу связей к сущности StarWarsFilm :
На момент написания статьи создать сущность с генерируемым идентификатором и использовать ее в связи «многие ко многим» в одной транзакции нельзя.
Нам приходится выполнять несколько транзакций:
В этом примере мы для удобства выполнили три транзакции, хотя двух было бы достаточно.
9. Заключение
В этой статье мы подробно рассмотрели фреймворк Kotlin Exposed. Дополнительную информацию и примеры можно найти в вики-учебнике по Exposed.
Варианты реализации рассмотренных примеров и фрагменты кода можно найти на GitHub.
Источник