接下来一段时间,Gevin将开一个系列专题,讲Flask RESTful API的开发,本文是第2篇《一个简单的Flask RESTful 实例》,本系列文章列表以下:html
所谓“麻雀虽小,五脏俱全”,博客就是这样一个东西:一个轻量级的应用,你们都很熟悉,作简单了,只要有一个地方建立文章、显示文章便可,作复杂了,文章管理、草稿管理、版本管理、用户管理、权限管理、角色管理…… 等等一系列的功能均可以作上去,而这些业务逻辑,在不少应用场景下都是通用的或者相似的,从无到有、从粗到精的作好一个博客的开发,不少其余应用的开发就举一反三了。本教程也将以博客为具体实例,开展接下来的Flask RESTful API的实现,博客的原型会一点点的变得复杂,相应的技能点也会逐一展开。python
本节先从博客的基础开始,即实现文章的建立、获取、更新和删除。git
一般设计并实现一个应用,是从数据模型的设计开始的,除非这个应用自己不包含数据的存取。按传统的说法,这个章节应该叫作“数据库的设计与实现”,其主要目标是,根据实际的数据存储需求,抽象出数据的实物模型(按关系型数据库的说法,即画E-R图),而后基于实物模型和采用的数据库,再设计出逻辑模型,逻辑模型即数据在数据库中真实的存储形式。随着数据库技术的发展,渐渐兴起了一种叫作ORM
的技术,随着NoSQL的发展,又出现了OGM
, ODM
等,这三个名词分别对应"Object Relationship Mapping","Object Graph Mapping"和"Object Document Mapping",关于ORM及与之相似的几个名词,这里就再也不赘述了,在Flask web开发的大背景下,若是哪位同窗不了解这类技术,确实须要补补课了。github
(注:上文的“基于实物模型和数据库技术,设计逻辑模型”的说法,省略了数据库选择这一步,技术发展至今,结合实践中的各类数据和需求,传统的关系型数据库已再也不是数据存储的万金油,须要根据实际的数据需求,在数据库选型中考虑诸如采用SQL, Document仍是Graph Database,要不要考虑空间数据库的支持等问题。)web
对于开发者而言,经过ORM(或ODM, OGM等)与数据库通讯,而非直接链接数据库、操做数据库,是一个很好的开发实践,一方面一个ORM,一般都支持多种关系型数据库,这样能够在开发中,将业务逻辑与存储数据的数据库解耦,另外一方面,将开发中与数据库交互的相关逻辑交给大神开发的ORM处理,既简化了本身开发中的工做量,也更加靠谱。所以,除非特定需求的开发,或者采用的数据库太冷门或太超前致使没有合适的ORM,Gevin建议在开发中,将数据存取相关的业务逻辑交给专业的ORM
来处理,与之对应的,当选择文档型数据库或图数据库时,配合ODM
或OGM
来开发。数据库
对于博客这样一个轻量级的应用,不管采用传统的关系型数据库,仍是近年来火起来的NoSQL数据库,都能很好的满意本应用的业务需求,本教程中Gevin采用MongoDB做为博客应用的数据库,缘由以下:编程
确立了MongoDB
这个数据库,就要去找可用的ODM
框架,Python的生态下有不少MongoDB的ODM框架,MongoEngine
是Gevin最喜欢的一个。MongoEngine
相对于其余不少ODM框架,更新维护要活跃不少,并且很是好用,其使用方法与一直广受好评的Django 内置的ORM很是相似,因此上手很是容易,推荐你们使用。json
接下来介绍的博客系统的数据模型,也将基于MongoEngine展开。flask
一篇博客,一般包含如下字段就够了:api
slug
字段须要专门说明一下,由于这个字段是惟一不直接存在于博客的概念模型里面的,而是考虑到博客系统的业务逻辑后,为了系统逻辑的优化而设计出来的。一般,一篇博客均对应一个数据库记录,这个记录必须是惟一的,须要有一个主键(候选键)来惟一识别这条记录。虽然每条数据库记录的id
能够用做主键,但一般id
是自动递增的,同一篇博客,建立成功后,删掉再新建,两次的数据库记录通常是不相同的,并且这确实是两条不一样的数据库记录,使用了不一样的id
也是理所应当的。而在业务逻辑中却并不是如此,在业务逻辑中,或者说从产品的角度看,同一篇博客,无论删除多少次再新建,依然是同一篇,始终能够经过一个永久不变的主键找到这条记录。在博客中,最典型的即是博客的导入功能,若是咱们迁移了博客系统的服务器,并试图经过博客的导入导出恢复文章时,若是经过id定位每篇博客,颇有可能切换服务器先后,文章的url就变了,这会致使原来放出去的博文连接均失效了,这是博客系统不但愿看到的,但经过slug就不存在这种问题了。
举例来讲:
好比『Gevin的博客』中,《RESTful API 编写指南》 一文,URL为
https://blog.igevin.info/posts/restful-api-get-started-to-write/
,URL最后一段的restful-api-get-started-to-write
就是这篇文章的slug
。Gevin就是用它来惟一识别每篇博客,每篇博客的永久连接也基于slug
生成,这样不管个人博客系统浴火重生多少次,不管之后采用哪一种编程语言开发,哪一种数据库技术存储,每篇博客的永久连接将永久有效。
说到这里,能够对数据模型的设计作一点深刻和经验的提炼:好的数据模型,在设计时不只会包含概念模型所涉及到的内容,还会站到产品的角度,深刻业务逻辑,增长一些支持整个产品逻辑的字段,也会综合考虑数据的一致性和查询效率等问题,设计必要的冗余字段
因此,在博客的数据模型中设计slug
字段,并不是一种特例,实际上大量常见的应用中,其数据模型中的id
永远都是候选键,只会应用于产品逻辑的某些特殊场景中,大部分状况下,让概念模型中有意义的某个字段或者某几个字段的组合做为主键,才能更好的支持整个业务逻辑,也能使代码逻辑更具可扩展性,更好的应对变化的需求
(画外音:做为一个讲话严密的人,Gevin在上文提到slug
是不直接存在于博客的概念模型中的表述很准确,你们能够当作课外题想一想,若是要设计一个优秀的、经得住用户考验的博客系统,在提炼数据的概念模型时,是否是会不自觉的引入相似于slug
的这样一个概念 :P)
理论说的太多了,让咱们赶忙进入show me the code
阶段吧~
上面提到的博客的数据模型,用MongoEngine表达出来时,代码以下:
class Post(db.Document):
title = db.StringField(max_length=255, required=True)
slug = db.StringField(max_length=255, required=True, unique=True)
abstract = db.StringField()
raw_content = db.StringField(required=True)
pub_time = db.DateTimeField()
update_time = db.DateTimeField()
author = db.StringField()
category = db.StringField(max_length=64)
tags = db.ListField(db.StringField(max_length=30))
def save(self, *args, **kwargs):
now = datetime.datetime.now()
if not self.pub_time:
self.pub_time = now
self.update_time = now
return super(Post, self).save(*args, **kwargs)复制代码
这里用了一个重写save()
函数的小技巧,由于每次更新博文时,文章对象的更新时间
字段都会修改,而发布时间
,只会在第一次发布时更新,这个小功能细节虽然也能够放到业务逻辑中实现,但那会使得业务逻辑变得冗长,在save()
中实现更加优雅。Gevin还会再save()
中还会作更多的事情,这个会再下一篇文章中讲到。
常规的RESTful API, 即资源的CRUD操做(create
, read
, update
和delete
)。一般RESTful API的read
操做,包含2种状况:资源列表的获取和某个指定资源的获取;update
操做存在两种形式:PUT
或PATCH
。如何合理组织资源的这些操做,Gevin的一个实践方案是,资料列表获取
和资源建立
两个操做,都是面向资源列表的,能够放到一个函数或类中实现;而资源的获取、更新和删除
,是面向某个指定资源的,这些能够放到一个函数或类中实现。
在博客这个实例中,代码上表现以下:
class PostListCreateView(MethodView):
def get(self):
return 'Not ready yet'
def post(self):
return 'Not ready yet', 201
class PostDetailGetUpdateDeleteView(MethodView):
def get(self, slug):
return 'Not ready yet'
def put(self, slug):
return 'Not ready yet'
def patch(self, slug):
return 'Not ready yet'
def delete(self, slug):
return 'Not ready yet', 204复制代码
上面代码阐述了博客相关API实现的思路框架,须要特别注意的是201
和204
两个http状态码,当建立数据成功时,要返回201(CREATED),删除数据成功时,要返回204(No Content),上面代码中没有体现出来的状态码为400
和404
,这两个状态码是面向客户端请求的,经常使用于函数体内,对应代码实现中的常见错误请求,即,当请求错误时(如传入参数不正确), 返回400
(Bad Request),当机遇请求条件查询不到数据时,返回404
(Not Found);经常使用的状态码还有401
和403
,与认证和权限有关,之后再展开。
接下来让咱们完成上面代码中没有实现的部分。因为博客这个例子很是简单,博客资源的CRUD操做,均围绕博客对应model
的相关操做完成,并且基于上一篇文章的基础,写出这些API的实现,应该不成问题。如博客资源的建立,其实现以下:
def post(self):
data = request.get_json()
article = Post()
article.title = data.get('title')
article.slug = data.get('slug')
article.abstract = data.get('abstract')
article.raw = data.get('raw')
article.author = data.get('author')
article.category = data.get('category')
article.tags = data.get('tags')
article.save()
return 'Succeed to create a new post', 201复制代码
当咱们使用post
请求上面API时,传入以下格式的json数据,便可完成博文的建立:
{
"title": "Title 1",
"slug": "title-1",
"abstract": "Abstract for this article",
"raw": "The article content",
"author": "Gevin",
"category": "default",
"tags": ["tag1", "tag2"]
}复制代码
相似的,获取博客资源的实现以下:
def get(self, slug):
obj = Post.objects.get(slug=slug)
return jsonify(obj) # This line will raise an error复制代码
资源获取功能的实现,比建立资源的代码更简洁,但正如上面代码中的注释所述,上面的实现会报错,由于jsonify只能序列化dict
和list
,不能序列化object
,因此若要解决上面的报错,须要把obj
序列化,而把obj
序列化只要把obj
包含的数据,转化到dict
中便可。
因此为修复bug,代码要作以下修改:
def get(self, slug):
obj = Post.objects.get(slug=slug)
post_dict = {}
post_dict['title'] = obj.title
post_dict['slug'] = obj.slug
post_dict['abstract'] = obj.abstract
post_dict['raw'] = obj.raw
post_dict['pub_time'] = obj.pub_time.strftime('%Y-%m-%d %H:%M:%S')
post_dict['update_time'] = obj.update_time.strftime('%Y-%m-%d %H:%M:%S')
post_dict['content_html'] = obj.content_html
post_dict['author'] = obj.author
post_dict['category'] = obj.category
post_dict['tags'] = obj.tags
return jsonify(post_dict)复制代码
一个比较好的写API的实践经验是,编写资源建立或更新的API时,实现功能后不要仅返回一个“资源建立(更新)成功”的消息,而是返回建立或更新后的结果,这既能验证这些操做是否正确实现,也会让客户端调用API时感受更舒服;另外,在获取资源时,若是资源不存在,就返回404
。
相似的,博客更新和删除的实现以下:
def put(self, slug):
try:
post = Post.objects.get(slug=slug)
except Post.DoesNotExist:
return jsonify({'error': 'post does not exist'}), 404
data = request.get_json()
if not data.get('title'):
return 'title is needed in request data', 400
if not data.get('slug'):
return 'slug is needed in request data', 400
if not data.get('abstract'):
return 'abstract is needed in request data', 400
if not data.get('raw'):
return 'raw is needed in request data', 400
if not data.get('author'):
return 'author is needed in request data', 400
if not data.get('category'):
return 'category is needed in request data', 400
if not data.get('tags'):
return 'tags is needed in request data', 400
post.title = data['title']
post.slug = data['slug']
post.abstract = data['abstract']
post.raw = data['raw']
post.author = data['author']
post.category = data['category']
post.tags = data['tags']
post.save()
return jsonify(post=post.to_dict())
def patch(self, slug):
try:
post = Post.objects.get(slug=slug)
except Post.DoesNotExist:
return jsonify({'error': 'post does not exist'}), 404
data = request.get_json()
post.title = data.get('title') or post.title
post.slug = data.get('slug') or post.slug
post.abstract = data.get('abstract') or post.abstract
post.raw = data.get('raw') or post.raw
post.author = data.get('author') or post.author
post.category = data.get('category') or post.category
post.tags = data.get('tags') or post.tags
return jsonify(post=post.to_dict())
def delete(self, slug):
try:
post = Post.objects.get(slug=slug)
except Post.DoesNotExist:
return jsonify({'error': 'post does not exist'}), 404
post.delete()
return 'Succeed to delete post', 204复制代码
更新和删除博客时,首先要找到对应的博客,若是博客记录不存在,则返回404
,使用PUT
方法更新资源时,请求API时,传入数据要包含资源的所有字段,而使用PATCH
时,只需传入须要更新的字段数据便可,因此在上面的实现中,当传入json
字段不完整时,会报400错误。(上面代码中的to_dict()
函数,下文再介绍)
Flask做为一个micro web framework
,只要用一个文件就能够开发一个web服务或网站,但随着业务逻辑的增长,把全部的代码放到一个文件中是不合理的,应该把不一样职责的代码放到不一样的功能模块中,其基本思路是,将flask 实例的建立、数据模型的设计和业务逻辑(API)的实现分别放到不一样的模块中。
Gevin在上一篇提到过,本教程对应的源码放到GitHub的restapi_exampl项目中,本篇涉及到的源码,将延续使用第一章搭好的框架,后续随着业务逻辑和代码愈来愈复杂,Gevin还会给你们更加深刻的介绍Flask代码的组织架构风格。
由app factory 负责flask实例的建立是Flask开发的惯例,正如flask官方文档中的Application Factories章节所述:
So why would you want to do this?
- Testing. You can have instances of the application with different settings to test every case.
- Multiple instances. Imagine you want to run different versions of the same application. Of course you could have multiple instances with different configs set up in your webserver, but if you use factories, you can have multiple instances of the same application running in the same application process which can be handy.
对于本应用而言,能够把app factory的实现放到factory.py
文件中,并包含如下factory功能的实现代码:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from flask import Flask
from flask.views import MethodView
from flask_mongoengine import MongoEngine
db = MongoEngine()
def create_app():
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['MONGODB_SETTINGS'] = {'DB': 'RestBlog'}
db.init_app(app)
return app复制代码
数据模型的设计能够放到models.py
文件中,其实现代码以下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import datetime
from factory import db
class Post(db.Document):
title = db.StringField(max_length=255, required=True)
slug = db.StringField(max_length=255, required=True, unique=True)
abstract = db.StringField()
raw = db.StringField(required=True)
pub_time = db.DateTimeField()
update_time = db.DateTimeField()
content_html = db.StringField()
author = db.StringField()
category = db.StringField(max_length=64)
tags = db.ListField(db.StringField(max_length=30))
def save(self, *args, **kwargs):
now = datetime.datetime.now()
if not self.pub_time:
self.pub_time = now
self.update_time = now
return super(Post, self).save(*args, **kwargs)
def to_dict(self):
post_dict = {}
post_dict['title'] = self.title
post_dict['slug'] = self.slug
post_dict['abstract'] = self.abstract
post_dict['raw'] = self.raw
post_dict['pub_time'] = self.pub_time.strftime('%Y-%m-%d %H:%M:%S')
post_dict['update_time'] = self.update_time.strftime('%Y-%m-%d %H:%M:%S')
post_dict['content_html'] = self.content_html
post_dict['author'] = self.author
post_dict['category'] = self.category
post_dict['tags'] = self.tags
return post_dict
meta = {
'indexes': ['slug'],
'ordering': ['-pub_time']
}复制代码
上面代码中,Gevin在博客的model中又增长了一个to_dict()
成员方法,该方法实现了把类的对象转化为dict
类型数据的功能,把对象序列化作的更优雅,这也是一种最基础的对象序列化方法。代码最后的meta
,表示在MongDB中建立博客的collection
时,要基于slug
字段(也就是本博客设计的主键)进行索引,查询博文记录时,默认按照发布时间倒序排列。关于MongoEngine更详细的介绍,能够去查阅MongoEngine官方文档
基于上一篇的源码,API实现部分的代码,能够继续放到app.py
文件中,下一篇会给你们介绍更加合理的代码组织方式。
本篇涉及到的源码,你们能够在restapi_exampl的chapter2
分支查阅
chapter2
分支中的源码,执行命令python app.py
便可运行,若是你没有安装相关依赖,请查阅requirements.txt
文件进行安装
下一讲预告:Gevin将介绍一些flask RESTful 开发中经常使用的Python库,把代码组织架构部分作必定调整和更详细的讲解