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

Posted

技术标签:

【中文标题】使用 Django 模型将 JSON 数据写入关系数据库的最优雅方法?【英文标题】:Most elegant approach for writing JSON data to a relational database using Django Models? 【发布时间】:2012-01-12 03:33:34 【问题描述】:

我在 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"
    

现在处理典型 ManyToManyField 的逻辑相当简单,

逻辑代码-

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 形式的数据保存到数据库而无需多次编码上述逻辑,您会采用哪种最优雅的方法建议?

感谢大家阅读这篇长文,

【问题讨论】:

感谢详细的解释,我会关注这个帖子的。开个玩笑(抱歉):您可以使用 MS 数据适配器执行此操作:da = New OleDb.OleDbDataAdapter(sql, con) , da.update(ninja_json)。 ;) 顺便说一句,您的模型上不需要id = models.IntegerField(primary_key=True, unique=True),Django 已经自动创建了该字段。在创建新实例docs.djangoproject.com/en/dev/ref/models/instances/… 时仍然可以提供乱序的 id 值 是的,我已经知道我不需要添加 id 自动字段,django 为我做了。但是,正如我之前所说,我正在保存从外部 api 获得的数据,该 api 已经包含一个唯一的 Id 字段,稍后我使用该 api 时将需要它。 在当今(2017)JSON 存储格式:见PostgreSQL 9.6+和成熟的JSONb数据类型....如果spring-boot有好的驱动,Django也可以使用好司机。 【参考方案1】:

在我看来,您需要的代码最干净的地方是作为 NinjaData 模型的自定义管理器上的新管理器方法(例如 from_json_string)。

我认为您不应该覆盖标准的 create、get_or_create 等方法,因为您所做的事情与他们通常所做的有点不同,最好让它们正常工作。

更新: 我意识到我可能会在某个时候自己想要这个,所以我已经编写了代码并稍微测试了一个通用函数。由于它递归地通过并影响其他模型,我不再确定它属于 Manager 方法,并且可能应该是一个独立的辅助函数。

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)

【讨论】:

我必须继续我的工作,所以我已经在为我的模型制作自定义管理器,并且我已经在其中使用了一个名为 create_or_update_and_get 的自定义方法(名称解释了它的作用),它需要从 json.loads(ninja_data) 生成的 python 对象使用大约 20-30 行魔法返回一个现有的(更新的)或新创建的模型实例。它工作正常,但为了能够使用过滤器等标准方法进行查找,我仍然必须覆盖过滤器方法。 +1 指出我在做什么,与此同时,我仍在寻找一种更自然、可扩展的方法。 我不太明白为什么您必须覆盖过滤器进行查找?我想要使​​管理方法更通用,您可以通过 Manager.model._meta.fields 查看 ManyToMany 的实例,并将它们与 json 中的键与列表值相匹配,对于 ForeignKey 和具有 dict 值的键也是如此。对于每个字段,您可以从例如Manager.model._meta.fields[index].rel.to 获取相关模型类,然后一般地 get_or_create 相关实例。 太棒了,经过一些调整,它可以完美运行,这是一种统治所有人的方法,这似乎是迄今为止最干净的方法,感谢您的回答 酷,很高兴它对你有用!如果任何调整是对上述代码的错误修复,请在此处或在 djangosn-p 上分享它们,我已将其复制到 djangosnippets.org/snippets/2621 谢谢!【参考方案2】:

我不知道你是否熟悉这些术语,但你基本上想要做的是 de-serialize 从序列化/字符串格式(在本例中为 JSON)到 Python 模型对象中。

我不熟悉使用 JSON 执行此操作的 Python 库,所以我不能推荐/认可任何,但使用诸如“python”、“反序列化”、“json”、“object”和“graph”似乎在 github 上显示 some Django documentation for serialization 和库 jsonpickle。

【讨论】:

但是,它确实声称将复杂的 python 对象转换为 json,反之亦然,似乎没有任何特定于 django 模型的东西,我将阅读更多关于 jsonpickle 的文档,看看有什么可以完成。 这没有回答问题,Optimus 已经研究出如何反序列化 json json.loads(ninja_json) @Anentropic - 虽然不是完整的解决方案,但 Weston 的帖子很有帮助, json.loads(ninja_data) 只是转换为 python 对象,理想情况下,如果某个库这样做,我会喜欢它,ninjaDataInstacne = awesomejson.get_or_create(model=NinjaData, data=ninja_json) (给定 json 和模型遵循一个模式),但生活可能并不那么容易:(【参考方案3】:

我实际上也有同样的需求,我编写了一个自定义数据库字段来处理它。只需将以下内容保存在项目的 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 中,这不是你想要的。在实际操作中,我发现这是一个小问题。

更多关于writing custom model fields的文档。

我断言这是做到这一点的唯一最好和最明显的方法。请注意,我还假设您不需要对这些数据进行 lookups - 例如您将根据其他标准检索记录,这将随之而来。如果您需要根据 JSON 中的某些内容进行查找,请确保它是一个真正的 SQL 字段(并确保它已编入索引!)。

【讨论】:

-1:这不是他想要的。他问是否有一种简单的方法可以将 JSON 转换为模型实例,当它遵循给定的标准格式时。 感谢您撰写详细的答案,但 Chris 是对的,@Chris 我目前正在覆盖 NinjaData 模型的 init () 方法,但这是一个肮脏的 hack。 @Luke - 我不仅需要对数据进行查找,我还需要将单个实体从 JSON 写入 apt 表,我正在研究对 ManyToManyField 和 ForeignKey 进行子类化是否足够干净。任何帮助表示赞赏

以上是关于使用 Django 模型将 JSON 数据写入关系数据库的最优雅方法?的主要内容,如果未能解决你的问题,请参考以下文章

Django 和 postgres - 在模型字段中将数据存储为 json 的缺点

TP5模型自动转换格式输出时间戳字段,求助如何关闭

在 Django 模板中生成 JSON 的陷阱

Django 使用自定义 SQL 而不是模型将 JSON 对象返回到模板

Django + Postgres:将 JSON 字符串作为 JSON 类型直接保存到模型中

Django 在模型中保存 JSON 数据时出错