Статьи по Kotlin

Работа с данными в Kotlin с использованием JDBC

В этой статье мы узнаем, как сохранять данные в Kotlin с помощью JDBC (Java Database Connectivity).

Пример проекта

В качестве примера я буду использовать Bytebank, проект, имитирующий цифровой банк, для регистрации счетов. Для регистрации счета требуется номер счета, имя клиента и баланс:

data class Account( val number: Int, val customer: String, val balance: Double )

В классе Account мы представляем наш счет, который может быть создан следующим образом:

fun main() { println("Добро пожаловать в Байтбанк") val contAlex = Account(1, "Алексей", 1000.0) println("информация о счёте $contaAlex") }
Code language: JavaScript (javascript)

И к этому результату мы пришли, запустив программу:

Добро пожаловать в Байтбанк информация о счёте Account(number=1, customer=Алексей, balance=1000.0)

После представления проекта и создания учетной записи давайте приступим к настройке JDBC.

Добавление коннектора базы данных MySQL

В качестве первого шага нам необходимо создать соединение между нашим приложением и базой данных.

В этом примере я буду использовать MySQL, но можно установить такое же соединение и с другими распространенными базами данных на рынке, поскольку каждая из них предлагает коннектор, способный осуществлять связь.

MySQL, например, имеет специальную страницу со всеми доступными драйверами, даже из других приложений.

На странице MySQL у нас есть возможность загрузить коннектор, однако, учитывая использование инструмента сборки (Gradle), мы можем добавить зависимость в файл сборки, build.gradle.kts:

dependencies{ implementation(kotlin("stdlib")) implementation("mysql:mysql-connector-java:8.0.15") testImplementation("junit", "junit", "4.12") }
Code language: JavaScript (javascript)

Далее нам просто нужно синхронизировать проект, чтобы Gradle загрузил и сделал драйвер доступным для использования. Затем мы можем приступить к настройке соединения.

Создание подключения

Когда драйвер доступен, мы можем создать соединение с MySQL следующим образом:

try { Class.forName("com.mysql.cj.jdbc.Driver") DriverManager.getConnection("jdbc:mysql://localhost/bytebank", "root", "") println("Подключение успешно выполнено") } catch (e: Exception) { e.printStackTrace() println("не удалось подключиться к банку") }
Code language: JavaScript (javascript)

У нас здесь много кода! Не пугайтесь, потому что мы будем разбираться, что означает каждая строчка кода:

  • Class.forName(): настраивает коннектор, который мы будем использовать в соединении с JDBC. Если бы вы использовали другой драйвер, вам пришлось бы поместить класс, соответствующий используемому драйверу, которым в данном случае является драйвер MySQL (“com.mysql.cj.jdbc.Driver”).
  • DriverManager.getConnection(): пытается установить соединение с базой данных через JDBC, в качестве аргумента получает адрес соединения, пользователя и пароль.

В этом вызове нам нужно обратить внимание в основном на адрес! Например, у нас по умолчанию стоит jdbc:mysql, что указывает на то, что мы будем устанавливать соединение JDBC с базой данных MySQL, т.е. если вы делаете конфигурацию для другой базы данных, то значение будет другим!

Примечание: еще один момент, который следует отметить, заключается в том, что я использую базу данных на своем компьютере, на порту 3306 (порт MySQL по умолчанию), поэтому достаточно localhost. Однако при интеграции с внешней базой данных адрес будет другим!

Обратите также внимание, что мы указываем базу данных, к которой хотим получить доступ, и в данном случае я создал банк bytebank. Не стесняйтесь использовать этот же пример или другой по вашему выбору.

Затем, в качестве второго и третьего аргумента, нам нужно отправить пользователя и пароль. Во время этого теста я буду использовать пользователя root с пустым паролем.

“Красота! Я понял конфигурацию коннектора и то, как мы создаем соединение, но почему мы используем try catch?”.

Перехват try необходим для того, чтобы наше приложение могло определить общие проблемы во время попытки соединения, например, недопустимый адрес, сбой аутентификации или любая другая проблема. Поэтому мы представляем трассировку стека исключения и сообщение ниже, указывающее на то, что мы не смогли подключиться к базе данных.

После ввода каждой строки кода, при запуске программы мы получаем следующий результат:

Добро пожаловать в Байтбанк информация о счёте Account(number=1, customer=Алексей, balance=1000.0) Подключение успешно выполнено

Вот и все! Мы можем начать выполнение операторов в MySQL.

Создание таблиц

Из всех возможностей, нашим первым действием с MySQL будет создание таблицы, в которой будет храниться информация о счете! Для этого рассмотрим следующий оператор SQL:

