使用 Slick (Scala) 将记录插入 Db,实体的最佳实践

Insert record into Db using Slick (Scala), Best practices for an Entity

首先要说的是,我是 Scala 的新手,确实需要一些帮助。我需要建立一个网站 api,我会尝试将一条记录插入数据库,但在将实体 (db table) 映射到模型 (class) 时遇到一些问题.我使用 .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(它是 db table / 实体的映射器)

  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))
  }
  }

我如何看待这个问题:

现在在控制器中:

如果我尝试将记录插入数据库,使用当前方法,首先我需要从 table 获取所有行,然后将新记录添加到列表中。如果我在 table 中有 3 400 万条记录会怎样?将使所有这些行都无用,只插入一个新行。

然后,在插入这个新行后,我需要将它 return 到客户端,但我如何更新它(Id 每次都是 -1,但如果我得到整个列表以查看它是什么包含,我可以看到最新实体的正确 ID)

感谢

最后,我找到了一个很好的解决方案,post 在这里,也许有人需要这个:

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!")
    }
  }

和用户控制器:

  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")
  }

无论如何,我知道我有一些错误的代码块...尤其是在创建和更新方法中,其中将 json 转换为 User。

我想试一试,这里是 Play 2.7/Scala 2.13/Slick 4.0.2 REST-API 控制器绑定到 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)

这里没什么好想的,我们只是将路由绑定到即将到来的控制器中的函数。 请注意,第二个 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: 
  def tupled = (User.apply _).tupled

  // provides implicit json mapping
  implicit val format: OFormat[User] = Json.format[User]
}

我用了 uuid 而不是数字 id,但基本上是一样的。 注意,一个Json serializer/deserializer 可以写在一行中(你不需要用case 类 来详细说明)。我认为不重写它以生成代码中的 Seq 也是一个好习惯,因为在控制器上将对象转换为 Json 时,此序列化程序将非常有用。

现在 tupled 的定义很可能是一个 hack(见评论),稍后将需要 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. 基本上,我们从 routes 文件中引用的 5 个函数中的每一个都检查输入,然后将工作委托给注入的 UserService(稍后会详细介绍)

  2. 对于createupdate函数,你可以看到我们使用Play Forms,我认为也是一个很好的做法。他们的作用是验证传入的 Json,并将其编组为 User 类型。

  3. 此外,您可以看到我们使用 Action.async:Scala 提供了非常强大的 Futures 杠杆作用,所以让我们使用它吧!这样做基本上可以确保您的代码不会阻塞,从而降低硬件上的 IOPS。

  4. 最后对于 GET(一个)、GET(全部)、POST 和 PUT 的情况,因为我们 return 用户,并且有一个反序列化器,一个简单的 Json.toJson(user) 做作业。

在跳转到 service 和 dao 之前,让我们看一下表格,在 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 的情况下,我们会生成一个)。

现在,服务...在这种情况下不需要那么多,但实际上在 app/services/UserService.scala 中有一个额外的层(例如处理 acls)可能是一件好事:

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 的包装,最终是 app/dao/UserDao.scala:

中的 dao
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 示例代码,所以我想,我没有比他们更好的评论了......

希望,整个事情有助于获得更好的画面 :) 如果有什么不清楚的,请随时询问!