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 подходит идеально.
Поделиться публикацией

Похожие публикации

Комментарии 10

    0
    Мне не понравился подход, который используется для описания сущностей — изменяемые структуры, какой-то лишний trait на сущность.
    Я считаю более удачный подход, который используется в slick — case classes, описание структуры таблицы отдельно от сущности. Это проще для восприятия и лишено side effects, так как возможно использование модели где угодно.

    Еще Ваше решение ну очень слабо тестируемо, так как package object models не замокаешь.
      0
      В Squeryl также можно использовать case classes. На счет описания, а вот вы попробуйте в рантайме получить список таблиц с алиасами и хотя бы список полей с типами и алиасами. А задача банальна — дать возможность через API получать только нужные поля и устанавливать любые фильтры на эти поля. В JPA это очень просто, в Squeryl это чуточку сложнее, в Slick это костыли
        0
        Если даже не касаться вопроса тестирования, то тот package object, кхм… попахивает.
        0
        Главным на мой взгляд недостатком является отсутствие механизма миграций, максимум что может сделать Squeryl, так это выдать текущий DDL.

        Разработчик библиотеки писал по этому поводу уже, и мне кажется он полностью прав. Для этой задачи есть специализированные библиотеки, которые прекрасно справляются со своей задачей
        https://groups.google.com/forum/#!searchin/squeryl/migration/squeryl/m9ruq6Z1j7A/N_9pxwF-kkUJ
          +2
          Где-то полгода назад отказался от использования squeryl. В нем очень не хватает возможности писать sql руками.
            0
            Сильно не хватает, особенно с учётом того, что она там есть :) Вот, например, вариант: gist.github.com/max-l/9250053

            Вообще Squeryl удивительно логичен внутри. Его можно расширять и видоизменять под любой вариант, практически. Первый ORM, с которым я не чувствую себя ограниченным.
              0
              спасибо за пример, хотя у меня не было необходимости писать SQL руками, думаю пригодится
                0
                О, спасибо огромное. Я этот способ не смог найти/придумать.

                Однако, не покидает ощущение костыльности происходящего. Да и в любом случае, 75 строчек вспомогательного кода ради данного функционала явный перебор. Хочется «просто взять, и выполнить запрос».
                  0
                  костыльности, как таковой и нет, скорее это просто дополнение для расширения функционала squeryl
                0
                Что то я не смог понять что вам мешает писать на простом jdbc внутри транзакции, если уж так надо писать «sql руками»?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое