Django提供的ContentType实在是太方便了

起步

在一些诸如“收藏”的业务场景下,用户能够收藏不同种类的记录,收藏文章,商品,评论等。如果仅用一张表来存储用户的收藏情况,那么模型中需要两个属性来分别表示类型和主键:

class Collect(models.Model):
    """
    用户收藏表
    """
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)

    target_table = models.CharField(verbose_name='表名')
    object_id = models.IntegerField(verbose_name='目标表中的主键')

本文介绍的也是这种实现形式,但如果用 django 提供的 ContentType 会方便很多。

使用ContentType

ContentType 是 django 内置的组件,组件能够帮助方便的连表操作,多张表外键关系关联到同一张表的场景,且并不能选择那张表的所有数据。

上述的模型就可以改造为:

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation

class Collect(models.Model):
    """
    用户收藏表
    """
    owner = models.ForeignKey(User, db_constraint=False, on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.IntegerField(verbose_name='目标表中的主键')
    # GenericForeignKey 是个辅助字段,不会在表中创建
    content_object = GenericForeignKey('content_type', 'object_id') 

组件的使用上的步骤:

  • 定义一个 ForeignKey 字段关联到 ContentType 表,通常这个字段名取 content_type;
  • 定义 PositiveIntergerField 字段, 用来存储关联表中的主键,通常我们用 object_id
  • 定义 GenericForeignKey 字段,传入上面两个字段的名字;
  • 方便反向查询可以定义 GenericRelation 字段。

基本操作

1. 用户收藏谋篇文章

user = request.user
article = models.ArticlePost.objects.get(id=5)
t = models.Collect(content_object=article, owner=user)
t.save()
print(repr(t.content_object))  # <ArticlePost: 测试文章标题> 

SQL执行语句为:

(0.000) SELECT `django_content_type`.`id`, `django_content_type`.`app_label`, `django_content_type`.`model` FROM `django_content_type` WHERE (`django_content_type`.`app_label` = 'article' AND `django_content_type`.`model` = 'articlepost'); args=('article', 'articlepost')
(0.000) INSERT INTO `article_collect` (`owner_id`, `created`, `content_type_id`, `object_id`) VALUES (1, '2019-12-29 16:58:43.572741', 7, 5);

2.获取用户的收藏列表

collects = models.Collect.objects.filter(owner=user)
for item in collects:
    print(repr(item.content_object))  # <ArticlePost: 测试文章标题>

SQL执行语句为:

(0.001) SELECT `article_collect`.`id`, `article_collect`.`owner_id`, `article_collect`.`created`, `article_collect`.`content_type_id`, `article_collect`.`object_id` FROM `article_collect` WHERE `article_collect`.`owner_id` = 1; args=(1,)
(0.000) SELECT `django_content_type`.`id`, `django_content_type`.`app_label`, `django_content_type`.`model` FROM `django_content_type` WHERE `django_content_type`.`id` = 7; args=(7,)
(0.000) SELECT * FROM `article_articlepost` WHERE `article_articlepost`.`id` = 1;
(0.000) SELECT * FROM `article_articlepost` WHERE `article_articlepost`.`id` = 2;
(0.000) SELECT * FROM `article_articlepost` WHERE `article_articlepost`.`id` = 3;

结果集中有3个记录,因此每个循环都会查询一次文章表。

3.取消收藏 收藏表也有自己的主键,因此可以通过自己的表主键来进行删除:

models.Collect.objects.filter(id=1).delete()

也可以通过已知的文章对象进行删除:

article = models.ArticlePost.objects.get(id=5)
content_type = ContentType.objects.get(app_label=article._meta.app_label, model=article._meta.model_name)
models.Collect.objects.filter(owner=user, content_type=content_type, object_id=article.id).delete()

4.获取某篇文章有收藏它的用户列表 这时候,用于反向查询的 GenericRelation 就能起作用了。使用它之前需要在文章的模型中定义:

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
class ArticlePost(models.Model):
    ...
    collect_users = GenericRelation(Collect)

GenericRelation 也属于辅助字段,不会在表中创建。反向查询如下:

article = models.ArticlePost.objects.get(id=5)
article.collect_users.all()  # <QuerySet [<User: .>, <User: .>]>

ContentType 详细介绍

Django包含一个 contenttypes 框架(框架放在 contrib 中),该应用程序可以跟踪Django驱动的项目中安装的所有模型,并提供了用于处理模型的高级通用接口。默认情况下,该应用默认加载了:

# settings.py
INSTALLED_APPS = [
    'django.contrib.auth',
    'django.contrib.contenttypes',  # 在这里
    ...
]

确保 contenttypes 的载入在 auth 之后。框架会创建一张 django_content_type 表,用户存放所有应用的所有表名:

20191230134517.png

结构拥有三个字段:

  • id:主键;
  • app_label: 模型所属的app的名字;
  • model:模型的名字

ContentType实例

ContentType 的实例可以通过 get_object_for_this_type(**kwargs) 获得对应模型下的示例:

>>> from django.contrib.contenttypes.models import ContentType
>>> user_type = ContentType.objects.get(app_label='auth', model='user')
>>> user_type
<ContentType: user>

# 也可以用 `got_for_model` 快捷获得实例
user_type = ContentType.objects.get_for_model(User)

# 然后通过get_object_for_this_type获得对应用户
>>> user_type.model_class()
<class 'django.contrib.auth.models.User'>
>>> user_type.get_object_for_this_type(username='Guido')
<User: Guido>

GenericForeignKey 与 GenericRelation 注意事项

如果 GenericForeignKey 关联的对象被删除了。content_typeobject_id 字段会保留原来的值,通过 GenericForeignKey 获取会返回 None

由于 GenericForeignKey 没有数据库字段,所以该属性不能通过 filter() 或exclude()` 查找:

# This will fail
>>> Collect.objects.filter(content_object=guido)
# This will also fail
>>> Collect.objects.get(content_object=guido)

如果文章模型里定义了 GenericRelation ,则在Collect中也能反向进行查询:

# collect_users = GenericRelation(Collect, related_query_name='relate_users')

Collect.object.filter(relate_users__username__contains='admin') # <QuerySet [<User .>,<User .>]>

总结

使用 ContentType 能节省代码,能用框架提供的就用框架的。

参考


本文由 hongweipeng 创作,采用 署名-非商业性使用-相同方式共享 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