【问题标题】:Mapping MongoDB documents to case class with types but without embedded documents将 MongoDB 文档映射到具有类型但没有嵌入文档的案例类
【发布时间】:2012-08-16 14:57:07
【问题描述】:

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(使其类型安全性降低)。我还必须为每个引用 User 的类编写相同的查询(或将其移动到某些特征)。

所以我想要实现的是在代码中拥有case class Tweet(_id: ObjectId, content: String, userId: User),在数据库中拥有ObjectId。这可能吗?如果可以,怎么做?有什么好的替代品?

【问题讨论】:

  • 在 Salat 中,他们有子集合,这可以解决问题,但不能完全解决。
  • 如果你想要关系,你不应该使用 RDBMS 吗?在这种情况下,我会使用子文档...
  • 当然,您将无法在引用对象上设置查询条件(相对于 SQL“join”)。但是您仍然可能希望将实体存储在其自己的集合中并从其他对象中引用它。

标签: scala mongodb subset salat


【解决方案1】:

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

我已经为你准备了一个简单的 REPLable sn-p 代码(它假设你有两个集合——“tweets”和“users”)。

准备工作...

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,然后转到另一个集合并从那里读取一个对象。

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

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
    }
}

那么,我们可以说我们从引用中读取Users 的类型类仅仅是

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))
}

【讨论】:

  • 感谢您的回答。我还没有尝试过你的通用解决方案,但另一个效果很好。另一个改进领域是使用 Casbah 代替 Java 驱动程序。
  • 我在 Casbah 没有经验,并试图将 Subset 设计为与驱动程序无关。我相信在这样一个简单的例子中使用 Casbah 是多余的,因为驱动程序只需要几个方法。无论如何,我很想看看是否有人发布了使用 Subset+Casbah 的经验
  • 例如我使用tweets.find(MongoDBObject("user" -> aUser._id)),因为我的ValueWriter 还不能工作。
  • 这里不需要ValueWriter,你可以定义另一个字段("user".fieldOf[ObjectId])或者直接写tweets.find("user".fieldOf[Object] -> aUser._id)tweets.find("user".fieldOf[Object](aUser._id))
  • 完美!我已经组装了一个small Play project,试图想象我将如何在一个真正的应用程序中使用它。我会在接下来的几天里添加一些代码......
【解决方案2】:

Alexander Azarov 的回答可能很好,但我个人不会这样做。

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

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

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

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

请记住,在您的应用程序中,在某些情况下您甚至不需要整个 User 对象,因此查询两次数据库的成本很高,而并非总是需要。当你真的需要用户数据时,你应该只使用包含完整用户数据的案例类,而不是总是加载完整的用户数据,因为它看起来更方便。

或者,如果您想操作用户对象,您将拥有一个用户代理,您可以在其上直接访问 id 属性,并且在任何其他访问时,将完成数据库查询。在 Java/SQL 中,Hibernate 正在处理关系的延迟加载,但我不确定将它与 MongoDB 一起使用是否是一个好主意,它会破坏不变性

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-09-04
    • 2013-07-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多