Squeryl - простота і витонченість

Добрий день, Хабр!
 
Вирішив написати невеликий огляд з прикладами на легковаговий ORM для Scala — Squeryl 0.9.5
 
Почнемо з основних переваг даного фреймворка
 
1) Squeryl надає DSL для SQL запитів. Наприклад
 
 
def songs =  from(MusicDb.songs)(s => where(s.artistId === id) select(s))

def fixArtistName = update(songs)(s =>
  where(s.title === "Prodigy")
  set(
    s.title := "The Prodigy",
  )
) 

 
Синтаксис нагадує C # LINQ. Як ви могли помітити в запитах використовуються лямбда виразу, що значно скорочує обсяг коду.
 
У даному прикладі метод songs повертає об'єкт Query [Song] який реалізує інтерфейс Iterable, що дозволяє працювати з ним як зі звичайною колекцією.
 
Також варто відзначити, що запити можна буде використовувати як підзапитів, для цього достатньо вказати запит в конструкції from замість таблиці.
 
2) Найпростіше опис моделей
 
 
class User(var id:Long, var username:String) extends KeyedEntity[Long]

object MySchema extends Schema{ 

  val userTable = table[User]

}

 
У даному прикладі ви описуємо модель з первинним ключем id типу Long і полем username типу String, якісь додаткові конфіги не потрібні. Після того як ми описали модель необхідно зареєструвати її в схемі.
 
За замовчуванням Squeryl використовує для імен таблиць імена класів і для імен полів імена властивостей класу.
Для явного зазначення назви таблиці можна використовувати:
 
 
val userTable = table[User]("USER_TABLE")

 
а для колонок можна використовувати атрибут @ Column
 
 
class User(var id:Long, @Column("USER_NAME") var username:String) extends KeyedEntity[Long]

 
Для складових ключів використовується типи CompositeKey2 [K1, K2], CompositeKey3 [K1, K2, K3] і тд, відповідно кількості полів у складеному ключі.
 
Для того щоб полі не зберігалося в БД достатньо позначити його анотацією Transient .
 
3) кастомними функції.
 
Squeryl містить в собі необхідний мінімум функцій для роботи з БД, цей набір можна легко доповнити.
 
Наприклад реалізуємо функцію date_trunc для PostgreSQL
 
 
class DateTrunc(span: String, e: DateExpression[Timestamp], m: OutMapper[Timestamp])
  extends FunctionNode[Timestamp](
    "date_trunc", Some(m), Seq(new TokenExpressionNode("'" + span + "'"), e)
  ) with DateExpression[Timestamp]

def dateTrunc(span: String, e: DateExpression[Timestamp])(implicit m: OutMapper[Timestamp]) = new DateTrunc(span, e, m)

 
Більш докладний опис ви можете знайти на офіційному сайті squeryl.org / getting-started.html
 
 

Ну що ж ближче до практики

 
Завдання
Для демонстрації роботи ORM напишемо невеликий додаток на Play Framework 2, яке надаватиме універсальний API для отримання об'єкта, збереження / створення об'єкта і видалення, за назвою класу і його ідентифікатором
 
В якості БД будемо використовувати PostgreSQL 9.3.
 
 
Інтеграція
Додаємо в build.sbt
 
 
"org.squeryl" %% "squeryl" % "0.9.5-7",
  "org.postgresql" % "postgresql" % "9.3-1101-jdbc41"

 
Додамо в conf / application.conf
 
 
db.default.driver = org.postgresql.Driver
db.default.url = "postgres://postgres:password@localhost/database"
db.default.logStatements = true
evolutionplugin = disabled

 
Створимо Global.scala в директорії app
 
 
import org.squeryl.adapters.PostgreSqlAdapter
import org.squeryl.{Session, SessionFactory}
import play.api.db.DB
import play.api.mvc.WithFilters
import play.api.{Application, GlobalSettings}

object Global extends GlobalSettings {
  override def onStart(app: Application) {
    SessionFactory.concreteFactory = Some(() => Session.create(DB.getConnection()(app), new PostgreSqlAdapter))
  }
}

 
Таким час запуску програми у нас инициализируется фабрика сесій з дефолтовая з'єднанням.
 
 
Моделі
Реалізуємо базовий трейт для моделей, який міститиме в собі поля id типу Long, created — час створення моделі в БД, updated — час останньої зміни, (можливо я викличу холлівар, але все ж) поле deleted типу Boolean, яке буде прапором видалений об'єкт чи ні, і при необхідності даний об'єкт можна буде відновити.
 
