Вставьте запись в Db, используя Slick (Scala), Лучшие практики для Entity - PullRequest
0 голосов
/ 24 сентября 2019

Во-первых, я новичок в Scala и действительно нуждаюсь в небольшой помощи.Мне нужно создать веб-API, и я попытаюсь вставить одну запись в базу данных, но у меня есть некоторые проблемы с отображением сущности (таблицы БД) в модель (класс).Я работал с .Net Core Web API (там я использовал Entity Framework Core, здесь в Scala использую Slick) и стараюсь сохранить ту же архитектуру в Scala, но мне нужно больше информации, потому что в Интернете я нахожу много версий и могуне выбирай лучшее.В качестве базы данных используется MySQL.

User.scala

        case class User(
                     id: Int = 0,
                     userName: String,
                     firstName: String,
                     lastName: String
                   ) {
      override def equals(that: Any): Boolean = true
    }

    object User {    
      implicit object UserFormat extends Format[User] {
        def writes(user: User): JsValue = {
          val userSeq = Seq(
            "id" -> JsNumber(user.id),
            "userName" -> JsString(user.userName),
            "firstName" -> JsString(user.firstName),
            "lastName" -> JsString(user.lastName)
          )
          JsObject(userSeq)
        }

        def reads(json: JsValue): JsResult[User] = {    
          JsSuccess(User(
            (json \ "id").as[Int].value,
            (json \ "userName").as[String].value,
            (json \ "firstName").as[String].value,
            (json \ "lastName").as[String].value)
          )
        }
      }

      def tupled = (this.apply _).tupled
    }

class UserMap @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)(implicit ex: ExecutionContext) {
  val dbConfig: DatabaseConfig[JdbcProfile] = dbConfigProvider.get[JdbcProfile]
  val db: JdbcBackend#DatabaseDef = dbConfig.db
  val dbUsers = TableQuery[UserDef]

  def getAll(): Unit = {
    val action = sql"SELECT Id, UserName, FirstName, LastName FROM Users".as[(Int, String, String, String)]
    return db.run(action)
  }

  def add(user: User): Future[Seq[User]] = {
    dbUsers += user
    db.run(dbUsers.result)
  }
}

UserDef.scala (который является отображением таблицы / сущности БД)

  class UserDef(tag: Tag) extends Table[User](tag, "Users") {
  def id = column[Int]("Id", O.PrimaryKey, O.AutoInc)
  def userName = column[String]("UserName")
  def firstName = column[String]("FirstName")
  def lastName = column[String]("LastName")

  override def * = (id, userName, firstName, lastName) <> (create, extract)

  def create(user: (Int, String, String, String)): User = User(user._1, user._2, user._3, user._4)
  def extract(user: User): Option[(Int, String, String, String)] = Some((user.id, user.userName,user.firstName,user.lastName))
}

UsersController.scala

    def createUser = Action(parse.json) { implicit request => {
    val userJson = request.body

    var user = new User(
      -1,
      (userJson \ "userName").as[String].value,
      (userJson \ "firstName").as[String].value,
      (userJson \ "lastName").as[String].value
    )

    var users = TableQuery[UserDef]
    Await.result(db.run(DBIO.seq(
      users += user,
      users.result.map(println))), Duration.Inf
    )

    Ok(Json.toJson(user))
  }
  }

Как я вижу проблему:

  • UserDef является сущностьюи должен оставаться чистым, только определения столбцов таблицы

  • UserMap - это мост между классом User и UserDef (entity), может использоваться как хранилище с методами crud (getAll (), getById (id), создать (пользователь), обновить (пользователь), удалить (id)).Он находится в том же файле, что и пользовательский класс, но, вероятно, должен быть перемещен в другой.

  • Пользовательский класс - это модель, которая должна содержать только свои параметры и записи / чтения (особенности Scala)

и теперь в контроллере:

Если я пытаюсь вставить запись в базу данных, используя текущий метод, сначала мне нужно получить все строки из таблицы, а затемдобавить новую запись в список.Что произойдет, если в этой таблице будет 3 записи по 4 миллиона?Получит все эти строки бесполезно, чтобы вставить только новую строку.

Затем, после вставки этой новой строки, мне нужно вернуть ее в клиент, но как я могу обновить его (Id каждый раз -1,но если я получу весь список, чтобы увидеть, что он содержит, я смогу увидеть правильный идентификатор для самой новой сущности)

thx

Ответы [ 2 ]

1 голос
/ 24 сентября 2019

Наконец-то я нашел хорошее решение и выложил его сюда, может кому-нибудь понадобится это:

UserMap , для меня по крайней мере станет UserRepository.Там у меня есть операции CRUD и, возможно, некоторые дополнительные:

  def getAll(): Future[Seq[User]] = {
    db.run(dbUsers.result)
  }

  def getById(id: Int): Future[Option[User]] ={
    val action = dbUsers.filter(_.id === id).result.headOption
    db.run(action)
  }

  def create(user: User): Future[User] = {
    val insertQuery = dbUsers returning dbUsers.map(_.id) into ((x, id) => x.copy(id = id))
    val action = insertQuery += user
    db.run(action)
  }

  def update(user: User) {
    Try( dbUsers.filter(_.id === user.id).update(user)) match {
      case Success(response) => db.run(response)
      case Failure(_) => println("An error occurred!")
    }
  }

  def delete(id: Int) {
    Try( dbUsers.filter(_.id === id).delete) match {
      case Success(response) => db.run(response)
      case Failure(_) => println("An error occurred!")
    }
  }

и UsersController:

  def getAll() = Action {
    var users = Await.result(usersRepository.getAll(), Duration.Inf)
    Ok(Json.toJson(users))
  }

  def getById(id: Int) = Action { implicit request => {
    val user = Await.result(usersRepository.getById(id), Duration.Inf)

    Ok(Json.toJson(user))
    }
  }

  def create = Action(parse.json) { implicit request => {
    val userJson = request.body

    var user = new User(
      -1,
      (userJson \ "userName").as[String].value,
      (userJson \ "firstName").as[String].value,
      (userJson \ "lastName").as[String].value
    )
    var createdUser = Await.result(usersRepository.create((user)), Duration.Inf)
    Ok(Json.toJson(createdUser))
    }
  }

  def update(id: Int) = Action(parse.json) { implicit request => {
    val userJson = request.body

    var user = new User(
      (userJson \ "id").as[Int].value,
      (userJson \ "userName").as[String].value,
      (userJson \ "firstName").as[String].value,
      (userJson \ "lastName").as[String].value
    )

    var updatedUser = usersRepository.update(user)
    Ok(Json.toJson(user))
    }
  }

  def delete(id: Int) = Action {
    usersRepository.delete(id)
    Ok("true")
  }

Во всяком случае, я знаю, что у меня есть некоторые плохие блоки кода ... особенно в create &обновить методы, где конвертировать JSON в пользователя.

0 голосов
/ 24 сентября 2019

Я хотел попробовать, и вот полный рабочий пример контроллера 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
            }
          }
      })
  }
}

Итакздесь:

  1. в основном, каждая из наших 5 функций ссылается на входные данные проверки файла routes, а затем делегирует работу введенному UserService (подробнее об этом позже)

  2. для функций create и update, вы можете видеть, что мы используем Play Forms , что я считаю также хорошей практикой.Их роль заключается в проверке входящего Json и превращении его в Marshall типа User.

  3. Кроме того, вы можете видеть, что мы используем Action.async: Scala предлагает очень мощный рычагс Futures так что давайте использовать его!Таким образом, вы гарантируете, что ваш код не является блокирующим, что упрощает IOPS на вашем оборудовании.

  4. Наконец, для случая 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, так что, думаю, у меня нет лучшего комментария, чем их ...

Надеюсь, все это помогает улучшить картину :) Если что-то неясно, не стесняйтесь спрашивать!

...