[Из песочницы] 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 подходит идеально.

© Habrahabr.ru