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

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

529

Есть библиотека, облегчающая внедрение SQL в Scala-програмках , упоминания о которой на хабре я не отыскал. Эту несправедливость я и желал бы поправить. Речь пойдет о ScalikeJDBC.

Основным соперником SkalikeJDBC является Anorm – библиотека от Play, решающая ровно те же задачки комфортного общения с РСУБД средством незапятнанного (без примесей ORM) SQL. Плодами данной аппробации в виде маленького демо приложения я и буду делиться в данной статье, чуток ниже. Услышав о SkalikeJDBC я, фактически сходу, решил его опробовать. Ожидать, когда оно окажется затруднительным и для меня, я не стал. Но Anorm глубоко погряз в Play, и внедрение его в проектах не связанных с Play может быть затруднительным.

Перед тем, как перейти к примеру использования библиотеки, стоит увидеть, что поддерживается и протестированна работа со последующими СУБД:

PostgreSQL
MySQL
H2 Database Engine
HSQLDB

А оставшиеся (Oracle, MS SQL Server, DB2, Informix, SQLite, тысячи их) также должны работать, ибо все общение c СУБД идет через обычный JDBC. Но их тестирование не производитстя, что может навлечь угнетение на корпоративного заказчика.

Пример приложения
Осуществим короткое погружение в способности библиотеки. Вообщем оставим корпоративного заказчика наедине с его невеселыми думами, и лучше займемся тем, ради чего же и писалась эта статья.

Полное описание способностей доступно в комфортной и довольно недлинной документации, а так же в Wiki на github. Покажу, как можно его сконфигурировать с помощью Typesafe Config, сделать таблицу в БД, делать CRUD-запросы к данной таблице и преобразовывать результаты Read-запросов в Scala-объекты. Дальше я приведу пример обычного приложения, использующего SkalikeJDBC для доступа к Postgresql. Я буду намеренно упускать почти все варианты конфигурирования (без внедрения Typesafe Config) и внедрения библиотеки, чтоб остаться коротким и обеспечить стремительный старт.

Приложение будет применять 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 для конфигурирования соединения с СУБД

В ней уже имеется юзер pguser с паролем securepassword и полным доступом к базе данных demo_db. В качестве СУБД будем применять локальную Postgresql на обычном (5432) порту.

В этом случае создаем файл конфигурации 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"
}
}
Мы могли бы ограничиться первыми 4-мя параметрами, тогда применились бы опции пула соединений по-умолчанию.

Дальше сделаем пакет demo в папке src/main/scala, куда и поместим весь scala-код.

DemoApp.scala
Начнем с главенствующего запускаемого объекта:

package demo
import scalikejdbc.config.DBs
object DemoApp extends App {
DBs.setup(‘demo_db)
}
Единственная строка снутри объекта – указание считать опции доступа к базе demo_db из файлов конфигурации. Typesafe Config, по конвенции, автоматом читает application.conf находящийся в classpath приложения. Объект DBs будет находить все пригодные ключи конфигурации ( driver, url, user, password, …) в узле db.demo_db во всех файлах конфигурации прочитанных Typesafe Config.

Результатом будет сконфигурированный 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) – для запросов на чтение. Можно было бы обойтись и без их, но тогда нам везде приходилось бы писать: В (3) и (4) мы создаем комфортные нам обертки для выполнения SQL-запросов.

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
Это будет обычный case-класс, определяющий юзера системы с 3-мя говорящими полями: Перед тем, как приступить к созданию нашего DAO-класса, сделаем доменный объект с которым он будет работать.

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 объект готов. Для этого изменим сделанный нами в начале объект DemoApp. Приложение учебное – можем для себя дозволить. Остается лишь применить этот DAO объект. Удивительно, естественно, созидать в нем способ сотворения таблицы… Он был добавлен просто для примера.
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! habrahabr.ru