Знакомьтесь — ORM для Scala под названием Slick
24 ноября 2014
Помните, как мы учились писать сайтики при помощи 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 следующие зависимости:
"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:
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 вы не забыли сказать:
Рассмотрим, как при помощи Slick определить схему базы данных для простенького форума. Создадим файл app/models/Tables.scala и напишем в нем:
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 play.api.db.slick._
import org.joda.time._
import scala.slick.driver.MySQLDriver.simple._
import com.github.tototoshi.slick.MySQLJodaSupport._
Вместо какого-нибудь:
// ...
}
… пишем:
// ...
}
Теперь рассмотрим некоторые примеры запросов.
Получение числа топиков в БД:
val res : Int = T.topics.length.run
Получение списка топиков на заданной странице с информацией об их авторах:
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:
(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
Выборка комментариев к соответствующему топику:
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:
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. Но что же на счет эффективности? С одной стороны, генерируемые запросы получаются не очень похожими на те, что были бы написаны руками. Все запросы будут выводиться в консоль во время работы приложения. Для этого ранее мы и прописали в конфиг строчку:
Запросы, несмотря на то, что выглядят они страшно, выполняются вполне себе быстро. Желательно, конечно, чтобы приложение писало, скажем, в Graphite, запросы для генерации каких страниц сколько времени выполняются. Ну или хотя бы чтобы в СУБД был включен лог медленных запросов.
Больше примеров и информации по Slick вы можете найти в его официальной документации.
ORM вообще, и Slick в частности, дают программисту кучу удобняшек, в том числе строгую статическую типизацию, по сути — кучу готовых функций типа createUser, updateComment и так далее, а также автоматические миграции схемы БД. Платить за это приходится тем, что некоторые запросы могут выполняться менее эффективно, чем если бы мы написали их руками. Но специально для этого случая в Slick и предусмотрена возможность писать запросы вручную. Как по мне, даже при самом худшем раскладе, как минимум, мы абсолютно ничего не теряем. А значит нет никаких причин не использовать ORM в своих проектах.
Вы согласны?
Метки: Scala, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.