← На главную

Знакомьтесь – ORM для Scala под названием Slick

Помните, как мы учились писать сайтики при помощи Play Framework? Вы, конечно же, обратили внимание, что вопрос работы с какой-либо СУБД был оставлен в стороне. Пришло время исправить эту вопиющую несправедливость!

Чего-чего, а ORM в мире Scala хоть отбавляй. Притом, ситуация с этими ORM быстро меняется. В книжках по Play, вышедших всего лишь год или около того назад, рекомендуется использовать либо Anorm, либо Squeryl. При этом Anorm – это и не ORM вовсе, а просто такая обертка для написания запросов на обычном SQL. Вот Squeryl является полноценным ORM’ом. Еще есть какие-то Ebean и SORM. В общем, любой найдет себе ORM по вкусу. Однако на момент написания этих строк, конкретно для Scala и Play Framework, насколько я могу судить, Typesafe рекомендует использовать Slick.

Как я уже отмечал, в мире Scala вещи очень быстро меняются. Когда я изучал Slick, то натыкался на десятки устаревших туториалов, примеры из которых либо не компилировались, либо компилировались, но не работали. Также к моменту, когда вы будете читать эту заметку, на смену Slick может прийти более совершенный ORM. Поэтому заклинаю вас, прежде, чем следовать чему-то, описанному в этой заметке, внимательно изучите сайт Typesafe, а также спросите у знакомых программистов на Scala или на StackOverflow, чем в это время суток модно ходить в базы данных при программировании на Scala + Play.

Итак, чтобы прикрутить Slick к своему проекту, открываем build.sbt и прописываем в libraryDependencies следующие зависимости:

"joda-time" % "joda-time" % "2.4", "org.joda" % "joda-convert" % "1.6", "mysql" % "mysql-connector-java" % "5.1.32", "com.typesafe.slick" % "slick_2.11" % "2.1.0", "com.github.tototoshi" %% "slick-joda-mapper" % "1.2.0", "com.typesafe.play" %% "play-slick" % "0.8.0",

Здесь и далее предполагается, что в качестве СУБД вы выбрали либо MySQL, либо MariaDB. Если это не так, соответствующие коннекторы и тп нужно модифицировать очевидным способом. Примите во внимание, что к моменту, когда вы будете читать эту заметку, могут появиться более свежие версии библиотек.

Далее прописываем в application.conf:

slick.default="models.*" logger.scala.slick.jdbc.JdbcBackend.statement=DEBUG db.default.driver=com.mysql.jdbc.Driver db.default.url="jdbc:mysql://localhost/dbname?user=aaa&password=bbb"

Убедитесь, что после создания базы данных dbname вы не забыли сказать:

ALTER DATABASE ... DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;

Рассмотрим, как при помощи Slick определить схему базы данных для простенького форума. Создадим файл app/models/Tables.scala и напишем в нем:

package models import org.joda.time.DateTime import play.api.db.slick.Config.driver.simple._ import com.github.tototoshi.slick.JdbcJodaSupport._ object T { val users = TableQuery[UsersTable] val topics = TableQuery[TopicsTable] val comments = TableQuery[CommentsTable] } case class User(id: Long = 0, login: String, email: String, password: String, salt: String, created: DateTime) case class Topic(id: Long = 0, author: Long, title: String, text: String, created: DateTime) case class Comment(id: Long = 0, theme: Long, author: Long, text: String, created: DateTime) class UsersTable(tag: Tag) extends Table[User](tag, "users") { def id = column[Long]("id", O.PrimaryKey, O.AutoInc) def login = column[String]("login", O.NotNull) def email = column[String]("email", O.NotNull) def password = column[String]("password", O.NotNull) def salt = column[String]("salt", O.NotNull) def created = column[DateTime]("created", O.NotNull, O.DBType("datetime")) def * = (id, login, email, password, salt, created) <> (User.tupled, User.unapply) def login_idx = index("users_login_idx", login, unique = true) def email_idx = index("users_email_idx", email, unique = true) } class TopicsTable(tag: Tag) extends Table[Topic](tag, "topics") { def id = column[Long]("id", O.PrimaryKey, O.AutoInc) def author = column[Long]("author", O.NotNull) def title = column[String]("title", O.NotNull) def text = column[String]("text", O.NotNull, O.DBType("text")) def created = column[DateTime]("created", O.NotNull, O.DBType("datetime")) def * = (id, author, title, text, created) <> (Topic.tupled, Topic.unapply) def author_fk = foreignKey("topics_author_fk", author, T.users)( _.id, onUpdate=ForeignKeyAction.Restrict, onDelete=ForeignKeyAction.Cascade) def created_idx = index("topics_created_idx", created) } class CommentsTable(tag: Tag) extends Table[Comment](tag, "comments") { def id = column[Long]("id", O.PrimaryKey, O.AutoInc) def topic = column[Long]("topic", O.NotNull) def author = column[Long]("author") def text = column[String]("text", O.NotNull, O.DBType("text")) def created = column[DateTime]("created", O.NotNull, O.DBType("datetime")) def * = (id, topic, author, text, created) <> (Comment.tupled, Comment.unapply) def author_fk = foreignKey("comments_author_fk", author, T.users)( _.id, onUpdate=ForeignKeyAction.Restrict, onDelete=ForeignKeyAction.Cascade) def theme_fk = foreignKey("comments_topic_fk", topic, T.topics)( _.id, onUpdate=ForeignKeyAction.Restrict, onDelete=ForeignKeyAction.Cascade) def created_idx = index("comments_topic_created_idx", (topic, created)) }

