Слово «реактивный» в последние годы используется в различных контекстах. Иногда это означает, что мы должны отказаться от традиционных СУБД и перейти на масштабируемую NoSQL. Однако на самом деле многие бизнес-системы все еще работают на РСУБД. РСУБД обладает достаточной функциональностью и производительностью почти для приложений, поэтому я думаю, что нам не следует выбирать NoSQL, если он нам на самом деле не нужен.

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

Slick

Slick - самая популярная библиотека доступа к базе данных в Scala. Slick имеет типобезопасный конструктор запросов, унаследованный от ScalaQuery, но по-настоящему отличительной особенностью является монадический API DBIO, принятый со времен Slick3.

DBIO - это монада ввода-вывода, которая при выполнении возвращает Future. Это позволяет встроить обработку базы данных в другую асинхронную и неблокирующую обработку на основе Future.

val q = for (c <- coffees) yield c.name
val a = q.result
val f: Future[Seq[String]] = db.run(a)
f.onSuccess { case s => println(s”Result: $s”) }

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

Кроме того, построитель запросов Slick генерирует сложный и избыточный SQL, подобный this. В частности, это вызывает серьезные проблемы с производительностью MySQL. Реализация компилятора запросов Slick очень сложна и хрупка, поэтому ее сложно поддерживать и, возможно, сложно улучшить сгенерированный SQL.

Еще одна большая проблема Slick заключается в том, что Slick3 не имеет обратной совместимости с Slick2. У нас есть много существующего кода, написанного с помощью Slick2. Их очень сложно перенести на Slick3, но необходимо обновить наши приложения до Scala 2.12, потому что Slick2 не работает на Scala 2.12.

Для этого я сделал blocking-slick, который предоставляет на Slick3 совместимый с Slick2 API блокировки. Если у вас есть существующие приложения на основе Slick2, и вы хотите использовать Slick3, эта библиотека может вам помочь.

Перо

Quill также имеет типобезопасный конструктор запросов, но он предлагает генерацию SQL во время компиляции на основе макросов. API построителя запросов Quill похож на Slick, но сгенерированный SQL проще и предсказуем, и не требует каких-либо определений для сопоставления таблиц и классов.

case class Account(userName: String, mailAddress: String)
def find(userName: String) = quote {
  query[Account].filter(_.userName == lift(userName))
    .map(_.mailAddress)
}
val account: Option[Account] = ctx.run(find("takezoe")).headOption

Quill также имеет монаду ввода-вывода и асинхронную обработку, но это дополнительные функции. Нам не нужно их использовать, если они нам не нужны. Это разумная стратегия.

Проблема в Quill заключается в том, что он также основан на макросах. Макро многое упрощает, но в некоторых случаях затрудняет. Например, мы должны поднять переменные, чтобы искать их в макросах, как вы видите в предыдущем примере. И для динамической сборки запроса в Quill необходима небольшая техника. Сначала посмотрите следующий код:

def find(email: String, includeRemoved: Boolean) = quote { 
  query[Account].filter { t =>
    if(lift(includeRemoved)){
     t.email.toLowerCase == lift(email).toLowerCase
    } else {
     t.email.toLowerCase == lift(email).toLowerCase &&
     t.removed == false
    }
  }
}

Quill генерирует следующий код SQL из этого кода. Оператор if-else заменяется на CASE WHEN в сгенерированном SQL:

SELECT ... 
FROM account t 
WHERE CASE WHEN ? THEN 
  LOWER (t.email) = LOWER (?)
ELSE
  (LOWER (t.email) = LOWER (?)) AND (t.removed = false)
END

Если мы хотим динамически собрать условие фильтра, мы должны написать следующее. При необходимости Quill откатывает генерацию SQL к среде выполнения.

def find(email: String, includeRemoved: Boolean) = {
  val accounts = quote { 
    query[Account].filter { t =>
      t.email.toLowerCase == lift(email).toLowerCase
    }
  }
  val filtered = if(includeRemoved)
    quote { accounts.filter(_.removed == false) }
  else
    accounts
}

