将MongoDB文档映射到具有类型但没有嵌入文档的案例类

时间:2012-08-16 14:57:07

标签: scala mongodb subset salat

Subset看起来像一个有趣的,瘦的MongoDB包装器。

在给出的一个示例中,有推文和用户。但是,UserTweet子文档。在经典SQL中,这将被归一化为两个单独的表,其中包含从Tweet到User的外键。在MongoDB中,这不需要DBRef,存储用户的ObjectId就足够了。

在Subset和Salat中都会产生这些案例类:

case class Tweet(_id: ObjectId, content: String, userId: ObjectId)
case class User(_id: ObjectId, name: String)

因此无法保证Tweet中的ObjectId实际上解析为用户(使其更少类型安全)。我还必须为引用User的每个类编写相同的查询(或将其移动到某个特征)。

所以我想要实现的是在代码中使用case class Tweet(_id: ObjectId, content: String, userId: User),在数据库中使用ObjectId。这是可能的,如果是的话,怎么样?什么是好的选择?

2 个答案:

答案 0 :(得分:3)

是的,这是可能的。实际上,它比在“推文”中使用“用户”子文档更简单。当“user”是引用时,它只是一个标量值,MongoDB和“Subset”没有查询子文档字段的机制。

我为您准备了一个简单的REPLable代码片段(它假设您有两个集合 - “推文”和“用户”)。

...制剂

import org.bson.types.ObjectId
import com.mongodb._
import com.osinka.subset._
import Document.DocumentId

val db = new Mongo("localhost") getDB "test"
val tweets = db getCollection "tweets"
val users = db getCollection "users"

我们的User案例类

case class User(_id: ObjectId, name: String)

推文和用户的许多字段

val content = "content".fieldOf[String]
val user = "user".fieldOf[User]
val name = "name".fieldOf[String]

这里开始发生更复杂的事情。我们需要的是ValueReader,它能够根据字段名称获取ObjectId,但随后转到另一个集合并从那里读取一个对象。

这可以写成一段代码,一次完成所有事情(您可能会在答案历史中看到这样的变体),但将它表达为读者组合会更加惯用。假设我们有一个ValueReader[User]来自DBObject

val userFromDBObject = ValueReader({
  case DocumentId(id) ~ name(name) => User(id, name)
})

剩下的是通用ValueReader[T],它需要ObjectId并使用提供的底层读者从特定集合中检索对象:

class RefReader[T](val collection: DBCollection, val underlying: ValueReader[T]) extends ValueReader[T] {
  override def unpack(o: Any):Option[T] =
    o match {
      case id: ObjectId =>
        Option(collection findOne id) flatMap {underlying.unpack _}
      case _ =>
        None
    }
}

然后,我们可以说我们从引用中读取User的类型类仅仅是

implicit val userReader = new RefReader[User](users, userFromDBObject)
  

(我很感谢你提出这个问题,因为这个用例非常好   很少见,我没有动力开发通用解决方案。我认为   我需要最终将这种助手包含在“子集”中。   我将非常感谢您对此方法的反馈意见)


这就是你如何使用它:

import collection.JavaConverters._

tweets.find.iterator.asScala foreach { 
  case Document.DocumentId(id) ~ content(content) ~ user(u) =>
    println("%s - %s by %s".format(id, content, u))
}

答案 1 :(得分:0)

亚历山大·阿扎罗夫的答案可能很好,但我个人不会这样做。

你所拥有的是只有对用户的ObjectId引用的推文。 并且您希望在推文加载期间加载用户,因为对于您的域,它可能更容易操作。在任何情况下,除非你使用子文档(并不总是一个好的选择),你必须再次查询数据库以检索用户数据,这就是Alexander Azarov所做的。

你宁愿做一个转换函数,将Tweet转换为TweetWithUser或类似的东西。

  def transform(tweet: Tweet) = TweetWithUser( tweet.id, tweet.content, findUserWithId(tweet.userId) ) 

我真的不明白为什么你会期望一个框架来解决你可以在一行代码中轻松完成的事情。

请记住,在您的应用程序中,在某些情况下您甚至不需要整个User对象,因此查询两次数据库并不总是需要它是很昂贵的。当您真正需要用户数据时,您应该只使用具有完整用户数据的案例类,而不是简单地始终加载完整的用户数据,因为它看起来更方便。

或者如果你想要操纵User对象,你可以拥有一个User proxy,你可以在其上直接访问id属性,在任何其他访问中,都可以进行db查询。在Java / SQL中,Hibernate正在使用延迟加载关系,但我不确定将它与MongoDB一起使用会破坏不变性