Статьи

Новый взгляд на доступ к базам данных на платформе JVM: Slick from Typesafe

В сегодняшнем посте мы собираемся открыть свой разум, отойти от традиционного  стека на основе Java EE  /  Java SE  JPA (что я считаю великолепным) и освежить взгляд на то, как получить доступ к базе данных в ваших Java-приложениях с помощью нового ребенка на Блок:  Slick 2.1  от  Typesafe . Так что, если  JPA  так хорош, зачем? Ну, иногда вам нужно делать очень простые вещи, и для этого не нужно создавать полный, хорошо смоделированный слой постоянства. В здесь  Slick  светит.

По сути,  Slick  — это библиотека доступа к базе данных, а не  ORM . Несмотря на то, что он написан на  Scala , примеры, которые мы собираемся рассмотреть, не требуют каких-либо особых знаний этого превосходного языка (хотя, именно  Scala сделал   возможным существование Slick ). Наша схема реляционной базы данных будет иметь только две таблицы:  клиенты  и  адреса , связанные отношениями «один ко многим». Для простоты  H2  был выбран в качестве движка базы данных в памяти.

Первый вопрос, который возникает, — это определение таблиц базы данных (схемы), и, естественно, специфичные для базы данных  DDL являются стандартным способом сделать это. Можем ли мы что-то с этим сделать и попробовать другой подход? Если вы используете Slick 2.1 , ответ — да, абсолютно. Давайте просто опишем наши таблицы как   классы Scala :

// The 'customers' relation table definition
class Customers( tag: Tag ) extends Table[ Customer ]( tag, "customers" ) {
  def id = column[ Int ]( "id", O.PrimaryKey, O.AutoInc )

  def email = column[ String ]( "email", O.Length( 512, true ), O.NotNull )
  def firstName = column[ String ]( "first_name", O.Length( 256, true ), O.Nullable )
  def lastName = column[ String ]( "last_name", O.Length( 256, true ), O.Nullable )

  // Unique index for customer's email
  def emailIndex = index( "idx_email", email, unique = true )
}

Очень простой и понятный, очень напоминающий типичную   конструкцию CREATE TABLE . Таблица  адресов  будет определена таким же образом, а  таблица пользователей будет ссылаться  на внешний ключ.

// The 'customers' relation table definition
class Addresses( tag: Tag ) extends Table[ Address ]( tag, "addresses" ) {
  def id = column[ Int ]( "id", O.PrimaryKey, O.AutoInc )

  def street = column[ String ]( "street", O.Length( 100, true ), O.NotNull )
  def city = column[ String ]( "city", O.Length( 50, true ), O.NotNull )
  def country = column[ String ]( "country", O.Length( 50, true ), O.NotNull )

  // Foreign key to 'customers' table
  def customerId = column[Int]( "customer_id", O.NotNull )
  def customer = foreignKey( "customer_fk", customerId, Customers )( _.id )
}

Отлично, оставим некоторые детали, вот и все: мы определили две таблицы базы данных в чистом  Scala . Но детали важны, и мы рассмотрим два следующих объявления:  Table [Customer]  и  Table [Address] . По сути, каждая таблица может быть представлена ​​в виде кортежа с таким количеством элементов, как определено в столбце. Например,  клиенты  — это кортеж  (Int, String, String, String) , а   таблица адресов — это кортеж  (Int, String, String, String, Int) . Кортежи в  Scala  великолепны, но с ними не очень удобно работать. К счастью,  пятно позволяет использовать классы case вместо кортежей, предоставляя так называемую   технику Lifted Embedding . Вот наши   классы дел с клиентами  и  адресами :

case class Customer( id: Option[Int] = None, email: String, 
  firstName: Option[ String ] = None, lastName: Option[ String ] = None)

case class Address( id: Option[Int] = None,  street: String, city: String, 
  country: String, customer: Customer )

