在Django ORM中避免多个引用相同对象

10 投票
2 回答
2478 浏览
提问于 2025-04-17 10:32

我们有一个应用程序,里面的数据关系非常复杂,也就是说,有很多情况下两个对象可能通过某种关系指向同一个对象。根据我的了解,Django 在你尝试通过一个之前没有评估过的关系来获取对象时,并不会尝试返回已经获取过的对象的引用。

举个例子:

class Customer( Model ):
    firstName = CharField( max_length = 64 )
    lastName = CharField( max_length = 64 )

class Order( Model ):
    customer = ForeignKey( Customer, related_name = "orders" )

假设我们有一个客户,他在数据库里有两个订单:

order1, order2 = Order.objects.all()
print order1.customer # (1) One DB fetch here
print order2.customer # (2) Another DB fetch here
print order1.customer == order2.customer # (3) True, because PKs match
print id( order1.customer ) == id( order2.customer ) # (4) False, not the same object

当数据之间关系非常复杂时,访问对象的关系会导致对数据库的重复查询,这样就会出现问题。

我们还开发 iOS 应用,其中 CoreData 有一个很好的特点,就是它会维护一个 上下文,在这个上下文中,某个模型只会有一个实例。在上面的例子中,CoreData 不会在第二次获取时再去数据库查询,因为它会使用已经在内存中的客户来解决关系。

即使把第二行换成一个故意让它强制再去数据库查询的例子(比如 print Order.objects.exclude( pk = order1.pk ).get( customer = order1.customer )),CoreData 也会意识到第二次查询的结果已经在内存中,并返回现有的模型,而不是新创建一个(也就是说,在 CoreData 中 (4) 会打印 True,因为它们实际上是同一个对象)。

为了应对 Django 的这种行为,我们正在写一些复杂的代码,试图通过 (type, pk) 来缓存模型在内存中,然后用 _id 后缀检查关系,尽量从缓存中获取,而不是盲目地再去数据库查询。这虽然减少了数据库的负担,但感觉很脆弱,如果在一些我们无法控制的框架或中间件中意外发生正常的关系查找,可能会引发问题。

有没有什么最佳实践或框架可以帮助 Django 避免这个问题?有没有人尝试在 Django 的 ORM 中安装某种线程本地上下文,以避免重复查找和多个内存实例映射到同一个数据库模型的情况?

我知道有像 JohnnyCache 这样的查询缓存工具(可以帮助减少数据库的负担),但即使有这些措施,多个实例映射到同一个底层模型的问题依然存在。

2 个回答

2

在Django的文档中,有一个关于数据库优化的页面,链接在这里:DB optimization page。简单来说,可调用的函数是不会被缓存的,但属性是会被缓存的(比如后续调用order1.customer时,不会再去数据库查询),不过这只是在它所属的对象上下文中有效(也就是说,不同的订单之间是不能共享的)。

使用缓存

正如你所说,解决你问题的一种方法是使用数据库缓存。我们使用的是bitbucket的johnny cache,这个几乎是完全透明的;另一个不错的透明缓存是mozilla的cache machine。你也可以选择一些不那么透明的缓存系统,这些可能更适合你的需求,具体可以查看djangopackages/caching

如果不同的请求需要重复使用同一个客户,添加缓存确实会非常有帮助;但请你阅读一下这个,这适用于大多数透明缓存系统,帮助你考虑一下你的写入/读取模式是否适合这样的缓存系统。

优化请求

对于你的具体例子,另一种方法是使用select_related

order1, order2 = Order.objects.all().select_related('customer')

这样,Customer对象会在同一个SQL请求中立即加载,成本很低(除非记录非常大),而且不需要尝试其他的包。

2

David Cramer 的 django-id-mapper 是一个尝试实现这个功能的工具。

撰写回答