如何从Scala中的持久层抽象域层

时间:2012-07-18 13:07:54

标签: scala architecture scalaquery

更新: 我编辑了标题并添加了这个文本以更好地解释我想要实现的目标:我正在尝试从头开始创建一个新的应用程序,但不希望业务层知道持久层,以同样的方式,人们不希望业务层知道REST API层。下面是我想要使用的持久层的示例。我正在寻找与此集成的好建议,即我需要设计/架构方面的帮助,以便在业务逻辑和持久性逻辑之间彻底分离责任。也许是将持久性对象编组和解组到域对象的概念。

从SLICK(a.k.a. ScalaQuery)test example,您可以创建多对多数据库关系。这将创建3个表:a,b和a_to_b,其中a_to_b保持表a和b中行的链接。

object A extends Table[(Int, String)]("a") {
  def id = column[Int]("id", O.PrimaryKey)
  def s = column[String]("s")
  def * = id ~ s
  def bs = AToB.filter(_.aId === id).flatMap(_.bFK)
}

object B extends Table[(Int, String)]("b") {
  def id = column[Int]("id", O.PrimaryKey)
  def s = column[String]("s")
  def * = id ~ s
  def as = AToB.filter(_.bId === id).flatMap(_.aFK)
}

object AToB extends Table[(Int, Int)]("a_to_b") {
  def aId = column[Int]("a")
  def bId = column[Int]("b")
  def * = aId ~ bId
  def aFK = foreignKey("a_fk", aId, A)(a => a.id)
  def bFK = foreignKey("b_fk", bId, B)(b => b.id)
}

(A.ddl ++ B.ddl ++ AToB.ddl).create
A.insertAll(1 -> "a", 2 -> "b", 3 -> "c")
B.insertAll(1 -> "x", 2 -> "y", 3 -> "z")
AToB.insertAll(1 -> 1, 1 -> 2, 2 -> 2, 2 -> 3)

val q1 = for {
  a <- A if a.id >= 2
  b <- a.bs
} yield (a.s, b.s)
q1.foreach(x => println(" "+x))
assertEquals(Set(("b","y"), ("b","z")), q1.list.toSet)

作为我的 next 步骤,我想把它提升到一个级别(我仍然希望使用SLICK但很好地包装),以处理对象。所以在伪代码中,做一些像这样的事情会很棒:

objectOfTypeA.save()
objectOfTypeB.save()
linkAtoB.save(ojectOfTypeA, objectOfTypeB)

或者类似的东西。我对如何在Java中解决这个问题有了自己的想法,但我开始意识到我从纯OO语言开始的一些面向对象的想法开始让我失望。任何人都可以给我一些关于如何在Scala中解决这个问题的指示。

例如:我是否创建只包装或扩展表对象的简单对象,然后将这些(组合)包含到另一个管理它们的类中?

任何想法,指导,示例(请),将有助于我更好地解决这个问题作为设计师和编码员将非常感激。

2 个答案:

答案 0 :(得分:6)

最好的想法是实现data mapper模式之类的东西。与活动记录相反,它不会违反SRP。

由于我不是Scala开发人员,因此我不会显示任何代码。

这个想法如下:

  • 创建域对象实例
  • 在元素上设置条件(例如setId(42),如果您要按ID查找元素)
  • 创建数据映射器实例
  • 通过传入域对象作为参数
  • 在mapper上执行fetch()方法

映射器将查找提供的域对象的当前参数,并根据这些参数从存储中检索信息(可能是SQL数据库,或JSON文件,也可能是远程REST API)。如果检索到信息,则会将值分配给域对象。

另外,我必须注意,数据映射器是为了使用特定域对象的接口而创建的,但是它们从域对象传递到存储和返回的信息可以映射到多个SQL表或多个REST资源。 / p>

