Kotlin и базы данных

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

На одном из проектов мы пишем автотесты для ETL сервисов, которые перегоняют данные из одной MSSQL базы в другую. Для работы с базой данных мы используем ванильную Java 8 и Apache DBUtils.

С применением DBUtils все получается достаточно неплохо. Мы создаем доменные объекты и потом с помощью BeanHandler можем конвертировать ответы в JavaPojo.

@Data
class User{
    String firstName;
    String lastName;
    int age;
}

Для более приятной работы мы написали парочку полезных методов:

private <T> T execute(String query, ResultSetHandler<T> handler, Object... params) {
   try {
     return queryRunner.query(query, handler, params);
   } catch (SQLException e) {
     throw new RuntimeException(e);
   }
}

private <T> T findOne(Class<T> tClass, String query, Object... params) {
   return execute(query, new BeanHandler<>(tClass), params);
}

private <T> List<T> findAll(Class<T> tClass, String query, Object... params) {
   return execute(query, new BeanListHandler<>(tClass), params);
}

В итоге код обращения к базе выходит таким:

final String query = "SELECT * "+
                     "FROM users "+
                     "WHERE userId = 1;"

User user = database.findOne(User.class,query)

Вроде бы, достаточно неплохо и лаконично, если это все завернуть еще в какой-то UserService, то будет прям:

User user = userService.findById(1);

Все в этом коде хорошо, но мне лично не нравится, как выглядит SQL запрос. Объединения и переносы строк. Запрос нельзя просто так взять, скопировать и выполнить в каком-то менеджере для баз данных. Увы, в Java нету поддержки форматированных строк, как в Groovy.

Ну да ладно с Java, а давайте попробуем реализовать то же самое на Kotlin. У них и форматирование строк есть, и еще пара фишек, которые могут упростить код.

Пробуем:

data class User(var id: Long, var firstName: String, var lastName:String) {
    constructor() : this(0, "","")
}

val sql = """
    SELECT *
    FROM users
    WHERE userId = 1;"
"""

val handler = BeanHandler(User::class.java)
val user = runner.query(sql, handler)

Пока, естественно, мы можем все завернуть так же, как и в Java варинте, но можно сделать намного лучше. Пишем extension метод для класса QueryRunner:

inline fun <reified T> QueryRunner.findOne(sql: String): T {
    return BeanHandler(T::class.java).run { query(sql, this) }
}

inline fun <reified T> QueryRunner.findAll(sql: String): MutableList<T> {
    return BeanListHandler(T::class.java).run { query(sql, this) }
}

С помощью этих методов мы теперь можем писать так:

val sql = """
    SELECT *
    FROM users
    WHERE userId = 1;"
"""


val user = runner.findOne<User>(sql)

или так:

val user:User = runner.findOne(sql)

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

Получается достаточно удобно и лаконично. Думаю, в следующий раз, используя полученный опыт, будем уже экспериментировать с конвертацией в Json. Об этом всем я буду рассказывать на конференции QAFest 2017.