一文读懂Django contenttypes框架


什么是Django ContentTypes?

Django ContentTypes是由Django框架提供的一个核心功能,它对当前项目中所有基于Django驱动的model提供了更高层次的抽象接口。

然而,对于Django ContentTypes不熟悉的人来说,上面这句话说了跟没说一样,因此,笔者将一步一步解释Django ContentTypes在Django框架中做了什么,以及如何使用Django ContentTypes。

当然,如果对于ContentTypes有了初步了解而只是不了解它的应用场景,可以直接查阅以下这两个链接:

Django official documentation:The contenttypes framework

stackoverflow: How exactly do Django content types work?

Django ContentTypes做了什么?

当使用django-admin初始化一个django项目的时候,可以看到在默认的INSTALL_APPS已经包含了django.contrib.contenttypes:

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

而且注意django.contrib.contenttypes是在django.contrib.auth之后,这是因为auth中的permission系统是根据contenttypes来实现的。

笔者紧接着查阅了一下django.contrib.contenttypes.models文件:

class ContentType(models.Model):
    app_label = models.CharField(max_length=100)
    model = models.CharField(_('python model class name'), max_length=100)
    objects = ContentTypeManager()
    class Meta:
        verbose_name = _('content type')
        verbose_name_plural = _('content types')
        db_table = 'django_content_type'
        unique_together = (('app_label', 'model'),)
    def __str__(self):
        return self.name

大家可以看到ContentType就是一个简单的django model,而且它在数据库中的表的名字为django_content_type。

有经验的Django开发者对于这个表的名字一般都不会陌生,在第一次对Django的model进行migrate之后,就可以发现在数据库中出现了一张默认生成的名为django_content_type的表。

如果没有建立任何的model,默认django_content_type是这样的:

sqlite> select * from django_content_type;
1|admin|logentry
2|auth|group
3|auth|user
4|auth|permission
5|contenttypes|contenttype
6|sessions|session

因此,django_content_type记录了当前的Django项目中所有model所属的app(即app_label属性)以及model的名字(即model属性)。

当然,django_content_type并不只是记录属性这么简单,在一开始的时候笔者就提及了contenttypes是对model的一次封装,因此可以通过contenttypes动态的访问model类型,而不需要每次import具体的model类型。

·ContentType实例提供的接口

    ·ContentType.model_class()

        ·获取当前ContentType类型所代表的模型类

    ·ContentType.get_object_for_this_type()

        ·使用当前ContentType类型所代表的模型类做一次get查询

·ContentType管理器(manager)提供的接口

    ·ContentType.objects.get_for_id()

        ·通过id寻找ContentType类型,这个跟传统的get方法的区别就是它跟get_for_model共享一个缓存,因此更为推荐。

    ·ContentType.objects.get_for_model()

        ·通过model或者model的实例来寻找ContentType类型

Django ContentTypes的使用场景

Permission对ContentType的使用

在之前,笔者简单地提及了auth中Permission有涉及到对ContentType的使用,下面来看一下Permission的model源码:

class Permission(models.Model):
    """
    The permissions system provides a way to assign permissions to specific
    users and groups of users.
    The permission system is used by the Django admin site, but may also be
    useful in your own code. The Django admin site uses permissions as follows:
        - The "add" permission limits the user's ability to view the "add" form
          and add an object.
        - The "change" permission limits a user's ability to view the change
          list, view the "change" form and change an object.
        - The "delete" permission limits the ability to delete an object.
    Permissions are set globally per type of object, not per specific object
    instance. It is possible to say "Mary may change news stories," but it's
    not currently possible to say "Mary may change news stories, but only the
    ones she created herself" or "Mary may only change news stories that have a
    certain status or publication date."
    Three basic permissions -- add, change and delete -- are automatically
    created for each Django model.
    """
    name = models.CharField(_('name'), max_length=255)
    content_type = models.ForeignKey(ContentType,models.CASCADE,verbose_name=_('content type'),)
    codename = models.CharField(_('codename'), max_length=100)
    objects = PermissionManager()
    class Meta:
        verbose_name = _('permission')
        verbose_name_plural = _('permissions')
        unique_together = (('content_type', 'codename'),)
        ordering = ('content_type__app_label', 'content_type__model','codename')

大家可以看到Permission模型中设置了一个对ContentType的外键,这意味着每一个Permission的实例都具有关于一个ContentType的id作为外键,而ContentType的id恰恰代表着一个Model。

