Frodo的第一个版本已经实现了,在下一个版本前,我将目前的开发思路整理成三篇文章,分别是数据篇、通信篇、异步篇。
简要系统分析
数据库设计是紧跟需求来的,在我本科学UML时,数据库设计是在需求分析和系统分析之后,架构设计之前的设计。但博客项目的需求比较简单,主要大需求:
- 内容管理(文章、用户、标签、评论、反馈、动态的增删改查)
- 管理员用户的验证、评论人用户的验证
- 小功能:边栏组件、归档、分类等
再简单地做一个系统分析:
- 博客前台页面(不需要认证,内容展示)
- 博文内容
- 博客作者
- 标签
- 访问量
- 管理页面(需要登录认证进行内容管理)
- 动态页面(需要认证)
- 评论(访问者需要登录认证)
接下来的工作就是根据功能需求设计前后台API,一般如果你是全栈自己开发的话,API形式可以随意些,因为后续还可以灵活调整。如果需要和前端同事合作的话,你需要严格按照restful风格编写,接口的参数、命名、方法和返回体的构造上严格体现需求。
API 格式理应最大化地体现功能需求
前后台API的形式也取决于所用技术,Frodo前台页面是选择模板渲染的,后台是使用Vue, 那么模板就可以在页面上编程,可以实时决定上下文,可以不事先指定。
后台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 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还是图数据库)设计表关系图,随后要考虑如下几个方面:
- 数据存储在哪里?
- 小型记录数据存储在mysql (查询较快)
- 长数据如「博客内容」查询较慢 适合存储在内存数据库
- 分布式还是单一式存储?
- 那些是高频使用数据?
- 经常需要做查询的
- 需要经常累加、统计计算的
- 经常不变化的
- 持久化方案
- 数据库如何定期备份?
- KV数据库的过期策略、定期存储策略
其实博客项目很多都不需要考虑,但再大的项目这些都需要考虑了。其实还应该考虑的是数据库并发访问的问题,这涉及到锁与同步机制,这部分我再通信
部分阐述。
思考过上述问题后,大致有如下图形:
[图片上传失败…(image-c76c8e-1592272635588)]
上图中不同的颜色字段考虑了不同的特点,分别是:
- 数据库存储,选用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)@classmethoddef 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
如id
和created_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
类的设计在「异步篇」阐述。
接下来就是迁移到数据库了,你可以直接使用sqlalchemy
的metadata.create(engine)
,但这不利于调试,alembic
是单独做数据库迁移管理的。把你写好的类都导入到models/__init__.py
中:
from .base import Basefrom .user import User, GithubUserfrom .post import Post, PostTag, Tagfrom .comment import Commentfrom .react import ReactItem, ReactStatsfrom .activity import Status, Activity
在alembic
的env.py
文件中导入Base
, 再规定迁移产生的行为。这样后连每次修改类(增加字段、更新字段属性等)可以使用alembic migrate
来自动迁移。
类设计模式
数据库表建立完成,接下来就是最重要的,编写数据类,涉及到增删改查的基本操作和类特定的一些方法。此时从「需求」到「接口」再到「类方法」的设计需要考虑如下两点:
- 语言的特性可以带来什么,比如Python类中的
@property
,__get__
、__call__
等特色函数能付发挥作用? - 类设计的思考,类方法,实例方法 甚至是 虚拟方法?
- 设计模式的使用,比如Frodo使用到的Mixin模式
本篇这是从类方法的功能设计来讲的,具体实现细节牵涉到的东西,比如负责通信的一些方法细节将在「通信篇」介绍。
接下来我们都能大致地画一个图:
[图片上传失败…(image-767c74-1592272635589)]
上图挑选了几个代表性的类设计,不同的颜色表示不同的设计思路,当然了这些都是根据需求场景来的,这一步也可以在开发过程中不断调整:
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_propertyasync 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.target
而target
的获取是十分昂贵的,此时可以存储在程序内存中。当然了这一特性早已进入python的标准库functools.lri_cached
, 但还没支持异步,@cached_property
是参考别人的项目创造的类装饰器,他的实现在models/utils.py
.
总结:数据库设计是十分重要的第一步,后续API的开发效率很大程度取决于此。而数据关系到具体的语言实现又需要综合考虑场景的多种特性。
PS: 写此文时,Frodo下一步的打算是Golang重写后台API,算是把Go真正用起来。Frodo的前端我没有全程手写,因此添加新功能模块有些困难,说到底我还只是后端工程师-.-…, 不过向全栈迈出一小步也算是进步吧~
文章来源:智云一二三科技
文章标题:Python-FastAPI 异步博客开发(一) 数据建模篇
文章地址:https://www.zhihuclub.com/6826.shtml