Работа с реляционными базами данных в Scala

в 16:16, , рубрики: database, github, scala, sql, tinkoff, Блог компании Тинькофф Кредитные Системы, функциональное программирование, метки: , , , ,

Работа с реляционными базами данных в ScalaДобрый день, сегодня хотелось бы рассказать, как наша команда работает с базами данных. У нас в компании в основном используется Oracle и в нашей команде много людей, кто умеет хорошо его готовить. Нам изначально хотелось получить полный доступ к его возможностям: иерархическим запросам, аналитическим функциям, передаче объектов и коллекций, как параметров запросов, и, может быть, если не будет другого способа — хинтам. Модель у нас не очень сложная, поэтому сознательно отказались от ORM.

В качестве основы взяли Apache DbUtils и сделали для него простую обёртку на Scala. Ниже я расскажу, как возможности Scala, особенно её последней версии 2.10, помогли упростить работу с базой данных.

А пытливых читателей, кто дочитает до конца, ждёт сюрприз.

Работа с соединениями

Как известно, при работе с базой данных, если только у вас не совсем простой проект, необходимо переиспользовать соединения. Для этого используются пулы (см. DBCP, c3po etc). Если использовать пул в лоб, то рано или поздно кто-то будет забывать отдавать пулу соединения. Обычно при тестах и отладке такие проблемы не видны, иногда они могут даже пройти через регресс и быть перенесёнными на боевую среду, где обнаружат себя не сразу.

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

def execute[R](operation: Connection => R): R = {
  val connection = driver.getConnection()
  try {
    operation(connection)
  } finally {
    connection.close()
  }
}

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

database.execute { connection =>
  database.query("select ? from dual", Seq(1))(connection)
}

или просто

database execute query ("select ? from dual", Seq(1))

Тут Scala уже здорово помогает, т.к. на Java тут надо порождать чудовищный анонимный класс, который убивает всё желание использовать подобный подход.

Импорты

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

Количество импортов удалось оптимизировать создав package object, куда переехали все публичные декларации: функции, неявные преобразования и константы. Классы остались в том же пакете. В итоге, для импорта всех необходимых для работы с базой данных деклараций необходимо написать:

	import ru.tcsbank.utils.database._	

Для того, чтобы с базой данных можно было работать просто, как `database execute query («select? from dual», Seq(1))` наш коллега предложил импортировать содержимое экземпляра объекта database для текущей области видимости:

	import database._

Интерполяция строк для SQL

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

Как только вышла Scala 2.10 я просмотрел список фич. С первого взгляда интерполяция строк мне не очень понравилась: «перетянули какую-то фигню из PHP, теперь для одного и того же будут разный синтаксис использовать, да ещё и перед строками будет магическое `s`!», думал я. Через какое-то время я писал запрос в базу и старался расставить все параметры в правильном порядке. И тут-то я и вспомнил про интерполяцию! «Вот оно!», пронеслось в голове, надо написать интерполятор `sql`, который будет расставлять параметры по местам, просто очевидно! Через 10 минут я уже нашёл нужный интерполятор в Интернете: http://gist.github.com/mnesarco/4515475.

Его ещё пришлось допилить, чтобы он поддерживал конкатенацию, пакетные запросы и out-параметры, но это был очень важный шаг, теперь наши запросы приобрели новый вид:

database execute query (
  sql"select ${magic} from dual where 1=${one}"
)

Для многострочных SQL запросов лучше использовать синтаксис для многострочных строк (многострочных строк? :-S), чем конкатенацию, так код чище и копировать в любимый редактор SQL проще.

database execute call (
  sql"""begin
          pack.proc(param1 => ${value1}, param2 => ${value2});
        end;"""
)

Самым большим сюрпризом для меня было то, что IntelliJ IDEA как-то автоматически поняла, что текст внутри моей собственной интерполяции был SQL`ем и начала правильно его подсвечивать, что было уже за гранью крутости.

Well, Pimp my library now!

Ни для кого не секрет, что java.util.ResultSet предоставляет слишком низкоуровневый API. Фундаментально, ResultSet можно прочитать только один раз и строки и столбцы у него — довольно несвязанные вещи. Поэтому абстракции для чтения из ResultSet у нас две — для получения коллекции определённого вида из всего него, а также для чтения и обёртки значения его столбца.

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

Вторая абстракция — неявное преобразование, которое расширяет возможности `ResultSet`, оно работает с текущей строкой таблицы и позволяет читать базовые доменные объекты для нашей компании: Id, Time, а также умеет создавать получать экземпляры класса Class, но самое ценное — то, что она умеет заворачивать в Option те значения, где значения собственно может и не быть.

В итоге мы можем беспрепятственно писать:

resultSet.getOptionalString("NICKNAME")

resultSet.getId[User]("USER_ID")

resultSet.getOptionalId[Account]("ACCOUNT_ID")

Целиком же вызов базы данных может выглядеть, как

database execute query (
  sql"select id, nickname from user",
  toSetMultiMap(_.getId[User]("id"), _.getOptionalString("nickname")))

Исходники

Как вы понимаете, мы коммерческая компания в сфере финансов и не можем просто так делиться своим кодом. Но сегодня мы пробуем себя в новом качестве. Исходный код, примеры работы которого я приводил, с приятными дополнениями, доступен на GitHub под лицензией MIT, то есть вы можете использовать его в своих проектах и приспосабливать под себя!

Также у нас появился репозиторий https://github.com/TinkoffCreditSystems, где мы постараемся и дальше выкладывать оригинальные решения. Надеемся на интересные комментарии и pull-реквесты.

Выводы

А вывод примерно таков: не обязательно использовать ORM, чтобы работа с реляционными базами данных была легка и непринуждённа. И достаточно для этого всего около 400 строк кода.

Единственное, наверно, что меня гложет, работа через JDBC всё равно происходит синхронно и блокирует потоки. Стать полностью реактивными получится только если полностью перейти на базу данных, имеющую асинхронный драйвер. Это достойная цель на будущее.

А как вы думаете, что можно было бы ещё улучшить в работе с базами данных?

Автор: vuspenskiy

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js