https://monicalent.com/blog/2014/10/31/django-tastypie-reverse-relationships-filtering/ -- 2014.10.31
Tastypie是Django最受欢迎的REST API框架之一,如果你已经熟悉了Django Model,那么使用它会相当轻松。
不过,它会很难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增加字段。如果你有需要,可以查看下面的一些文章资源:
- Tastypie文档
- 一篇blog,介绍如何在
alter_list_data_to_serialize
中加入字段 - StackOverflow问题:如何为Tastypie返回的JSON增加额外对象
- StackOverflow问题:Tastypie是否可以在List和Detail中显示不同的字段集合
排除故障
'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的关系。
请考虑下面的使用场景:
- 你有一个
Task
model - 你想要使用另一个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。