Django contenttype 组件使用 及源码分析

通常情况下如果一张表与另一张表有外键关系,我们只需要使用foreignkey即可以做到,但如果一张表与很多张表有外键关系呢?不停的加外键吗?很显然不应该是这样的,事实上django已经提供了这样的组件,通用关系组件:contenttype.

ContentType是Django的内置的一个应用,可以追踪项目中所有的APP和model的对应关系,并记录在ContentType表中。当我们的项目做数据迁移后,会有很多django自带的表,其中就有django_content_type表

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

ContentType组件应用:

  -- 在model中定义ForeignKey字段,并关联到ContentType表,通常这个字段命名为content-type

  -- 在model中定义PositiveIntergerField字段, 用来存储关联表中的主键,通常我们用object_id

  -- 在model中定义GenericForeignKey字段,传入上面两个字段的名字

  --  方便反向查询可以定义GenericRelation字段

 

下面看一个demo, 有两张表:文章表,评论表,在最开始阶段,我们通过外键来关联。

一篇文章可以有很多评论,所以我把外键放在评论表:

# blog.models.py
from django.utils import timezone
from django.db import models
from ckeditor_uploader.fields import RichTextUploadingField

# Create your models here.
class Article(models.Model):
    title = models.CharField('Title', max_length=70)
    content = RichTextUploadingField('content', config_name='my_config')
    created_time = models.DateTimeField('created time', default=timezone.now)
    read = models.PositiveIntegerField('read times', default=0)

    def __str__(self):
        return  self.title

 

#comment.models.py
from django.db import models

# Create your models here.
class Comment(models.Model):
    # 评论
    objects = models.Manager()  # 指定管理器
    name = models.CharField('name', max_length=30)
    email = models.EmailField('email', max_length=100)
    content = models.TextField('content', )
    created_time = models.DateTimeField('created time', auto_now_add=True)

    article = models.ForeignKey(to='Article', to_field='id', on_delete=models.CASCADE)

    def __str__(self):
        return self.name

在很显然,在这种简单的情况下,不使用contenttype也能实现功能查询某文章评论的功能。

下面使用contenttype:

更改comment

from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType

# Create your models here.
class Comment(models.Model):
    # 评论
    objects = models.Manager()  # 指定管理器
    name = models.CharField('name', max_length=30)
    email = models.EmailField('email', max_length=100)
    content = models.TextField('content', )
    created_time = models.DateTimeField('created time', auto_now_add=True)

    content_type = models.ForeignKey(ContentType, on_delete=models.DO_NOTHING)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    def __str__(self):
        return self.name

表面上看comment已经与articel脱钩了,没有任何关系了,但是通过contenttype它却可以与任意的model建立关系。

同步数据库后,我们要怎么获取id为一的文章的评论呢?django shell中操作如下:

In [1]: from django.contrib.contenttypes.models import ContentType

In [2]: from comment.models import Comment

In [3]: from blog.models import Article

In [4]: article_obj = Article.objects.get(id=1)

In [5]: article_content_type = ContentType.objects.get_for_model(article_obj)

In [6]: comment_list = Comment.objects.filter(content_type=article_content_type, object_id=1)

In [7]: list(comment_list)
Out[7]: [<Comment: fafafad>]

原理:每个app注册后都会在django的contentType这个app中注册一个,id, app_label, 以及model

id   app_label model
1   admin   logentry
2   auth    permission
3   auth    group
4   auth    user
5   contenttypes    contenttype
6   sessions    session
7   blog    article
8   comment comment

我们来看上面的查询 语句:

article_obj = Article.objects.get(id=1)

首先,我们获取了一个article对象,接着我们获取article对象的contenttype

article_content_type = ContentType.objects.get_for_model(article_obj)

In [8]: article_content_type
Out[8]: <ContentType: article>

可以看到它从contentType表中找到了article model,当然这还不够,要找到具体的article,还需要article id,也就是指定object_id

在这里contentType充当中间商的角色:

文章和评论彼此不认识,但中间商知道文章,也知道评论,并且有他们两个的电话号码:通过这个中间商可以将任意的model之间相互联系起来:

因为每个app,以及app中的model都会在contentType中注册一下,相当于留下了自己的电话号码。

 

源码分析:

contentTypeManager即我们平时使用的model后面的objects,在django中每个model默认的manager指定为objects = modelsManager()

比如Article中, 默认的名字就叫objects,你也可能 对Objects赋值,使用自定义的管理器。

我们看看ContentType的管理器:(去年不重要的部分)

