Repository Pattern

Trong bài viết này chúng ta sẽ cùng nhau tìm hiểu về Repository Pattern được sử dụng trong Scala. Ngoài ra trong bài viết chúng ta cũng sử dụng cả DAO pattern và Slick.

Thuật ngữ Repository Pattern rất quan trọng và sử dụng phổ biến trong DDD. Trong DDD, Model dùng để biểu diễn nghiệp vụ hơn là kỹ thuật.

Khi chúng ta phát triển một ứng dụng cho doanh nghiệp, với hàng trăm bảng (table) khác nhau, cách tốt nhất là nên dựa theo một phong cách đặc biệt để phát triển. Repository giúp chúng ta quản lý một số lượng lớn các hoạt động một cách dễ dàng. Nó cũng cung cấp một abstract dựa trên nhiều thứ như là nhận kết nối, filter các bản ghi…

Chúng ta đã sử dụng Repository Pattern trong một thời gian dài. Dưới đây là một số cách mà chúng tôi sử dụng Repository Pattern

  • Thống nhất trong Implement, đặc biệt hữu ích cho các ứng dụng phân lớp
  • Không cần viết code cho các hoạt động chung (giống nhau) giúp cho phát triển phần mềm dễ dàng hơn và nhanh hơn.
  • Cơ chế tập trung giúp kiểm soát tất cả các hoạt động chung
  • Dễ dàng thêm/xoá một chức năng chung mà không cần phải chỉnh sửa lại toàn bộ hay nhiều
  • Cung cấp một lớp Abstract tốt hơn

Nào sau đây chúng ta sẽ chứng minh làm thế nào để thực hiện các hoạt động chung tương tác với cơ sở dữ liệu. Chúng ta sẽ sử dụng Slick và PosgresSql.

Đầu tiên, chúng ta sẽ quyết định các phương thức / hoạt động nên cho phép trên mỗi bảng trong cơ sở dữ liệu.

Giả sử chúng ta cần có các phương thức hoạt động CRUD thống nhất. Các phương thức có thể như sau:

  • getAll: Lấy tất cả các bản ghi trong table
  • getById: Lấy bản ghi dựa theo ID được cung cấp
  • filter: Filter các bản ghi trong table dựa theo điều kiện được cung cấp
  • save: Chèn vào một bản ghi
  • updateById: Cập nhập một bản ghi theo ID được cung cấp
  • deleteById: Xoá bản ghi theo ID được cung cấp

Nào tiếp theo chúng ta sẽ xây dựng một class chung để thực hiện tất cả các phương thức bên trên

Đầu tiên chúng ta cần tạo một trait và abstract class cho Slick nơi mà tất cả các Entity của table cần thực thi.

trait BaseEntity {
  val id: Long
  val isDeleted: Boolean
}
 
abstract class BaseTable[E: ClassTag](tag: Tag, schemaName: Option[String], tableName: String)
  extends Table[E](tag, schemaName, tableName) {
  val classOfEntity = classTag[E].runtimeClass
  val id: Rep[Long] = column[Long]("Id", O.PrimaryKey, O.AutoInc)
  val isDeleted: Rep[Boolean] = column[Boolean]("IsDeleted", O.Default(false))
}

Trong ứng dụng, chúng ta chỉ thực hiện xoá logic, do đó chúng ta sẽ sử dụng cờ isDeleted với giá trị true hoặc false. Tất cả các bảng của chúng ta sẽ có 2 trường là idisDeleted

Nào bây giờ chúng ta có thể định nghĩa một trait chứa tất cả các phương thức sẵn sàng cho sử dụng

trait BaseRepositoryComponent[T <: BaseTable[E], E <: BaseEntity] {
  def getById(id: Long) : Future[Option[E]]
  def getAll : Future[Seq[E]]
  def filter[C <: Rep[_]](expr: T => C)(implicit wt: CanBeQueryCondition[C]): Future[Seq[E]]
  def save(row: E) : Future[E]
  def deleteById(id: Long) : Future[Int]
  def updateById(id: Long, row: E) : Future[Int]
}

Trong đoạn code ở trên, chúng ta có thể thấy rằng trait nhận 2 kiểu tham số là E và T, <: nghĩa là nhận tất cả các sub-type của BaseTableBaseEntity tương ứng.

Tiếp theo chúng ta định nghĩa một trait dùng để định nghĩa các các Query của Slick để xử lý các phương thức bên trên

trait BaseRepositoryQuery[T <: BaseTable[E], E <: BaseEntity] {
 
  val query: PostgresDriver.api.type#TableQuery[T]
 
  def getByIdQuery(id: Long) = {
    query.filter(_.id === id).filter(_.isDeleted === false)
  }
 
  def getAllQuery = {
    query.filter(_.isDeleted === false)
  }
 
  def filterQuery[C <: Rep[_]](expr: T => C)(implicit wt: CanBeQueryCondition[C]) = {
    query.filter(expr).filter(_.isDeleted === false)
  }
 
  def saveQuery(row: E) = {
    query returning query += row
  }
 
