Я хотел попробовать, и вот полный рабочий пример контроллера REST-API Play 2.7 / Scala 2.13 / Slick 4.0.2, привязанного к базе данных MySQL.
Так как вы начинаетесо Scala, может быть, сначала немного сложно с Play, Slick и т. д. ...
Итак, вот скромный скелет (полученный из Play-Slick GitHub )
Итак, во-первых, поскольку мы хотим написать API, вот файл conf/routes
:
GET /users controllers.UserController.list()
GET /users/:uuid controllers.UserController.get(uuid: String)
POST /users controllers.UserController.create()
PUT /users controllers.UserController.update()
DELETE /users/:uuid controllers.UserController.delete(uuid: String)
Ничего особенного здесь нет, мы просто привязываем маршруты к функциям в предстоящем контроллере.Просто обратите внимание, что 2nd GET и DELETE ожидают UUID
в качестве параметра запроса, в то время как тела Json будут использоваться для POST и PUT.
Было бы неплохо увидеть модель прямо сейчас, в app/models/User.scala
:
package models
import java.util.UUID
import play.api.libs.json.{Json, OFormat}
case class User(
uuid: UUID,
username: String,
firstName: String,
lastName: String
) {
}
object User {
// this is because defining a companion object shadows the case class function tupled
// see: https://stackoverflow.com/questions/22367092/using-tupled-method-when-companion-object-is-in-class
def tupled = (User.apply _).tupled
// provides implicit json mapping
implicit val format: OFormat[User] = Json.format[User]
}
Я использовал uuid
вместо числового идентификатора, но в основном это то же самое.Обратите внимание, что сериализатор / десериализатор Json может быть записан в одну строку (вам не нужно детализировать его с помощью case-классов).Я думаю, что это также хорошая практика - не переопределять его для получения Seq
, как указано в вашем коде, поскольку этот сериализатор будет очень полезен при преобразовании объектов в Json на контроллере.
Теперь tupled
определение, скорее всего, взломать (см. комментарий), которое потребуется позже на DAO
...
Далее, нам нужен контроллер в app/controllers/UserController.scala
:
package controllers
import java.util.UUID
import forms.UserForm
import javax.inject.Inject
import play.api.Logger
import play.api.data.Form
import play.api.i18n.I18nSupport
import play.api.libs.json.Json
import play.api.mvc._
import services.UserService
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
class UserController @Inject()(userService: UserService)
(implicit ec: ExecutionContext) extends InjectedController with I18nSupport {
lazy val logger: Logger = Logger(getClass)
def create: Action[AnyContent] = Action.async { implicit request =>
withFormErrorHandling(UserForm.create, "create failed") { user =>
userService
.create(user)
.map(user => Created(Json.toJson(user)))
}
}
def update: Action[AnyContent] = Action.async { implicit request =>
withFormErrorHandling(UserForm.create, "update failed") { user =>
userService
.update(user)
.map(user => Ok(Json.toJson(user)))
}
}
def list: Action[AnyContent] = Action.async { implicit request =>
userService
.getAll()
.map(users => Ok(Json.toJson(users)))
}
def get(uuid: String): Action[AnyContent] = Action.async { implicit request =>
Try(UUID.fromString(uuid)) match {
case Success(uuid) =>
userService
.get(uuid)
.map(maybeUser => Ok(Json.toJson(maybeUser)))
case Failure(_) => Future.successful(BadRequest(""))
}
}
def delete(uuid: String): Action[AnyContent] = Action.async {
Try(UUID.fromString(uuid)) match {
case Success(uuid) =>
userService
.delete(uuid)
.map(_ => Ok(""))
case Failure(_) => Future.successful(BadRequest(""))
}
}
private def withFormErrorHandling[A](form: Form[A], onFailureMessage: String)
(block: A => Future[Result])
(implicit request: Request[AnyContent]): Future[Result] = {
form.bindFromRequest.fold(
errors => {
Future.successful(BadRequest(errors.errorsAsJson))
}, {
model =>
Try(block(model)) match {
case Failure(e) => {
logger.error(onFailureMessage, e)
Future.successful(InternalServerError)
}
case Success(eventualResult) => eventualResult.recover {
case e =>
logger.error(onFailureMessage, e)
InternalServerError
}
}
})
}
}
Итакздесь:
в основном, каждая из наших 5 функций ссылается на входные данные проверки файла routes
, а затем делегирует работу введенному UserService
(подробнее об этом позже)
для функций create
и update
, вы можете видеть, что мы используем Play Forms , что я считаю также хорошей практикой.Их роль заключается в проверке входящего Json и превращении его в Marshall типа User
.
Кроме того, вы можете видеть, что мы используем Action.async
: Scala предлагает очень мощный рычагс Futures
так что давайте использовать его!Таким образом, вы гарантируете, что ваш код не является блокирующим, что упрощает IOPS на вашем оборудовании.
Наконец, для случая GET (один), GET (все),POST и PUT, так как мы возвращаем пользователей и имеем десерализатор, простой Json.toJson(user)
делает всю работу.
Прежде чем перейти к службе и дао, давайте посмотрим на форму в app/forms/UserForm.scala
:
package forms
import java.util.UUID
import models.User
import play.api.data.Form
import play.api.data.Forms.{mapping, nonEmptyText, _}
object UserForm {
def create: Form[User] = Form(
mapping(
"uuid" -> default(uuid, UUID.randomUUID()),
"username" -> nonEmptyText,
"firstName" -> nonEmptyText,
"lastName" -> nonEmptyText,
)(User.apply)(User.unapply)
)
}
Ничего особенного, как говорит док, хотяесть только хитрость: когда uuid не определен (в случае POST, то мы его сгенерируем).
Теперь служба ... не так уж и требуется в этом самом случае, но на практике это можетбыло бы неплохо иметь дополнительный слой (например, для работы с acls), в app/services/UserService.scala
:
package services
import java.util.UUID
import dao.UserDAO
import javax.inject.Inject
import models.User
import scala.concurrent.{ExecutionContext, Future}
class UserService @Inject()(dao: UserDAO)(implicit ex: ExecutionContext) {
def get(uuid: UUID): Future[Option[User]] = {
dao.get(uuid)
}
def getAll(): Future[Seq[User]] = {
dao.all()
}
def create(user: User): Future[User] = {
dao.insert(user)
}
def update(user: User): Future[User] = {
dao.update(user)
}
def delete(uuid: UUID): Future[Unit] = {
dao.delete(uuid)
}
}
Как вы можете видеть, здесь это всего лишь обертка вокруг дао, и, наконец,dao in app/dao/UserDao.scala
:
package dao
import java.util.UUID
import javax.inject.Inject
import models.User
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import play.db.NamedDatabase
import slick.jdbc.JdbcProfile
import scala.concurrent.{ExecutionContext, Future}
class UserDAO @Inject()(@NamedDatabase("mydb") protected val dbConfigProvider: DatabaseConfigProvider)(implicit executionContext: ExecutionContext) extends HasDatabaseConfigProvider[JdbcProfile] {
import profile.api._
private val users = TableQuery[UserTable]
def all(): Future[Seq[User]] = db.run(users.result)
def get(uuid: UUID): Future[Option[User]] = {
db.run(users.filter(_.uuid === uuid).result.headOption)
}
def insert(user: User): Future[User] = {
db.run(users += user).map(_ => user)
}
def update(user: User): Future[User] = {
db.run(users.filter(_.uuid === user.uuid).update(user)).map(_ => user)
}
def delete(uuid: UUID): Future[Unit] = {
db.run(users.filter(_.uuid === uuid).delete).map(_ => ())
}
private class UserTable(tag: Tag) extends Table[User](tag, "users") {
def uuid = column[UUID]("uuid", O.PrimaryKey)
def username = column[String]("username")
def firstName = column[String]("firstName")
def lastName = column[String]("lastName")
def * = (uuid, username, firstName, lastName) <> (User.tupled, User.unapply)
}
}
Итак, здесь я только что адаптировал код из официального примера play-slick, так что, думаю, у меня нет лучшего комментария, чем их ...
Надеюсь, все это помогает улучшить картину :) Если что-то неясно, не стесняйтесь спрашивать!