В сегодняшнем посте мы собираемся открыть свой разум, отойти от традиционного стека на основе 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 .