Помните, как мы учились писать сайтики при помощи 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 в своих проектах.
Вы согласны?