[Из песочницы] SQL доступ к РСУБД посредством SkalikeJDBC
Есть библиотека, облегчающая использование SQL в Scala-программах, упоминания о которой на хабре я не нашел. Эту несправедливость я и хотел бы исправить. Речь пойдет о ScalikeJDBC.Главным конкурентом SkalikeJDBC является Anorm — библиотека от Play, решающая ровно те же задачи удобного общения с РСУБД посредством чистого (без примесей ORM) SQL. Однако Anorm глубоко погряз в Play, и использование его в проектах не связанных с Play может быть затруднительно. Ждать, когда оно окажется затруднительно и для меня, я не стал. Услышав о SkalikeJDBC я, практически сразу, решил его опробовать. Результатами этой аппробации в виде небольшого демо приложения я и буду делиться в этой статье, чуть ниже.
Перед тем, как перейти к примеру использования библиотеки, стоит заметить, что поддерживается и протестированна работа со следующими СУБД:
PostgreSQL MySQL H2 Database Engine HSQLDB А оставшиеся (Oracle, MS SQL Server, DB2, Informix, SQLite, тыщи их) также должны работать, ибо все общение c СУБД идет через стандартный JDBC. Однако их тестирование не производитстя, что может навлечь уныние на корпоративного заказчика.Пример приложенияВпрочем оставим корпоративного заказчика наедине с его невеселыми думами, и лучше займемся тем, ради чего и писалась эта статья. Осуществим короткое погружение в возможности библиотеки.Далее я приведу пример простого приложения, использующего SkalikeJDBC для доступа к Postgresql. Покажу, как можно его сконфигурировать с помощью Typesafe Config, создать таблицу в БД, делать CRUD-запросы к этой таблице и преобразовывать результаты Read-запросов в Scala-объекты. Я буду намеренно упускать многие варианты конфигурирования (без применения Typesafe Config) и применения библиотеки, чтобы остаться кратким и обеспечить быстрый старт. Полное описание возможностей доступно в удобной и достаточно короткой документации, а так же в Wiki на github.Приложение будет использовать SBT для сборки и управления зависимостями, так что создаем в корне пустого проекта файл build.sbt следующего содержания:
name:= «scalike-demo»
version:= »0.0»
scalaVersion:= »2.11.6»
val scalikejdbcV = »2.2.5»
libraryDependencies ++= Seq ( «org.postgresql» % «postgresql» % »9.4–1201-jdbc41», «org.scalikejdbc» %% «scalikejdbc» % scalikejdbcV, «org.scalikejdbc» %% «scalikejdbc-config» % scalikejdbcV ) В нем объявлены следующие зависимости: postgresql — jdbc драйвер postgres scalikejdbc — собственно библиотека SkalikeJDBC scalikejdbc-config — модуль поддержки Typesafe Config для конфигурирования соединения с СУБД В качестве СУБД будем использовать локальную Postgresql на стандартном (5432) порту. В ней уже имеется пользователь pguser с паролем securepassword и полным доступом к базе данных demo_db.В этом случае создаем файл конфигурации src/main/resources/application.conf следующего содержания:
db { demo_db { driver = org.postgresql.Driver url = «jdbc: postgresql://localhost:5432/demo_db» user = pguser password = securepassword
poolInitialSize=10 poolMaxSize=20 connectionTimeoutMillis=1000 poolValidationQuery=«select 1 as one» poolFactoryName=«commons-dbcp» } } Мы могли бы ограничиться первыми четырьмя параметрами, тогда применились бы настройки пула соединений по-умолчанию.Далее создадим пакет demo в папке src/main/scala, куда и поместим весь scala-код.
DemoApp.scala Начнем с главного запускаемого объекта: package demo import scalikejdbc.config.DBs object DemoApp extends App { DBs.setup ('demo_db) } Единственная строчка внутри объекта — указание считать настройки доступа к базе demo_db из файлов конфигурации. Объект DBs будет искать все подходящие ключи конфигурации (driver, url, user, password, …) в узле db.demo_db во всех файлах конфигурации прочитанных Typesafe Config. Typesafe Config, по конвенции, автоматически читает application.conf находящийся в classpath приложения.Результатом будет сконфигурированный ConnectionPool к БД.
DbConnected.scala Далее создадим трейт, в котором инкапсулируем получение коннекта к БД из пула package demo import scalikejdbc.{ConnectionPool, DB} trait DbConnected { def connectionFromPool: Connection = ConnectionPool.borrow ('demo_db) // (1) def dbFromPool: DB = DB (connectionFromPool) // (2) def insideLocalTx[A](sqlRequest: DBSession => A): A = { // (3) using (dbFromPool) { db => db localTx { session => sqlRequest (session) } } }
def insideReadOnly[A](sqlRequest: DBSession => A): A = { // (4) using (dbFromPool) { db => db readOnly { session => sqlRequest (session) } } } } В (1) мы получаем соединение (java.sql.Connection) из созданного и сконфигурированного в прошлом шаге пула.В (2) мы оборачиваем полученное соединение в удобный для scalikeJDBC объект доступа к БД (Basic Database Accessor).В (3) и (4) мы создаем удобные нам обертки для выполнения SQL-запросов. (3) — для запросов на изменение, (4) — для запросов на чтение. Можно было бы обойтись и без них, но тогда нам везде приходилось бы писать: def delete (userId: Long) = { using (dbFromPool) { db => db localTx { implicit session => sql«DELETE FROM t_users WHERE id = ${userId}».execute ().apply () } } } вместо: def delete (userId: Long) = { insideLocalTx { implicit session => sql«DELETE FROM t_users WHERE id = ${userId}».execute ().apply () } } , a DRY еще никто не отменял.Разберемся подробнее, что же происходит в пунктах (3) и (4):
using (dbFromPool)- позволяет обернуть открытие и закрытие коннекта к БД в один запрос. Без этого потребовалось бы открывать (val db = ThreadLocalDB.create (connectionFromPool)) и не забывать закрывать (db.close ()) соединения самостоятельно.
db.localTx — создает блокирующую транзакцию, внутри которой выполняеются запросы. Если внутри блока произойдет исключение транзакция откатится. Подробнее.
db.readOnly — исполняет запросы в режиме чтения. Подробнее.
Данный трейт мы можем использовать в наших DAO-классах, коих в нашем учебном приложении будет ровно 1 штука.
User.scala Перед тем, как приступить к созданию нашего DAO-класса, создадим доменный объект с которым он будет работать. Это будет простой case-класс, определяющий пользователя системы с тремя говорящими полями: package demo case class User (id: Option[Long] = None, name: String, email: Option[String] = None, age: Option[Int] = None) Только поле name является обязательным. Если id == None, то это говорит о том, что объект еще не сохранен в БД.UserDao.scala Теперь все готово для того, чтобы создать наш DAO-объект. package demo import scalikejdbc._ class UserDao extends DbConnected { def createTable () : Unit = { insideLocalTx { implicit session => sql»«CREATE TABLE t_users ( id BIGSERIAL NOT NULL PRIMARY KEY, name VARCHAR (255) NOT NULL, email VARCHAR (255), age INT)»«.execute ().apply () } } def create (userToSave: User): Long = { insideLocalTx { implicit session => val userId: Long = sql»«INSERT INTO t_users (name, email, age) VALUES (${userToSave.name}, ${userToSave.email}, ${userToSave.age})»« .updateAndReturnGeneratedKey ().apply () userId } } def read (userId: Long) : Option[User] = { insideReadOnly { implicit session => sql«SELECT * FROM t_users WHERE id = ${userId}».map (rs => User (rs.longOpt («id»), rs.string («name»), rs.stringOpt («email»), rs.intOpt («age»))) .single.apply () } } def readAll () : List[User] = { insideReadOnly { implicit session => sql«SELECT * FROM t_users».map (rs => User (rs.longOpt («id»), rs.string («name»), rs.stringOpt («email»), rs.intOpt («age»))) .list.apply () } } def update (userToUpdate: User) : Unit = { insideLocalTx { implicit session => sql»«UPDATE t_users SET name=${userToUpdate.name}, email=${userToUpdate.email}, age=${userToUpdate.age} WHERE id = ${userToUpdate.id} »«.execute ().apply () } } def delete (userId: Long) : Unit= { insideLocalTx { implicit session => sql«DELETE FROM t_users WHERE id = ${userId}».execute ().apply () } } } Здесь уже несложно догадаться, что делает каждая функция.
Создается объект SQL с помощью нотаций:
sql»«
Finita Чтож, наш DAO объект готов. Странно, конечно, видеть в нем метод создания таблицы… Он был добавлен просто для примера. Приложение учебное — можем себе позволить. Остается только применить этот DAO объект. Для этого изменим созданный нами в начале объект DemoApp. Например, он может принять такую форму: package demo import scalikejdbc.config.DBs object DemoApp extends App { DBs.setup ('demo_db) val userDao = new UserDao userDao.createTable () val userId = userDao.create (User (name = «Vasya», age = Some (42))) val user = userDao.read (userId).get val fullUser = user.copy (email = Some («vasya@domain.org»), age = None) userDao.update (fullUser) val userToDeleteId = userDao.create (User (name = «Petr»)) userDao.delete (userToDeleteId) userDao.readAll ().foreach (println) } Заключение В этом кратком обзоре мы взглянули на возможности библиотеки SkalikeJDBC и ощутили легкость и мощь, с которой она позволяет создавать объекты доступа к реляционным данным. Меня радует, что в эпоху засилья ORM-ов есть такой инструмент, который хорошо решает возложенные на него задачи и при этом продолжает активно развиваться.Спасибо за внимание. Да прибудет с вами Scala!