Search code examples
mysqldatabasescalaslick

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


First to say, I'm newcomer in Scala and really need a little help. I need to build a web api, and I'll try to insert one record into database, but have some problems with mapping the entity (db table) into a model (class). I worked with .Net Core Web API (there I used Entity Framework Core, here in Scala use Slick) and try to keep same arhitecture in Scala, but need some more informations, because on the internet I find a lot of versions, and can not choose the best. As database, MySQL is used.

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 (which is a mapper of db table / entity)

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

How I see the problem:

  • UserDef is an Entity and must remain clean, only table columns definitions

  • UserMap is the bridge between User class and UserDef (entity), can be used as a repository with crud methods (getAll(), getById(id), create(user), update(user), delete(id)). This is in same file as User class, but probably must be moved in another.

  • User class is the model and need to contain only their parameters and writes/reads (Scala specifics)

and now in the controller:

If I try to insert a record into database, with current method, first I need to get all rows from table, and then to add the new record in the list. What happening if I have 3 4mil records in this table? Will get all these rows useless to insert only a new row.

Then, after inserting this new row, I need to return it into client, but how I can get it updated (Id is every time -1, but if I get entire list to see what it contain, I can see the correct id for the newest entity)

thx


Solution

  • Finally, I found a good solution and post it here, maybe somebody need this:

    UserMap, for me at least will become UserRepository. There I have CRUD operations and maybe some extra :

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

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

    Anyway, I know I have some bad blocks of code there...especially in create & update methods, where convert json to User.