Последний, но не менее важный вопрос: как  Slick  будет преобразовывать кортежи в классы дел и наоборот? Было бы здорово иметь такое преобразование из коробки, но на этом этапе  Слику  нужна небольшая помощь. Используя  терминологию Slick , мы собираемся сформировать  *  табличную проекцию (что соответствует конструкции  SELECT * FROM …  SQL ). Давайте посмотрим, как это выглядит для  клиентов :

// Converts from Customer domain instance to table model and vice-versa
def * = ( id.?, email, firstName.?, lastName.? ).shaped <> ( 
  Customer.tupled, Customer.unapply )

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

// Converts from Customer domain instance to table model and vice-versa
def * = ( id.?, street, city, country, customerId ).shaped <> ( 
  tuple => {
    Address.apply(
      id = tuple._1,
      street = tuple._2,
      city = tuple._3,
      country = tuple._4,
      customer = Customer( Some( tuple._5 ), "" )
    )
  }, {
    (address: Address) =>
      Some { (
        address.id,
        address.street,
        address.city,
        address.country,
        address.customer.id getOrElse 0 
      )
    }
  }
)

Теперь, когда все детали были объяснены, как мы можем материализовать наши   определения таблиц Scala в реальные таблицы базы данных? К счастью для  Слика , это так просто:

implicit lazy val DB = Database.forURL( "jdbc:h2:mem:test", driver = "org.h2.Driver" )
  
DB withSession { implicit session =>
  ( Customers.ddl ++ Addresses.ddl ).create
}

У Slick  есть много способов запрашивать и обновлять данные в базе данных. Самый красивый и мощный — просто использование чисто функциональных конструкций языка  Scala  . Самый простой способ сделать это — определить сопутствующий объект и реализовать в нем типичные   операции CRUD . Например, здесь есть метод , который вставляет новую  клиентскую  запись в  клиентах  таблицу:

object Customers extends TableQuery[ Customers ]( new Customers( _ ) ) {
  def create( customer: Customer )( implicit db: Database ): Customer = 
    db.withSession { implicit session =>
      val id = this.autoIncrement.insert( customer )
      customer.copy( id = Some( id ) )
    } 
}

И это можно использовать так:

Customers.create( Customer( None, "[email protected]",  Some( "Tom" ), Some( "Tommyknocker" ) ) )
Customers.create( Customer( None, "[email protected]",  Some( "Bob" ), Some( "Bobbyknocker" ) ) )

Точно так же семейство   функций поиска может быть реализовано с использованием обычного  Scala  для  понимания:

def findByEmail( email: String )( implicit db: Database ) : Option[ Customer ] = 
  db.withSession { implicit session =>
    ( for { customer <- this if ( customer.email === email.toLowerCase ) } 
      yield customer ) firstOption
  }
   
def findAll( implicit db: Database ): Seq[ Customer ] = 
  db.withSession { implicit session =>      
    ( for { customer <- this } yield customer ) list
  }

А вот примеры использования:

val customers = Customers.findAll
val customer = Customers.findByEmail( "[email protected]" )

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

def update( customer: Customer )( implicit db: Database ): Boolean = 
  db.withSession { implicit session =>
    val query = for { c <- this if ( c.id === customer.id ) } 
      yield (c.email, c.firstName.?, c.lastName.?)
    query.update(customer.email, customer.firstName, customer.lastName) > 0
  }
  
def remove( customer: Customer )( implicit db: Database ) : Boolean = 
  db.withSession { implicit session =>
    ( for { c <- this if ( c.id === customer.id ) } yield c ).delete > 0
  }

Давайте посмотрим на эти два метода в действии:

Customers.findByEmail( "[email protected]" ) map { customer =>
  Customers.update( customer.copy( firstName = Some( "Tommy" ) ) )
  Customers.remove( customer )
}

Выглядит очень аккуратно. Лично я все еще изучаю  Slick,  но я очень взволнован этим Это помогает мне выполнять работу намного быстрее, наслаждаясь красотой языка  Scala  и функциональным программированием. Без сомнения, в следующей версии  3.0  появятся еще более интересные функции, я с нетерпением жду этого.

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

Полный проект доступен на  GitHub .