В сегодняшнем посте мы собираемся открыть свой разум, отойти от традиционного стека на основе 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 definitionclass 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 definitionclass 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-versadef * = ( 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-versadef * = ( 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
|
Customers.create( Customer( None, "tom@b.com", Some( "Tom" ), Some( "Tommyknocker" ) ) )Customers.create( Customer( None, "bob@b.com", Some( "Bob" ), Some( "Bobbyknocker" ) ) ) |
Точно так же семейство функций поиска может быть реализовано с использованием обычного 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.findAllval customer = Customers.findByEmail( "bob@b.com" ) |
Обновления и удаления немного отличаются, хотя и очень просты, давайте посмотрим на них:
|
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.findByEmail( "bob@b.com" ) map { customer => Customers.update( customer.copy( firstName = Some( "Tommy" ) ) ) Customers.remove( customer )} |
Выглядит очень аккуратно. Лично я все еще изучаю Slick, но я очень взволнован этим. Это помогает мне выполнять работу намного быстрее, наслаждаясь красотой языка Scala и функциональным программированием. Без сомнения, в следующей версии 3.0 появятся еще более интересные функции, я с нетерпением жду этого.
Этот пост является просто введением в мир Slick , многие детали реализации и варианты использования были оставлены в стороне, чтобы сделать его кратким и простым. Однако документация Слика довольно хорошая, и, пожалуйста, не стесняйтесь обращаться к ней.
- Полный проект доступен на GitHub .
| Ссылка: | Свежий взгляд на доступ к базе данных на платформе JVM: Slick от Typesafe от нашего партнера по JCG Андрея Редько в блоге Андрея Редько {devmind} . |