内容简介:Flask RESTful API 开发----基础篇 (2)
0. 前言
接下来一段时间,Gevin将开一个系列专题,讲Flask RESTful API的开发,本文是第2篇《一个简单的Flask RESTful 实例》,本系列文章列表如下:
- 基础篇(1) --- Flask RESTful 基础
- 基础篇(2) --- 一个简单的Flask RESTful 实例
- To Be Continued...
1. 准备
所谓“麻雀虽小,五脏俱全”,博客就是这样一个东西:一个轻量级的应用,大家都很熟悉,做简单了,只要有一个地方创建文章、显示文章即可,做复杂了,文章管理、草稿管理、版本管理、用户管理、权限管理、角色管理…… 等等一系列的功能都可以做上去,而这些业务逻辑,在很多应用场景下都是通用的或者类似的,从无到有、从粗到精的做好一个博客的开发,很多其他应用的开发就触类旁通了。本教程也将以博客为具体实例,开展接下来的Flask RESTful API的实现,博客的原型会一点点的变得复杂,相应的技能点也会逐一展开。
本节先从博客的基础开始,即实现文章的创建、获取、更新和删除。
2. Model的设计与实现
2.1 前提背景
通常设计并实现一个应用,是从数据模型的设计开始的,除非这个应用本身不包含数据的存取。按传统的说法,这个章节应该叫做“数据库的设计与实现”,其主要目标是,根据实际的数据存储需求,抽象出数据的实物模型(按关系型数据库的说法,即画E-R图),然后基于实物模型和采用的数据库,再设计出逻辑模型,逻辑模型即数据在数据库中真实的存储形式。随着数据库技术的发展,渐渐兴起了一种叫做 ORM
的技术,随着NoSQL的发展,又出现了 OGM
, ODM
等,这三个名词分别对应"Object Relationship Mapping","Object Graph Mapping"和"Object Document Mapping",关于ORM及与之类似的几个名词,这里就不再赘述了,在Flask web开发的大背景下,如果哪位同学不了解这类技术,确实需要补补课了。
(注:上文的“基于实物模型和数据库技术,设计逻辑模型”的说法,省略了数据库选择这一步,技术发展至今,结合实践中的各种数据和需求,传统的关系型数据库已不再是数据存储的万金油,需要根据实际的数据需求,在数据库选型中考虑诸如采用SQL, Document还是Graph Database,要不要考虑空间数据库的支持等问题。)
对于开发者而言,通过ORM(或ODM, OGM等)与数据库通信,而非直接连接数据库、操作数据库,是一个很好的开发实践,一方面一个ORM,通常都支持多种关系型数据库,这样可以在开发中,将业务逻辑与存储数据的数据库解耦,另一方面,将开发中与数据库交互的相关逻辑交给大神开发的ORM处理,既简化了自己开发中的工作量,也更加靠谱。因此,除非特定需求的开发,或者采用的数据库太冷门或太超前导致没有合适的ORM,Gevin建议在开发中,将数据存取相关的业务逻辑交给专业的 ORM
来处理,与之对应的,当选择文档型数据库或图数据库时,配合 ODM
或 OGM
来开发。
2.2 基于MongoEngine的model设计与实现
2.2.1 数据库选型
对于博客这样一个轻量级的应用,无论采用传统的关系型数据库,还是近年来火起来的NoSQL数据库,都能很好的满意本应用的业务需求,本教程中Gevin采用 MongoDB 作为博客应用的数据库,原因如下:
- Flask入门很经典的那本 《Flask Web开发》 ,采用了关系型数据库,相关的实现作者大神远比我写的好,所以Gevin没必要做重复的轮子;
- MongoDB比较灵活,没有关系型数据库的那些约束限制,本教程随着不断深入,数据模型也会不断修改完善,MongoDB不会像关系型数据库那样,每次修改以创建的数据模型,都要直接或间接通过 SQL 命令修改数据表,开发体验更爽;
- MongoDB天生分布式(本应用用不到这样的特性),其诞生之日就号称最适合web开发的数据库,有很多很好的特性,值得大家去使用;当然更重要的是,MongoDB与Flask配合使用非常好,不像Django对MongoDB的支持那么有限(相关内容,我在Flask 入门指南中有更详细的描述),Gevin推荐Flask + MongoDB 这样的搭配。
确立了 MongoDB
这个数据库,就要去找可用的 ODM
框架,Python的生态下有很多MongoDB的ODM框架, MongoEngine
是Gevin最喜欢的一个。 MongoEngine
相对于其他很多ODM框架,更新维护要活跃很多,而且非常好用,其使用方法与一直广受好评的Django 内置的ORM非常类似,所以上手非常容易,推荐大家使用。
接下来介绍的博客系统的数据模型,也将基于MongoEngine展开。
2.2.2 数据模型的设计与实现
一篇博客,通常包含以下字段就够了:
- 标题
- slug
- 作者
- 正文
- 目录
- 标签
- 创建时间
- 更新时间
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()
中还会做更多的事情,这个会再下一篇文章中讲到。
3. API 的设计与实现
3.1 设计思路
常规的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
,与认证和权限有关,以后再展开。
3.2 实现
接下来让我们完成上面代码中没有实现的部分。由于博客这个例子非常简单,博客资源的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()
函数,下文再介绍)
4. 代码的组织架构
Flask作为一个 micro web framework
,只要用一个文件就可以开发一个web服务或网站,但随着业务逻辑的增加,把所有的代码放到一个文件中是不合理的,应该把不同职责的代码放到不同的功能模块中,其基本思路是,将flask 实例的创建、数据模型的设计和业务逻辑(API)的实现分别放到不同的模块中。
Gevin在上一篇提到过,本教程对应的源码放到GitHub的 restapi_exampl 项目中,本篇涉及到的源码,将延续使用第一章搭好的框架,后续随着业务逻辑和代码越来越复杂,Gevin还会给大家更加深入的介绍Flask代码的组织架构风格。
4.1 App Factory
由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
4.2 数据模型
数据模型的设计可以放到 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官方文档
4.3 API
基于上一篇的源码,API实现部分的代码,可以继续放到 app.py
文件中,下一篇会给大家介绍更加合理的代码组织方式。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- springmvc教程--注解开发基础详解
- C++开发EOS基础指南
- flume 1.8.0 开发基础
- 移动端开发基础知识扫盲
- 业务开发转基础开发,这三种 “高可用” 架构你会么?
- Python开发【第二章】入门基础
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C#入门经典
[美] Karli Watson、Christian Nagel / 齐立波、黄静 / 清华大学出版社 / 2008-12 / 118.00元
这是一本成就无数C#程序员的经典名著,厚而不“重”,可帮助您轻松掌握C#的各种编程知识,为您的职业生涯打下坚实的基础,《C#入门经典》自第1版出版以来,全球销量已经达数万册,在中国也有近8万册的销量,已经成为广大初级C#程序员首选的入门教程,也是目前国内市场上最畅销的C#专业店销书,曾两次被CSDN、《程序员》等机构和读者评选为“最受读者喜爱的十大技术开发类图书”!第4版面向C#2008和.NET......一起来看看 《C#入门经典》 这本书的介绍吧!
在线进制转换器
各进制数互转换器
Base64 编码/解码
Base64 编码/解码