Django REST框架:API版本控制

46

谷歌搜索结果显示,普遍共识是在REST URIs中嵌入版本号是一种不好的做法和坏主意。

即使在SO上也有强烈支持者。
例如:API版本控制的最佳实践?

我的问题是如何通过django-rest-framework使用接受头/内容协商来实现所提出的解决方案。

似乎框架中的内容协商已经配置好了,
http://django-rest-framework.org/api-guide/content-negotiation/ 可根据接受的MIME类型自动返回预期值。如果我开始使用Accept header用于自定义类型,则将失去该框架的此优点。

在框架中是否有更好的方法来实现这一点?

3个回答

54

更新:

版本控制现在得到了很好的支持。


你提供的链接中有一些答案:

我们认为,在 URL 中加入版本号是实用且有用的。这可以让用户一目了然地知道正在使用的版本。我们将 /foo 别名为 /foo/(最新版本),以便于使用、缩短 URL 并使其更加规范,如所接受的答案所建议的那样。 永远保持向后兼容通常是代价高昂和/或非常困难的。我们更喜欢提前公告弃用、重定向等方式,以及文档和其他机制。

因此,我们采用了这种方法,并允许客户端在请求头(X-Version)中指定版本,以下是我们的实现方式:

API 应用程序内部结构:

.
├── __init__.py
├── middlewares.py
├── urls.py
├── v1
│   ├── __init__.py
│   ├── account
│   │   ├── __init__.py
│   │   ├── serializers.py
│   │   └── views.py
│   └── urls.py
└── v2
    ├── __init__.py
    ├── account
    │   ├── __init__.py
    │   ├── serializers.py
    │   └── views.py
    └── urls.py

项目 urls.py:

url(r'^api/', include('project.api.urls', namespace='api')),

API 应用程序级别的 urls.py:

from django.conf.urls import *

urlpatterns = patterns('',
    url(r'', include('project.api.v2.urls', namespace='default')),
    url(r'^v1/', include('project.api.v1.urls', namespace='v1')),
)

版本层级urls.py

from django.conf.urls import *
from .account import views as account_views
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('account', account_views.AccountView)
router.register('myaccount', account_views.MyAccountView)
urlpatterns = router.urls

创建一个中间件,通过更改path_info来切换到正确的代码,请注意,项目级别urls中定义的命名空间('api')不够灵活,需要在中间件中进行知晓。

from django.core.urlresolvers import resolve
from django.core.urlresolvers import reverse


class VersionSwitch(object):

    def process_request(self, request):
        r = resolve(request.path_info)
        version = request.META.get('HTTP_X_VERSION', False)
        if r.namespace.startswith('api:') and version:
            old_version = r.namespace.split(':')[-1]
            request.path_info = reverse('{}:{}'.format(r.namespace.replace(old_version, version), r.url_name), args=r.args, kwargs=r.kwargs)

示例网址:

curl -H "X-Version: v1" http://your.domain:8000/api/myaccount/

4
除了它会破坏超链接字段(如'HyperlinkedRelatedField'),这种方法是可行的。有什么想法吗? - maroux
恐怕在版本控制嵌入到包中之前,没有简单的方法可以修复此问题。因此,它将为“HyperlinkedRelatedField”生成版本感知的URL。 - James Lin
... 除非你喜欢猴子补丁? - James Lin
2
那我们把模型放在哪里呢? - Eyeball
James,你是每个版本有一个应用程序还是一个名为“api”的应用程序和其中的模块? 你是否意味着每个版本都有单独的模型? - Eduard Gamonal
显示剩余7条评论

35

其中一种方法是将版本控制指定为媒体类型的一部分。

这就是GitHub 目前在其API中采用的方式

您还可以在接受标头中包含媒体类型参数,例如Accept: application/json; version=beta,这将成功匹配JSONRenderer。然后,您可以编写视图以根据接受的媒体类型表现出不同的行为,参见此处

API中有许多不同的版本控制模式,我不会说有任何关于正确方法的共识,但这可能是一个合理的选择。


2015年1月更新: 更好的版本支持将在3.1.0发布中推出。请参见[此拉取请求]。 2015年3月更新: 版本API的文档现已提供
(https://github.com/tomchristie/django-rest-framework/pull/2285) 了解更多详情。

1
显然,这个问题已经获得了一个受欢迎的问题徽章,我刚意识到我从未接受答案。感谢Tom在框架上的所有辛勤工作! - w--

1

@James Lin提供了一份很棒的答案。在回答中,@Mar0ux问如何处理破碎的HyperlinkedRelatedField字段。

我通过将HyperlinkedRelatedField更改为SerializerMethodField并使用非常不明显的参数current_app调用reverse来解决这个问题。

例如,我有一个名称为“fruits_app”的应用程序,命名空间版本为“v1”、“v2”。我有一个水果模型的序列化器。因此,要序列化URL,我创建了一个字段

url = serializers.SerializerMethodField()

并对应的方法:

def get_url(self, instance):
    reverse.reverse('fruits_app:fruit-detail',
        args=[instance.pk],
        request=request,
        current_app=request.version)

使用嵌套命名空间时,您需要将这些命名空间添加到current_app中。例如,如果您有一个名为“fruits_app”的应用程序,其中包含命名空间版本“v1”、“v2”和实例命名空间“bananas”,则序列化Fruit url的方法如下:

def get_url(self, instance):
    reverse.reverse('fruits_app:fruit-detail',
        args=[instance.pk],
        request=request,
        current_app='bananas:{}'.format(request.version))

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