創建博客-REST的資源

資源和JSON的序列化轉換

開發Web程序時,經常需要在資源的內部表示和JSON之間進行轉換,JSON是HTTP請求和響應使用的傳輸格式,下例是新添加到Post類中的to_json()方法

# app/models.py
class Post(db.Model):

    #...

    def to_json(self):
        json_post = {
            "url": url_for('api.get_post', id=self.id, _external=True),
            "body": self.body,
            "doby_html": self.body_html,
            "timestamp": self.timestamp,
            "author": url_for('api.get_user', id=self.author_id,
                              _external=True),
            "comments": url_for('api.get_post_comments', id=self.id,
                              _external=True),
            "comment_count": self.comments.count()
            }
        return json_post    

url、author和comments字段分別返回各自資源的URL,因此它們使用url_for()生成,所調用的路由即將在API藍本中定義,注意,所有的url_for()方法都指定了參數_external=True,這麼做是爲了生成完整的URL,而不是生成傳統Web程序中經常使用的相對URL

這段代碼還說明表示資源時可以使用虛構的屬性,comment_count字段是博客文章的評論數量,並不是模型的真實屬性,它之所以包含在這個資源中是爲了便於客戶端使用

User模型的to_json()方法可以按照Post模型的方式定義,如下

# app/models.py

class User(UserMixin, db.Model):
#...
    def to_json(self):
        json_user = {
            "url": url_for('api.get_post', id=self.id, _external=True),
            "username": self.username,
            "member_since": self.member_since,
            "last_seen":self.last_seen,
            "posts": url_for('api.get_user_posts', id=self.id, _external=True),
            "followed_posts": url_for('api.get_user_followed_posts'
                                      id=self.id, _external=True),
            "post_count":self.posts.count()

        }
        return json_user

爲了保護隱私,這個方法中用戶的某些屬性沒有加入響應,例如email和role,這段代碼再次聲明,提供給客戶端的資源表示沒必要和數據庫模型的內部表示完全一致

把JSON轉換成模型時面臨的問題是,客戶端提供的數據可能無效、錯誤或者多餘,下例是從JSON格式數據創建Post模型實例的方法

# app/models.py
from app.exceptions import ValidationError

    class Post(db.Model):

    #...

    @staticmethod
    def from_json(json_post):
        body = json_post.get('body')
        if body is None or body == '':
            raise ValidationError('post does not have a body')
        return Post(body=body)

上述代碼在實現過程中只選擇使用JSON字典中的屬性,而把body_html屬性忽略了,因爲只要body屬性的值發生變化,就會觸發一個SQLAlchemy事件,自動在服務器端渲染Markdown,除非允許客戶端倒填日期(這個程序並不提供此功能),否則無需指定timestamp屬性,由於客戶端無權選擇博客文章的作者,所以沒有使用author字段,author字段唯一能使用的值是通過認證的用戶,commentscomment_count屬性使用數據庫關係自動生成,因此其中沒有創建模型所需的有用信息,最後,url字段也被忽略了,因爲在這個實現中資源的URL由服務器指派,而不是客戶端

如何檢查錯誤,如果沒有body字段或者其值爲空,from_json()方法會拋出ValidationError異常,在這種情況下,拋出異常纔是處理錯誤的正確處理方法,因爲from_json()方法並沒有掌握處理問題的足夠信息,唯有把錯誤交給調用者,由上層代碼處理這個錯誤,ValidationError類是Python中的ValueError類的簡單子類,具體定義如下:

# app/exceptions.py

class ValidationError(ValueError):
    pass

現在程序需要向客戶端提供適當的響應以處理這個異常,爲了避免在視圖函數中編寫捕獲異常的代碼,我們可創建一個全局異常處理程序,對於ValidationError異常,其處理程序如下:

# app/api_1_0/errors.py

@api.errorhandler(ValidationError)
def validation_error(e):
    return bad_request(e.args[0])

這裏使用的errorhandler裝飾器和註冊HTTP狀態碼處理程序時使用的是同一個,只不過此時接受的參數是Exception類,只要拋出了指定類的異常,就會調用被裝飾的函數,注意這個裝飾器從API藍本中調用,所以只有當處理藍本中的路由時拋出了異常纔會調用這個處理程序

使用這個技術時,視圖函數中的代碼可以寫的十分簡潔明瞭,而且無需檢查錯誤

@api.route('/posts/', methods=['POST'])
def new_post():
    post = Post.from_json(request.json)
    post.author = g.current_user
    db.session.add(post)
    db.session.commit()
    return jsonify(post.to_json())

實現資源端點

現在我們需要實現用於處理不同資源的路由,GET請求往往是最簡單的,因爲它們只返回信息,無需修改信息,下例是博客文章的兩個GET請求處理程序

# app/api_1_0/posts.py

