【问题标题】:Insert record into Db using Slick (Scala), Best practices for an Entity使用 Slick (Scala) 将记录插入 Db,实体的最佳实践
【发布时间】:2019-09-24 10:35:30
【问题描述】:

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

  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(实体)之间的桥梁,可以用作带有 crud 方法(getAll()、getById(id)、create(user)、update(user)、delete( ID))。这与 User 类在同一个文件中,但可能必须移到另一个文件中。

  • 用户类是模型,只需要包含它们的参数和写入/读取(Scala 特性)

现在在控制器中:

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

然后,在插入这个新行之后,我需要将它返回给客户端,但是我如何才能更新它(ID 每次都是 -1,但是如果我得到整个列表来查看它包含的内容,我可以看到最新实体的正确 ID)

谢谢

【问题讨论】:

  • 为什么需要从表中获取所有行来插入新记录? users += user 只进行插入,不获取任何行。
  • 要获取插入的行id,可以在insert上使用returning子句,见stackoverflow.com/a/31448129/14955
  • UserMap 是个好东西。为什么你的控制器中有db.run 代码而不是在UserMap 中调用add 方法?为什么add 返回一个Seq[User] 而不仅仅是新添加的单个User 或其新ID?

标签: mysql database scala slick


【解决方案1】:

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

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 转换为用户。

【讨论】:

  • 是的,好的开始!熟悉 Scala 和 Slick 后,请查看如何使用 Action.async 摆脱 Await.result 代码。
  • 谢谢。是的,我需要在另外两种方法(更新/删除)中实现异步等待并处理错误
【解决方案2】:

我想试一试,这里是绑定到 MySQL 数据库的 Play 2.7/Scala 2.13/Slick 4.0.2 REST-API 控制器的完整工作示例。

由于您是从 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: 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 代替数字ID,但基本上是相同的。 请注意,一个 Json 序列化器/反序列化器可以只用一行编写(您不需要用案例类详细说明它)。我认为不重写它以生成代码中的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. 1234563

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-08-21
    • 2015-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多