val sql = """ CREATE TABLE contas ( id INT PRIMARY KEY AUTO_INCREMENT, cliente VARCHAR(255), saldo DOUBLE ); """.trimIndent()
Code language: PHP (php)

Чтобы создать запрос, нам нужно обернуть весь оператор в строку (мы можем использовать строковый литерал или необработанную строку).

Затем нам нужно подготовить наш запрос из метода prepareStatement() соединения и выполнить его с помощью execute():

try { Class.forName("com.mysql.cj.jdbc.Driver") val connection = DriverManager.getConnection("jdbc:mysql://localhost/bytebank", "root", "") println("Подключение успешно выполнено") val sql = """ CREATE TABLE accounts ( id INT PRIMARY KEY AUTO_INCREMENT, cliente VARCHAR(255), saldo DOUBLE ); """.trimIndent() val query = connection.prepareStatement(sql) query.execute() println("Создана таблица счетов") } catch (e: Exception) { e.printStackTrace() println("не удалось подключиться к банку") }

При запуске программы мы получаем следующий результат в консоли:

Добро пожаловать в Байтбанк информация о счёте Account(number=1, customer=Алексей, balance=1000.0) Подключение успешно выполнено Создана таблица счетов

Сообщение об успехе! При проверке базы данных из ведомости DESC счетов:

mysql> DESC accounts; +----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+----------------+ | id | int | NO | PRI | NULL | auto_increment | | customer | varchar(255) | YES | | NULL | | | balance | double | YES | | NULL | | +----------+--------------+------+-----+---------+----------------+ 3 rows in set (0.01 sec)
Code language: PHP (php)

У нас есть наша таблица! Теперь мы можем вносить счета в базу данных!

Внесение счетов

Чтобы вставить счет в таблицу, сначала мы выполняем шаги, аналогичные тем, что мы делали для создания таблицы, объявляем и подготавливаем запрос, а затем выполняем:

val insertAccountSql = "INSERT INTO accounts (customer, balance) VALUES (?, ?);" val querynIsertAccount = connection.prepareStatement(insertAccountSql) querynIsertAccount.execute() println("зарегистрированный счёт: $accountAlex")
Code language: JavaScript (javascript)

Большая разница заключается в том, что нам нужно выполнить процесс связывания, чтобы связать данные нашего объекта с запросом. Для этого мы используем сеттеры PrepareStatement:

val insertAccountSql = "INSERT INTO accounts (customer, balance) VALUES (?, ?);" val querynIsertAccount = connection.prepareStatement(insertAccountSql) querynIsertAccount.setString(1, accountAlex.customer) querynIsertAccount.setDouble(2, accountAlex.balance) querynIsertAccount.execute() println("зарегистрированный счёт: $accountAlex")
Code language: JavaScript (javascript)

“Но почему бы нам просто не конкатенировать информацию об объекте непосредственно в операторе SQL?”.

Довольно часто возникают подобные сомнения, чтобы увидеть это решение, когда мы используем технику связывания данных, чтобы избежать проблем безопасности, таких как SQL Injection. Обратите внимание, что в этой технике мы используем значение 1 для функции setString(), которая получает клиента в качестве аргумента, и значение 2 для функции setDouble(), которая получает баланс.

Это означает, что значение 1 представляет первую колонку (клиент), а 2 – вторую колонку (баланс), в случае большего количества колонок, просто добавьте последовательно другие числа, как 3, 4 ….

После выполнения процесса связывания мы можем запустить программу, однако важно закомментировать или удалить код создания таблицы, поскольку в противном случае мы получим следующее исключение:

Добро информациямация(number=1, customer=Алексей, balance=1000.0) Подключение java.sql.SQLSyntaxErrorException: Table 'accounts' already exists at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120) at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:970) at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:387) at br.com.alura.bytebank.MainKt.main(Main.kt:25) at br.com.alura.bytebank.MainKt.main(Main.kt) нее
Code language: JavaScript (javascript)

Обратите внимание, что это java.sql.SQLSyntaxErrorException, указывающий на то, что таблица счетов уже была создана… Мы можем использовать некоторые приемы, чтобы избежать этой проблемы, например, использовать if в операторе создания таблицы:

val sql = """ CREATE TABLE NOT IF EXISTS accounts ( id INT PRIMARY KEY AUTO_INCREMENT, customer VARCHAR(255), balance DOUBLE ); """.trimIndent()
Code language: PHP (php)

При повторном тестировании программы наш счет вставляется в таблицу:

Добро пожаловать в Байтбанк информация о счёте Account(number=1, customer=Алексей, balance=1000.0) Подключение успешно выполнено зарегистрированный счёт: Account(number=1, customer=Алексей, balance=1000.0)

