将Django Model对象转换成字典并保留所有字段

463

如何将Django模型对象转换为包含所有字段的字典?最好包括外键和editable=False的字段。

让我详细解释一下。假设我有一个像以下这样的Django模型:

from django.db import models

class OtherModel(models.Model): pass

class SomeModel(models.Model):
    normal_value = models.IntegerField()
    readonly_value = models.IntegerField(editable=False)
    auto_now_add = models.DateTimeField(auto_now_add=True)
    foreign_key = models.ForeignKey(OtherModel, related_name="ref1")
    many_to_many = models.ManyToManyField(OtherModel, related_name="ref2")

在终端上,我已经完成了以下操作:

other_model = OtherModel()
other_model.save()
instance = SomeModel()
instance.normal_value = 1
instance.readonly_value = 2
instance.foreign_key = other_model
instance.save()
instance.many_to_many.add(other_model)
instance.save()

我想将这个转换为以下字典:

{'auto_now_add': datetime.datetime(2015, 3, 16, 21, 34, 14, 926738, tzinfo=<UTC>),
 'foreign_key': 1,
 'id': 1,
 'many_to_many': [1],
 'normal_value': 1,
 'readonly_value': 2}

无满意答案的问题:

Django:将模型对象集转换为单个字典

如何将Django模型对象转换为字典并仍然保留其外键?


2
你可以声明一个名为 to_dict 的方法,并按照你想要的方式处理它。 - karthikr
2
@karthikr 是的,我可以。问题是如何创建这样的方法。手动从模型的所有字段构建字典不是一个合适的答案。 - Zags
我会利用现有的ReST库,如Django Rest Framework,Tastypie或Piston,因为它们都提供将Django模型实例转换为序列化原语的机制。如果您更好奇其中的原理,可以查看它们的代码,但主要是通过遍历模型的'_meta'定义来查找与模型相关联的字段,并在实例上检索它们的值。 - Kevin Stone
18个回答

1
我喜欢将模型实例转换为字典以进行快照测试,以下是我的做法:
注意:有camelize选项,因为如果API响应返回的对象已经camelized,最好保持所有快照一致,无论是从模型实例还是API调用。
from rest_framework import serializers
from djangorestframework_camel_case.util import camelize as _camelize

def model_to_dict(instance, camelize=False):
    """
    Convert a model instance to dict.
    """
    class Serializer(serializers.ModelSerializer):
        class Meta:
            model = type(instance)
            fields = "__all__"
    data = Serializer(instance).data
    if camelize:
        data = _camelize(data)
    # convert from ordered dict to dict
    return dict(data)

1

已经有很多很好的答案了,但是没有一个提到Django的内置序列化器

Django Rest Framework的ModelSerializer被提到了好几次,但这是一个额外的依赖。

内置python.Serializer看起来是一个不错的选择。从 docstring中可以看到:

一个Python“序列化器”。并不会真正地执行序列化操作 - 只是将数据转换为基本的Python数据类型(列表、字典、字符串等)。作为其他序列化器的基础非常有用。

以下是它的使用方法:

from django.core.serializers.python import Serializer

instance = ...  # create instance as in OP

serialized_instance = Serializer().serialize([instance])[0]

输出结果与请求的字典略有不同,但所需字段均已包含。例如:

{
    'fields': {
        'auto_now_add': datetime.datetime(2023, 3, 21, 12, 12, 27, 270255, tzinfo=datetime.timezone.utc),
        'foreign_key': 1,
        'many_to_many': [1],
        'normal_value': 1,
        'readonly_value': 2,
    },
    'model': 'various.somemodel',
    'pk': 1,
}

如果需要,这可以被重构以匹配 OP 的字典。例如:

instance_dict = {
    'id': serialized_instance['pk'],
    **serialized_instance['fields'],
}

如果您已经将一个查询集评估为对象列表,那么这种方法也会很有效。


0

或许这可以帮到你。虽然它不能转换多对多关系,但当你想以JSON格式发送你的模型时,它非常方便。

def serial_model(modelobj):
  opts = modelobj._meta.fields
  modeldict = model_to_dict(modelobj)
  for m in opts:
    if m.is_relation:
        foreignkey = getattr(modelobj, m.name)
        if foreignkey:
            try:
                modeldict[m.name] = serial_model(foreignkey)
            except:
                pass
  return modeldict

0
我使用了next函数将模型转换为字典。
def model_to_dict(obj):
    return {x: obj.__dict__[x] for x in obj.__dict__ if x in {y.column for y in obj._meta.fields}}

示例

{'id': 8985,
 'title': 'Dmitro',
 'email_address': 'it9+8985@localhost',
 'workspace_id': 'it9',
 'archived': False,
 'deleted': False,
 'inbox': False,
 'read': True,
 'created_at': datetime.datetime(2022, 5, 5, 16, 55, 29, 791844, tzinfo=    <UTC>),
 'creator': 'An So',
 'last_message_id': 500566,
 'stat_data': {'count_messages': 1, 'count_attachments': 0},
 'stat_dirty': False,
 'assign_to_id': None,
 'assigned_at': None,
 'assignment_note': None,
 'initial_last_update_ts': 1651769728,
 'renamed_manually': False,
 'unread_timestamp': datetime.datetime(2022, 5, 5, 16, 55, 29, 842507, tzinfo=<UTC>)}

{'id': 6670,
 'email_id': 473962,
 'message_id': 500620,
 'filename': 'Screenshot.png',
 'size': 6076854,
 'mimetype': 'image/png',
 'aws_key': 'dev/RLpdcza46KFpITDWO_kv_fg2732waccB43z5RmT9/Screenshot.png',
 'aws_key1': '',
 'aws_key_thumb': 'dev/iaCdvcZmUKq-gJim7HT33ID46Ng4WOdxx-TdVuIU/f4b0db49-7f2d-4def-bdc1-8e394f98727f.png',
 's3stored_file_id': 4147}

0

@zags的回答很全面,应该足够了但是#5方法(在我看来是最好的)会抛出一个错误,所以我改进了辅助函数。

由于OP要求将many_to_many字段转换为主键列表而不是对象列表,因此我增强了函数,使返回值现在可以JSON序列化-通过将datetime对象转换为strmany_to_many对象转换为id列表。

import datetime

def ModelToDict(instance):
    '''
    Returns a dictionary object containing complete field-value pairs of the given instance

    Convertion rules:

        datetime.date --> str
        many_to_many --> list of id's

    '''

    concrete_fields = instance._meta.concrete_fields
    m2m_fields = instance._meta.many_to_many
    data = {}

    for field in concrete_fields:
        key = field.name
        value = field.value_from_object(instance)
        if type(value) == datetime.datetime:
            value = str(field.value_from_object(instance))
        data[key] = value

    for field in m2m_fields:
        key = field.name
        value = field.value_from_object(instance)
        data[key] = [rel.id for rel in value]

    return data

你遇到了什么错误?我很乐意更新答案。 - Zags
目前,通过“concrete_fields”和“m2m_fields”的循环功能看起来是相同的,因此假设这里“m2m_fields”循环的实现不正确。 - Daniel Himmelstein
@Zags,错误是AttributeError:'list' object has no attribute 'values_list',我找不到原因。我正在使用Django 2.1.1。 - Amin Hemati Nik
@daniel-himmelstein 谢谢您指出问题,我已经修复了代码。相同循环的原因是因为我的本地代码中执行了不同的操作,我忘记为 SO 回答进行优化了。 - Amin Hemati Nik
@ArminHemati Django改变了field.value_from_object的实现方式,因此也影响了model_to_dict的结果。我已经更新了我的答案中的第5节来反映这一点。 - Zags

0

你曾经见过的最佳解决方案。

将django.db.models.Model实例及其所有相关的ForeignKey、ManyToManyField和@Property函数字段转换为字典。

"""
Convert django.db.models.Model instance and all related ForeignKey, ManyToManyField and @property function fields into dict.
Usage:
    class MyDjangoModel(... PrintableModel):
        to_dict_fields = (...)
        to_dict_exclude = (...)
        ...
    a_dict = [inst.to_dict(fields=..., exclude=...) for inst in MyDjangoModel.objects.all()]
"""
import typing

import django.core.exceptions
import django.db.models
import django.forms.models


def get_decorators_dir(cls, exclude: typing.Optional[set]=None) -> set:
    """
    Ref: https://dev59.com/3VTTa4cB1Zd3GeqPpR6_
    :param exclude: set or None
    :param cls:
    :return: a set of decorators
    """
    default_exclude = {"pk", "objects"}
    if not exclude:
        exclude = default_exclude
    else:
        exclude = exclude.union(default_exclude)

    return set([name for name in dir(cls) if name not in exclude and isinstance(getattr(cls, name), property)])


class PrintableModel(django.db.models.Model):

    class Meta:
        abstract = True

    def __repr__(self):
        return str(self.to_dict())

    def to_dict(self, fields: typing.Optional[typing.Iterable]=None, exclude: typing.Optional[typing.Iterable]=None):
        opts = self._meta
        data = {}

        # support fields filters and excludes
        if not fields:
            fields = set()
        else:
            fields = set(fields)
        default_fields = getattr(self, "to_dict_fields", set())
        fields = fields.union(default_fields)

        if not exclude:
            exclude = set()
        else:
            exclude = set(exclude)
        default_exclude = getattr(self, "to_dict_exclude", set())
        exclude = exclude.union(default_exclude)

        # support syntax "field__childField__..."
        self_fields = set()
        child_fields = dict()
        if fields:
            for i in fields:
                splits = i.split("__")
                if len(splits) == 1:
                    self_fields.add(splits[0])
                else:
                    self_fields.add(splits[0])

                    field_name = splits[0]
                    child_fields.setdefault(field_name, set())
                    child_fields[field_name].add("__".join(splits[1:]))

        self_exclude = set()
        child_exclude = dict()
        if exclude:
            for i in exclude:
                splits = i.split("__")
                if len(splits) == 1:
                    self_exclude.add(splits[0])
                else:
                    field_name = splits[0]
                    if field_name not in child_exclude:
                        child_exclude[field_name] = set()
                    child_exclude[field_name].add("__".join(splits[1:]))

        for f in opts.concrete_fields + opts.many_to_many:
            if self_fields and f.name not in self_fields:
                continue
            if self_exclude and f.name in self_exclude:
                continue

            if isinstance(f, django.db.models.ManyToManyField):
                if self.pk is None:
                    data[f.name] = []
                else:
                    result = []
                    m2m_inst = f.value_from_object(self)
                    for obj in m2m_inst:
                        if isinstance(PrintableModel, obj) and hasattr(obj, "to_dict"):
                            d = obj.to_dict(
                                fields=child_fields.get(f.name),
                                exclude=child_exclude.get(f.name),
                            )
                        else:
                            d = django.forms.models.model_to_dict(
                                obj,
                                fields=child_fields.get(f.name),
                                exclude=child_exclude.get(f.name)
                            )
                        result.append(d)
                    data[f.name] = result
            elif isinstance(f, django.db.models.ForeignKey):
                if self.pk is None:
                    data[f.name] = []
                else:
                    data[f.name] = None
                    try:
                        foreign_inst = getattr(self, f.name)
                    except django.core.exceptions.ObjectDoesNotExist:
                        pass
                    else:
                        if isinstance(foreign_inst, PrintableModel) and hasattr(foreign_inst, "to_dict"):
                            data[f.name] = foreign_inst.to_dict(
                                fields=child_fields.get(f.name),
                                exclude=child_exclude.get(f.name)
                            )
                        elif foreign_inst is not None:
                            data[f.name] = django.forms.models.model_to_dict(
                                foreign_inst,
                                fields=child_fields.get(f.name),
                                exclude=child_exclude.get(f.name),
                            )

            elif isinstance(f, (django.db.models.DateTimeField, django.db.models.DateField)):
                v = f.value_from_object(self)
                if v is not None:
                    data[f.name] = v.isoformat()
                else:
                    data[f.name] = None
            else:
                data[f.name] = f.value_from_object(self)

        # support @property decorator functions
        decorator_names = get_decorators_dir(self.__class__)
        for name in decorator_names:
            if self_fields and name not in self_fields:
                continue
            if self_exclude and name in self_exclude:
                continue

            value = getattr(self, name)
            if isinstance(value, PrintableModel) and hasattr(value, "to_dict"):
                data[name] = value.to_dict(
                    fields=child_fields.get(name),
                    exclude=child_exclude.get(name)
                )
            elif hasattr(value, "_meta"):
                # make sure it is a instance of django.db.models.fields.Field
                data[name] = django.forms.models.model_to_dict(
                    value,
                    fields=child_fields.get(name),
                    exclude=child_exclude.get(name),
                )
            elif isinstance(value, (set, )):
                data[name] = list(value)
            else:
                data[name] = value

        return data

https://gist.github.com/shuge/f543dc2094a3183f69488df2bfb51a52


0

将模型转换为字典并保留所有ForiegnKey模型关系。我使用了以下方法:

不带冗长名称

from django.forms.models import model_to_dict

instance = MyModel.objects.get(pk=1) # EXAMPLE

instance_dict = {key: getattr(instance, key) for key in model_to_dict(instance).keys()}

输出

{'foreign_key': [<OtherModel: OtherModel object>],
 'id': 1,
 'many_to_many': [<OtherModel: OtherModel object>],
 'normal_value': 1}

这在想要在外键关系的模板中显示__str __()值时非常有用。
将关键字参数fields= exclude= 包含在model_to_dict(instance,[...])中,使您能够筛选特定字段。 使用详细名称
from django.forms.models import model_to_dict

instance = MyModel.objects.get(pk=1) # EXAMPLE

instance_dict = {instance._meta.get_field(key).verbose_name if hasattr(instance._meta.get_field(key), 'verbose_name') else key: getattr(instance, key) for key in model_to_dict(instance).keys()}

示例输出(如果给定示例有冗长名称)

{'Other Model:': [<OtherModel: OtherModel object>],
 'id': 1,
 'My Other Model:': [<OtherModel: OtherModel object>],
 'Normal Value:': 1}

-2

我创建了一个小 片段, 它利用了 Django 的 model_to_dict, 但遍历对象的关系。

对于循环依赖,它终止递归并放置一个字符串引用依赖对象。您可以扩展它以包括非编辑字段。

我在测试期间使用它来创建模型快照。

from itertools import chain

from django.db.models.fields.files import FileField, ImageField
from django.forms.models import model_to_dict


def get_instance_dict(instance, already_passed=frozenset()):
    """Creates a nested dict version of a django model instance
    Follows relationships recursively, circular relationships are terminated by putting
    a model identificator `{model_name}:{instance.id}`.
    Ignores image and file fields."""
    instance_dict = model_to_dict(
        instance,
        fields=[
            f
            for f in instance._meta.concrete_fields
            if not isinstance(f, (ImageField, FileField))
        ],
    )

    already_passed = already_passed.union(
        frozenset((f"{instance.__class__.__name__}:{instance.id}",))
    )
    # Go through possible relationships
    for field in chain(instance._meta.related_objects, instance._meta.concrete_fields):
        if (
            (field.one_to_one or field.many_to_one)
            and hasattr(instance, field.name)
            and (relation := getattr(instance, field.name))
        ):
            if (
                model_id := f"{relation.__class__.__name__}:{relation.id}"
            ) in already_passed:
                instance_dict[field.name] = model_id
            else:
                instance_dict[field.name] = get_instance_dict(relation, already_passed)

        if field.one_to_many or field.many_to_many:
            relations = []
            for relation in getattr(instance, field.get_accessor_name()).all():
                if (
                    model_id := f"{relation.__class__.__name__}:{relation.id}"
                ) in already_passed:
                    relations.append(model_id)
                else:
                    relations.append(get_instance_dict(relation, already_passed))
            instance_dict[field.get_accessor_name()] = relations

    return instance_dict

虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。-【来自审查】 - Simas Joneliunas
我认为这段文本中包含了必要的部分(遍历树、终止递归和引用对象),但是我可以在此处添加整个代码片段。 - sascha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接