文章目录

  • Django JSONField的自动转换(django自定义模型字段)
  • 背景
  • 思路
  • 使用DRF的JSONField
  • 【不推荐】django自定义模型字段 @models.register_field()
  • 【推荐】自定义一个字段类并覆盖from_db_value()和get_prep_value()方法
  • 实力demo
  • python使用模型使用 TextField字段,pg使用jsonb格式问题


Django JSONField的自动转换(django自定义模型字段)

背景

Django v3.1的主要更新之一便是完善了对JSON数据存储的支持,新增models.JSONField和forms.JSONField,可在所有受支持的数据库后端上使用。

通过models.JSONField可指定此字段为存储类型为JSON格式。null=True表示此字段可以为空。

from django.db import models
 
class Hello(models.Model):
    name = models.CharField(max_length=200)
    data = models.JSONField(null=True)
 
    def __str__(self):
        return self.name

思路

如果您想实现JSONField的自动转换,可以使用Django REST framework的JSONField,或者自定义一个字段类并覆盖from_db_value()和get_prep_value()方法来实现这个功能。

DRF的JSONField更简单,但使用上相对复杂一些。自定义字段类的方法更轻量,但需要我们自己完成一定的编码工作。

这里推荐使用自定义字段类的方法!

使用DRF的JSONField

要使用DRF的JSONField,主要是在Serializer中导入并应用于需要自动转换JSON的字段,然后在视图进行序列化和反序列化,JSONField会自动完成与之相关的所有转换工作。

【不推荐】django自定义模型字段 @models.register_field()

@models.register_field()是一个模型注册装饰器。使用它可以注册自定义字段,使其可以像内置字段一样在模型中使用。

例如,使用了这个装饰器的JSONField可以在模型中像此使用:

class Product(models.Model):
    info = models.JSONField()

而不用导入字段类:

from .fields import JSONField

class Product(models.Model):
    info = JSONField()

总结:这样的话,就不用单独导入字段类了(我们只需要在django启动入口的位置,如apps.py中 导入JSONField。目的是为了使用@models.register_field装饰器注册这个字段,使其在Django知道并可以在任何模型中像内置字段一样使用)。如果不使用这个装饰器,我们必须导入字段类后才能在模型中使用它。

另外,即使使用了@models.register_field装饰器,我们也可以直接导入JSONField字段类并在模型中使用。

举例:apps.py导入了,在具体的模型类中又单独引入了这个JSONField,用的是哪个?
在这种情况下,Django会使用您在模型中直接导入的JSONField字段类。
也就是说,apps.py中的导入会被忽略,模型中导入的字段类会生效并在模型中实际使用。

这是因为:

  1. Django会根据实际使用解析哪个字段类,而不是根据哪个被导入了。
  2. 如果同一个字段类被 imports 了两次,Python也只会使用最后一个导入的那个

使用@models.register_field()带来的好处是:

  1. 使自定义字段的使用看起来像内置字段,较为简洁直接,易于理解。
  2. 不用导入自定义字段类,模型可以独立定义,解耦了字段的导入依赖。
  3. 自动处理字段的参数,无需在模型中传递,使用起来像内置字段。
  4. 方便第三方库的集成,可直接在模型中使用第三方提供的自定义字段。

另外,需要注意的一点是,您使用了@models.register_field装饰器,将JSONField注册为了一个可以像内建字段一样使用的模型字段。
但是,Django在首次运行时需要导入这个字段类,才知道JSONField代表什么字段。
所以,您需要在首次使用JSONField的模型对应的apps.py中导入这个字段类。

经过测试Django-4.2.1,里没有register_field装饰器。

因此总结起来,直接定义自定义模型字段类,使用时单独引入即可,这样也不污染环境(不用在不用入口apps.py导一次,IDE还报灰色),即用即可,感觉更清晰。

【推荐】自定义一个字段类并覆盖from_db_value()和get_prep_value()方法

直接使用JSONField不会自动转换,是因为:

  • JSONField只是一个简单的继承自TextField的字段
  • 它本身并未实现from_db_value()和get_prep_value()方法
  • 所以当我们访问instance.JSONField时,得到的仅是JSON编码后的字符串,而非Python对象
  • 它也不会在保存实例时自动将Python对象重新转换为JSON字符串

覆盖模型字段的from_db_value()和get_prep_value()方法可以实现「自动转换」的效果。

  • from_db_value()方法用于数据库读取值时将值转换为Python对象
  • get_prep_value()方法用于数据库保存值前将Python对象转换为值
    通过覆盖这两个方法,我们可以实现自定义的转换逻辑,从而达到自动转换的效果。

实力demo

import json
from django.core import serializers
from django.core.exceptions import ValidationError
from django.db import models


class JSONField(models.TextField):
    description = 'JSON Encoded Data'

    def from_json(self, value):
        try:
            return json.loads(value)
        except ValueError:
            raise ValidationError('Invalid JSON')

    def to_json(self, value):

        return json.dumps(value, cls=serializers.json.DjangoJSONEncoder)

    def from_db_value(self, value, *args):
        if value == '':
            return None
        return self.from_json(value)

    def get_db_prep_save(self, value, *args, **kwargs):
        if isinstance(value, str):
            return value
        if value == "":
            return None
        return self.to_json(value)

    def save(self, *args, **kwargs):
        if self.value == '':
            self.value = None
        if self.value is None:
            self.value = json.dumps(None)
        return super().save(*args, **kwargs)
  • 在save()中对None值也进行判断,并转换为JSON null:
  • 在get_db_prep_save()中,使用了self.to_json()将值转换为JSON字符串进行存储。但如果字段值已经是字符串,则无需再次转换。 这个很重要,平时我们有时间业务代码,已经json.dumps了一次,变成了字符串。如果这里不判断,直接再json.dumps一次,到时候解析的时候发现解析出来还是字符串!

python使用模型使用 TextField字段,pg使用jsonb格式问题

jsonb对数据的检查是在 PostgreSQL 端完成的。
也就是说,即使你使用 Django 的 TextField 字段,数据实际上是以 JSONB 的格式存储在数据库中的。
PostgreSQL 在接受数据时,会对 JSON 数据进行检查:

  • 检查数据是否符合 JSON 语法
  • 检查数据类型是否合法(数值、字符串、对象、数组)
  • 检查数组元素是否一致
    只有通过检查的 JSON 数据才会被存储为 JSONB 格式。
    反之,如果 JSON 存在语法错误或类型错误,则存储时会报错。

总结:

  • JSONB 对 JSON 数据的检查是在数据库(PostgreSQL)端完成的
  • 即使 Django 模型使用的是 TextField 字段,但真正存储的是 JSONB 格式
  • JSONB 检查了JSON 语法和类型,可以过滤无效 JSON
  • 而 Django 只能检查字符串格式是否合法

使用TextField来对应一个PostgreSQL的JSONB字段,取数据会存在以下问题:
通过TextField来获取数据,实际上是把JSONB的字符串格式取出来。只能拿到JSON的原始字符串
获取到JSON字符串后,还需要调用.loads()来解析JSON。

使用TextField对应JSONB字段的问题在于:

  • 只能读取JSON的字符串,无法访问内置数据
  • 需要额外解析JSON字符串
  • 丢失JSON类型信息
  • 索引支持有限
  • 无法使用JSON相关的函数
    而使用JSONField可以:
  • 直接访问JSON内部数据
  • 保留JSON的类型
  • 支持JSON索引
  • 调用JSON相关的函数
    所以总的来说,使用JSONField对应JSONB字段更加推荐。