【干货】Flask-REST-JSONAPI官方文档翻译
github: https://github.com/miLibris/flask-rest-jsonapi/
英文文档: https://flask-rest-jsonapi.readthedocs.io
中文翻译: https://flask-rest-jsonapi-cn.readthedocs.io
00-简介
Flask-REST-JSONAPI 是一款快速构建REST API的Flask插件。框架在遵循JSONAPI 1.0规范的同时,保持着极强的灵活性。框架设计之初,就考虑到了生产环境的复杂性,所以在Flask-REST-JSONAPI中就引入了逻辑数据抽象层的概念,框架中叫做Resource(资源)。Resource可以通过Data layer(数据层)接入任何ORM框架或数据库。(译者注:逻辑数据对应的是物理数据,数据库储存的是物理数据,但有时API返回的数据,不能直接返回数据库的物理数据,需要进行逻辑加工后,才能返回给用户。)
核心概念

-
JSON API 1.0 规范](http://jsonapi.org/):广受欢迎的REST JSON API规范,规定了客户端与服务端进行数据交互的方式。遵循规范有助于进行团队合作开发,因为规范的表达十分精确,且易于分享。正是由于规范,API也包含了许多特性,像健壮的request和response结构、过滤、分页、稀疏字段、外键查询以及强大的错误格式化等。
-
逻辑数据抽象层:实际开发中,我们需要返回数据资源给客户端,但通常来说,返回给用户的数据并与数据库的表结构完全相同。比如,有时我们不想暴露锁一个表的所有属性,亦或需要计算额外的属性,亦或创建资源的数据来自于不同的数据库。Flask-REST-JSONAPI 通过Marshmallow / marshmallow-jsonapi 为我们创建逻辑数据抽象层,便于你以灵活的方式暴露你的数据。
-
数据层:数据层在框架中作为数据CRUD的接口,将资源和数据的连接起来。正是由于引入了数据层,我们能使用任何数据库或者ORM框架来对接资源。框架已经实现了SQLALchemy ORM的所有功能,但同时我们也可以定制数据层,来接入自己数据库的数据。我们还可以在数据层接入多个数据库和ORM,发送通知或在CRUD过程中加入定制功能。
特性
Flask-REST-JSONAPI 拥有的特性包括:
-
关系数据管理
-
强大的过滤功能
-
外键包含查询
-
稀疏字段
-
分页
-
排序
-
权限管理
-
OAuth支持
用户指南
以下文档演示了如何配合Flask使用Flask-REST-JSONAPI。
目录
API参考
如果你想要查找函数、类或方法的具体信息,以下文档可供你参考。
01-安装
使用pip
安装Flask-REST-JSONAPI
$ pip install flask-rest-jsonapi
开发版本可以在its page at GitHub下载。
git clone https://github.com/miLibris/flask-rest-jsonapi.git
cd flask-rest-jsonapi
mkvirtualenv venv
python setup.py install
提醒
如果你没有安装
virtualenv
,请先安装它$ pip install virtualenv
Flask-REST-JSONAPI
需要Python 2.7或3.4+。
02-快速上手
现在我们就来写第一个REST API。这个教程会假设你是熟悉Flask的,并且确保你已经安装了Flask和Flask-REST-JSONAPI。如果还没有的话,可以先查看安装章节的教程。
本章节,我们会围绕实际的例子和简短的教程,来介绍Flask-REST-JSONAPI的基础使用方法。本教程的数据层,我们将选用默认的Flask-REST-JSONAPI数据层SQLAlchemy。下面,我们就来演示person和computers的API例子。
第一个例子
如下所示,代码构建了Flask-REST-JSONAPI的API:
# -*- coding: utf-8 -*-
from flask import Flask
from flask_rest_jsonapi import Api, ResourceDetail, ResourceList, ResourceRelationship
from flask_rest_jsonapi.exceptions import ObjectNotFound
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
from marshmallow_jsonapi.flask import Schema, Relationship
from marshmallow_jsonapi import fields
# Create the Flask application
app = Flask(__name__)
app.config['DEBUG'] = True
# Initialize SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
# Create data storage
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
email = db.Column(db.String)
birth_date = db.Column(db.Date)
password = db.Column(db.String)
class Computer(db.Model):
id = db.Column(db.Integer, primary_key=True)
serial = db.Column(db.String)
person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
person = db.relationship('Person', backref=db.backref('computers'))
db.create_all()
# Create logical data abstraction (same as data storage for this first example)
class PersonSchema(Schema):
class Meta:
type_ = 'person'
self_view = 'person_detail'
self_view_kwargs = {'id': '<id>'}
self_view_many = 'person_list'
id = fields.Integer(as_string=True, dump_only=True)
name = fields.Str(required=True, load_only=True)
email = fields.Email(load_only=True)
birth_date = fields.Date()
display_name = fields.Function(lambda obj: "{} <{}>".format(obj.name.upper(), obj.email))
computers = Relationship(self_view='person_computers',
self_view_kwargs={'id': '<id>'},
related_view='computer_list',
related_view_kwargs={'id': '<id>'},
many=True,
schema='ComputerSchema',
type_='computer')
class ComputerSchema(Schema):
class Meta:
type_ = 'computer'
self_view = 'computer_detail'
self_view_kwargs = {'id': '<id>'}
id = fields.Integer(as_string=True, dump_only=True)
serial = fields.Str(required=True)
owner = Relationship(attribute='person',
self_view='computer_person',
self_view_kwargs={'id': '<id>'},
related_view='person_detail',
related_view_kwargs={'computer_id': '<id>'},
schema='PersonSchema',
type_='person')
# Create resource managers
class PersonList(ResourceList):
schema = PersonSchema
data_layer = {'session': db.session,
'model': Person}
class PersonDetail(ResourceDetail):
def before_get_object(self, view_kwargs):
if view_kwargs.get('computer_id') is not None:
try:
computer = self.session.query(Computer).filter_by(id=view_kwargs['computer_id']).one()
except NoResultFound:
raise ObjectNotFound({'parameter': 'computer_id'},
"Computer: {} not found".format(view_kwargs['computer_id']))
else:
if computer.person is not None:
view_kwargs['id'] = computer.person.id
else:
view_kwargs['id'] = None
schema = PersonSchema
data_layer = {'session': db.session,
'model': Person,
'methods': {'before_get_object': before_get_object}}
class PersonRelationship(ResourceRelationship):
schema = PersonSchema
data_layer = {'session': db.session,
'model': Person}
class ComputerList(ResourceList):
def query(self, view_kwargs):
query_ = self.session.query(Computer)
if view_kwargs.get('id') is not None:
try:
self.session.query(Person).filter_by(id=view_kwargs['id']).one()
except NoResultFound:
raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
else:
query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
return query_
def before_create_object(self, data, view_kwargs):
if view_kwargs.get('id') is not None:
person = self.session.query(Person).filter_by(id=view_kwargs['id']).one()
data['person_id'] = person.id
schema = ComputerSchema
data_layer = {'session': db.session,
'model': Computer,
'methods': {'query': query,
'before_create_object': before_create_object}}
class ComputerDetail(ResourceDetail):
schema = ComputerSchema
data_layer = {'session': db.session,
'model': Computer}
class ComputerRelationship(ResourceRelationship):
schema = ComputerSchema
data_layer = {'session': db.session,
'model': Computer}
# Create endpoints
api = Api(app)
api.route(PersonList, 'person_list', '/persons')
api.route(PersonDetail, 'person_detail', '/persons/<int:id>', '/computers/<int:computer_id>/owner')
api.route(PersonRelationship, 'person_computers', '/persons/<int:id>/relationships/computers')
api.route(ComputerList, 'computer_list', '/computers', '/persons/<int:id>/computers')
api.route(ComputerDetail, 'computer_detail', '/computers/<int:id>')
api.route(ComputerRelationship, 'computer_person', '/computers/<int:id>/relationships/owner')
if __name__ == '__main__':
# Start application
app.run(debug=True)
这个例子提供了以下API接口:
url | method | endpoint | action |
---|---|---|---|
/persons | GET | person_list | 获取persons集合 |
/persons | POST | person_list | 创建一个person |
/persons/<int:id> | GET | person_detail | 获取一个person的详情 |
/persons/<int:id> | PATCH | person_detail | 更新一个person |
/persons/<int:id> | DELETE | person_detail | 删除一个person |
/persons/<int:id>/computers | GET | computer_list | 获取同某个person关联的computers集合 |
/persons/<int:id>/computers | POST | computer_list | 创建一个computer,并将其关联到某个person |
/persons/<int:id>/relationship/computers | GET | person_computers | 获取person与computers的关系 |
/persons/<int:id>/relationship/computers | POST | person_computers | 创建person与computers的关系 |
/persons/<int:id>/relationship/computers | PATCH | person_computers | 更新person与computers的关系 |
/persons/<int:id>/relationship/computers | DELETE | person_computers | 删除person与computers的关系 |
/computers | GET | computer_list | 获取computers集合 |
/computers | POST | computer_list | 创建一个computer |
/computers/<int:id> | GET | computer_detail | 获取一个computer的详情 |
/computers/<int:id> | PATCH | computer_detai | 更新一个computer |
/computers/<int:id> | DELETE | computer_detail | 删除一个computer |
/computers/<int:id>/owner | GET | person_detail | 获取某个computer的owner的详情 |
/computers/<int:id>/owner | PATCH | person_detail | 更新一个computer的owner |
/computers/<int:id>/owner | DELETE | person_detail | 删除一个computer的owner |
/computers/<int:id>/relationship/owner | GET | person_computers | 获取person与computer的关系 |
/computers/<int:id>/relationship/owner | POST | person_computers | 创建person与computer的关系 |
/computers/<int:id>/relationship/owner | PATCH | person_computers | 更新person与computer的关系 |
/computers/<int:id>/relationship/owner | DELETE | person_computers | 删除person与computer的关系 |
警告
本例中,我们使用了Flask-SQLAlchemy,所以确保你运行前已经安装。
$ pip install flask_sqlalchemy
保存这个文件到api.py,然后用Python运行。请注意代码中我们开启了 Flask debugging 模式,这样代码就在修改后自动重载,并且能更好地提示报错信息。
$ python api.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader
警告
请在生产环境中关闭Debug模式。
经典的CRUD操作
创建对象
Request:
POST /computers HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "computer",
"attributes": {
"serial": "Amstrad"
}
}
}
Response:
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
"data": {
"type": "computer",
"id": "1",
"attributes": {
"serial": "Amstrad"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/1/owner",
"self": "/computers/1/relationships/owner"
}
}
},
"links": {
"self": "/computers/1"
}
},
"links": {
"self": "/computers/1"
},
"jsonapi": {
"version": "1.0"
}
}
获取对象列表
Request:
GET /computers HTTP/1.1
Accept: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"type": "computer",
"id": "1",
"attributes": {
"serial": "Amstrad"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/1/owner",
"self": "/computers/1/relationships/owner"
}
}
},
"links": {
"self": "/computers/1"
}
}
],
"meta": {
"count": 1
},
"links": {
"self": "/computers"
},
"jsonapi": {
"version": "1.0"
},
}
更新对象
Request:
PATCH /computers/1 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "computer",
"id": "1",
"attributes": {
"serial": "Amstrad 2"
}
}
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"type": "computer",
"id": "1",
"attributes": {
"serial": "Amstrad 2"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/1/owner",
"self": "/computers/1/relationships/owner"
}
}
},
"links": {
"self": "/computers/1"
}
},
"links": {
"self": "/computers/1"
},
"jsonapi": {
"version": "1.0"
}
}
删除对象
Request:
DELETE /computers/1 HTTP/1.1
Accept: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"meta": {
"message": "Object successfully deleted"
},
"jsonapi": {
"version": "1.0"
}
}
关系
现在我们来看一下关系的使用。第一步,如同上例中,分别创建3个名叫Halo,Nestor和Comodor的电脑。
完成了?接下来,我们继续。
我们假设Halo的id是2,Nestor的id是3,Comodor的id是4。
创建带有关系的对象
Request:
POST /persons?include=computers HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "person",
"attributes": {
"name": "John",
"email": "john@gmail.com",
"birth_date": "1990-12-18"
},
"relationships": {
"computers": {
"data": [
{
"type": "computer",
"id": "1"
}
]
}
}
}
}
Response:
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json
{
"data": {
"type": "person",
"id": "1",
"attributes": {
"display_name": "JOHN <john@gmail.com>",
"birth_date": "1990-12-18"
},
"links": {
"self": "/persons/1"
},
"relationships": {
"computers": {
"data": [
{
"id": "1",
"type": "computer"
}
],
"links": {
"related": "/persons/1/computers",
"self": "/persons/1/relationships/computers"
}
}
},
},
"included": [
{
"type": "computer",
"id": "1",
"attributes": {
"serial": "Amstrad"
},
"links": {
"self": "/computers/1"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/1/owner",
"self": "/computers/1/relationships/owner"
}
}
}
}
],
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "/persons/1"
}
}
你可以看到,我在url中添加了查询语句参数“include”。
POST /persons?include=computers HTTP/1.1
正是使用了这个参数,在创建人的同时,也允许添加了关联的电脑。如果你想了解更多细节,请查看包含关联对象章节。
更新对象及其关系
现在,John卖了他的电脑Amstrad,同时买了台新电脑Nestor(id:3)。所以我们需要关联这个新电脑给John。另外,John还填错了生日,所以我们现在需要同时更新两个数据。
Request:
PATCH /persons/1?include=computers HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "person",
"id": "1",
"attributes": {
"birth_date": "1990-10-18"
},
"relationships": {
"computers": {
"data": [
{
"type": "computer",
"id": "3"
}
]
}
}
}
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"type": "person",
"id": "1",
"attributes": {
"display_name": "JOHN <john@gmail.com>",
"birth_date": "1990-10-18",
},
"links": {
"self": "/persons/1"
},
"relationships": {
"computers": {
"data": [
{
"id": "3",
"type": "computer"
}
],
"links": {
"related": "/persons/1/computers",
"self": "/persons/1/relationships/computers"
}
}
},
},
"included": [
{
"type": "computer",
"id": "3",
"attributes": {
"serial": "Nestor"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/3/owner",
"self": "/computers/3/relationships/owner"
}
}
},
"links": {
"self": "/computers/3"
}
}
],
"links": {
"self": "/persons/1"
},
"jsonapi": {
"version": "1.0"
}
}
创建关系
现在John买了台叫Comodor的新电脑,让我们把他关联给John。
Request:
POST /persons/1/relationships/computers HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": [
{
"type": "computer",
"id": "4"
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"type": "person",
"id": "1",
"attributes": {
"display_name": "JOHN <john@gmail.com>",
"birth_date": "1990-10-18"
},
"relationships": {
"computers": {
"data": [
{
"id": "3",
"type": "computer"
},
{
"id": "4",
"type": "computer"
}
],
"links": {
"related": "/persons/1/computers",
"self": "/persons/1/relationships/computers"
}
}
},
"links": {
"self": "/persons/1"
}
},
"included": [
{
"type": "computer",
"id": "3",
"attributes": {
"serial": "Nestor"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/3/owner",
"self": "/computers/3/relationships/owner"
}
}
},
"links": {
"self": "/computers/3"
}
},
{
"type": "computer",
"id": "4",
"attributes": {
"serial": "Comodor"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/4/owner",
"self": "/computers/4/relationships/owner"
}
}
},
"links": {
"self": "/computers/4"
}
}
],
"links": {
"self": "/persons/1/relationships/computers"
},
"jsonapi": {
"version": "1.0"
}
}
删除关系
现在,John卖了旧的Nestor电脑,让我们给John移除关联。
Request:
DELETE /persons/1/relationships/computers HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": [
{
"type": "computer",
"id": "3"
}
]
}
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"type": "person",
"id": "1",
"attributes": {
"display_name": "JOHN <john@gmail.com>",
"birth_date": "1990-10-18"
},
"relationships": {
"computers": {
"data": [
{
"id": "4",
"type": "computer"
}
],
"links": {
"related": "/persons/1/computers",
"self": "/persons/1/relationships/computers"
}
}
},
"links": {
"self": "/persons/1"
}
},
"included": [
{
"type": "computer",
"id": "4",
"attributes": {
"serial": "Comodor"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/4/owner",
"self": "/computers/4/relationships/owner"
}
}
},
"links": {
"self": "/computers/4"
}
}
],
"links": {
"self": "/persons/1/relationships/computers"
},
"jsonapi": {
"version": "1.0"
}
}
如果你想了解更多例子,请访问 JSON API 1.0 specification
03-逻辑数据抽象层
当开始一个项目时,我们要做的第一件事就是创建逻辑数据抽象层。这一步,是为了描述Schema(资源结构),它表示哪些数据是完全映射数据库的数据结构,哪些是不完全映射。声明资源结构是使用Marshmallow / marshmallow-jsonapi框架。Marshmallow是一个非常著名的序列化和反序列化的框架,它提供了许多可以用来抽象数据结构的特性;marshmallow-jsonapi则更进一步实现了JSONAPI 1.0规范,并且支持Flask集成。
例:
本例中,我们假设已经有了Person和Computer的ORM model,我们需要为它们创建数据抽象层。
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
email = db.Column(db.String)
birth_date = db.Column(db.String)
password = db.Column(db.String)
class Computer(db.Model):
computer_id = db.Column(db.Integer, primary_key=True)
serial = db.Column(db.String)
person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
person = db.relationship('Person', backref=db.backref('computers'))
接下来,让我们创建逻辑数据抽象层,来解释model与资源的映射关系。
from marshmallow_jsonapi.flask import Schema, Relationship
from marshmallow_jsonapi import fields
class PersonSchema(Schema):
class Meta:
type_ = 'person'
self_view = 'person_detail'
self_view_kwargs = {'id': '<id>'}
self_view_many = 'person_list'
id = fields.Integer(as_string=True, dump_only=True)
name = fields.Str(required=True, load_only=True)
email = fields.Email(load_only=True)
birth_date = fields.Date()
display_name = fields.Function(lambda obj: "{} <{}>".format(obj.name.upper(), obj.email))
computers = Relationship(self_view='person_computers',
self_view_kwargs={'id': '<id>'},
related_view='computer_list',
related_view_kwargs={'id': '<id>'},
many=True,
schema='ComputerSchema',
type_='computer',
id_field='computer_id')
class ComputerSchema(Schema):
class Meta:
type_ = 'computer'
self_view = 'computer_detail'
self_view_kwargs = {'id': '<id>'}
id = fields.Str(as_string=True, dump_only=True, attribute='computer_id')
serial = fields.Str(required=True)
owner = Relationship(attribute='person',
self_view='computer_person',
self_view_kwargs={'id': '<id>'},
related_view='person_detail',
related_view_kwargs={'computer_id': '<id>'},
schema='PersonSchema',
type_='person')
我们能看到数据库model 与 schema的几个区别。
首先,让我们来比较下Person和PersonSchema:
- 我们能看到Person有password字段,但我们不想在API中暴露它,所以在PersonSchema中,没有设定该属性。
- PersonSchema中,有一个display_name属性,该属性实际上是拼接了name和email字段。
- 在PersonSchema中,我们定义了一个computers=Relationship(...),我们设定了id_field='computer_id',因为它是Computer(db.model)的主键。如果你没有设定id_field,那么它的默认值就是‘id’。
其次,让我们再来比较下Computer和ComputerSchema:
- computer_id字段,在api中,是以id属性呈现的。
- Computer与Person的关联字段person,在api中,是以owner属性呈现的,因为这样表达更直观。
最终,可以发现,基于数据库中的数据结构,我们能以非常灵活的方式,选择性地暴露数据。
04-资源管理器
资源管理器,起着连接器的作用,它连接着数据抽象层、数据层以及其他可选的功能。其内部可以管理资源的各项逻辑。
Flask-REST-JSONAPI提供了三种资源管理器,实现了JSONAPI 1.0规范的默认方法。
- ResourceList:提供GET和POST方法,分别用于读取资源列表和创建资源。
- ResourceDetail:提供GET、PATCH和DELETE方法,分别用于读取资源详情、更新资源以及删除资源。
- ResourceRelationship:提供GET、POST、PATCH和DELETE方法,对关系进行增删改查。
我们能重写任意的默认方法,来定制功能。整个资源管理器是保持解耦的,我们可以在不配置其他属性的情况下,对所有方法重写、对某个方法重写,亦或禁用某个方法。
必选属性
如果我们想使用资源管理器的默认方法实现,我们必须设置两个属性:schema和data_layer。
- schema: 逻辑数据抽象层,它必须继承自marshmallow_jsonapi.schema.Schema。
- data_layer: 这个属性用于初始化你的数据层。如果想了解更多,请查看数据层文档。
例:
from flask_rest_jsonapi import ResourceList
from your_project.schemas import PersonSchema
from your_project.models import Person
from your_project.extensions import db
class PersonList(ResourceList):
schema = PersonSchema
data_layer = {'session': db.session,
'model': Person}
可选属性
所有的资源管理器都继承自flask.views.MethodView,所以我们能在资源管理器中,使用Flask的可选属性。
- methods: 资源管理器可用的HTTP方法,如果没指定,那么所有方法都可用。
- decorators: 针对所有方法的装饰器元组。
另外,我们还能给资源管理器的每个HTTP方法,设定不同schema的参数:
- get_schema_kwargs: GET方法的schema默认字典参数
- post_schema_kwargs: POST方法的schema默认字典参数
- patch_schema_kwargs: PATCH方法的schema默认字典参数
- delete_schema_kwargs: DELETE方法的schema默认字典参数
同时,每个方法,都有一对前置和后置处理方法。前置处理方法,会将view args和kwarg作为参数;后置处理方法,会将返回的结果作为参数。正是有了这对方法,我们才能对前置和后置做定制化的处理。可以重写的方法如下:
- before_get: GET的前置处理方法
- after_get: GET的后置处理方法
- before_post: POST的前置处理方法
- after_post: POST的后置处理方法
- before_patch: PATCH的前置处理方法
- after_patch: PATCH的后置处理方法
- before_delete: DELETE的前置处理方法
- after_delete: DELETE的后置处理方法
ResourceList 资源列表
ResourceList 管理器有它特有的可选参数:
- view_kwargs: 如果你设置了这个参数为True,那么读取列表url时,就会读取view kwargs的值。如果我们配置了如下路由:/persons/<int:id>/computers,那么这个值则必须为True。
例:
from flask_rest_jsonapi import ResourceList
from your_project.schemas import PersonSchema
from your_project.models import Person
from your_project.extensions import db
class PersonList(ResourceList):
schema = PersonSchema
data_layer = {'session': db.session,
'model': Person}
以上为最小的ResourceList实现。它提供了GET和POST方法,用于读取对象集合以及创建对象,同时,它实现了分页、排序、稀疏字段、过滤以及包含关联字段的特性。
如果我们的schema包含关系字段,我们也能够创建对象时,同时创建关联对象。具体例子,请查看快速上手章节。
ResourceDetail 资源详情
例:
from flask_rest_jsonapi import ResourceDetail
from your_project.schemas import PersonSchema
from your_project.models import Person
from your_project.extensions import db
class PersonDetail(ResourceDetail):
schema = PersonSchema
data_layer = {'session': db.session,
'model': Person}
以上为最小的ResourceDetail实现。它提供了GET,PATCH和DELETE方法,用于读取对象详情、更新对象以及删除对象,同时,它实现了所有强大的特性,如稀疏字段和包含关联字段的特性等。
如果我们的schema包含关系字段,我们也能够更新对象时,同时更新关联对象。具体例子,请查看快速上手章节。
ResourceRelationship 资源关系
例:
from flask_rest_jsonapi import ResourceRelationship
from your_project.schemas import PersonSchema
from your_project.models import Person
from your_project.extensions import db
class PersonRelationship(ResourceRelationship):
schema = PersonSchema
data_layer = {'session': db.session,
'model': Person}
以上为最小的ResourceRelationship实现。它提供了GET,POST,PATCH和DELETE方法,用于读取、创建关系、更新关系以及删除关系,同时,它实现了所有强大的特性,如稀疏字段和包含关联字段的特性等。
05-数据层
数据层是资源管理器和数据交互的CRUD接口。它能以非常灵活的方式使用任何ORM或数据储存。我们甚至还能创建基于多个ORM和数据储存的数据层,以此来管理我们的对象。数据层针对对象和关系实现了CRUD接口,同时管理着分页、过滤和排序功能。
Flask-REST-JSONAPI基于知名的ORM SQLAlchemy框架已经实现了一个全功能的数据层。
注:
资源管理器默认的数据层使用的是SQLAlchemy。所以,如果我们要使用它,不必在资源管理器中明确声明。
为了配置数据层,我们必须在资源管理器中设置一些必要参数。
例:
from flask_rest_jsonapi import ResourceList
from your_project.schemas import PersonSchema
from your_project.models import Person
class PersonList(ResourceList):
schema = PersonSchema
data_layer = {'session': db.session,
'model': Person}
我们也能在资源管理中,给数据层,嵌入额外的方法。这里有两种额外的方法:
- query(查询方法):“query”方法使用view_kwargs作为参数,并返回替代的查询结果。使用ResourceList资源管理器的GET方法时,会调用query,获取对象集合。
- pre / post process(前置后置方法):所有CRUD和关系操作都有一对前置/后置处理方法。因此,我们能在数据层的每次操作前和后,添加额外的处理。每个前置/后置方法中,使用的具体参数可以参考基类flask_rest_jsonapi.data_layers.base.Base。
例:
from sqlalchemy.orm.exc import NoResultFound
from flask_rest_jsonapi import ResourceList
from flask_rest_jsonapi.exceptions import ObjectNotFound
from your_project.models import Computer, Person
class ComputerList(ResourceList):
def query(self, view_kwargs):
query_ = self.session.query(Computer)
if view_kwargs.get('id') is not None:
try:
self.session.query(Person).filter_by(id=view_kwargs['id']).one()
except NoResultFound:
raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
else:
query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
return query_
def before_create_object(self, data, view_kwargs):
if view_kwargs.get('id') is not None:
person = self.session.query(Person).filter_by(id=view_kwargs['id']).one()
data['person_id'] = person.id
schema = ComputerSchema
data_layer = {'session': db.session,
'model': Computer,
'methods': {'query': query,
'before_create_object': before_create_object}}
注:
数据层方法可以不必在资源管理器中声明。我们能在专用的模块或模型中声明它。
例:
from sqlalchemy.orm.exc import NoResultFound
from flask_rest_jsonapi import ResourceList
from flask_rest_jsonapi.exceptions import ObjectNotFound
from your_project.models import Computer, Person
from your_project.additional_methods.computer import before_create_object
class ComputerList(ResourceList):
schema = ComputerSchema
data_layer = {'session': db.session,
'model': Computer,
'methods': {'query': Computer.query,
'before_create_object': before_create_object}}
SQLAlchemy
必要参数:
- session:数据层使用的session连接
- model:数据层使用的ORM模型
可选参数:
- id_field:自定义识别列,用于替代模型的主键
- url_field:自定义路由中的参数名,用于替代默认的参数名id
默认情况下,当我们使用inlcude参数查询时,SQLAlchemy会懒加载关联的数据。如果我们想停用这个特性,我们可以将数据层的eagerload_includes设置为False。
定制数据层
之前提到过,我们也能创建使用我们自己的数据层。定制的数据层必须继承自基类flask_rest_jsonapi.data_layers.base.Base。通过这个基类,我们能看到在数据层中,所有可以定制的内容。
用例:
from flask_rest_jsonapi import ResourceList
from your_project.schemas import PersonSchema
from your_project.data_layers import MyCustomDataLayer
class PersonList(ResourceList):
schema = PersonSchema
data_layer = {'class': MyCustomDataLayer,
'param_1': value_1,
'param_2': value_2}
注:
在资源管理器的data_layer字典中,除了“class”的所有参数,都会作为数据层实例的属性。这样,便于数据层内部进行调用。
06-路由
路由系统很简单,具体可以参考如下框架:
api.route(<Resource manager>, <endpoint name>, <url_1>, <url_2>, ...)
例:
# all required imports are not displayed in this example
from flask_rest_jsonapi import Api
api = Api()
api.route(PersonList, 'person_list', '/persons')
api.route(PersonDetail, 'person_detail', '/persons/<int:id>', '/computers/<int:computer_id>/owner')
api.route(PersonRelationship, 'person_computers', '/persons/<int:id>/relationships/computers')
api.route(ComputerList, 'computer_list', '/computers', '/persons/<int:id>/computers')
api.route(ComputerDetail, 'computer_detail', '/computers/<int:id>')
api.route(ComputerRelationship, 'computer_person', '/computers/<int:id>/relationships/owner')
07-过滤
Flask-REST-JSONAPI有非常灵活的过滤系统。过滤系统的操作,完全由ResourceList管理器的数据层提供的接口来实现。这里,我们将介绍SQLAlchemy数据层的过滤接口,同样,我们也可以实现定制的数据层过滤接口。过滤功能的使用,我只需要在url中添加“filter”查询参数即可。当然,语句也必须符合JSONAPI 1.0的规范。
注:
为了可读性,以下例子未进行url编码。
SQLAlchemy
SQLAlchemy数据层的过滤系统接口与Flask-Restless提供的完全一样。以下为第一个例子:
GET /persons?filter=[{"name":"name","op":"eq","val":"John"}] HTTP/1.1
Accept: application/vnd.api+json
在这个例子中,我们过滤得到了名为John的人。我们发现,过滤接口其实就是SQLalchemy原生提供的方法:列表+过滤信息。
- name: 我们想要过滤的字段名
- op: 操作符(这要SQLAlchemy提供,就可以使用)
- val: 我们想要比较的值。我们也可以在此用“field”替换,即另一个字段,来进行比较。
GET /persons?filter=[{"name":"name","op":"eq","field":"birth_date"}] HTTP/1.1
Accept: application/vnd.api+json
在上面的例子中,我们想要过滤人名和生日一样的人。虽然例子比较搞笑,但我们只是为了解释下这种用法。
如果,我们想要过滤关系型数据,我们可以:
GET /persons?filter=[
{
"name": "computers",
"op": "any",
"val": {
"name": "serial",
"op": "ilike",
"val": "%Amstrad%"
}
}
] HTTP/1.1
Accept: application/vnd.api+json
注:
当我们过滤关系时,使用“any”操作符来过滤“对多”关系;使用“has”操作符来过滤“对一”关系。
另外,我们也能使用一种快捷的方式实现同样的功能:
GET /persons?filter=[{"name":"computers__serial","op":"ilike","val":"%Amstrad%"}] HTTP/1.1
Accept: application/vnd.api+json
我们也能用“与或非”来联结操作:
GET /persons?filter=[
{
"name":"computers__serial",
"op":"ilike",
"val":"%Amstrad%"
},
{
"or": {
[
{
"not": {
"name": "name",
"op": "eq",
"val":"John"
}
},
{
"and": [
{
"name": "name",
"op": "like",
"val": "%Jim%"
},
{
"name": "birth_date",
"op": "gt",
"val": "1990-01-01"
}
]
}
]
}
}
] HTTP/1.1
Accept: application/vnd.api+json
常用操作符:
- any: 过滤对多关系
- between: 过滤一个字段位于两个值之间
- endswith: 检查是否以某个字符串结尾
- eq: 等于
- ge: 大于或等于
- gt: 大于
- has: 过滤对一关系
- ilike: 检查是否包含某个字符串(大小写不明感)
- in_: 检查是否包含在list中
- is_: 检查字段是否是某个值
- isnot: 检查字段是否不是某个值
- like: 检查是否包含某个字符串
- le: 小于或等于
- lt: 小于
- match: 匹配
- ne: 不等于
- notilike: 检查是否不包含某个字符串(大小写不明感)
- notin_: 检查是否不包含在list中
- notlike: 检查是否不包含某个字符串
- startswith: 检查是否以某个字符串开始
注:
使用常用操作符时,要看model的字段类型是否支持该操作。
简单过滤
简单过滤仅支持“eq”操作符。每个简单过滤语句,都会转换成其默认的过滤语句,并添加到过滤列表的末尾。
例:
GET /persons?filter[name]=John HTTP/1.1
Accept: application/vnd.api+json
等同于:
GET /persons?filter[name]=[{"name":"name","op":"eq","val":"John"}] HTTP/1.1
Accept: application/vnd.api+json
同样,我们也能在请求中,使用多个简单过滤:
GET /persons?filter[name]=John&filter[gender]=male HTTP/1.1
Accept: application/vnd.api+json
等同于:
GET /persons?filter[name]=[{"name":"name","op":"eq","val":"John"}, {"name":"gender","op":"eq","val":"male"}] HTTP/1.1
08-包含关联对象
使用include查询参数,我们能在Response中返回关联对象的详情。同样,我们能在任何路由中(传统CRUD路由,或关系路由),以及任何返回数据的HTTP方法中使用include参数。
返回结果会多一个“included”的key。
例:
Request:
GET /persons/1?include=computers HTTP/1.1
Accept: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"type": "person",
"id": "1",
"attributes": {
"display_name": "JEAN <jean@gmail.com>",
"birth_date": "1990-10-10"
},
"relationships": {
"computers": {
"data": [
{
"type": "computer",
"id": "1"
}
],
"links": {
"related": "/persons/1/computers",
"self": "/persons/1/relationships/computers"
}
}
},
"links": {
"self": "/persons/1"
}
},
"included": [
{
"type": "computer",
"id": "1",
"attributes": {
"serial": "Amstrad"
},
"relationships": {
"owner": {
"links": {
"related": "/computers/1/owner",
"self": "/computers/1/relationships/owner"
}
}
},
"links": {
"self": "/computers/1"
}
}
],
"links": {
"self": "/persons/1"
},
"jsonapi": {
"version": "1.0"
}
}
我们也能在relationship中使用include。
例:
Request:
GET /persons/1?include=computers.owner HTTP/1.1
Accept: application/vnd.api+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"type": "person",
"id": "1",
"attributes": {
"display_name": "JEAN <jean@gmail.com>",
"birth_date": "1990-10-10"
},
"relationships": {
"computers": {
"data": [
{
"type": "computer",
"id": "1"
}
],
"links": {
"related": "/persons/1/computers",
"self": "/persons/1/relationships/computers"
}
}
},
"links": {
"self": "/persons/1"
}
},
"included": [
{
"type": "computer",
"id": "1",
"attributes": {
"serial": "Amstrad"
},
"relationships": {
"owner": {
"data": {
"type": "person",
"id": "1"
},
"links": {
"related": "/computers/1/owner",
"self": "/computers/1/relationships/owner"
}
}
},
"links": {
"self": "/computers/1"
}
},
{
"type": "person",
"id": "1",
"attributes": {
"display_name": "JEAN <jean@gmail.com>",
"birth_date": "1990-10-10"
},
"relationships": {
"computers": {
"links": {
"related": "/persons/1/computers",
"self": "/persons/1/relationships/computers"
}
}
},
"links": {
"self": "/persons/1"
}
}
],
"links": {
"self": "/persons/1"
},
"jsonapi": {
"version": "1.0"
}
}
尽管例子比较搞笑,但我们只是为了解释用法。
09-稀疏字段
使用查询参数“fields”,我们能限制api返回的字段。这种做法有助于提升性能,因为客户端可以按需所取。同样,我们能在任何路由中(传统CRUD路由,或关系路由),以及任何返回数据的HTTP方法中使用fields参数。
注:
为了可读性,例子的url未进行编码。
稀疏字段的语法如下:
?fields[<resource_type>]=<list of fields to return>
例:
GET /persons?fields[person]=display_name HTTP/1.1
Accept: application/vnd.api+json
上述例子中,结果只会返回person的display_name字段,并不会返回关系数据。这样就可以达到提高性能的目的,因为查询外键是很费时的。
我们能管理整个响应返回的字段,甚至是include中的数据。
例:
如果我们不想得到某个人电脑的关系字段,我们可以做如下操作:
GET /persons/1?include=computers&fields[computer]=serial HTTP/1.1
Accept: application/vnd.api+json
当然我们也可以组合使用多个稀疏字段:
GET /persons/1?include=computers&fields[computer]=serial&fields[person]=name,computers HTTP/1.1
Accept: application/vnd.api+json
注:
如果我们同时使用了fields和include,别忘了在fields中注明关系的名称,否则include可能会失败。(可以观察上例中的computers,在两处都出现了)
10-分页
我们时候ResourceList默认实现的GET方法时,返回结果默认会分页。默认页面大小是30,同时,我们也能通过查询参数“page”,从客户端的角度来控制页面大小。
注:
为了可读性,以下例子未进行url编码。
页面大小
如下操作,控制页面大小:
GET /persons?page[size]=10 HTTP/1.1
Accept: application/vnd.api+json
页码
如下操作,控制页面数量:
GET /persons?page[number]=2 HTTP/1.1
Accept: application/vnd.api+json
页面大小+页码
当然,我们也能同时控制两者:
GET /persons?page[size]=10&page[number]=2 HTTP/1.1
Accept: application/vnd.api+json
禁用分页
我们也能禁用分页,通过设置size=0
GET /persons?page[size]=0 HTTP/1.1
Accept: application/vnd.api+json
11-排序
我们也能通过查询参数“sort”,从客户端的角度来控制页面大小。
注:
为了可读性,以下例子未进行url编码。
例:
GET /persons?sort=name HTTP/1.1
Accept: application/vnd.api+json
多项排序
我们能为多个字段进行排序:
GET /persons?sort=name,birth_date HTTP/1.1
Accept: application/vnd.api+json
降序
我们能使用字符“-”来实现降序:
GET /persons?sort=-name HTTP/1.1
Accept: application/vnd.api+json
多项排序+降序
当然,我们也能同时使用两者:
GET /persons?sort=-name,birth_date HTTP/1.1
Accept: application/vnd.api+json
12-错误
JSONAPI 1.0规范推荐的返回错误形式如下:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/vnd.api+json
{
"errors": [
{
"status": "422",
"source": {
"pointer":"/data/attributes/first-name"
},
"title": "Invalid Attribute",
"detail": "First name must contain at least three characters."
}
],
"jsonapi": {
"version": "1.0"
}
}
其中,“source”字段给出的是,输入数据或查询字段的错误信息。
上例,演示了出现输入数据的错误信息。下例,演示的是查询字段“include”的错误信息:
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
"errors": [
{
"status": "400",
"source": {
"parameter": "include"
},
"title": "BadRequest",
"detail": "Include parameter is invalid"
}
],
"jsonapi": {
"version": "1.0"
}
}
Flask-REST-JSONAPI 提供了两类错误处理的模块:
- the errors module:我们能从jsonapi_errors模块导入errors module。通过这个错误处理的包,我们能创建一系列,符合JSONAPI 1.0规范,结构化的错误返回结果。
- the exception module:我们能从这个模块中导入许多异常,同样,我们也能创建一系列,符合JSONAPI 1.0规范,结构化的异常返回结果。
当我们定制api时,强烈建议使用Flask-REST-JSONAPI的exception模块。因为JsonApiException会被框架捕获,并输出符合JSONAPI 1.0规范的内容。
例:
# all required imports are not displayed in this example
from flask_rest_jsonapi.exceptions import ObjectNotFound
class ComputerList(ResourceList):
def query(self, view_kwargs):
query_ = self.session.query(Computer)
if view_kwargs.get('id') is not None:
try:
self.session.query(Person).filter_by(id=view_kwargs['id']).one()
except NoResultFound:
raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
else:
query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
return query_
13-Api
我们在实例化Api时,以元组形式,提供全局装饰器。
例:
from flask_rest_jsonapi import Api
from your_project.security import login_required
api = Api(decorators=(login_required,))
14-权限
Flask-REST-JSONAPI 提供了强大的权限系统支持。
例:
from flask import Flask
from flask_rest_jsonapi import Api
from your_project.permission import permission_manager
app = Flask(__name__)
api = Api()
api.init_app(app)
api.permission_manager(permission_manager)
上例中,API会在每个方法方法前,检查权限。检查权限的方式就是依次调用permission_manager中的函数。
定义permission_manager必须符合下述规范:
def permission_manager(view, view_args, view_kwargs, *args, **kwargs):
"""The function use to check permissions
:param callable view: the view
:param list view_args: view args
:param dict view_kwargs: view kwargs
:param list args: decorator args
:param dict kwargs: decorator kwargs
"""
注:
Flask-REST-JSONAPI利用装饰器has_permission,来为每个方法检查权限。我们能给装饰器提供args和kwargs,这样,我们就能在permission_manager中获取args和kwargs了。默认情况下,权限系统不会给装饰器,提供任何的args或kwargs。
当发生权限不足的情况,建议使用以下方式抛出异常。
raise JsonApiException(<error_source>,
<error_details>,
title='Permission denied',
status='403')
我们能为某个资源禁用权限系统,或者,定制权限检查的方式,如下:
from flask_rest_jsonapi import ResourceList
from your_project.extensions import api
class PersonList(ResourceList):
disable_permission = True
@api.has_permission('custom_arg', custom_kwargs='custom_kwargs')
def get(*args, **kwargs):
return 'Hello world !'
警告:
如果我们要同时使用权限系统和oauth支持,比如在权限系统下,从oauth读取用户信息(requests.oauth.user)。我们必须先初始化oauth,再初始化权限系统,因为装饰器检查是串联执行的。
例:
from flask import Flask
from flask_rest_jsonapi import Api
from flask_oauthlib.provider import OAuth2Provider
from your_project.permission import permission_manager
app = Flask(__name__)
oauth2 = OAuth2Provider()
api = Api()
api.init_app(app)
api.permission_manager(permission_manager) # initialize permission system first
api.oauth_manager(oauth2) # initialize oauth support second
15-OAuth
Flask-REST-JSONAPI通过Flask-OAuthlib包,支持OAuth功能。
例:
from flask import Flask
from flask_rest_jsonapi import Api
from flask_oauthlib.provider import OAuth2Provider
app = Flask(__name__)
oauth2 = OAuth2Provider()
api = Api()
api.init_app(app)
api.oauth_manager(oauth2)
上述例子中,Flask-REST-JSONAPI的资源只要添加了装饰器,就会进行保护
oauth2.require_oauth(<scope>)
指定范围模式如下:
<action>_<resource_type>
动作包括:
- list:ResourceList的get方法
- create:ResourceList的post方法
- get:ResourceDetail的get方法
- update:ResourceDetail的get方法
- delete:ResrouceDetail的delete方法
例:
list_person
如果我们想定制权限范围,我们可以提供一个计算定制范围的函数。函数格式如下:
def get_scope(resource, method):
"""Compute the name of the scope for oauth
:param Resource resource: the resource manager
:param str method: an http method
:return str: the name of the scope
"""
return 'custom_scope'
用例:
from flask import Flask
from flask_rest_jsonapi import Api
from flask_oauthlib.provider import OAuth2Provider
app = Flask(__name__)
oauth2 = OAuth2Provider()
api = Api()
api.init_app(app)
api.oauth_manager(oauth2)
api.scope_setter(get_scope)
注:
我们能给定制的范围方法,起任意的名字。但是,我们必须设定两个必须参数:resource和method。就像上述例子一样。
如果我们需要禁用OAuth,或者为某个资源定制保护方法,我们可以给资源管理器添加如下选项:
例:
from flask_rest_jsonapi import ResourceList
from your_project.extensions import oauth2
class PersonList(ResourceList):
disable_oauth = True
@oauth2.require_oauth('custom_scope')
def get(*args, **kwargs):
return 'Hello world !'
16-配置
我们有5个可以配置的参数:
- PAGE_SIZE:一次列表请求,默认返回的数量(默认为30)
- MAX_PAGE_SIZE:一次列表请求,最多返回的数量。如果请求的数量大于这个数字,就会返回400。
- MAX_INCLUDE_DEPTH:调用include关系参数时,允许的最大深度。
- ALLOW_DISABLE_PAGINATION:如果我们想要禁止关闭分页,那么把该参数设为False。
- CATCH_EXCEPTIONS:如果我们想让flask_rest_jsonapi捕捉所有异常并返回JsonApiException标准结果设置为True。(默认为True)