@api.route('/posts/')
@auth.login_required
def get_posts():
    posts = Post.query.all()
    return jsonify({ 'posts': [post.to_json() for post in posts] })

@api.route('/posts/<int:id>')
@auth.login_required
def get_post(id):
    post = Post.query.get_or_404(id)
    return jsonify(post.to_json())

第一個路由處理獲取文章集合的請求,這個函數使用列表推導生成所有文章的JSON版本,第二個路由返回單篇博客文章,如果在數據庫中沒找到指定id對應的文章,則返回404錯誤

404錯誤的處理程序在程序層定義,如果客戶端請求JSON格式,就要返回JSON格式響應,如果要根據Web服務定製響應內容,也可在API藍本中重新定義404錯誤處理程序

博客文章資源的POST請求處理程序把一篇新博客文章插入數據庫,路由的定義如下例:

# app/api_1_0/posts.py
@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE_ARTICLES)
def new_post():
    post = Post.from_json(request.json)
    post.author = g.current_user
    db.session.add(post)
    db.session.commit()
    return jsonify(post.to_json()), 201, \
            {"Location":url_for('api.get_post', id=post.id, _external=True)}

這個視圖函數包含在permission_required裝飾器中,確保通過認證的用戶有寫博客文章的權限,得益於前面實現的錯誤處理程序,創建博客文章的過程變得很直觀,博客文章從JSON數據中創建,其作者是通過認證的用戶,這個模型寫入數據庫之後,會返回201狀態碼,並把Location首部的值設爲剛創建的這個資源的URL

爲便於客戶端操作,響應的主體中包含了新建的資源,如此一來,客戶端就無需再創建資源後再立即發起一個GET請求以獲取資源

用來防止未授權用戶創建新博客文章的permission_required裝飾器和程序中使用的類似,但會針對API藍本進行自定義,具體實現如下:

# app/api_1_0/decotators.py

def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def decoreted_function(*args, **kwargs):
            if not g.current_user.can(permission):
                return forbidden('Insufficient permissions')
            return f(*args, **kwargs)
    return decorator

博客文章PUT請求的處理程序用來更新現有資源,如下:

# app/api_1_0/posts.py

@api.route('/posts/<int:id>', methods =["PUT"])
@permission_required(Permission.WRITE_ARTICLES)
def edit_post(id):
    post = Post.query.get_or_404(id)
    if g.current_user != post.author and\
            not g.current_app.can(Psermission.ADMINISTER):
        return forbidden("Insufficient permissions")
    post.body = request.json.get('body', post.body)
    db.session.add(post)
    return jsonify(post.to_json())

本例中要進行的權限檢查更爲複雜,裝飾器用來檢查用戶是否有寫博客文章的權限,但爲了確保用戶能編輯博客文章,這個函數還要保證用戶是文章的作者或是管理員,這個檢查直接添加到視圖函數中,如果這種函數要應用於多個視圖函數,爲避免代碼重複,最好的方法是爲其創建裝飾器

因爲程序不允許刪除文章,所以沒必要實現DELETE請求方法的處理程序

用戶資源和評論資源的處理程序實現方法類似,下表列出了這個程序要實現的部分資源:

資源URL 方法 說明
/users/<int:id> GET 一個用戶
/users/<int:id>/posts/ GET 一個用戶發佈的博客文章
/user/<int:id>/timeline/ GET 一個用戶所關注用戶發佈的文章
/posts/ GET、POST 所有博客文章
/posts/<int:id> GET、PUT 一篇博客文章
/posts/<int:id/>comments/ GET、POST 一篇博客文章中的評論
/comments/ GET 所有評論
/comments/<int:id> GET 一篇評論

這些資源只允許客戶端實現Web程序提供的部分功能,支持的資源可以按需擴展,比如說提供關注者資源,支持評論管理,以及實現客戶端需要的其他功能

分頁大型資源集合

對大型資源集合來說,獲取集合的GET請求消耗很大,而且難以管理,和Web程序一樣,Web服務也可以對集合進行分頁,實現方式見下:

# app/api_1_0/posts.py

@api.route('/posts/')
def get_posts():
    page = request.args.get('page', 1, type=int)
    pagination = Post.query.paginate(
        page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
        error_out=False)
    posts = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_posts', page=page-1, _external=True)
    next = None
    if pagination.has_next:
        next = url_for('api.get_posts', page=page+1, _external=True)
    return jsonify({
        'posts':[post.to_json() for post in posts],
        'prev': prev,
        'next': next,
        'count': pagination.total
                })

JSON響應格式中的posts字段依舊包含各篇文章,但現在這只是完整集合的一部分,如果資源有上一頁和下一頁,prevnext字段分別表示上一頁和下一頁資源的URLcount是集合中博客文章的總數,這種技術可應用於所有返回集合的路由

發佈了34 篇原創文章 · 獲贊 23 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章