Хотя Quill подходит для статических запросов, он может не подходить для приложений, в которых много динамических запросов. Также, как потенциальная проблема, это может увеличить время компиляции, потому что оно основано на макросах и генерирует SQL во время компиляции. Библиотеки на основе макросов обычно имеют такую ​​тенденцию.

Дуби

Doobie - это чисто функциональный уровень JDBC для Scala. В нем нет конструктора запросов, безопасного для типов, поэтому нам приходится писать необработанный SQL в виде строкового литерала, например:

def find(n: String): ConnectionIO[Option[Country]] = 
  sql"select code, name, population from country where name = $n"
    .query[Country].option
// And then
scala> find("France").transact(xa).unsafePerformIO

Как видите, мы не можем воспользоваться преимуществами безопасности типов при написании запроса или отображении результата на класс случая. Однако Doobie обеспечивает проверку запросов и сопоставлений во время выполнения. Мы также можем проверить их в модульном тесте, отправив сгенерированный запрос в фактическую базу данных службой поддержки модульного теста Doobie.

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

К слову, Doobie возвращает объект Task Scalaz (Doobie также поддерживает кошек), поэтому он подходит для других асинхронных библиотек, таких как http4s, но нам нужны некоторые знания Scalaz, чтобы использовать Doobie. Кроме того, динамическая сборка SQL возможна, но очень сложна в Doobie.

Doobie все еще имеет версию 0.4.x и, похоже, находится в стадии разработки. Так что API может быть изменен в будущем. Это интересная библиотека, но использовать ее в продакшене еще рано.

ScalikeJDBC

ScalikeJDBC - это аккуратная библиотека доступа к БД для Scala. Он предлагает несколько стилей для доступа к базе данных. Один из них - это написание необработанного SQL с использованием строковой интерполяции.

// defines entity object and extractor
import org.joda.time._
case class Member(id: Long, name: Option[String], createdAt: DateTime)
object Member extends SQLSyntaxSupport[Member] {
  override val tableName = "members"
  def apply(rs: WrappedResultSet) = new Member(
    rs.long("id"), 
    rs.stringOpt("name"), 
    rs.jodaDateTime("created_at")
  )
}
// find all members
val members: List[Member] = 
  sql"select * from members".map(rs => Member(rs)).list.apply()

Для быстрого написания SQL доступен трюк под названием SQLSyntaxSupport с интерполяцией строк.

val (m, g) = (GroupMember.syntax("m"), Group.syntax("g"))
val groupMember: Option[GroupMember] = sql"""
  select
    ${m.result.*}, ${g.result.*}
  from
    ${GroupMember.as(m)} left join ${Group.as(g)} 
    on ${m.groupId} = ${g.id}
  where
    ${m.id} = ${id}
  """
  .map(GroupMember(m.resultName, g.resultName)).single.apply()

Другой - QueryDSL, который является SQL-подобным и типобезопасным DSL для построения SQL. Он генерирует простой SQL, поэтому мы можем интуитивно предсказать, какой SQL будет сгенерирован.

val m = Member.syntax("m")
val name = "Alice"
val alice: Option[Member] = withSQL {
  select.from(Member as m).where.eq(m.name, name)
}.map(rs => Member(rs)).single.apply()

Кроме того, ScalikeJDBC не навязывает функциональный стиль программирования, такой как монада ввода-вывода, или асинхронное программирование с использованием Future. Этому легко научиться, и он очень практичен.

Заключение

На мой взгляд, ScalikeJDBC - лучшая библиотека доступа к базе данных в Scala на данный момент. У него достаточно функциональности и качества, полезных API, а также активных сопровождающих. Я думаю, что он подойдет почти для приложений Scala, поэтому рекомендую его, если вам интересно, какую библиотеку следует использовать для доступа к СУБД в Scala прямо сейчас.