Как видите, Slick предоставляет мощный DSL для определения таблиц и связей между ними. Все, что можно сделать с помощью CREATE TABLE ... можно сделать и здесь. При первом запуске приложения Slick автоматически создаст все таблицы, никакой предварительной инициализации не требуется.

Чтобы делать запросы к БД, в контроллере прописываем следующие импорты:

import models._ import play.api.db.slick._ import org.joda.time._ import scala.slick.driver.MySQLDriver.simple._ import com.github.tototoshi.slick.MySQLJodaSupport._

Вместо какого-нибудь:

def page(page: Long) = Action { // ... }

… пишем:

def page(page: Long) = DBAction { implicit rs => // ... }

Теперь рассмотрим некоторые примеры запросов.

Получение числа топиков в БД:

// SELECT count(*) ... val res : Int = T.topics.length.run

Получение списка топиков на заданной странице с информацией об их авторах:

// SELECT ... LEFT JOIN ... ORDER BY ... LIMIT ... val topics = T.topics.sortBy(_.created.desc) .drop((page - 1)*topicsPerPage) .take(topicsPerPage) val topicsSeq = (topics leftJoin T.users on (_.author === _.id)).map { case (t, u) => (t.id, t.title, u.login, t.created) }.list

Получение информации об одном топике по topicId:

// SELECT ... LEFT JOIN ... WHERE ... (T.topics.filter(_.id === topicId) leftJoin T.users on (_.author === _.id)).map { case (t, u) => (t.id, t.title, u.login, t.text, t.created) }.run.headOption

Выборка комментариев к соответствующему топику:

// SELECT ... LEFT JOIN ... WHERE ... val commTmp = T.comments.filter(_.topic === topicId) .sortBy(_.created.asc) val comments = (commTmp leftJoin T.users on (_.author === _.id)).map { case (c, u) => (c.id, c.text, u.login, c.created) }.list

Наконец, для сильных духом, пример с UNION и GROUP BY:

def sitemapXml() = DBAction { implicit rs => val indexUpdateTime = T.topics.map(_.created) .max.run.getOrElse(new DateTime()) val topics1 = T.topics.map { r => (r.id, r.created) } val topics2 = T.comments.map { r => (r.topic, r.created) } val allTopics = topics1 union topics2 val groupedTopics = allTopics.groupBy(t => t._1).map { case (topicId, group) => (topicId, group.map(_._2).max) } // где-то при группировке Slick превращает created из DateTime // в Option[DateTime], поэтому здесь сделан map val sortedPagesInfo = groupedTopics.sortBy(_._2.desc).take(50) .list.map { case (topicId, optCreated) => (topicId, optCreated.get) } Ok(views.xml.sitemap(indexUpdateTime, sortedPagesInfo)) }

Хорошо, мы убедились в выразительной мощи Slick. Но что же на счет эффективности? С одной стороны, генерируемые запросы получаются не очень похожими на те, что были бы написаны руками. Все запросы будут выводиться в консоль во время работы приложения. Для этого ранее мы и прописали в конфиг строчку:

logger.scala.slick.jdbc.JdbcBackend.statement=DEBUG

Запросы, несмотря на то, что выглядят они страшно, выполняются вполне себе быстро. Желательно, конечно, чтобы приложение писало, скажем, в Graphite, запросы для генерации каких страниц сколько времени выполняются. Ну или хотя бы чтобы в СУБД был включен лог медленных запросов.

Больше примеров и информации по Slick вы можете найти в его официальной документации.

ORM вообще, и Slick в частности, дают программисту кучу удобняшек, в том числе строгую статическую типизацию, по сути – кучу готовых функций типа createUser, updateComment и так далее, а также автоматические миграции схемы БД. Платить за это приходится тем, что некоторые запросы могут выполняться менее эффективно, чем если бы мы написали их руками. Но специально для этого случая в Slick и предусмотрена возможность писать запросы вручную. Как по мне, даже при самом худшем раскладе, как минимум, мы абсолютно ничего не теряем. А значит нет никаких причин не использовать ORM в своих проектах.

Вы согласны?