Есть библиотека, облегчающая использование 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/
Добавить комментарий