使用Django模型将JSON数据写入关系数据库的最优雅方法是什么?

17 投票
3 回答
18763 浏览
提问于 2025-04-17 07:36

我在Django中有一个典型的关系数据库模型,通常这个模型里会包含一些ForeignKeys(外键)、一些ManyToManyFields(多对多字段),还有一些扩展了Django的DateTimeField(日期时间字段)的字段。

我想把从外部API接收到的JSON格式的数据(不是扁平化的)保存到数据库里。我希望这些数据能保存到相应的表中,而不是把整个JSON字符串放到一个字段里。请问有什么简单又干净的方法来实现这个?有没有现成的库可以让这个任务更简单?

下面是一个例子来帮助说明我的问题,

模型-

class NinjaData(models.Model):
    id = models.IntegerField(primary_key=True, unique=True)
    name = models.CharField(max_length=60)  
    birthdatetime = MyDateTimeField(null=True)
    deathdatetime = MyDatetimeField(null=True)
    skills = models.ManyToManyField(Skills, null=True)
    weapons = models.ManyToManyField(Weapons, null=True)
    master = models.ForeignKey(Master, null=True)

class Skills(models.Model):
    id = models.IntegerField(primary_key=True, unique=True)
    name = models.CharField(max_length=60)
    difficulty = models.IntegerField(null=True)

class Weapons(models.Model):
    id = models.IntegerField(primary_key=True, unique=True)
    name = models.CharField(max_length=60)
    weight = models.FloatField(null=True)

class Master(models.Model):
    id = models.IntegerField(primary_key=True, unique=True)
    name = models.CharField(max_length=60)
    is_awesome = models.NullBooleanField()

现在,我通常需要把从外部API(秘密忍者API)获取到的JSON字符串数据保存到这个模型里,JSON的格式如下:

JSON-

{
"id":"1234",
"name":"Hitori",
"birthdatetime":"11/05/1999 20:30:00",
"skills":[
    {
    "id":"3456",
    "name":"stealth",
    "difficulty":"2"
    },
    {
    "id":"678",
    "name":"karate",
    "difficulty":"1"
    }
],
"weapons":[
    {
    "id":"878",
    "name":"shuriken",
    "weight":"0.2"
    },
    {
    "id":"574",
    "name":"katana",
    "weight":"0.5"
    }
],
"master":{
    "id":"4",
    "name":"Schi fu",
    "is_awesome":"true"
    }
}

处理典型的多对多字段的逻辑其实很简单,

逻辑代码 -

data = json.loads(ninja_json)
ninja = NinjaData.objects.create(id=data['id'], name=data['name'])

if 'weapons' in data:
    weapons = data['weapons']
    for weapon in weapons:
        w = Weapons.objects.get_or_create(**weapon)  # create a new weapon in Weapon table
        ninja.weapons.add(w)

if 'skills' in data:
    ...
    (skipping rest of the code for brevity)

我可以用很多方法来实现这个,

  • view函数中编写上面的逻辑,负责把JSON转换成模型实例
  • 重写模型的__init__方法来编写上面的逻辑
  • 重写模型的save()方法来编写上面的逻辑
  • 为每个模型创建一个管理器,并在它的createget_or_createfilter等方法中编写这个逻辑
  • 扩展ManyToManyField并把逻辑放在那里
  • 使用一个外部库?

我想知道有没有一种最明显的单一方法可以将这种JSON格式的数据保存到数据库,而不需要多次编写上述逻辑,你会建议什么样的优雅方法呢?

感谢大家耐心阅读这篇长文,

3 个回答

1

我之前也遇到过类似的需求,所以我写了一个自定义的数据库字段来处理这个问题。你只需要把下面的代码保存到你项目中的一个Python模块里(比如,可以放在一个叫fields.py的文件里),然后导入并使用它:

