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 ^^