Есть библиотека, облегчающая использование SQL в Scala-программах, упоминания о которой на хабре я не нашел. Эту несправедливость я и хотел бы исправить. Речь пойдет о ScalikeJDBC.
Главным конкурентом SkalikeJDBC является Anorm – библиотека от Play, решающая ровно те же задачи удобного общения с РСУБД посредством чистого (без примесей ORM) SQL. Однако Anorm глубоко погряз в Play, и использование его в проектах не связанных с Play может быть затруднительным. Ждать, когда оно окажется затруднительным и для меня, я не стал. Услышав о SkalikeJDBC я, практически сразу, решил его опробовать. Результатами этой аппробации в виде небольшого демо приложения я и буду делиться в этой статье, чуть ниже.
Перед тем, как перейти к примеру использования библиотеки, стоит заметить, что поддерживается и протестированна работа со следующими СУБД:
А оставшиеся (Oracle, MS SQL Server, DB2, Informix, SQLite, тыщи их) также должны работать, ибо все общение c СУБД идет через стандартный JDBC. Однако их тестирование не производитстя, что может навлечь уныние на корпоративного заказчика.
Впрочем оставим корпоративного заказчика наедине с его невеселыми думами, и лучше займемся тем, ради чего и писалась эта статья. Осуществим короткое погружение в возможности библиотеки.
Далее я приведу пример простого приложения, использующего SkalikeJDBC для доступа к Postgresql. Покажу, как можно его сконфигурировать с помощью Typesafe Config, создать таблицу в БД, делать CRUD-запросы к этой таблице и преобразовывать результаты Read-запросов в Scala-объекты. Я буду намеренно упускать многие варианты конфигурирования (без применения Typesafe Config) и применения библиотеки, чтобы остаться кратким и обеспечить быстрый старт. Полное описание возможностей доступно в удобной и достаточно короткой документации, а так же в Wiki на github.
Приложение будет использовать SBT для сборки и управления зависимостями, так что создаем в корне пустого проекта файл build.sbt следующего содержания:
В нем объявлены следующие зависимости:
В качестве СУБД будем использовать локальную Postgresql на стандартном (5432) порту. В ней уже имеется пользователь pguser с паролем securepassword и полным доступом к базе данных demo_db.
В этом случае создаем файл конфигурации src/main/resources/application.conf следующего содержания:
Мы могли бы ограничиться первыми четырьмя параметрами, тогда применились бы настройки пула соединений по-умолчанию.
Далее создадим пакет demo в папке src/main/scala, куда и поместим весь scala-код.
Начнем с главного запускаемого объекта:
Единственная строчка внутри объекта – указание считать настройки доступа к базе demo_db из файлов конфигурации. Объект DBs будет искать все подходящие ключи конфигурации ( driver, url, user, password, ...) в узле db.demo_db во всех файлах конфигурации прочитанных Typesafe Config. Typesafe Config, по конвенции, автоматически читает application.conf находящийся в classpath приложения.
Результатом будет сконфигурированный ConnectionPool к БД.
Далее создадим трейт, в котором инкапсулируем получение коннекта к БД из пула
В (1) мы получаем соединение(java.sql.Connection) из созданного и сконфигурированного в прошлом шаге пула.
В (2) мы оборачиваем полученное соединение в удобный для scalikeJDBC объект доступа к БД (Basic Database Accessor).
В (3) и (4) мы создаем удобные нам обертки для выполнения SQL-запросов. (3) – для запросов на изменение, (4) – для запросов на чтение. Можно было бы обойтись и без них, но тогда нам везде приходилось бы писать:
вместо:
, a DRY еще никто не отменял.
Разберемся подробнее, что же происходит в пунктах (3) и (4):
using(dbFromPool)- позволяет обернуть открытие и закрытие коннекта к БД в один запрос. Без этого потребовалось бы открывать (val db = ThreadLocalDB.create(connectionFromPool)) и не забывать закрывать (db.close()) соединения самостоятельно.
db.localTx – создает блокирующую транзакцию, внутри которой выполняеются запросы. Если внутри блока произойдет исключение транзакция откатится. Подробнее.
db.readOnly – исполняет запросы в режиме чтения. Подробнее.
Данный трейт мы можем использовать в наших DAO-классах, коих в нашем учебном приложении будет ровно 1 штука.
Перед тем, как приступить к созданию нашего DAO-класса, создадим доменный объект с которым он будет работать. Это будет простой case-класс, определяющий пользователя системы с тремя говорящими полями:
Только поле name является обязательным. Если id == None, то это говорит о том, что объект еще не сохранен в БД.
Теперь все готово для того, чтобы создать наш DAO-объект.
Здесь уже несложно догадаться, что делает каждая функция.
Создается объект SQL с помощью нотаций:
У этого объекта применяются методы:
Завершает цепочку операция apply(), которая выполняет созданный запрос посредством объявленной implicit session.
Так же надо заметить, что все вставки параметров типа ${userId} – это вставка параметров в PreparedStatement и никаких SQL-инъекций опасаться не стоит.
Чтож, наш DAO объект готов. Странно, конечно, видеть в нем метод создания таблицы… Он был добавлен просто для примера. Приложение учебное – можем себе позволить. Остается только применить этот DAO объект. Для этого изменим созданный нами в начале объект DemoApp. Например, он может принять такую форму:
В этом кратком обзоре мы взглянули на возможности библиотеки SkalikeJDBC и ощутили легкость и мощь, с которой она позволяет создавать объекты доступа к реляционным данным. Меня радует, что в эпоху засилья ORM-ов есть такой инструмент, который хорошо решает возложенные на него задачи и при этом продолжает активно развиваться.
Спасибо за внимание. Да прибудет с вами Scala!
Главным конкурентом 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!
Комментарии (7)
bormotov
03.05.2015 01:02не заметил никаких сложностей использовать Anorm без Play!
в build.sbt добавлено
"com.typesafe.play" %% "anorm" % "2.3.8"
Взяв jdbc наперевес в базу хожу руками (уже не помню почему, но можно обернуть удобнее).
В итоге есть экземпляр java.sql.Connection, который отдается всем, кому нужно сходить в базу. Делать это трейтом или еще как — дело вкуса. Конект примерно так же передается имплиситом.
А дальше примерно та же картина, плюс-минус
val result = SQL("select some_column from some_table").executeQuery.as(...)
а вот то, что можно написать внутри as() понравилось.vadim_shb Автор
13.05.2015 12:44Ок, каюсь, не пробовал использовать Anorm без Play. Потому и написал так обтекаемо про «возможные» сложности этого процесса. Вообще мне синтаксис и подход Skalike кажется чуточку более стройным, чем у Anorm, и уже этого хватило, чтобы поделиться знаниями о существовании этой библиотеки с хабром. Возможно, узнав о ней, некоторые скалисты озадачатся уже вопросом об использовании Skalike в Play =).
vba
А как насчет Spring JDBC? Что дает представленная вами библиотека по сравнению со творением Spring?
vadim_shb Автор
Spring JDBC — отличная библиотека для Java. Погрязла в Spring глубже, чем Anorm в Play, но это не минус.
Если их сравнивать, то получится больше похожего, чем различий. Все-таки обе команды воплащают лучшие практики, просто на разных языках. Но парочку различий выделить можно.
Что в Spring JDBC сделано удобнее, чем в ScalikeJDBC:
Что в ScalikeJDBC сделано удобнее, чем в Spring JDBC:
vadim_shb Автор
Хотя, конечно, сложно сравнивать… Все-таки эти библиотеки из разных миров. Тогда уж нужно полностью сравнивать, приложения на Java+Spring+SpringJDBC vs Scala+Smth+ScalikeJDBC и те возможности, что предоставляют эти связки. А это уже на отдельную, причем холиварную, статью тянет.