https://monicalent.com/blog/2014/10/31/django-tastypie-reverse-relationships-filtering/ -- 2014.10.31

Tastypie是Django最受欢迎的REST API框架之一,如果你已经熟悉了Django Model,那么使用它会相当轻松。

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

不过,它会很难debug,会生成一些诡秘的错误消息。

下面是一些我在使用框架工作时需要解决的问题,贴士和解决问题的方法,以及一些反馈意见。

为Resource加入字段

为Resource加入字段看起来很简单,也确实很简单 -- 但是,有很多方式可以做到,所以你需要决定使用哪种方式才是适合你的。

1.为字段实现专门的dehydrate函数

from tastypie import fields
from tastypie.resources import ModelResource

from app.models import MyModel


class MyModelResource(ModelResource):
    FOO = fiels.CharField()
    
    class Meta:
        queryset = MyModel.objects.all()
        
    def dehydrate_FOO(self):
        return bundle.obj.data.FOO.upper()

在这里,我们在函数名中的下划线后面指明了对象引用(也就是函数dehydrate_FOO会操作FOO字段,在函数中通过bundle.obj来访问.如果你通过某种方式进行了更新,Tastypie会为你自动更新bundle.data['FOO'].

2.实现(resource级别的)dehydrate方法

from tastypie import fields
from tastypie.resources import ModelResource

from app.models import MyModel


class MyModelResource(ModelResource):
    class Meta:
        queryset = MyModel.objects.all()
        
    def dehydrate(self, bundle):
        bundle.data['new_Foo'] = 'This came from nowhere!'
        return bundle

如果你要基于或者不基于其它现有的字段来增加新的字段,使用这种方法就说的通。

3.额外的方法

另外还有一些不同的方式可以手动为Tastypie Resource增加字段。如果你有需要,可以查看下面的一些文章资源:

排除故障

'Bundle'对象不可以使用字典赋值语法.

如果你想要为bundle而不是bundle.data进行字典赋值,会出错。请确保你在操作字段的时候,作用的是bundle.data这个字典.

bundle['new_field'] = 'This will not work.'
bundle.data['new_field'] = 'This works!'

通过外键,外键的反向关系来映射一个对象的属性

这个章节的标题不太好理解,让我给你一些使用场景:

  • 我有一个Grammer话题对象列表
  • 这些话题的内容,以很多不同的语言来编写
  • 每个Content都对Grammer话题有一个ForeignKey关系
  • 在查看Grammer的list view的时候,我想同时看到可用内容的不同语言标题

我的初始JSON:

{
    "meta": {
        "limit": 20,
        "next": "/api/v1/grammar/?offset=20&limit=20&format=json",
        "offset": 0,
        "previous": null,
        "total_count": 1
    },
    "objects": [
        {
            "id": 18,
            "resource_uri": "/api/v1/grammar/18/",
            "name": "First Declension Nouns - Feminine (α-stem)",
        }
    ]
}

我的目标JSON:

{
    meta: {
        limit: 20,
        next: "/api/v1/grammar/?offset=20&limit=20&format=json",
        offset: 0,
        previous: null,
        total_count: 1
    },
    objects: [
        {
            id: 18,
            resource_uri: "/api/v1/grammar/18/",
            name: "First Declension Nouns - Feminine (α-stem)",
            titles: {
                de: "Die a-Deklination",
                en: "First Declension Nouns",
                it: "Sostantivi femminili"
            }
        }
    ]
}

你可以看到,目标是把关联的content标题组建为字典的形式,使用它们语言的short_code作为字典的键。我们需要首选获取content,通过grammer来过滤,最后将它们映射为字典。

下面是相关的Django Models:

import textwrap

from django.db import models


class Language(models.Model):
    name = models.CharField("Language name(english)",
                            max_length=200,
                            help_text=('e.g. German)')
    short_code = models.CharField('shortcode',
                                  max_length=5,
                                  
    def __unicode__(self):
        return unicode(self.name) or u""
        
        
class Grammer(models.Model):
    name = models.CharField("title of grammer section",
                            max_length=200,
                            help_text=textwrap.dedent("""
                                Short, descriptive title of the grammer 
                                concept.
                            """))
        

class Content(models.Model):
    title = models.CharField('title',
                             max_length=200,
                             help_text=textwrap.dedent("""
                                Short, descriptive title of the grammer 
                                concept.
                            """))
    grammer_ref = models.ForeignKey(Grammer,
                                    verbose_name='grammer topic',
                                    null=True,
                                    blank=True,
                                    help_text=textwrap.dedent("""
                                        The morphology directly
                                        described by this content.
                                    """))
    source_lang = models.ForeignKey(Language,
                                    related_name='content_written_in',
                                    help_text=textwrap.dedent("""
                                        Language the content is written
                                        in.
                                    """))
    target_lang = models.ForeignKey(Language,
                                    related_name='content_written_about',
                                    help_text="Language the content teaches.")
    content = models.TextField("Learning Content",
                               help_text=textwrap.dedent("""
                                    Write this in Markdown.
                               """))
    
    def __unicode__(self):
        return unicode(self.title) or u""

api/grammer.py - GrammerResource使用dehydrate函数来为resource对象增加新的字段,另外加入了一个helper函数用来帮助reduce掉content对象列表。

from tastypie import fields
from tastypie.resources import ModelResource

from app.models import Grammer, Content
from api.content import ContentResource


class GrammerResource(ModelResource):
    # 在这里,我们使用反向关系来获取和这个grammer关联的content对象
    content = fields.ToManyField(ContentResource, 'content_set',
                                 related_name='content',
                                 blank=True,
                                 null=True,
                                 use_in='detail',
                                 full=True)
    
    class Meta:
        queryset = Grammer.objects.all()
        allowed_methods = ['get']
        
    def build_title(self, memo, content):
        lang = content.resource_lang.short_code
        memo[lang] = content.title
        return momo
        
    def dehydrate(self, bundle):
        bundle.data['titles'] = reduce(self.build_title, 
                                       Content.objects.filter(grammer_ref=bundle.obj), {})
        return bundle

如果你已经习惯map/reduce,那么这个代码是能够自解释的。

额外的资源

通过关系来过滤

有一件事,Tastypie看起来没有良好的支持,就是通过值来过滤model的关系。

请考虑下面的使用场景:

  • 你有一个Taskmodel
  • 你想要使用另一个model(TaskSequence)来排序这个tasks
  • 你将一个Task和一个TaskSequence以及其它的元数据关联起来(通过through关系,一个叫做TastContext的model,它包含task顺序的信息)

如果你只告诉Tastypie要`TaskSequence,你只会得到下面的数据:

{
   id: 60,
   name: "The Aorist Tense",
   query: "pos=verb&tense;=aor",
   ref: "s542,s546",
   resource_uri: "/api/v1/grammar/60/",
   task_sequence: {
      id: 2,
      name: "Verbs for Beginners",
      resource_uri: "",
      tasks: [
          {
              endpoint: "word",
              hint_msg: "Try again.",
              id: 4,
              name: "identify_morph:person",
              success_msg: "Good job!"
          },
          {
              endpoint: "word",
              hint_msg: "Try again.",
              id: 5,
              name: "identify_morph:number"
              success_msg: "Good job!"
          }
    ]
}

不过,我们需要关心的through表,来决定task的顺序。我们希望的JSON是下面这样:

{
   id: 60,
   name: "The Aorist Tense",
   query: "pos=verb&tense;=aor",
   ref: "s542,s546",
   resource_uri: "/api/v1/grammar/60/",
   task_sequence: {
      id: 2,
      name: "Verbs for Beginners",
      resource_uri: "",
      tasks: [
          {
              id: 4,
              max_attempts: 10,
              order: 0,
              resource_uri: "",
              target_accuracy: 0.5,
              task: {
                  endpoint: "word",
                  hint_msg: "Try again.",
                  id: 4,
                  name: "identify_morph:person",
                  success_msg: "Good job!"
              }
          },
          {
              id: 5,
              max_attempts: 5,
              order: 1,
              target_accuracy: 0.8,
              task: {
                  endpoint: "word",
                  hint_msg: "Try again.",
                  id: 5,
                  name: "identify_morph:number",
                  success_msg: "Good job!"
              }
          }
    ]
}

相关Resources

请注意看下面的三个Tastypie Resource。有趣的代码出现在`TaskSequenceResource。我们在这里通过through表来过滤了tasks.

"""
api/task.py
"""
from tastypie.resources import ModelResource

from app.models import Task


class TaskResource(ModelResource):
    
    class Meta:
        queryset = Task.objects.all()
        allowed_methods = ['get']
"""
api/task_context.py
"""
from tastypie import fields
from tastypie.resources import ModelResource

from app.models import TaskContext


class TaskContextResource(ModelResource):
    task = fields.ToOneField('api.task.TaskResource',
                             'task',
                             full=True,
                             null=True,
                             blank=True)
    
    class Meta:
        queryset = TaskContext.objects.all()
        allowed_methods = ['get']
"""
api/task_consequence.py
"""
from tastypie import fields
from tastypie.resources import ModelResource

from app.models import TaskSequence


class TaskSequenceResource(ModelResource):
    tasks = fields.ManyToManyFields('api.task.TaskContextResource',
                                    attribute=lambda bundle:
                                    bundle.obj.tasks.through.objects.filter(
                                        task_sequence=bundle.obj) or bundle.obj.tasks,
                                    full=True)
    
    class Meta:
        queryset = TaskSequence.objects.all()
        allowed_methods = ['get']

故障排查

对象是没有through属性的.

请注意,lambda bundle: bundle.obj.through.objects是错的,因为中间缺少了tasks。through只有在queryset中有。

自引用Resources

有时候,Model需要引用自身。例如,有一个Person model,这个model可能会有很多人际关系(关系的类型也是Person)。

难点出来了,在每个Person Model都有一个关系列表的时候。很可能会出现下面的错误:

Maximum recursion depth exceeded

1.让Model的关系不对称(并且不要设置full=True)

在我们的例子中,这意味着PersonA可以关联PersonB,但是反过来不行。

这让Tastypie可以很容易的处理:

# app/models.py

from django.db import models


class Person(models.Model):
    relatives = models.ManyToManyField('self',
                    related_name='relates_to',
                    symmetrical=False,
                    null=True,
                    blank=True)
# api/person.py

from tastypie import fields
from tastypie.resources import ModelResource

from myapp.models import Person


class PersonResource(ModelResource):
    relatives = fields.ToManyField('self', 'relatives')
    
    class Meta:
        queryset = Person.objects.all()

2.使用use_in选项

from tastypie import fields
from tastypie.resources import ModelResource

from myapp.models import Person


class PersonResource(ModelResource):
    relatives = fields.ToManyField('self', 'relatives', use_in='list')
    
    class Meta:
        queryset = Person.objects.all()

使用这种方式,relatives字段不会在detail view中显示。

3.创建'shallow'版本的resource

如果你需要在你的list view中使用full=True。最简单避免无限递归的方式是,创建两个resource:

# api/person.py
from tastypie import fields
from tastypie.resources import ModelResource

from myapp.models import Person
from api.relative import RelativeResource


class PersonResource(ModelResource):
    relatives = fields.ManyToManyField(RelativeResource,
                                       'relatives',
                                       null=True,
                                       blank=True,
                                       full=True)
                                       
    class Meta:
        queryset = Person.objects.all()
        allow_methods = ['get'] 
# api/relative.py
from tastypie import fields
from tastypie.resource import ModelResource

from myapp.models import Person


class RelativeResource(ModelResource):
    class Meta:
        queryset = Person.objects.all()
        allow_methods = ['get']

请注意,只有PersonResource设定了full=True。因为RelativeResource没有设定m2m字段,所以就不会进入无尽循环。

故障排查

Options对象没有api_name属性

请注意你要指向resource,而不是model。

额外资源

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