我最近开始设计一组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。)
以上代码的优缺点列表(无特定顺序):
优点:
CONS:
现在我想知道是否有更好的方法可以做到这一点,或者是否有办法改进上面的代码,具体是:如何避免实体的访问者中的强制转换;如何在实体的构造函数中以更有效的方式构建映射。最后,你们有没有看到更多的PRO或CON?请注意,我最关心的是外部安全性,即API用户,而不是必须添加一些我可以彻底测试的样板文件。
请注意,设计MongoDB ORM可视为设计任何类型的ORM,NoSQL或RDBMS的练习。