使用 slick 聚合根实现

Aggregate root implementation with slick

我正在尝试在 slick 中实现一个简单的聚合根。 但我真的不知道什么是最好的方法。

这是我的域对象:

case class Project(id: UUID,
               name: String,
               state: ProjectState,
               description: String,
               team: String,
               tags: Set[String] 

我想将 "tags" 存储在单独的 table 中,并从 "projects_table" 和 "project_tags_table"[=12= 构建 "Project" 对象]

这是我的 table 定义:

class ProjectTable(tag: Tag) extends Table[ProjectTableRecord](tag, Some("octopus_service"), "projects") {

      def id: Rep[UUID] = column[UUID]("id", O.PrimaryKey)

      def name: Rep[String] = column[String]("name")

      def state: Rep[ProjectState] = column[ProjectState]("state")

      def description: Rep[String] = column[String]("description")

      def team: Rep[String] = column[String]("team")



      override def * : ProvenShape[ProjectTableRecord] = (id, name, state, description, team, created, lastModified) <> (
        (ProjectTableRecord.apply _).tupled, ProjectTableRecord.unapply
      )
    }

class ProjectTagTable(tag: Tag) extends Table[ProjectTag](tag, Some("octopus_service"), "project_tags") {

  def projectID: Rep[UUID] = column[UUID]("project_id")

  def name: Rep[String] = column[String]("name")

  def project = foreignKey("PROJECT_FK", projectID, TableQuery[ProjectTable])(_.id, onUpdate = ForeignKeyAction.Restrict, onDelete = ForeignKeyAction.Cascade)

  override def * : ProvenShape[ProjectTag] = (projectID, name) <> (
    ProjectTag.tupled, ProjectTag.unapply
  )
}

如何通过连接这 2 个 table 生成 "Project" 个对象?

提前致谢:)

我认为对责任级别存在误解。 Slick 允许您访问关系数据库(在某种程度上与 SQL 允许您访问的方式相同)。它基本上是一个 DAO 层。

聚合根确实比这个高一个级别(这是一个领域的东西,而不是数据库级别的东西 - 尽管它们在很大程度上通常是相同的)。

所以基本上你需要有一个级别高于 Slick 的表,这样你就可以执行不同的查询和聚合结果化为一体。

不过在我们开始之前 - 您应该创建并存储您的 TableQuery 对象,也许像这样:

lazy val ProjectTable = TableQuery[ProjectTable]
lazy val ProjectTagTable = TableQuery[ProjectTagTable]

你可以把它们放在你附近的某个地方 Table definitions

首先,正如我提到的,您的聚合根 Project 需要被某些东西拉动。我们称它为 ProjectRepository.

假设它将有一个方法def load(id: UUID): Future[Project]

此方法可能如下所示:

class ProjectRepository {
    def load(id: UUID): Future[Project] = {
        db.run(
            for {
                project <- ProjectTable.filter(_.id === id).result
                tags <- ProjectTagTable.filter(_.projectId === id).result 
            } yield {
                Project(
                    id = project.id,
                    name = project.name,
                    state = project.state,
                    description = project.description,
                    team = project.team,
                    tags = tags.map(_.name)                
                )
            }
        )
    }

    // another example - if you wanted to extract multiple projects
    // (in reality you would probably apply some paging here)
    def findAll(): Future[Seq[Project]] = {
        db.run(
            ProjectTable
                .join(ProjectTag).on(_.id === _.projectId)
                .result
                .map { _.groupBy(_._1)
                        .map { case (project, grouped) =>
                             Project(
                               id = project.id,
                               name = project.name,
                               state = project.state,
                               description = project.description,
                               team = project.team,
                               tags = grouped.map(_._2.name)
                             )
                         }
                }
        )
    }
}

题外话: 如果你想在 findAll 方法中进行分页,你需要做这样的事情:

ProjectTable
    .drop(pageNumber * pageSize)
    .take(pageSize)
    .join(ProjectTag).on(_.id === _.projectId)
    .result

上面会产生子查询,但它基本上是使用多个连接关系进行分页的典型方式(如果没有子查询,您将对整个结果集进行分页,这在大多数情况下不是您所需要的!)。

回到正文:

显然,如果您将 Project 定义为:

case class Project(project: ProjectRecord, tags: Seq[ProjectTag])

那么你的收益就是:

yield {
   Project(project, tags)
}

但这绝对是个人喜好问题(按照您的方式制作它实际上是有意义的 - 隐藏内部记录布局)。

基本上这里有很多地方可以改进。我并不是真正的 DDD 专家,但至少从 Slick 的角度来看,应该做的第一个改变是改变方法:

def load(id: UUID): Future[Project]

def load(id: UUID): DBIO[Project]

并在更高级别上执行 db.run(...) 操作。这样做的原因是,在 Slick 中,一旦你触发 db.run(因此将 DBIO 转换为 Future),你就失去了在单个事务中组合多个操作的能力。因此,一种常见的模式是将 DBIO 推到应用层的相当高的位置,基本上达到定义事务边界的某些业务级别。