Django的ORM如何获取外键对象?

23 投票
3 回答
7895 浏览
提问于 2025-04-16 03:28

我已经试了几个小时,还是没搞明白这个问题。

class other(models.Model):
    user = models.ForeignKey(User)


others = other.objects.all()
o = others[0]

到目前为止,ORM(对象关系映射)还没有请求o.user这个对象,但只要我做任何与这个对象相关的事情,它就会从数据库中加载这个对象。

type(o.user)

这会导致从数据库加载数据。

我想弄明白的是,他们是怎么做到这种“魔法”的。是什么样的python技巧让这一切发生的。是的,我看过源代码,但还是搞不懂。

3 个回答

0

可以使用属性来实现这个功能。简单来说,你的类定义会生成一个类似下面的类:

class other(models.Model):
    def _get_user(self):
        ## o.users being accessed
        return User.objects.get(other_id=self.id)

    def _set_user(self, v):
        ## ...

    user = property(_get_user, _set_user)

在你访问另一个实例的 .user 之前,关于 User 的查询是不会执行的。

1

这段话不会详细解释Django是怎么做到的,但你看到的其实是“懒加载”在发挥作用。懒加载是一种常见的设计模式,它的意思是推迟对象的初始化,直到真正需要它们的时候。在你的例子中,就是在执行o = others[0]或者type(o.user)这两行代码之前,相关的对象才会被初始化。你可以看看这篇维基百科文章,它可能会让你对这个过程有一些了解。

52

Django使用一种叫做元类的东西(具体是django.db.models.base.ModelBase)来定制模型类的创建过程。对于在模型中定义的每个类属性(这里我们关注的是user),Django首先会检查它是否定义了一个叫做contribute_to_class的方法。如果这个方法存在,Django就会调用它,这样对象就可以在模型类创建时进行自定义。如果对象没有定义contribute_to_class,那么它就会直接通过setattr被赋值给类。

因为ForeignKey是Django的一个模型字段,它定义了contribute_to_class。当ModelBase这个元类调用ForeignKey.contribute_to_class时,赋值给ModelClass.user的值是一个django.db.models.fields.related.ReverseSingleRelatedObjectDescriptor的实例。

ReverseSingleRelatedObjectDescriptor是一个实现了Python的描述符协议的对象,它用来定制当这个类的实例作为另一个类的属性被访问时发生的事情。在这个情况下,这个描述符用于懒加载,也就是第一次访问时从数据库中加载并返回相关的模型实例。

# make a user and an instance of our model
>>> user = User(username="example")
>>> my_instance = MyModel(user=user)

# user is a ReverseSingleRelatedObjectDescriptor
>>> MyModel.user
<django.db.models.fields.related.ReverseSingleRelatedObjectDescriptor object>

# user hasn't been loaded, yet
>>> my_instance._user_cache
AttributeError: 'MyModel' object has no attribute '_user_cache'

# ReverseSingleRelatedObjectDescriptor.__get__ loads the user
>>> my_instance.user
<User: example>

# now the user is cached and won't be looked up again
>>> my_instance._user_cache
<User: example>

每次访问模型实例上的user属性时,都会调用ReverseSingleRelatedObjectDescriptor.__get__方法,但它很聪明,只会查找一次相关对象,然后在后续的调用中返回一个缓存的版本。

撰写回答