这样,当您切换到不同的存储介质时,您可以轻松地替换映射器,甚至可以在不触及真实存储的情况下对域对象中的逻辑进行单元测试。此外,如果您决定在某个时刻添加缓存,那将只是另一个映射器,它试图从缓存中获取信息,如果失败,持久存储的映射器就会启动。

域对象(或者,在某些情况下,域对象的集合)将完全不知道它是存储还是检索。这将是数据映射器的责任。

如果这一切都在MVC上下文中,那么,要完全实现这一点,您需要在模型层中使用另一组结构。我称之为“服务”(请分享,你们想出更好的名字)。它们负责包含数据映射器和域对象之间的交互。这样,您可以防止业务逻辑泄漏到表示层(确切地说是控制器),并且这些服务为业务(也称为模型)层和表示层之间的交互创建了一个自然的界面。

P.S。再一次,抱歉我无法提供任何代码示例,因为我是一名PHP开发人员,不知道如何在Scala中编写代码。

P.P.S。如果您使用的是数据映射器模式,最好的选择是手动编写映射器而不使用声称实现它的任何第三方ORM。它可以让您更好地控制代码库并避免无意义的技术债务 [1] [2]

答案 1 :(得分:5)

简单持久性要求的一个很好的解决方案是ActiveRecord模式:http://en.wikipedia.org/wiki/Active_record_pattern。这是在Ruby和Play中实现的!框架1.2,您可以在独立应用程序中轻松地在Scala中实现它

唯一的要求是使用单例数据库或单例服务来获取所需数据库的引用。我个人会根据以下内容进行实施:

  • 一般特征ActiveRecord
  • 通用类型类ActiveRecordHandler

利用implicits的力量,你可以获得惊人的语法:

trait ActiveRecordHandler[T]{

  def save(t:T):T

  def delete[A<:Serializable](primaryKey:A):Option[T]

  def find(query:String):Traversable[T]
}

object ActiveRecordHandler {
  // Note that an implicit val inside an object with the same name as the trait 
  // is  one of the way to have the implicit in scope.
  implicit val myClassHandler = new ActiveRecordHandler[MyClass] {

    def save(myClass:MyClass) = myClass

    def delete[A <: Serializable](primaryKey: A) = None

    def find(query: String) = List(MyClass("hello"),MyClass("goodbye"))
  }
}

trait ActiveRecord[RecordType] {
  self:RecordType=>


  def save(implicit activeRecordHandler:ActiveRecordHandler[RecordType]):RecordType = activeRecordHandler.save(this)

  def delete[A<:Serializable](primaryKey:A)(implicit activeRecordHandler:ActiveRecordHandler[RecordType]):Option[RecordType] = activeRecordHandler.delete(primaryKey)
}

case class MyClass(name:String) extends ActiveRecord[MyClass] 

object MyClass {
  def main(args:Array[String]) = {
    MyClass("10").save
  }
}

使用这样的解决方案,您只需要您的类扩展ActiveRecord [T]并使用隐式ActiveRecordHandler [T]来处理此问题。

实际上还有一个实现:https://github.com/aselab/scala-activerecord基于类似的想法,但它不是使ActiveRecord具有抽象类型,而是声明一个通用的伴随对象。


对ActiveRecord模式的一般但非常重要的评论是它有助于满足持久性方面的简单要求,但无法处理更复杂的要求:例如,当您想要在同一事务下持久化多个对象时。

如果您的应用程序需要更复杂的持久性逻辑,最好的方法是引入一个持久性服务,该服务仅向客户端类公开一组有限的函数,例如

def persist(objectsofTypeA:Traversable[A],objectsOfTypeB:Traversable[B])

请注意,根据您的应用程序复杂性,您可能希望以不同的方式公开此逻辑:

  • 作为单例对象,如果您的应用程序很简单,并且您不希望您的持久性逻辑可插入
  • 通过单个对象作为“应用程序上下文”的排序,以便在启动时在应用程序中决定要使用哪个持久性逻辑。
  • 使用某种查找服务模式,如果您的应用程序已分发。