SQL доступ к РСУБД посредством SkalikeJDBC

от автора

imageЕсть библиотека, облегчающая использование 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"""<SQL Here>""" sql"<SQL Here>"

У этого объекта применяются методы:

  • execute – для исполнения без возвращения результата
  • map – для преобразования полученных данных из набора WrappedResultSet’ов в необходимый нам вид. В нашем случае в коллекцию User’ов. После преобразования необходимо задать ожидаемое количество возвращаемых значений:
    • single – для возвращения одной строки результата в виде Option.
    • list – для возвращения всей результирующей коллекции.
  • UpdateAndReturnGeneratedKey – для вставки и возвращения идентификатора создаваемого объекта.

Завершает цепочку операция apply(), которая выполняет созданный запрос посредством объявленной implicit session.

Так же надо заметить, что все вставки параметров типа ${userId} – это вставка параметров в PreparedStatement и никаких 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!

ссылка на оригинал статьи http://habrahabr.ru/post/256545/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *