Эта статья является частью нашего академического курса под названием « Разработка современных приложений с помощью 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 => val user = User(None, "a@b.com", Some("Tom"), Some("Tommyknocker")) users.insert(user) must be_== (user.copy(id = Some(1))).awaitFor(timeout) }} |
Очень базовая спецификация теста Specs2 с одним шагом теста, чтобы убедиться, что новый пользователь правильно вставлен в таблицу базы данных.
В случае, если по каким-либо причинам вы разрабатываете свой собственный драйвер базы данных для Slick , есть полезный модуль Slick TestKit, доступный вместе с примером реализации драйвера.
14. Выводы
Slick — чрезвычайно выразительная и мощная библиотека для доступа к реляционным базам данных из приложений Scala . Она очень гибкая и в большинстве случаев предлагает несколько альтернативных способов выполнения задач, одновременно пытаясь сохранить баланс между повышением производительности труда разработчиков и не скрывая тот факт, что они набирают реляционную модель и SQL под капотом.
Надеемся, что мы все заражены Slick прямо сейчас и хотим попробовать это. Официальная документация — это очень хорошее место для начала изучения Slick и начала работы.
15. Что дальше
В следующем разделе руководства мы поговорим о разработке приложений Scala для командной строки (или просто консоли).