В сегодняшнем посте мы собираемся открыть свой разум, отойти от традиционного стека на основе Java EE / Java SE JPA (что я считаю великолепным) и освежить взгляд на то, как получить доступ к базе данных в ваших Java-приложениях с помощью нового ребенка на Блок: Slick 2.1 от Typesafe . Так что, если JPA так хорош, зачем? Ну, иногда вам нужно делать очень простые вещи, и для этого не нужно создавать полный, хорошо смоделированный слой постоянства. Здесь блестит пятно.
По сути, Slick — это библиотека доступа к базе данных, а не ORM . Хотя он написан на Scala , примеры, которые мы собираемся рассмотреть, не требуют каких-либо особых знаний об этом превосходном языке (хотя, именно Scala сделал возможным существование Slick ). Наша схема реляционной базы данных будет иметь только две таблицы: клиенты и адреса , связанные отношениями «один ко многим». Для простоты H2 был выбран в качестве движка базы данных в памяти.
Первый вопрос, который возникает, — это определение таблиц базы данных (схемы), и, естественно, специфичные для базы данных DDL являются стандартным способом сделать это. Можем ли мы что-то с этим сделать и попробовать другой подход? Если вы используете Slick 2.1 , ответ — да, абсолютно. Давайте просто опишем наши таблицы как классы Scala :
01
02
03
04
05
06
07
08
09
10
11
|
// 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 . Таблица адресов будет определена таким же образом, а таблица пользователей будет ссылаться на внешний ключ.
01
02
03
04
05
06
07
08
09
10
11
12
|
// 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 великолепны, но с ними не очень удобно работать. К счастью, Slick позволяет использовать классы case вместо кортежей, предоставляя так называемую технику Lifted Embedding . Вот наши классы дел с клиентами и адресами :
1
2
3
4
5
|
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 ). Давайте посмотрим, как это выглядит для клиентов :
1
2
3
|
// Converts from Customer domain instance to table model and vice-versa def * = ( id.?, email, firstName.?, lastName.? ).shaped <> ( Customer.tupled, Customer.unapply ) |
Для таблицы адресов формирование выглядит немного более многословно из-за того, что у Slick нет способа ссылаться на экземпляр класса case клиента по внешнему ключу. Тем не менее, это довольно просто, мы просто создаем временного Customer по его идентификатору.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 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 в реальные таблицы базы данных? К счастью для Слика , это так просто:
1
2
3
4
5
|
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 . Например, вот метод, который вставляет новую запись клиента в таблицу клиентов :
1
2
3
4
5
6
7
|
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 ) ) } } |
И это можно использовать так:
1
2
|
|
Точно так же семейство функций поиска может быть реализовано с использованием обычного Scala для понимания:
01
02
03
04
05
06
07
08
09
10
|
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 } |
А вот примеры использования:
1
2
|
val customers = Customers.findAll |
Обновления и удаления немного отличаются, хотя и очень просты, давайте посмотрим на них:
01
02
03
04
05
06
07
08
09
10
11
|
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 } |
Давайте посмотрим на эти два метода в действии:
1
2
3
4
|
Customers.update( customer.copy( firstName = Some( "Tommy" ) ) ) Customers.remove( customer ) } |
Выглядит очень аккуратно. Лично я все еще изучаю Slick, но я очень взволнован этим. Это помогает мне выполнять работу намного быстрее, наслаждаясь красотой языка Scala и функциональным программированием. Без сомнения, в следующей версии 3.0 появятся еще более интересные функции, я с нетерпением жду этого.
Этот пост является просто введением в мир Slick , многие детали реализации и варианты использования были оставлены в стороне, чтобы сделать его кратким и простым. Однако документация Слика довольно хорошая, и, пожалуйста, не стесняйтесь обращаться к ней.
- Полный проект доступен на GitHub .
Ссылка: | Свежий взгляд на доступ к базе данных на платформе JVM: Slick от Typesafe от нашего партнера по JCG Андрея Редько в блоге Андрея Редько {devmind} . |