Також відразу реалізуємо функціонал для перетворення об'єкту в json, для цього скористаємося бібліотекою Gson, щоб додати її пропишете в build.sbt:
 
 

  "com.google.code.gson" % "gson" % "2.2.4"

 
Звичайно у Play Framework є вже вбудовані механізми для роботи з json, але на мій погляд вони мають недоліки, тому ми будемо комбінувати їх разом з Gson.
 
Для цього створимо app / models / Entity.scala
 
 
package models

import com.google.gson.Gson
import org.joda.time.DateTime
import org.squeryl.KeyedEntity
import play.api.libs.json.JsValue

trait EntityBase[K] extends KeyedEntity[K] {
  def table = findTablesFor(this).head

  def json(implicit gson: Gson): JsValue = play.api.libs.json.Json.parse(gson.toJson(this))

  def isNew: Boolean

  def save(): this.type = transaction {
    if (isNew) table.insert(this)
    else table.update(this)
    this
  }
}

trait EntityC[K] extends EntityBase[K] {
  var created: TimeStamp = null

  override def save(): this.type = {
    if (isNew) created = DateTime.now()
    super.save()
  }
}

trait EntityCUD[K] extends EntityC[K] {
  var updated: TimeStamp = null
  var deleted = false

  override def save(): this.type = {
    updated = DateTime.now()
    super.save()
  }

  def delete(): this.type = {
    deleted = true
    save()
  }
}

class Entity extends EntityCUD[Long] {
  var id = 0L

  override def isNew = id == 0L
}


 
У даному коді реалізовані кілька трейтов, які успадковується один від одного додаючи нову функціональність.
 
Основний концепт: метод save (), перевіряє збережений цей об'єкт у БД чи ні і залежно від цього викликається у відповідній йому таблиці викликається метод create або update.
 
Для зберігання часу Squeryl використовує тип java.sql.Timestamp, який для мене (і багато хто зі мною погодяться) дуже не зручна у використанні. Для роботи з часом я віддаю перевагу використовувати joda.DateTime. Благо Scala надає зручний механізм для неявних перетворень типів.
 
Створимо схему даних і набір корисних утиліт, для зручності створимо package object, для цього створюємо файл app / models / package.scala зі наступному кодом:
 
 
import java.sql.Timestamp

import com.google.gson.Gson
import org.joda.time.DateTime
import org.squeryl.customtypes._
import org.squeryl.{Schema, Table}
import play.api.libs.json.{JsObject, JsValue, Json}

import scala.language.implicitConversions

package object models extends Schema with CustomTypesMode {

  val logins = table[Login]

  def getTable[E <: Entity]()(implicit manifestT: Manifest[E]): Table[E]
  = tables.find(_.posoMetaData.clasz == manifestT.runtimeClass).get.asInstanceOf[Table[E]]

  def getTable(name: String): Table[_ <: Entity] = tables.find(_.posoMetaData.clasz.getSimpleName.toLowerCase == name)
    .get.asInstanceOf[Table[_ <: Entity]]

  def get[T <: Entity](id: Long)(implicit manifestT: Manifest[T]): Option[T] = getTable[T]().lookup(id).map(e => {
    if (e.deleted) None
    else Some(e)
  }).getOrElse(None)

  def get(table: String, id: Long): Option[Entity] = getTable(table).lookup(id).map(e => {
    if (e.deleted) None
    else Some(e)
  }).getOrElse(None)

  def getAll(table: String): Seq[Entity] = from(getTable(table))(e => select(e)).toSeq

  def save(table: String, json: String)(implicit gson: Gson) = gson.fromJson(
    json, getTable(table).posoMetaData.clasz
  ).save()

  def delete(table: String, id: Long) = get(table, id).map(_.delete())

  class TimeStamp(t: Timestamp) extends TimestampField(t)

  implicit def jodaToTimeStamp(dateTime: DateTime): TimeStamp = new TimeStamp(new Timestamp(dateTime.getMillis))

  implicit def timeStampToJoda(timeStamp: TimeStamp): DateTime = new DateTime(timeStamp.value.getTime)

  class Json(s: String) extends StringField(s)

  implicit def stringToJson(s: String): Json = new Json(s)

  implicit def jsonToString(json: Json): String = json.value

  implicit def jsValueToJson(jsValue: JsValue): Json = new Json(jsValue.toString())

  implicit def jsonToJsObject(json: Json): JsObject = Json.parse(json.value).asInstanceOf[JsObject]

  class ForeignKey[E <: Entity](l: Long) extends LongField(l) {
    private var _entity = Option.empty[E]

    def entity(implicit manifestT: Manifest[E]): E = _entity.getOrElse({
      val res = get[E](value).get
      _entity = Some(res)
      res
    })

    def entity_=(value: E) {
      _entity = Some(value)
    }
  }

  implicit def entityToForeignKey[E <: Entity](entity: E): ForeignKey[E] = {
    val fk = new ForeignKey[E](entity.id)
    fk.entity = entity
    fk
  }

  implicit def foreignKeyToEntity[T <: Entity](fk: ForeignKey[T])(implicit manifestT: Manifest[T]): T = fk.entity

  implicit def longToForeignKey[T <: Entity](l: Long)(implicit manifestT: Manifest[T]) = new ForeignKey[T](l)
}


 
Тут реалізовані основні методи для роботи з БД, створений свій клас для часу TimeStamp, свій клас для зберігання json в БД і свій клас для зовнішніх ключів з усіма необхідними неявними перетвореннями. Багато порахують код оверкиль, але відразу скажу в більшості завдань на практиці подібний код зовсім ні до чого, я прагнув продемонструвати вам який функціональністю володіє Squeryl.
 
