【问题标题】:Aggregate root implementation with slick使用 slick 聚合根实现
【发布时间】:2016-12-29 12:01:00
【问题描述】:

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

这是我的域对象:

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

我想将“标签”存储在单独的表中,并从“projects_table”和“project_tags_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 个表来生成“项目”对象?

提前致谢:)

【问题讨论】:

    标签: scala domain-driven-design slick


    【解决方案1】:

    我认为对责任级别存在误解。 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 推到应用程序层的相当高的位置,基本上到定义事务边界的某些业务级别。

    【讨论】:

    • ActiveRecord 不是 DDD AR 的适当持久性模式。
    • 正是 - 这就是上述答案的重点。您的根不需要(必须)映射到特定的数据库元组,但 Slick 基本上在这个级别上运行。因此,您不要将裸 Slick 层保留为 AR。
    • 我的意思是 AR 不应该访问数据库。持久性细节应在存储库中抽象出来。好吧,如果你想做纯 DDD,模型只关注业务。没有明确的规则,但是对于复杂的域逻辑,您不希望在其中添加持久性逻辑。
    • 你完全正确。看看我上面的更正答案。 (我并不是真正的 DDD 专家)。
    • 您好帕维尔,感谢您的回复。此解决方案的唯一问题是它只能在最后一步(结果)中发生,对吗?例如如果我想对数据应用分页怎么办?
    猜你喜欢
    • 1970-01-01
    • 2013-02-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-30
    • 1970-01-01
    相关资源
    最近更新 更多