Scala ORM设计使用RDBMS和NoSQL的特征和映射实现

时间:2013-12-19 20:22:24

标签: mongodb scala orm rdbms nosql

我最近开始设计一组ORM类,用于在使用Akka构建的服务中使用scala和MongoDB。我希望我的实体成为具有可选字段的不可变类(理想情况下为case类)并具有一些内置的智能,例如,返回MongoDB集合名称的方法和将对象的内容作为地图返回的方法它们在MongoDB集合中的键/值。由于22个字段的内在限制,在这里使用案例类是不可能的(此外,将案例类转换为地图是not trivial and requires using reflection),因此我不得不采用不同的方法。

在做了一些研究之后,例如this question,我想出了一个具有上述属性的可能解决方案。这是一个简单的实体:

sealed trait MongoEntity {
  val id: Option[String]
  val collectionName: String
  val properties: Map[String, Any]
}

sealed trait FirstEntity extends MongoEntity {
  val collectionName = "first"
  val x: String
  val y: Option[String]
  val z: Option[Int]
}

object FirstEntity {
  def apply(id: Option[String], 
            x: String, 
            y: Option[String] = None, 
            z: Option[Int] = None): FirstEntity = 
    new FirstEntityImpl(id, x, y, z)

  // not required, just "nice to have"
  def unapply(f: FirstEntity) = Some((f.id, f.x, f.y, f.z))

  private case class FirstEntityImpl(id: Option[String] = None, 
                                     properties: Map[String, Any]) 
    extends FirstEntity {
    def this(id: Option[String], 
             x: String, 
             y: Option[String], 
             z: Option[Int]) =
      this(id,{
        val m = collection.mutable.Map[String, Any]()
        m("x") = x
        addTo(m, "y", y)
        addTo(m, "z", z)
        Map() ++ m.toMap // results in an immutable map
      })

    val x: String = properties("x").asInstanceOf[String]
    val y: Option[String] = properties.get("y").asInstanceOf[Option[String]]
    val z: Option[Int] = properties.get("z").asInstanceOf[Option[Int]]
  }
}

private def addTo(map: collection.mutable.Map[String, Any], 
                  k: String, 
                  v: Option[Any]) = v.foreach(map(k) = _)

以下是一个可以包含另一个实体(或与JPA世界有关系)的实体示例:

sealed trait SecondEntity extends MongoEntity {
  val collectionName = "second"
  val w: Option[FirstEntity]
  val t: Float
  val i: Option[String]
}

object SecondEntity {

  def apply(id: Option[String], 
            w: Option[FirstEntity] = None, 
            t: Float, 
            i: Option[String] = None): SecondEntity = 
    new SecondEntityImpl(id, w, t, i)

  private case class SecondEntityImpl(id: Option[String] = None, 
                                      properties: Map[String, Any]) 
    extends SecondEntity {
    def this(id: Option[String], 
             w: Option[FirstEntity], 
             t: Float, 
             i: Option[String]) =
      this(id,{
        val m = collection.mutable.Map[String, Any]()
        m("t") = t
        addTo(m, "w", w)
        addTo(m, "i", i)
        Map() ++ m.toMap
      })

    val w: Option[FirstEntity] = properties.get("w").asInstanceOf[Option[FirstEntity]]
    val t: Float = properties("t").asInstanceOf[Float]
    val i: Option[String] = properties.get("i").asInstanceOf[Option[String]]
  }
}

以下是如何使用实体的示例:

object SomeORM extends App {
  val f = FirstEntity(id = Some("123"), x = "asdf", z = Some(5324))
  val g = FirstEntity(id = Some("123"), x = "asdf", z = Some(5324))
  assert(f == g)
  assert(f.id.get == "123")
  assert(f.x == "asdf")
  assert(f.y.isEmpty)
  assert(f.z.get == 5324)
  assert(f.properties == Map("x" -> "asdf", "z" -> 5324))

  val x = SecondEntity(id = None, w = Some(f), t = 123.232f, i = Some("blah"))
  // this line will cause: recursive value f needs type
  // val z = SecondEntity(id = None, w = Some(f), t = 123.232f, i = Some("blah"))
  val y = SecondEntity(id = None, w = Some(g), t = 123.232f, i = Some("blah"))
  assert(x == y)
  assert(x.properties == Map("w" -> f, "t" -> 123.232f, "i" -> "blah"))

  doSomething(f)

  def doSomething(f: FirstEntity) {
    f match {
      case FirstEntity(Some(id), xx, Some(yy), Some(zz)) => println(s"$id, $xx, $yy, $zz")
      case FirstEntity(Some(id), xx, None, Some(zz)) => println(s"$id, $xx, $zz")
      case _ => println("not matched")
    }
  }
}

(作为旁注,注释行似乎触发this bug。)

以上代码的优缺点列表(无特定顺序):

优点:

  • 所有实体都是特征 - >额外的逻辑/混合可能;
  • 编译器生成的equals和hashCode方法 - >更少的样板/错误;
  • 模式匹配是可能的(少于22个字段) - >很高兴;
  • 不可变实体(案例类/不可变映射) - >更安全;
  • 地图容易表示BSON结构 - >更少的样板/错误;
  • 地图允许以序列化形式轻松重命名字段 - >不需要注释;
  • 可以使用Scala宏生成与每个特征对应的对象 - >更少的样板/错误;
  • 所有实体都是实际密封的,无法扩展 - >更安全。

CONS:

  • 对于具有超过22个字段的实体,模式匹配不可能 - >不是交易破坏者;
  • 需要编写:apply(和unapply),case类中的访问器,附加构造函数 - >更多样板(除非宏生成);
  • 在案例类创建中每个访问者需要一个强制转换 - >更多样板/更少安全。

现在我想知道是否有更好的方法可以做到这一点,或者是否有办法改进上面的代码,具体是:如何避免实体的访问者中的强制转换;如何在实体的构造函数中以更有效的方式构建映射。最后,你们有没有看到更多的PRO或CON?请注意,我最关心的是外部安全性,即API用户,而不是必须添加一些我可以彻底测试的样板文件。

请注意,设计MongoDB ORM可视为设计任何类型的ORM,NoSQL或RDBMS的练习。

0 个答案:

没有答案