  def deleteByIdQuery(id: Long) = {
    query.filter(_.id === id).map(_.isDeleted).update(true)
  }
 
  def updateByIdQuery(id: Long, row: E) = {
    query.filter(_.id === id).filter(_.isDeleted === false).update(row)
  }
 
}

Trong BaseRepositoryQuery trait, tất cả các phương thức đều sử dụng điều kiện filter là isDeleted. Nó giúp đảm bảo chỉ các bản ghi chưa bị xoá được lấy ra và cập nhập.

Tiếp theo chúng ta định nghĩa một BaseRepository để thực thi tất cả các phương thức và chạy các query cần thiết.

abstract class BaseRepository[T <: BaseTable[E], E <: BaseEntity : ClassTag](clazz: TableQuery[T]) extends BaseRepositoryQuery[T, E] with BaseRepositoryComponent[T,E] {
 
  val clazzTable: TableQuery[T] = clazz
  lazy val clazzEntity = classTag[E].runtimeClass
  val query: PostgresDriver.api.type#TableQuery[T] = clazz
  val db: PostgresDriver.backend.DatabaseDef = DriverHelper.db
 
  def getAll: Future[Seq[E]] = {
    db.run(getAllQuery.result)
  }
 
  def getById(id: Long): Future[Option[E]] = {
    db.run(getByIdQuery(id).result.headOption)
  }
 
  def filter[C <: Rep[_]](expr: T => C)(implicit wt: CanBeQueryCondition[C]) = {
    db.run(filterQuery(expr).result)
  }
 
  def save(row: E) = {
    db.run(saveQuery(row))
  }
 
  def updateById(id: Long, row: E) = {
    db.run(updateByIdQuery(id, row))
  }
 
  def deleteById(id: Long) = {
    db.run(deleteByIdQuery(id))
  }
 
}

Đây chính là tất cả những gì mà chúng ta cần. Bây giờ chỉ cần extends BaseRepository, tất cả 6 phương thức luôn sẵn sàng được sử dụng tự động mà không cần viết thêm một tí code nào. Nếu bài toán yêu cầu xử lý nhiều hơn những gì BaseRepository đang cung cấp, chúng ta chỉ cần override và sử dụng code xử lý của chúng ta.

Tiếp theo hãy cùng xem làm thế nào để tạo một repository để xử lý các hoạt động trên bảng User

Chúng ta có một bảng User với các trường Id, UserName, Email, Password, IsDelete. Slick table được định nghĩa như User table.

User Repository sẽ được định nghĩa như sau:

class UserRepository extends BaseRepository[UserTable, User](TableQuery[UserTable])

Bây giờ thì chúng ta có thể tạo một object của UserRepository và truy cập toàn bộ 6 phương thức mà không cần viết thêm bất cứ dòng code nào.

Giả sử bạn không muốn để lộ thông tin password của user, chúng ta có thể xử lý như sau

class UserRepository extends BaseRepository[UserTable, User](TableQuery[UserTable]) {
  val empRepo = new EmployeeRepository
 
  override def getById(id: Long): Future[Option[User]] = {
    val superRes = super.getById(id)
    //remove the password field with some dummy data while sending back
    superRes.map(_.map(_.copy(password = "*****")))
  }
}

Nhưng chúng ta sẽ xử lý như thế nào, nếu muốn thêm một vài phương thức ngoài 6 phương thức hiện tại.

BaseRepositoryQuery giúp chúng ta giải quyết vấn đề này, nó cho phép chúng ta extends và thực hiện các chức năng riêng.

Giả sử chúng ta có 2 bảng User và Employee. Chúng ta cần nối 2 bảng lại với nhau để lấy toàn bộ thông tin chi tiết về user. Chúng ta có thể thực hiện như sau:

def getUserDetails: Future[Seq[UserDetails]] = {
    val joinQuery = getAllQuery.join(empRepo.getAllQuery).on(_.employeeId === _.id)
    val joinRes: Future[Seq[(User, Employee)]] = db.run(joinQuery.result)
    joinRes map { tupleList =>
      tupleList.map { tuple =>
        UserDetails(tuple._2.id, tuple._1.userName, tuple._2.firstName, tuple._2.lastName)
      }
    }
}

Tiếp theo, chúng ta sẽ làm gì nếu cần thực hiện workflow cho một vài table trong ứng dụng?

Chúng ta có thể định nghĩa thêm một lớp abstract repository mà có thể được thực hiện bởi các bảng yêu cầu chức năng trên.

abstract class WorkflowRepository[T <: WorkflowBaseTable[E], E <: WorkflowBaseEntity : ClassTag](clazz: TableQuery[T]) extends BaseRepository[T, E](clazz) {
  def approve(id:Long): Future[Int] = {
    db.run(getByIdQuery(id).map(_.isApproved).update(true))
  }
}

Bất kỳ Repository nào extends WorkflowRepository sẽ có được phương thức approve, mà có thể được sử dụng để xác thực một hoạt động.

 

You May Also Like

About the Author: Nguyen Dinh Thuc

Leave a Reply

Your email address will not be published.