class JSONField(models.TextField):
    """Specialized text field that holds JSON in the database, which is
    represented within Python as (usually) a dictionary."""

    __metaclass__ = models.SubfieldBase

    def __init__(self, blank=True, default='{}', help_text='Specialized text field that holds JSON in the database, which is represented within Python as (usually) a dictionary.', *args, **kwargs):
        super(JSONField, self).__init__(*args, blank=blank, default=default, help_text=help_text, **kwargs)

    def get_prep_value(self, value):
        if type(value) in (str, unicode) and len(value) == 0:
            value = None
        return json.dumps(value)

    def formfield(self, form_class=JSONFormField, **kwargs):
        return super(JSONField, self).formfield(form_class=form_class, **kwargs)

    def bound_data(self, data, initial):
        return json.dumps(data)

    def to_python(self, value):
        # lists, dicts, ints, and booleans are clearly fine as is
        if type(value) not in (str, unicode):
            return value

        # empty strings were intended to be null
        if len(value) == 0:
            return None

        # NaN should become null; Python doesn't have a NaN value
        if value == 'NaN':
            return None

        # try to tell the difference between a "normal" string
        # and serialized JSON
        if value not in ('true', 'false', 'null') and (value[0] not in ('{', '[', '"') or value[-1] not in ('}', ']', '"')):
            return value

        # okay, this is a JSON-serialized string
        return json.loads(value)

有几点需要注意。首先,如果你在使用South这个工具,你需要告诉它你的自定义字段是怎么工作的:

from south.modelsinspector import add_introspection_rules
add_introspection_rules([], [r'^feedmagnet\.tools\.fields\.models\.JSONField'])

其次,虽然我做了很多工作来确保这个自定义字段在各个地方都能正常使用,比如在序列化格式和Python之间能顺利转换,但有一个地方它的表现不太好,就是在使用manage.py dumpdata的时候,它会把Python数据合并成字符串,而不是转成JSON格式,这可能不是你想要的。在实际使用中,我发现这只是个小问题。

关于如何编写自定义模型字段的更多文档,可以参考这个链接

我认为这是最简单明了的方法。需要注意的是,我假设你不需要对这些数据进行查找,也就是说,你会根据其他条件来获取记录,而这些数据会随之而来。如果你需要根据JSON中的某些内容进行查找,确保它是一个真正的SQL字段(并且要确保它是有索引的!)。

2

我不知道你对这些术语是否熟悉,但你基本上想做的事情是把一种叫做反序列化的操作,从一种串行化的格式(在这个例子中是JSON)转换成Python中的模型对象。

我对Python中处理JSON的库不太熟悉,所以不能推荐具体的库,但你可以用“python”、“反序列化”、“json”、“对象”和“图”等关键词搜索一下,似乎能找到一些关于序列化的Django文档,还有一个叫做jsonpickle的库在github上。

10

在我看来,最干净的做法是把你需要的代码放在一个新的管理器方法里,比如叫做 from_json_string,这个方法是为 NinjaData 模型定制的。

我觉得不应该去覆盖标准的 create、get_or_create 等方法,因为你要做的事情和它们通常的功能有点不同,保持它们正常工作是比较好的。

更新:我意识到我可能在某个时候也会需要这个功能,所以我写了一个通用的函数并进行了简单测试。因为这个函数会递归地处理其他模型,所以我现在不太确定它是否应该作为管理器方法,可能更适合做一个独立的辅助函数。

def create_or_update_and_get(model_class, data):
    get_or_create_kwargs = {
        model_class._meta.pk.name: data.pop(model_class._meta.pk.name)
    }
    try:
        # get
        instance = model_class.objects.get(**get_or_create_kwargs)
    except model_class.DoesNotExist:
        # create
        instance = model_class(**get_or_create_kwargs)
    # update (or finish creating)
    for key,value in data.items():
        field = model_class._meta.get_field(key)
        if not field:
            continue
        if isinstance(field, models.ManyToManyField):
            # can't add m2m until parent is saved
            continue
        elif isinstance(field, models.ForeignKey) and hasattr(value, 'items'):
            rel_instance = create_or_update_and_get(field.rel.to, value)
            setattr(instance, key, rel_instance)
        else:
            setattr(instance, key, value)
    instance.save()
    # now add the m2m relations
    for field in model_class._meta.many_to_many:
        if field.name in data and hasattr(data[field.name], 'append'):
            for obj in data[field.name]:
                rel_instance = create_or_update_and_get(field.rel.to, obj)
                getattr(instance, field.name).add(rel_instance)
    return instance

# for example:
from django.utils.simplejson import simplejson as json

data = json.loads(ninja_json)
ninja = create_or_update_and_get(NinjaData, data)

撰写回答