Subset看起来像一个有趣的,薄的MongoDB包装器。
在给出的一个例子中,有推文和用户。但是,User
是Tweet
的子文档。在经典 SQL 中,这将规范化为两个单独的表,其中包含从推文到用户的外键。在MongoDB中,这不需要DBRef
,存储用户的ObjectId
就足够了。
在子集和萨拉特中,这将导致以下案例类:
case class Tweet(_id: ObjectId, content: String, userId: ObjectId)
case class User(_id: ObjectId, name: String)
因此,无法保证推文中的 ObjectId 实际上解析为用户(使其类型安全性降低)。我还必须为引用User的每个类编写相同的查询(或将其移动到某些特征)。
因此,我想实现的是将case class Tweet(_id: ObjectId, content: String, userId: User)
,在代码中,并在数据库中ObjectId
。这是否可能,如果可能,如何?什么是好的选择?
是的,这是可能的。实际上,这甚至比在"推文"中拥有"用户"子文档更简单。当"user"是一个引用时,它只是一个标量值,MongoDB和"Subset"没有查询子文档字段的机制。
我为你准备了一个简单的可重复代码片段(它假设你有两个集合——"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]
在这里,更复杂的事情开始发生。我们需要的是一个能够根据字段名称获取ObjectId
的ValueReader
,但随后转到另一个集合并从那里读取对象。
这可以写成一段代码,一次完成所有事情(你可能会在答案历史中看到这样的变体),但将其表达为读者的组合会更习惯。假设我们有一个从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
}
}
然后,我们可以说我们用于从引用中读取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))
}
亚历山大·阿扎罗夫的回答可能很好,但我个人不会这样做。
你拥有的是一条推文,它只有对用户的 ObjectId 引用。并且您希望在推文加载期间加载用户,因为对于您的域来说,它可能更容易操作。无论如何,除非你使用子文档(并不总是一个好的选择),否则你必须再次查询数据库来检索用户数据,这就是亚历山大·阿扎罗夫所做的。
你宁愿做一个转换函数,将推文转换为TweetWithUser或类似的东西。
def transform(tweet: Tweet) = TweetWithUser( tweet.id, tweet.content, findUserWithId(tweet.userId) )
我真的不明白为什么你会期望一个框架来解决一些你本可以在一行代码中很容易完成的事情。
请记住,在您的应用程序中,在某些情况下,您甚至不需要整个 User 对象,因此在并不总是需要数据库的情况下查询两倍数据库的成本很高。仅当您确实需要完整用户数据时,才应将 case 类与该用户数据一起使用,而不是简单地加载完整的用户数据,因为它看起来更方便。
或者,如果您无论如何都想操作 User 对象,您将拥有一个 User 代理,您可以在其上直接访问 id 属性,而在任何其他访问中,将完成数据库查询。在Java/SQL中,Hibernate正在延迟加载关系,但我不确定将其与MongoDB一起使用是一个好主意,它破坏了不变性。