Эта статья является частью нашего академического курса под названием « Разработка современных приложений с помощью Scala» .
В этом курсе мы предоставляем среду и набор инструментов, чтобы вы могли разрабатывать современные приложения Scala. Мы охватываем широкий спектр тем: от сборки SBT и реактивных приложений до тестирования и доступа к базе данных. С нашими простыми учебными пособиями вы сможете запустить и запустить собственные проекты за минимальное время. Проверьте это здесь !
Мы, безусловно, уходим в эпоху процветающих хранилищ данных. За последние пару лет появилось множество решений NoSQL и NewSQL , и даже в эти дни время от времени появляются новые.
Тем не менее, давние игроки в форме реляционной базы данных все еще используются в подавляющем большинстве программных систем. Пуленепробиваемые и проверенные в бою, они являются выбором номер один для хранения важных для бизнеса данных.
Содержание
1. Введение
Когда мы говорим о платформе Java и JVM в целом, JDBC — это стандартный способ взаимодействия с реляционными базами данных . Таким образом, в основном каждый поставщик реляционных баз данных обеспечивает реализацию драйвера JDBC, поэтому становится возможным подключаться к ядру базы данных из кода приложения.
Во многих отношениях JDBC — это устаревшая спецификация, которая вряд ли вписывается в современные парадигмы реактивного программирования . Хотя есть некоторые дискуссии по обновлению спецификации, в действительности никакой активной работы в этом направлении не происходит, по крайней мере, публично.
2. Доступ к базе данных, функциональный путь
Неспособность ускорить изменения в спецификации не означает, что ничего нельзя сделать. В JVM доступно множество различных структур и библиотек для доступа к реляционным базам данных . Некоторые из них стремятся быть как можно ближе к SQL , другие идут дальше, чем предлагают «плавное» отображение реляционной модели на конструкции языка программирования (так называемый класс ORM или решения объектно-реляционного отображения ). Хотя, честно говоря, большинство из них построены на основе абстракций JDBC .
В этом отношении Slick (или полностью Scala Language-Integrated Connection Kit ) — это библиотека, обеспечивающая доступ к реляционным базам данных из приложения Scala . Он в значительной степени основан на парадигме функционального программирования и поэтому часто упоминается как библиотека функционального реляционного отображения (или FRM ). Главное обещание Slick — заново изобрести способ доступа к реляционным базам данных с помощью регулярных операций над коллекциями, знакомых каждому разработчику Scala , с упором на безопасность типов.
Несмотря на то, что выпуск 3.1.1 вышел не так давно, это относительно молодая библиотека, которая все еще находится на пути к достижению определенного уровня зрелости. Многие ранние пользователи помнят, насколько существенной была разница между версиями 2.x и 3.x. К счастью, все становится более стабильным и гладким, а первая веха в предстоящем выпуске 3.2 — всего несколько недель.
Слик идет в ногу со временем и является полностью асинхронной библиотекой. И, как мы скоро увидим, также реализует спецификацию реактивных потоков .
3. Конфигурация
Slick полностью поддерживает целый ряд популярных движков с открытым исходным кодом и коммерческих реляционных баз данных . Чтобы продемонстрировать, что мы собираемся использовать по крайней мере два из них: MySQL для производственного развертывания и H2 для интеграционного тестирования.
Чтобы отдать должное Slick , очень легко начать работу, когда приложение разрабатывается на основе единого механизма реляционной базы данных . Но немного сложно настроить и использовать Slick независимо от JDBC- драйвера из-за различий в возможностях базы данных. Поскольку мы нацелены как минимум на два разных движка, MySQL и H2 , мы, безусловно, пойдем по этому пути.
Типичным способом настройки соединений с базой данных в Slick является наличие выделенного именованного раздела в файле application.conf
, например:
1
2
3
4
5
6
7
8
9
|
db { driver = "slick.driver.MySQLDriver$" db { url = "jdbc:mysql://localhost:3306/test?user=root&password=password" driver = com.mysql.jdbc.Driver maxThreads = 5 } } |
Конфигурация должна выглядеть знакомой разработчикам JVM, работающим с реляционной базой данных через JDBC . Стоит отметить, что Slick поддерживает пул соединений с базой данных из коробки, используя блестящую библиотеку HikariCP . Параметр maxThreads
Slick, чтобы настроить пул соединений с максимальным размером 5
.
Если вам интересно, почему в конфигурации есть две настройки драйвера, вот причина. Первый параметр драйвера определяет специфичный для Slick профиль JDBC ( Slick driver), а второй указывает на использование драйвера JDBC для использования.
Чтобы позаботиться об этой конфигурации, мы собираемся определить DbConfiguration
черту DbConfiguration
, хотя цель введения этой черты может пока не быть столь очевидной:
1
2
3
|
trait DbConfiguration { lazy val config = DatabaseConfig.forConfig[JdbcProfile]( "db" ) } |
4. Отображение таблицы
Возможно, первое, с чего нужно начать во вселенной реляционных баз данных, — это моделирование данных. По сути, это означает создание схемы базы данных, таблиц, их отношений и ограничений. К счастью, Slick делает это чрезвычайно легко.
В качестве упражнения давайте создадим пример приложения для управления пользователями и их адресами, представленными этими двумя классами.
1
2
3
4
5
|
case class User(id : Option[Int], email : String, firstName : Option[String], lastName : Option[String]) case class Address(id : Option[Int], userId : Int, addressLine : String, city : String, postalCode : String) |
В свою очередь, наша модель данных отношений будет составлена из двух таблиц: USERS
и ADDRESSES
. Давайте воспользуемся возможностями Slick , чтобы сформировать это в Scala .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
trait UsersTable { this : Db = > import config.driver.api. _ private class Users(tag : Tag) extends Table[User](tag, "USERS" ) { // Columns def id = column[Int]( "USER_ID" , O.PrimaryKey, O.AutoInc) def email = column[String]( "USER_EMAIL" , O.Length( 512 )) def firstName = column[Option[String]]( "USER_FIRST_NAME" , O.Length( 64 )) def lastName = column[Option[String]]( "USER_LAST_NAME" , O.Length( 64 )) // Indexes def emailIndex = index( "USER_EMAIL_IDX" , email, true ) // Select def * = (id.?, email, firstName, lastName) <> (User.tupled, User.unapply) } val users = TableQuery[Users] } |
Для людей, знакомых с языком SQL , определенно очень похоже на оператор CREATE TABLE
. Однако у Slick также есть способ определить плавное преобразование между сущностью домена, представленной классом Scala ( User
), в строку таблицы ( Users
) и наоборот, используя *
projection (буквально переводится как SELECT * FROM USERS
).
Одна тонкая деталь, которую мы еще не затронули, это черта Db
(на которую ссылается this: Db =>
construct). Давайте посмотрим, как это определяется:
1
2
3
4
|
trait Db { val config : DatabaseConfig[JdbcProfile] val db : JdbcProfile # Backend # Database = config.db } |
config
является той из DbConfiguration
то время как db
является новым экземпляром базы данных. Позже в характеристике UsersTable
соответствующие типы для соответствующего профиля JDBC вводятся в область с import config.driver.api._
объявления import config.driver.api._
.
Отображение для таблицы ADDRESSES
выглядит очень похоже, за исключением того факта, что нам нужна ссылка внешнего ключа на таблицу USERS
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
trait AddressesTable extends UsersTable { this : Db = > import config.driver.api. _ private class Addresses(tag : Tag) extends Table[Address](tag, "ADDRESSES" ) { // Columns def id = column[Int]( "ADDRESS_ID" , O.PrimaryKey, O.AutoInc) def addressLine = column[String]( "ADDRESS_LINE" ) def city = column[String]( "CITY" ) def postalCode = column[String]( "POSTAL_CODE" ) // ForeignKey def userId = column[Int]( "USER_ID" ) def userFk = foreignKey( "USER_FK" , userId, users) ( _ .id, ForeignKeyAction.Restrict, ForeignKeyAction.Cascade) // Select def * = (id.?, userId, addressLine, city, postalCode) <> (Address.tupled, Address.unapply) } val addresses = TableQuery[Addresses] } |
users
и addresses
участников служат фасадом для выполнения любых операций доступа к базе данных для соответствующих таблиц.
5. Хранилища
Хотя репозитории не являются специфическими для Slick как таковые, определение выделенного слоя для взаимодействия с ядром базы данных всегда является хорошим принципом проектирования. В нашем приложении будет только два репозитория: UsersRepository
и AddressesRepository
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
class UsersRepository( val config : DatabaseConfig[JdbcProfile]) extends Db with UsersTable { import config.driver.api. _ import scala.concurrent.ExecutionContext.Implicits.global ... } class AddressesRepository( val config : DatabaseConfig[JdbcProfile]) extends Db with AddressesTable { import config.driver.api. _ import scala.concurrent.ExecutionContext.Implicits.global ... } |
Все манипуляции с данными, которые мы собираемся продемонстрировать позже, станут частью одного из этих классов. Также обратите внимание на наличие признака Db
в цепочке наследования.
6. Управление схемами
После определения отображений таблиц (или упрощения схемы базы данных) Slick может проецировать их на последовательность операторов DDL , например:
1
2
3
4
5
|
def init() = db.run(DBIOAction.seq(users.schema.create)) def drop() = db.run(DBIOAction.seq(users.schema.drop)) def init() = db.run(DBIOAction.seq(addresses.schema.create)) def drop() = db.run(DBIOAction.seq(addresses.schema.drop)) |
7. Вставка
В простейших сценариях добавить новую строку в таблицу так же просто, как добавить элемент к users
или addresses
(экземпляры TableQuery ), например:
1
|
def insert(user : User) = db.run(users + = user) |
Это прекрасно работает, когда первичные ключи назначаются из кода приложения. Однако в случае, когда первичные ключи генерируются на стороне базы данных (например, с использованием автоинкрементов ), например, для таблиц Users
и Addresses
, мы должны запросить нам вернуть эти первичные идентификаторы:
1
2
3
|
def insert(user : User) = db .run(users returning users.map( _ .id) + = user) .map(id = > user.copy(id = Some(id))) |
8. Запросы
Запрос является одной из отличительных черт Slick, которая действительно сияет. Как мы уже упоминали, Slick старается разрешить использование семантики коллекции Scala над операциями с базами данных. Однако это работает на удивление хорошо, пожалуйста, обратите внимание, что вы работаете не со стандартными типами Scala, а с поднятыми: техника, известная как поднятое встраивание .
Давайте взглянем на этот быстрый пример одного из возможных способов получения пользователя из таблицы по его первичному ключу:
1
2
|
def find(id : Int) = db.run(( for (user <- users if user.id === id) yield user).result.headOption) |
В качестве альтернативы for
понимания мы могли бы просто использовать операцию фильтрации, например:
1
|
def find(id : Int) = db.run(users.filter( _ .id === id).result.headOption) |
Результаты (и, кстати, сгенерированный SQL- запрос) точно такие же. В случае, если нам нужно получить пользователя и его адрес, мы могли бы также использовать несколько вариантов запросов, начиная с типичного соединения:
1
2
3
|
def find(id : Int) = db.run( ( for ((user, address) <- users join addresses if user.id === id) yield (user, address)).result.headOption) |
Или, альтернативно:
1
2
3
4
5
|
def find(id : Int) = db.run( ( for { user <- users if user.id === id address <- addresses if address.userId === id } yield (user, address)).result.headOption) |
Возможности Slick Query действительно очень мощные, выразительные и удобочитаемые. Мы только что рассмотрели несколько типичных примеров, но, пожалуйста, просмотрите официальную документацию, чтобы найти гораздо больше.
9. Обновление
Обновления в Slick представлены в виде комбинации запроса (который в основном обрисовывает в общих чертах, что должно быть обновлено) и, по сути, самого обновления. Например, давайте введем метод для обновления имени и фамилии пользователя:
1
2
3
4
5
6
|
def update(id : Int, firstName : Option[String], lastName : Option[String]) = { def update(id : Int, firstName : Option[String], lastName : Option[String]) = { val query = for (user <- users if user.id === id) yield (user.firstName, user.lastName) db.run(query.update(firstName, lastName)) map { _ > 0 } } |
10. Удаление
Как и в случае с обновлениями, операция удаления — это просто запрос для фильтрации удаляемых строк, например:
1
2
|
def delete(id : Int) = db.run(users.filter( _ .id === id).delete) map { _ > 0 } |
11. Потоковое
Slick предлагает возможность потоковой передачи результатов запроса к базе данных. Мало того, его потоковая реализация полностью поддерживает спецификацию реактивных потоков и может быть использована сразу же вместе с Akka Streams .
Например, давайте Sink.fold
результаты из таблицы users
и соберем их в виде последовательности, используя Sink.fold
обработки Sink.fold
.
1
2
3
4
|
def stream( implicit materializer : Materializer) = Source .fromPublisher(db.stream(users.result.withStatementParameters(fetchSize = 10 ))) .to(Sink.fold[Seq[User], User](Seq())( _ : + _ )) .run() |
Обратите внимание, что потоковая функция Slick действительно очень чувствительна к реляционной базе данных и используемому вами драйверу JDBC и может потребовать дополнительных исследований и настройки. Обязательно проведите обширное тестирование, чтобы убедиться, что данные передаются правильно.
12. SQL
В случае необходимости выполнения пользовательских SQL- запросов Slick ничего не имеет против этого и, как всегда, старается сделать его максимально безболезненным, предоставляя полезные макросы. Допустим, мы хотели бы прочитать имя и фамилию пользователя напрямую, используя простой старый SELECT
.
1
2
3
|
def getNames(id : Int) = db.run( sql "select user_first_name, user_last_name from users where user_id = #$id" .as[(String, String)].headOption) |
Это так просто. В случае, если форма запроса SQL не известна заранее, Slick предоставляет механизмы для настройки извлечения результирующего набора. Если вам интересно, в официальной документации есть очень хороший раздел, посвященный простым старым SQL-запросам .
13. Тестирование
Есть несколько способов приблизиться к тестированию уровня доступа к базе данных при использовании библиотеки Slick . Традиционным является использование базы данных в памяти (например, H2 ), что в нашем случае приводит к незначительным изменениям конфигурации внутри application.conf
:
01
02
03
04
05
06
07
08
09
10
|
db { driver = "slick.driver.H2Driver$" db { url = "jdbc:h2:mem:test1;DB_CLOSE_DELAY=-1" driver=org.h2.Driver connectionPool = disabled keepAliveConnection = true } } |
Обратите внимание, что если в производственной конфигурации мы включили пул соединений с базой данных, то при тестировании используется только одно соединение, и пул явно отключен. Все остальное по сути остается прежним. Единственное, о чем мы должны позаботиться, — это создание и удаление схемы между тестовыми прогонами. К счастью, как мы видели в разделе « Управление схемами» , это очень легко сделать с помощью Slick .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
class UsersRepositoryTest extends Specification with DbConfiguration with FutureMatchers with OptionMatchers with BeforeAfterEach { sequential val timeout = 500 milliseconds val users = new UsersRepository(config) def before = { Await.result(users.init(), timeout) } def after = { Await.result(users.drop(), timeout) } "User should be inserted successfully" >> { implicit ee : ExecutionEnv = > users.insert(user) must be _== (user.copy(id = Some( 1 ))).awaitFor(timeout) } } |
Очень базовая спецификация теста Specs2 с одним шагом теста, чтобы убедиться, что новый пользователь правильно вставлен в таблицу базы данных.
В случае, если по каким-либо причинам вы разрабатываете свой собственный драйвер базы данных для Slick , есть полезный модуль Slick TestKit, доступный вместе с примером реализации драйвера.
14. Выводы
Slick — чрезвычайно выразительная и мощная библиотека для доступа к реляционным базам данных из приложений Scala . Она очень гибкая и в большинстве случаев предлагает несколько альтернативных способов выполнения задач, одновременно пытаясь сохранить баланс между повышением производительности труда разработчиков и не скрывая тот факт, что они набирают реляционную модель и SQL под капотом.
Надеемся, что мы все заражены Slick прямо сейчас и хотим попробовать это. Официальная документация — это очень хорошее место для начала изучения Slick и начала работы.
15. Что дальше
В следующем разделе руководства мы поговорим о разработке приложений Scala для командной строки (или просто консоли).