Một example CRUD đơn giản : ActiveJ Framework (Phần 2)

phần đầu tiên, mình đã giới thiệu đôi nét về ActiveJ framework và tạo 1 project hello-world. Trong phần 2 này, mình sẽ sử dụng tiếp source code từ phần 1 để tạo ra các CRUD-API đơn giản.

LET’s START

Ngoài file Main, trong phần src, mình tạo thêm 4 package:

– controller: Chứa class controller để nhận request từ client
– model: Chứa class mô phỏng đối tượng nghiệp vụ
– repository: Chứa class persistence để thực hiện các thao tác lưu trữ, xóa, lấy đối tượng từ database
– service: Chứa class logic nghiệp vụ

Package Model

Giả sử đối tượng nghiệp vụ là User, có các thông tin cơ bản như sau: firstName, lastName, age
Ta có một mô phỏng như sau:

case class User (id: Long, firstName: String, lastName: String, age: Int)

object User {
  def apply(firstName: String, lastName: String, age: Int): User = {
    User(randomId, firstName, lastName, age)
  }

  private def randomId: Int = {
    val rd = new Random()
    rd.nextInt(5000)
  }
}

Sử dụng companion object (User) để tạo ra constructor thứ 2 phù hợp với việc tạo mới đối tượng.

Mỗi lần tạo mới một đối tượng user thì userId sẽ được tạo với giá trị ngẫu nhiên

Package Repository

Trong ví dụ này, để đơn giản hóa ta tạo 1 in-memory-storage thay thế cho database: ArrayBuffer[User]()
Ta có 1 số hàm CRUD như sau:

@Inject
class UserRepository {
  private val storage = ArrayBuffer[User]()

  def store(user: User): Try[Unit] = Try {
    val isExisted = storage.exists(_.id == user.id)

    if (isExisted) throw new Exception("User is existed")
    else storage += user
  }

  def update(user: User): Try[Unit] = Try {
    val userOpt = storage.find(_.id == user.id)

    if (userOpt.isDefined) {
      storage -= userOpt.get
      storage += user
    } else throw new Exception("User is not existed")
  }

  def getById(id: Long): Option[User] = storage.find(_.id == id)
  def getAll: Seq[User] = storage.toSeq

  def deleteById(id: Long): Try[Unit] = Try {
    storage.find(_.id == id)
      .map(user => storage -= user)
      .getOrElse(throw new Exception("User is not existed"))
  }
}

Phía trên tên class UserRepository ta cần đánh dấu @Inject để ActiveJ-DI tự động tạo 1 instance trong DI container, sau này ta có thể inject nó vào bất cứ đâu trong project

Package Service

Ta có class UserService

@Inject
class UserService {
  @Inject var userRepository: UserRepository = _

  def store(firstName: String, lastName: String, age: Int): Try[User] = {
    val user = User(firstName, lastName, age)
    userRepository.store(user).map(_ => user)
  }

  def getAll: Seq[UserDTO] = userRepository.getAll.map(user => {
     UserDTO(
       user.id,
       mergeName(user.firstName, user.lastName),
       user.age
    )
  })

  def getById(id: Long): Option[UserDTO] = userRepository.getById(id).map(user => {
     UserDTO(
       user.id,
       mergeName(user.firstName, user.lastName),
       user.age
    )
  })

  def deleteById(id: Long): Try[Unit] = userRepository.deleteById(id)
  def update(user: User): Try[Unit] = userRepository.update(user)

  private def mergeName(firstName: String, lastName: String): String = s"$firstName $lastName"
}

Trong class UserService ta khai báo inject đối tượng userRepository thông qua cú pháp:

@Inject var userRepository: UserRepository = _

+ Class trong Scala yêu cầu các field cần phải khai báo việc khởi tạo giá trị nên chúng ta sử dụng `= _` tương đương với `= null`
+ Active-DI sẽ thực hiện việc Inject thông qua reflection nên cần sử dụng `var` để set lại giá trị từ `null` -> instance

Package Controller

Ta có class UserDTO để đặc tả json response.
Trong object UserDTO, ta khai báo 1 implicit writer để thuận tiện cho việc convert class tới json

case class UserDTO (id: Long, name: String, age: Int)
object UserDTO {
  implicit val writer: OWrites[UserDTO] = Json.writes[UserDTO]
}

Ta tạo ra file UserController.scala để xử lý request từ client

Hàm save sẽ trích xuất thông tin: firstName, lastName, age từ request và sử dụng userService để tạo mới đối tượng User

def save(request: HttpRequest): Promise[HttpResponse] = {
  request
    .loadBody()
    .map(_ => new String(request.getBody.getArray))
    .map(bodyStr => Json.parse(bodyStr))
    .map { json =>
      val firstName = (json \ "firstName").as[String]
      val lastName = (json \ "lastName").as[String]
      val age = (json \ "age").as[Int]

      val result = userService.store(firstName, lastName, age)
      result match {
        case Success(user) => HttpResponse.ok200().withPlainText(s"User is stored with id: ${ user.id}")
        case Failure(ex) => HttpResponse.ofCode(400).withPlainText(ex.getMessage)
      }
   }
}

Các implement còn lại:

def getAll: HttpResponse = {
  val json = Json.toJson(userService.getAll)
  HttpResponse.ok200().withJson(json.toString())
}

def getById(request: HttpRequest): HttpResponse = {
  val id = request.getPathParameter("id").toInt

  userService.getById(id) match {
    case Some(user) => HttpResponse.ok200().withJson(Json.toJson(user).toString())
    case None => HttpResponse.ofCode(400).withPlainText("User is not existed")
  }
}

def deleteById(request: HttpRequest): HttpResponse = {
  val id = request.getPathParameter("id").toInt

  userService.deleteById(id) match {
    case Success(_) => HttpResponse.ok200().withPlainText("User is deleted")
    case Failure(ex) => HttpResponse.ofCode(400).withPlainText(ex.getMessage)
  }
}

File Main

Ta cập nhật lại file Main sau:

object Main extends HttpServerLauncher {
  @Provides
  def servlet(userController: UserController): AsyncServlet = {
    RoutingServlet.create
      .map(POST, "/users", (req: HttpRequest) => userController.save(req))
      .map(GET, "/users", (_: HttpRequest) => userController.getAll)
      .map(GET, "/users/:id", (req: HttpRequest) => userController.getById(req))
      .map(DELETE, "/users/:id", (req: HttpRequest) => userController.deleteById(req))
  }

  def main(args: Array[String]): Unit = {
    Main.launch(args)
  }
}

Ta thực hiện jnject đối tượng UserController vào Servlet Center, sau đó điều hướng request tới các hàm xử lý cụ thể

Test

Ở đây mình sử dụng Insomnia để làm client

Case 1: Tạo mới user:

Case 2: Lấy user theo id

Case 3: Lấy tất cả user đang có

Case 4: Xóa user theo id

Như vậy, chúng ta đã hoàn thành 1 sample CRUD đơn giản nhất.

Trong phần tiếp theo, ta sẽ tích hợp thêm phần tương tác database MySQL & migration tự động.

Toàn bộ source code ở đây. Cảm ơn bạn đã dành thời gian để đọc ^^

Add a Comment

Scroll Up