І нарешті те напишемо модель Login з полем login, password і зовнішнім ключем на його запросила Login і не забудемо створити відповідну таблицю в БД з тестовими даними.
 
 
package models

class Login extends Entity {
  var login = ""
  var password = ""

  var parent: ForeignKey[Login] = null
}

 
 
Actions
Для того щоб виконати запит, необхідно поміщати код в inTransaction {} або transaction {}.
 
inTransaction {} додає запит до поточної транзакції.
 
transaction {} виконує код в рамках однієї транзакції.
 
Будемо вважати що один action відповідають одній транзакції і для того щоб не писати в кожному action блок transaction створимо DbAction у файлі app / controller / BaseController.scala
 
 

package controllers

import models._
import play.api.mvc._
import utils.Jsons

import scala.concurrent.Future
import scala.language.implicitConversions

trait BaseController extends Controller {
  implicit val gson = new Gson

  object DbAction extends ActionBuilder[Request] {
    override def invokeBlock[A](request: Request[A],
                                block: (Request[A]) => Future[Result]): Future[Result] = transaction {
      block(request)
    }
  }
}

 
Тут же ми вказали об'єкт gson, який буде використовуватися перетворення моделі у формат json /
 
Ну і нарешті, напишемо контролер для API, app / controllers / Api.scala
 
 
package controllers

import play.api.libs.json.Json
import play.api.mvc.Action

object Api extends BaseController {
  def get(cls: String, id: Long) = DbAction {
    Ok(models.get(cls, id).map(_.json).getOrElse(Json.obj()))
  }

  def save(cls: String) = DbAction{
    request => Ok(models.save(cls, request.form.getOrElse("data", "{}")).json)
  }

  def delete(cls: String, id: Long) = DbAction {
    Ok(models.delete(cls, id).map(_.json).getOrElse(Json.obj()))
  }

}


 
Додамо actions в Роут conf / routes
 
 
# Api

GET         /api/:cls/:id               controllers.Api.get(cls:String,id:Long)
POST        /api/save/:cls              controllers.Api.save(cls:String)
POST        /api/delete/:cls/:id        controllers.Api.delete(cls:String,id:Long)

 
І нарешті запускаємо:
 
 image
 
При тому ви можете прописати в url будь id, будь-який клас замість login і отримаєте у відповідь необхідний вам Json. При необхідності в моделях можна перевантажити метод json, для додавання / приховування будь-яких даних. Варто відзначити, що Gson НЕ серіалізуются колекції Scala, так що для цього доведеться скористатися перетвореннями в Java-колекції, або скористатися вбудованим в Play Framework механізмом для роботи з Json.
 
 
Підіб'ємо підсумок
Написаний код прекрасно демонструє широкі можливості Squeryl, але варто відзначити що для невеликих завдань зовсім необов'язково реалізовувати щось подібне, Squeryl зможе забезпечити вас повноцінною роботою з БД буквально за 5 рядків.
 
Головним на мій погляд недоліком є ​​відсутність механізму міграцій, максимум що може зробити Squeryl, так це видати поточний DDL.
 
Я не буду проводити порівняльний аналіз Squeryl з іншими ORM (принаймні в цій статті), але особисто для мене людини дуже ледачого і не бажає писати щось зайве при додаванні нових сутностей в БД, ця ORM підходить ідеально.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.