回想Permission模型在初始化的时候发生了什么,它为每个模型设置了三个权限,分别是add,change以及delete,那么它是如何跟每个模型联系起来的呢?就是通过一个到ContentType的外键。

大家可以看一下Permission表,其中第二行就是content_type,然后将主键于django_content_type对比:

sqlite> select * from auth_permission;
1|1|add_logentry|Can add log entry
2|1|change_logentry|Can change log entry
3|1|delete_logentry|Can delete log entry
4|2|add_group|Can add group
5|2|change_group|Can change group
6|2|delete_group|Can delete group
7|3|add_user|Can add user
8|3|change_user|Can change user
9|3|delete_user|Can delete user
10|4|add_permission|Can add permission
11|4|change_permission|Can change permission
12|4|delete_permission|Can delete permission
13|5|add_contenttype|Can add content type
14|5|change_contenttype|Can change content type
15|5|delete_contenttype|Can delete content type
16|6|add_session|Can add session
17|6|change_session|Can change session
18|6|delete_session|Can delete session

如此,Permission模型借助ContentType表达了对一个model的权限操作。

ContentType的通用类型

笔者将引用在顶部的stackoverflow中回答的例子讲述对通用类型的理解。

假设以下的应用场景:

from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class Post(models.Model):
    author = models.ForeignKey(User)
    title = models.CharField(max_length=75)
    slug = models.SlugField(unique=True)
    body = models.TextField(blank=True)
class Picture(models.Model):
    author = models.ForeignKey(User)
    image = models.ImageField()
    caption = models.TextField(blank=True)
class Comment(models.Model):
    author = models.ForeignKey(User)
    body = models.TextField(blank=True)
    post = models.ForeignKey(Post, null=True)
    picture = models.ForeignKey(Picture, null=True)

注意笔者这里跟原回答做了一些更改,在原回答中Comment中没有null的选项,笔者觉得回答者真正要表达的是Comment是分别和Picture或者Post中其中一个对应即可,一个Comment并不既需要Post又需要Picture才能建立,可能是回答者写错没注意的缘故。

当笔者对以上model进行migrate之后,发现Comment表中的foreignkey是可以被设置为null的。

那么,如何通过Contenttype框架对以上代码进行改进呢?

ContentType提供了一种GenericForeignKey的类型,通过这种类型可以实现在Comment对其余所有model的外键关系。

修改后的Comment模型如下:

class Comment(models.Model):
    author = models.ForeignKey(User)
    body = models.TextField(blank=True)
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = fields.GenericForeignKey()

在这里,通过使用一个content_type属性代替了实际的model(如Post,Picture),而object_id则代表了实际model中的一个实例的主键,其中,content_type和object_id的字段命名都是作为字符串参数传进content_object的。即:

content_object = fields.GenericForeignKey('content_type', 'object_id')

笔者先准备一些测试用的数据:

user = User.objects.create_user(username='user1', password='2333')
post = Post.objects.create(author=user,title='title1',slug=slugify('title1'),body='')
picture = Picture.objects.create(author=user,image="http://www.picture1.com",caption='picture1')

接着在shell中创建Comment:

>>> from foreign.models import Post, Picture, Common
>>> from django.contrib.auth.models import User
>>> user = User.objects.get(username='user1')
>>> post = Post.objects.get(title='title1')
>>> c = Comment.objects.create(author=user, body='', content_object=post)
>>> c
<Comment: Comment object>
>>> c.content_type
<ContentType: post>
>>> c.object_id
1
>>> picture = Picture.objects.get(caption='picuture1')
>>> c = Comment.objects.create(author=user, body='', content_object=picture)
>>> c.content_type
<ContentType: picture>
>>> c.object_id
1

在django中,也提供从诸如Post,Picture访问Comment的查询,通过GenericRelation类型。如:

class Post(models.Model):
    author = models.ForeignKey(User)
    title = models.CharField(max_length=75)
    slug = models.SlugField(unique=True)
    body = models.TextField(blank=True)
    comment = GenericRelation('Comment')

值得注意的是,如果在Post中定义了GenericRelation,删除了一个实例,在Comment中所有的相关实例也会被删除,GenericForeignKey不支持设置on_delete参数。

因此,如果对级联删除不满意的话就不要设置GenericRelation。

众多python教程,尽在云海天教程网,欢迎在线学习!

来源:PY学习网:原文地址:https://www.py.cn/article.html

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » 一文读懂Django contenttypes框架