class ContentTypeManager(models.Manager):
    use_in_migrations = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Cache shared by all the get_for_* methods to speed up
        # ContentType retrieval.
        self._cache = {}



    def _get_from_cache(self, opts):
        key = (opts.app_label, opts.model_name)
        return self._cache[self.db][key]

    def get_for_model(self, model, for_concrete_model=True):
        """
        Return the ContentType object for a given model, creating the
        ContentType if necessary. Lookups are cached so that subsequent lookups
        for the same model don't hit the database.
        """
        opts = self._get_opts(model, for_concrete_model)
        try:
            return self._get_from_cache(opts)
        except KeyError:
            pass

        # The ContentType entry was not found in the cache, therefore we
        # proceed to load or create it.
        try:
            # Start with get() and not get_or_create() in order to use
            # the db_for_read (see #20401).
            ct = self.get(app_label=opts.app_label, model=opts.model_name)
        except self.model.DoesNotExist:
            # Not found in the database; we proceed to create it. This time
            # use get_or_create to take care of any race conditions.
            ct, created = self.get_or_create(
                app_label=opts.app_label,
                model=opts.model_name,
            )
        self._add_to_cache(self.db, ct)
        return ct

    def get_for_models(self, *models, for_concrete_models=True):
        """
        Given *models, return a dictionary mapping {model: content_type}.
        """
        results = {}
        # Models that aren't already in the cache.
        needed_app_labels = set()
        needed_models = set()
        # Mapping of opts to the list of models requiring it.
        needed_opts = defaultdict(list)
        for model in models:
            opts = self._get_opts(model, for_concrete_models)
            try:
                ct = self._get_from_cache(opts)
            except KeyError:
                needed_app_labels.add(opts.app_label)
                needed_models.add(opts.model_name)
                needed_opts[opts].append(model)
            else:
                results[model] = ct
        if needed_opts:
            # Lookup required content types from the DB.
            cts = self.filter(
                app_label__in=needed_app_labels,
                model__in=needed_models
            )
            for ct in cts:
                model = ct.model_class()
                opts_models = needed_opts.pop(ct.model_class()._meta, [])
                for model in opts_models:
                    results[model] = ct
                self._add_to_cache(self.db, ct)
        # Create content types that weren't in the cache or DB.
        for opts, opts_models in needed_opts.items():
            ct = self.create(
                app_label=opts.app_label,
                model=opts.model_name,
            )
            self._add_to_cache(self.db, ct)
            for model in opts_models:
                results[model] = ct
        return results

    def get_for_id(self, id):
        """
        Lookup a ContentType by ID. Use the same shared cache as get_for_model
        (though ContentTypes are obviously not created on-the-fly by get_by_id).
        """
        try:
            ct = self._cache[self.db][id]
        except KeyError:
            # This could raise a DoesNotExist; that's correct behavior and will
            # make sure that only correct ctypes get stored in the cache dict.
            ct = self.get(pk=id)
            self._add_to_cache(self.db, ct)
        return ct

    def clear_cache(self):
        """
        Clear out the content-type cache.
        """
        self._cache.clear()

    def _add_to_cache(self, using, ct):
        """Insert a ContentType into the cache."""
        # Note it's possible for ContentType objects to be stale; model_class() will return None.
        # Hence, there is no reliance on model._meta.app_label here, just using the model fields instead.
        key = (ct.app_label, ct.model)
        self._cache.setdefault(using, {})[key] = ct
        self._cache.setdefault(using, {})[ct.id] = c

 可以看到它在内部维护着一个_cache字典,其中放着{app_label, model} 一一对应。并且放在缓存中提高查询效率。

它主要提供三个方法:

get_for_model, get_for_models, get_for_id:

get_for_id通过content_type的id 查找contenttype,每个contenttype对象也有对应的id,但一般情况下应该不知道id。

我们看get_for_model:通过model去获取它的contentType对象,正如我们上面使用的,传入article对象,去获取它对应的contentType对象

In [5]: article_content_type = ContentType.objects.get_for_model(article_obj)

然后再使用文章id过滤即可。

 

而get_for_models其实做的也是一样的事,只不过它返回的是一个字典{model, contenttpe_obj}即model对应的contentype组成的字典:

In [9]: article_content_type = ContentType.objects.get_for_models(article_obj)

In [10]: article_content_type
Out[10]: {<Article: Salt-Stack简单使用>: <ContentType: article>}

ContentType的简单分析就到这里了,对应的源码后续会上传到github: https://github.com/Andy963

事实上contentType有其方便性,但不可否认这样使用效率低的问题。而对于个人博客这种,要求不高可以使用,比如写一个评论组件,写一个点赞组件,这样它就可以对任意一个其它app进行评论,点赞了。

上一篇:JQuery this和$(this)的区别

下一篇:Flask 源码之Local, LocalStack