Python-FastAPI 使用asyncio生態圈開發異步博客(一)數據篇

項目地址前端

博客地址python

Frodo的第一個版本已經實現了,在下一個版本前,我將目前的開發思路整理成三篇文章,分別是數據篇、通訊篇、異步篇。

簡要系統分析

數據庫設計是緊跟需求來的,在我本科學UML時,數據庫設計是在需求分析和系統分析以後,架構設計以前的設計。但博客項目的需求比較簡單,主要大需求:mysql

  • 內容管理(文章、用戶、標籤、評論、反饋、動態的增刪改查)
  • 管理員用戶的驗證、評論人用戶的驗證
  • 小功能:邊欄組件、歸檔、分類等

再簡單地作一個系統分析:react

  • 博客前臺頁面(不須要認證,內容展現)git

    • 博文內容
    • 博客做者
    • 標籤
    • 訪問量
  • 管理頁面(須要登陸認證進行內容管理)
  • 動態頁面(須要認證)
  • 評論(訪問者須要登陸認證)

接下來的工做就是根據功能需求設計先後臺API,通常若是你是全棧本身開發的話,API形式能夠隨意些,由於後續還能夠靈活調整。若是須要和前端同事合做的話,你須要嚴格按照restful風格編寫,接口的參數、命名、方法和返回體的構造上嚴格體現需求。github

API 格式理應最大化地體現功能需求

先後臺API的形式也取決於所用技術,Frodo前臺頁面是選擇模板渲染的,後臺是使用Vue, 那麼模板就能夠在頁面上編程,能夠實時決定上下文,能夠不事先指定。redis

後臺API

url method params response info
api/posts GET limit:1
page: 頁面數
with_tag
{'items': [post.*.,], 'total': number} 查詢Posts
須要登陸
api/posts/new POST FormData
title
slug
summary
content
is_page
can_comment
author_id
status
x x
api/post/<post_id> GET/PUT/DELETE x items.*.created_at
items.*.author_id
items.*.slug
items.*.id
items.*.title
items.*.type
items.*._pageview
items.*.summary
status
items.*.can_comment
items.*.author_name
items.*.tags.*
total
須要登陸
api/users GET x {'items':[user.*.,], 'total': num} 須要登陸
api/user/new POST FormData
active
name
email
password
avatar: avatar.png
x 須要登陸
api/user/<user_id> GET/PUT x user.created_at
user.avatar
user.id
user.active
user.email
user.name
user.url(/user/3/)
ok (true)
須要登陸
api/upload POST/OPTIONS x x na
api/user/search GET name items.*.id
items.*.name
須要登陸
api/tags GET x items.*.name 須要登陸
api/user/info GET user (token) user{'name', 'avartar'} 至關於current_user
api/get_url_info POST url x na
api/status POST text, url, fids = ["png", ...] r, msg, activity.id, activity.layout, activity.n_comments, activity.n_likes, activity.created_at, activity.can_comment, activity.attachments.*.layout, activity.attachments.*.url, activity.attachments.*.title, activity.attachments.*.size

數據庫設計

設計數據庫就是設計表、表字段、表關係。嚴格上要先繪製E-R模型圖,他金石停留在邏輯層面的關係圖。下一步根據E-R圖,結合使用的數據庫類型(關係、Nosql、KV仍是圖數據庫)設計表關係圖,隨後要考慮以下幾個方面:sql

  • 數據存儲在哪裏?數據庫

    • 小型記錄數據存儲在mysql (查詢較快)
    • 長數據如「博客內容」查詢較慢 適合存儲在內存數據庫
    • 分佈式仍是單一式存儲?
  • 那些是高頻使用數據?編程

    • 常常須要作查詢的
    • 須要常常累加、統計計算的
    • 常常不變化的
  • 持久化方案

    • 數據庫如何按期備份?
    • KV數據庫的過時策略、按期存儲策略

其實博客項目不少都不須要考慮,但再大的項目這些都須要考慮了。其實還應該考慮的是數據庫併發訪問的問題,這涉及到鎖與同步機制,這部分我再通訊 部分闡述。

思考過上述問題後,大體有以下圖形:

上圖中不一樣的顏色字段考慮了不一樣的特色,分別是:

  • 數據庫存儲,選用mysql
  • KV存儲,選用redis
  • 高頻字段項,須要緩存選用redis或memcached

ORM類設計模式

ORM 是簡化SQL操做的產物,python將其作的最好的就是Django框架,主要作兩件事:

  • 類到數據庫表的映射(經過改造元類實現,達到建立這些_類_時便有了table屬性,注意不是類實例)
  • 提供簡化的面向對象的sql操做接口

表結構與表遷移

表結構在類中體現,Frodo使用的sqlalchemy是採用Column()類的形式。在類比較可能是,建議先寫一個_基類_,規定共有字段,在會面還能夠規定共有方法。

from sqlalchemy import Column, Integer, String, DateTime, and_, desc
@as_declarative()
class Base():
    __name__: str
    @declared_attr
    def __tablename__(cls) -> str:
        return cls.__name__.lower()

    @property
    def url(self):
        return f'/{self.__class__.__name__.lower()}/{self.id}/'

    @property
    def canonical_url(self):
        pass

上述的基類就規定了表名稱爲類名稱的小寫。接下來能夠規定一些公共字段和方法:

id = Column(Integer, primary_key=True, index=True)
created_at = Column(DateTime)

@classmethod
def to_dict(cls, 
            results: Union[RowProxy, List[RowProxy]]) -> Union[List[dict], dict]:
    if not isinstance(results, list):
        return {col: val for col, val in zip(results.keys(), results)}
    list_dct = []
    for row in results:
        dct = {col: val for col, val in zip(row.keys(), row)}
        list_dct.append(dct)
    return list_dct

idcreated_at 都是公共字段,而to_dict是很是經常使用的序列化方法。

接下來就是單獨的表,如Post表:

class Post(BaseModel, CommentMixin, ReactMixin):
    STATUSES = (
        STATUS_UNPUBLISHED,
        STATUS_ONLINE
    ) = range(2)

    status = Column(SmallInteger(), default=STATUS_UNPUBLISHED)
    (TYPE_ARTICLE, TYPE_PAGE) = range(2)
    created_at = Column(DateTime, server_default=func.now(), nullable=False)
    title = Column(String(100), unique=True)
    author_id = Column(Integer())
    slug = Column(String(100))
    summary = Column(String(255))
    can_comment = Column(Boolean(), default=True)
    type = Column(Integer(), default=TYPE_ARTICLE)
    pageview = Column(Integer(), default=0)

    kind = config.K_POST

這其中是Column的類屬性纔是對應到數據庫的屬性,其餘的是類其餘功能須要而設定的。

須要注意的Post類重寫了created_at字段, 這是規定默認的建立日期。

爲何繼承的是 Basemodel ,這一點採用了一些元類編程方法,主要緣由是異步, Basemodel類的設計在「異步篇」闡述。

接下來就是遷移到數據庫了,你能夠直接使用sqlalchemymetadata.create(engine),但這不利於調試,alembic是單獨作數據庫遷移管理的。把你寫好的類都導入到models/__init__.py中:

from .base import Base
from .user import User, GithubUser
from .post import Post, PostTag, Tag
from .comment import Comment
from .react import ReactItem, ReactStats
from .activity import Status, Activity

alembicenv.py文件中導入Base, 再規定遷移產生的行爲。這樣後連每次修改類(增長字段、更新字段屬性等)可使用alembic migrate來自動遷移。

類設計模式

數據庫表創建完成,接下來就是最重要的,編寫數據類,涉及到增刪改查的基本操做和類特定的一些方法。此時從「需求」到「接口」再到「類方法」的設計須要考慮以下兩點:

  • 語言的特性能夠帶來什麼,好比Python類中的@property, __get____call__等特點函數能付發揮做用?
  • 類設計的思考,類方法,實例方法 甚至是 虛擬方法?
  • 設計模式的使用,好比Frodo使用到的Mixin模式

本篇這是從類方法的功能設計來說的,具體實現細節牽涉到的東西,好比負責通訊的一些方法細節將在「通訊篇」介紹。

接下來咱們都能大體地畫一個圖:

上圖挑選了幾個表明性的類設計,不一樣的顏色表示不一樣的設計思路,固然了這些都是根據需求場景來的,這一步也能夠在開發過程當中不斷調整:

  • Classmethod: 類方法,不須要實例化的方法,由於數據庫字段屬性都是類屬性,所以不少數據操做的方法都不須要實例化,適合設計爲類方法
  • Property: 屬性方法,適合的場景可能是須要頻繁訪問,但又須要數據io的狀況,好比不少類都依賴做者id:
await cls.get_user_id()
await cls.user
  • Cached Decorator: 須要將結果緩存在redis的方法使用此類裝飾器,例如:
@classmethod
@cache(MC_KEY_ALL_POSTS % '{with_page}')
async def get_all(cls, with_page=True):
    if with_page:
        posts = await Post.async_filter(status=Post.STATUS_ONLINE)
    else:
        posts = await Post.async_filter(status=Post.STATUS_ONLINE,
                        type=Post.TYPE_ARTICLE)
    return sorted(posts, key=lambda p: p['created_at'], reverse=True)

@cache的處理規則將在「通訊篇」介紹。

  • Cached Property: 須要將結果緩存在內存的方法使用此類裝飾器,他的場景是在一個調用過程當中須要反覆使用的數據,但獲取昂貴。
@cached_property
async def target(self) -> dict:
    kls = None
    if self.target_kind == config.K_POST:
        kls = Post
        data = await kls.cache(ident=self.target_id)
    elif self.target_kind == config.K_STATUS:
        kls = Status
        data = await kls.cache(id=self.target_id)
    if kls is None:
        return
    return await kls(**data).to_async_dict(**data)

例如一個請求中須要屢次使用到await self.targettarget的獲取是十分昂貴的,此時能夠存儲在程序內存中。固然了這一特性早已進入python的標準庫functools.lri_cached, 但還沒支持異步,@cached_property是參考別人的項目創造的類裝飾器,他的實如今models/utils.py.

總結:數據庫設計是十分重要的第一步,後續API的開發效率很大程度取決於此。而數據關係到具體的語言實現又須要綜合考慮場景的多種特性。

PS: 寫此文時,Frodo下一步的打算是Golang重寫後臺API,算是把Go真正用起來。Frodo的前端我沒有全程手寫,所以添加新功能模塊有些困難,說到底我還只是後端工程師-.-..., 不過向全棧邁出一小步也算是進步吧~

相關文章
相關標籤/搜索