Мы даже можем проверить результат непосредственно в MySQL:

mysql> SELECT * FROM accounts; +----+----------+---------+ | id | customer | balance | +----+----------+---------+ | 1 | Алексей| 1000 | +----+----------+---------+ 1 row in set (0.00 sec)
Code language: JavaScript (javascript)

Теперь, когда мы научились сохранять учетные записи, мы можем приступить к реализации поиска учетных записей.

Поиск счетов

Для получения счетов мы выполняем ту же процедуру, но разница в том, что теперь мы используем функцию executeQuery(), которая возвращает ResultSet, представляющий собой таблицу базы данных в соответствии с выполненным запросом.

val searchAccounts = "SELECT * FROM accounts;" val searchAccountsQuery = connection.prepareStatement(searchAccounts) val result = searchAccountsQuery.executeQuery()
Code language: JavaScript (javascript)

В данном запросе, в частности, у нас есть доступ ко всем зарегистрированным счетам!

Чтобы получить каждый счет, нам нужно просмотреть каждую строку ResultSet, мы можем сделать это с помощью метода next(), который переходит к следующей строке ResultSet и возвращает true, если есть данные, или false, если нет:

val searchAccounts = "SELECT * FROM accounts;" val searchAccountsQuery = connection.prepareStatement(searchAccounts) val result = searchAccountsQuery.executeQuery() while (result.next()){ val number = result.getInt(1) val customer = result.getString(2) val balance = result.getDouble(3) val account = Account(number, customer, balance) println("счёт $conta") }
Code language: JavaScript (javascript)

Таким образом, для каждой строки мы можем получить значение столбцов из геттеров, например, в первом столбце, который представляет id типа Int, мы используем getInt() с аргументом 1, указывающим на первый столбец, во втором мы используем getString() с аргументом 2, чтобы получить второй столбец, который является клиентом, и так далее…

Перед запуском программы мы можем даже сохранить новую учетную запись, если результат показывает более одной учетной записи:

Добро пожаловать в Байтбанк информация о счёте Account(number=1, customer=Алексей, balance=1000.0) Подключение успешно выполнено счёт Account(number=1, customer=Алексей, balance=1000.0) счёт Account(number=2, customer=Владимир, balance=2000.0)

Обратите внимание, что даже при номере счета 1, на счете Владимир он был записан со значением 2. Это происходит из-за того, что в процессе привязки не передается номер счета и сохраняется настройка автоматического увеличения в таблице.

Одна из проблем с JDBC

Особенностью этого решения является то, что нам необходимо точно знать тип значения, которое мы хотим получить для каждого столбца, потому что если мы выполним getInt(), а значением столбца будет текст (строка), у нас возникнет исключение при преобразовании:

java.lang.NumberFormatException: For input string: "Алексей" at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054) at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110) at java.base/java.lang.Double.parseDouble(Double.java:549) at com.mysql.cj.protocol.a.MysqlTextValueDecoder.decodeDouble(MysqlTextValueDecoder.java:228) at com.mysql.cj.result.StringConverter.createFromBytes(StringConverter.java:114) at com.mysql.cj.protocol.a.MysqlTextValueDecoder.decodeByteArray(MysqlTextValueDecoder.java:238) at com.mysql.cj.protocol.result.AbstractResultsetRow.decodeAndCreateReturnValue(AbstractResultsetRow.java:143) at com.mysql.cj.protocol.result.AbstractResultsetRow.getValueFromBytes(AbstractResultsetRow.java:250) at com.mysql.cj.protocol.a.result.ByteArrayRow.getValue(ByteArrayRow.java:91) at com.mysql.cj.jdbc.result.ResultSetImpl.getNonStringValueFromRow(ResultSetImpl.java:656) at com.mysql.cj.jdbc.result.ResultSetImpl.getInt(ResultSetImpl.java:896) at br.com.alura.bytebank.MainKt.main(Main.kt:43) at br.com.alura.bytebank.MainKt.main(Main.kt)
Code language: JavaScript (javascript)

Вы можете сделать эту имитацию, попытавшись получить столбец customer, как если бы это было целое число val test = result.getInt(2).

Это одна из проблем JDBC. Обратите внимание, что он показывает исключение NumberFormatException, указывающее на строковую запись со значением Алексей.

Заключение

Как представлено в этой статье, мы можем использовать те же решения на Java с помощью Kotlin. Если вы имели дело с JDBC, вы, вероятно, не заметили большой разницы с реализацией в Java, потому что в Kotlin мы можем изучить всю концепцию взаимодействия с Java, что позволяет использовать библиотеки Java в Kotlin! Это означает, что вы можете пойти дальше и даже использовать JPA, например, с Hibernate.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *