Django中具有模型继承的RESTful API

3
我需要使用Django的REST Framwork实现一个关注模型继承的RESTful API。我认为是一种整洁的方法来构建API。其基本思想是仅通过呈现的属性集来区分对象类。但如何在Django中实现呢?让我们坚持一个简单的示例场景:
  • Animal具有属性age
  • Bird通过属性wing(例如其大小)扩展了Animal
  • Dog通过属性tail(例如其长度)扩展了Animal

示例需求:

  • 如果/animals/列出所有动物及其通用属性,即它们的年龄,则足以满足要求。
  • 假设带有ID = 1的动物是一只鸟,则/animals/1应提供以下内容:

{
    'age': '3',
    'class': 'Bird',
    'wing': 'big'
}

假设ID为2的动物是一只狗,那么/animals/2应该返回类似以下内容的信息:
{
    'age': '8',
    'class': 'Dog',
    'tail': 'long'
}

我尝试使用Django的REST框架,但运气不佳,主要是因为我没有成功地添加/删除特定类别的字段。特别是我想知道,在这种情况下如何实现创建操作?

1个回答

4

tl;dr: 这个解决方案提供了一个可完全重用的类,以最少的代码实现所需用例,如下面的示例使用情况所示。不要被外表吓到,它并不复杂。


经过一些尝试,我想出了以下解决方案,我相信这是非常令人满意的。我们将需要一个辅助函数和两个类,没有进一步的依赖关系。

HyperlinkedModelSerializer 的扩展

假设一个查询返回了一个Animal类的对象,而实际上它是一只Bird。那么get_actual将解析该AnimalBird类的对象:

def get_actual(obj):
    """Expands `obj` to the actual object type.
    """
    for name in dir(obj):
        try:
            attr = getattr(obj, name)
            if isinstance(attr, obj.__class__):
                return attr
        except:
            pass
    return obj

ModelField 定义了一个字段,用于命名在序列化器中作为基础模型的模型名称:

class ModelField(serializers.ChoiceField):
    """Defines field that names the model that underlies a serializer.
    """

    def __init__(self, *args, **kwargs):
        super(ModelField, self).__init__(*args, allow_null=True, **kwargs)

    def get_attribute(self, obj):
        return get_actual(obj)

    def to_representation(self, obj):
        return obj.__class__.__name__

通过使用HyperlinkedModelHierarchySerializer,可以实现以下功能:

class HyperlinkedModelHierarchySerializer(serializers.HyperlinkedModelSerializer):
    """Extends the `HyperlinkedModelSerializer` to properly handle class hierearchies.

    For an hypothetical model `BaseModel`, serializers from this
    class are capable of also handling those models that are derived
    from `BaseModel`.

    The `Meta` class must whitelist the derived `models` to be
    allowed. It also must declare the `model_dependent_fields`
    attribute and those fields must also be added to its `fields`
    attribute, for example:

        wing = serializers.CharField(allow_null=True)
        tail = serializers.CharField(allow_null=True)

        class Meta:
            model = Animal
            models = (Bird, Dog)
            model_dependent_fields = ('wing', 'tail')
            fields = ('model', 'id', 'name') + model_dependent_fields
            read_only_fields = ('id',)

    The `model` field is defined by this class.
    """
    model = ModelField(choices=[])

    def __init__(self, *args, **kwargs):
        """Instantiates and filters fields.

        Keeps all fields if this serializer is processing a CREATE
        request. Retains only those fields that are independent of
        the particular model implementation otherwise.
        """
        super(HyperlinkedModelHierarchySerializer, self).__init__(*args, **kwargs)
        # complete the meta data
        self.Meta.models_by_name = {model.__name__: model for model in self.Meta.models}
        self.Meta.model_names = self.Meta.models_by_name.keys()
        # update valid model choices,
        # mark the model as writable if this is a CREATE request
        self.fields['model'] = ModelField(choices=self.Meta.model_names, read_only=bool(self.instance))
        def remove_missing_fields(obj):
            # drop those fields model-dependent fields that `obj` misses
            unused_field_keys = set()
            for field_key in self.Meta.model_dependent_fields:
                if not hasattr(obj, field_key):
                    unused_field_keys |= {field_key}
            for unused_field_key in unused_field_keys:
                self.fields.pop(unused_field_key)
        if not self.instance is None:
            # processing an UPDATE, LIST, RETRIEVE or DELETE request
            if not isinstance(self.instance, QuerySet):
                # this is an UPDATE, RETRIEVE or DELETE request,
                # retain only those fields that are present on the processed instance
                self.instance = get_actual(self.instance)
                remove_missing_fields(self.instance)
            else:
                # this is a LIST request, retain only those fields
                # that are independent of the particular model implementation
                for field_key in self.Meta.model_dependent_fields:
                    self.fields.pop(field_key)

    def validate_model(self, value):
        """Validates the `model` field.
        """
        if self.instance is None:
            # validate for CREATE
            if value not in self.Meta.model_names:
                raise serializers.ValidationError('Must be one of: ' + (', '.join(self.Meta.model_names)))
            else:
                return value
        else:
            # model cannot be changed
            return get_actual(self.instance).__class__.__name__

    def create(self, validated_data):
        """Creates instance w.r.t. the value of the `model` field.
        """
        model = self.Meta.models_by_name[validated_data.pop('model')]
        for field_key in self.Meta.model_dependent_fields:
            if not field_key in model._meta.get_all_field_names():
                validated_data.pop(field_key)
                self.fields.pop(field_key)
        return model.objects.create(**validated_data)

使用示例

以下是我们如何使用它。在serializers.py中:

class AnimalSerializer(HyperlinkedModelHierarchySerializer):

    wing = serializers.CharField(allow_null=True)
    tail = serializers.CharField(allow_null=True)

    class Meta:
        model = Animal
        models = (Bird, Dog)
        model_dependent_fields = ('wing', 'tail')
        fields = ('model', 'id', 'name') + model_dependent_fields
        read_only_fields = ('id',)

views.py中:

class AnimalViewSet(viewsets.ModelViewSet):
    queryset = Animal.objects.all()
    serializer_class = AnimalSerializer

请注意,如果您在模型中使用类继承,将会有多个表格和任何查询的隐式连接。您最好使用抽象基类 - dukebody
1
@dukebody:没错,但是如果使用一个抽象基类Animal,我就不能像这样查询Animal.objects.all(),也不能将Dog.objects.all()Bird.objects.all()合并起来,这会让处理Animal对象变得非常不方便。 - theV0ID
好的!你可能想要考虑在动物模型中使用单独的“额外”JSON字段来存储特定的动物属性,或者使用 django-rest-framework-mongoengine 这样的工具,如果你的动物与其他关联字段无关。 - dukebody
@dukebody:你能否详细解释一下在“Animal”模型中“extra” JSON字段的含义? - theV0ID
请参阅 https://www.vividcortex.com/blog/2015/06/02/json-support-postgres-mysql-mongodb-sql-server/,其中介绍了关于Postgres、MySQL、MongoDB和SQL Server的JSON支持。此外,Django也有JSON字段实现:https://www.djangopackages.com/grids/g/json-fields/。 - dukebody
Django rest已经发生了变化,不幸的是,它似乎不再能够直接使用。看起来"super(HyperlinkedModelHierarchySerializer, self)"试图验证给定的字段是否实际上是我们初始化的模型的一部分。 - Alex Davies

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