Flask RESTful API 開發----基礎篇 (2)

0. 前言

接下來一段時間,Gevin將開一個系列專題,講Flask RESTful API的開發,本文是第2篇《一個簡單的Flask RESTful 實例》,本系列文章列表以下:html

1. 準備

所謂「麻雀雖小,五臟俱全」,博客就是這樣一個東西:一個輕量級的應用,你們都很熟悉,作簡單了,只要有一個地方建立文章、顯示文章便可,作複雜了,文章管理、草稿管理、版本管理、用戶管理、權限管理、角色管理…… 等等一系列的功能均可以作上去,而這些業務邏輯,在不少應用場景下都是通用的或者相似的,從無到有、從粗到精的作好一個博客的開發,不少其餘應用的開發就舉一反三了。本教程也將以博客爲具體實例,開展接下來的Flask RESTful API的實現,博客的原型會一點點的變得複雜,相應的技能點也會逐一展開。python

本節先從博客的基礎開始,即實現文章的建立、獲取、更新和刪除。git

2. Model的設計與實現

2.1 前提背景

一般設計並實現一個應用,是從數據模型的設計開始的,除非這個應用自己不包含數據的存取。按傳統的說法,這個章節應該叫作「數據庫的設計與實現」,其主要目標是,根據實際的數據存儲需求,抽象出數據的實物模型(按關係型數據庫的說法,即畫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來處理,與之對應的,當選擇文檔型數據庫或圖數據庫時,配合ODMOGM來開發。數據庫

2.2 基於MongoEngine的model設計與實現

2.2.1 數據庫選型

對於博客這樣一個輕量級的應用,不管採用傳統的關係型數據庫,仍是近年來火起來的NoSQL數據庫,都能很好的滿意本應用的業務需求,本教程中Gevin採用MongoDB做爲博客應用的數據庫,緣由以下:編程

  1. Flask入門很經典的那本《Flask Web開發》,採用了關係型數據庫,相關的實現做者大神遠比我寫的好,因此Gevin不必作重複的輪子;
  2. MongoDB比較靈活,沒有關係型數據庫的那些約束限制,本教程隨着不斷深刻,數據模型也會不斷修改完善,MongoDB不會像關係型數據庫那樣,每次修改以建立的數據模型,都要直接或間接經過SQL命令修改數據表,開發體驗更爽;
  3. MongoDB天生分佈式(本應用用不到這樣的特性),其誕生之日就號稱最適合web開發的數據庫,有不少很好的特性,值得你們去使用;固然更重要的是,MongoDB與Flask配合使用很是好,不像Django對MongoDB的支持那麼有限(相關內容,我在Flask 入門指南中有更詳細的描述),Gevin推薦Flask + MongoDB 這樣的搭配。

確立了MongoDB這個數據庫,就要去找可用的ODM框架,Python的生態下有不少MongoDB的ODM框架,MongoEngine是Gevin最喜歡的一個。MongoEngine相對於其餘不少ODM框架,更新維護要活躍不少,並且很是好用,其使用方法與一直廣受好評的Django 內置的ORM很是相似,因此上手很是容易,推薦你們使用。json

接下來介紹的博客系統的數據模型,也將基於MongoEngine展開。flask

2.2.2 數據模型的設計與實現

一篇博客,一般包含如下字段就夠了:api

  • 標題
  • 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, updatedelete)。一般RESTful API的read操做,包含2種狀況:資源列表的獲取和某個指定資源的獲取;update操做存在兩種形式:PUTPATCH。如何合理組織資源的這些操做,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實現的思路框架,須要特別注意的是201204兩個http狀態碼,當建立數據成功時,要返回201(CREATED),刪除數據成功時,要返回204(No Content),上面代碼中沒有體現出來的狀態碼爲400404,這兩個狀態碼是面向客戶端請求的,經常使用於函數體內,對應代碼實現中的常見錯誤請求,即,當請求錯誤時(如傳入參數不正確), 返回400(Bad Request),當機遇請求條件查詢不到數據時,返回404(Not Found);經常使用的狀態碼還有401403,與認證和權限有關,之後再展開。

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只能序列化dictlist,不能序列化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?

  1. Testing. You can have instances of the application with different settings to test every case.
  2. 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文件中,下一篇會給你們介紹更加合理的代碼組織方式。

5. What's More

  • 本篇涉及到的源碼,你們能夠在restapi_examplchapter2分支查閱

  • chapter2分支中的源碼,執行命令python app.py便可運行,若是你沒有安裝相關依賴,請查閱requirements.txt文件進行安裝

  • 下一講預告:Gevin將介紹一些flask RESTful 開發中經常使用的Python庫,把代碼組織架構部分作必定調整和更詳細的講解

相關文章
相關標籤/搜索