项目地址博客地址Frodo第一版已经实现。在下个版本之前,我会将目前的开发思路整理成三篇文章,分别是数据篇、通信篇、异步篇。系统简要分析数据库设计遵循需求。本科学习UML的时候,数据库设计是在需求分析和系统分析之后,架构设计之前。不过博客项目的需求比较简单,主要需求是:内容管理(文章、用户、标签、评论、反馈、动态增删改查)管理员用户验证、审稿人用户验证小功能:侧边栏组件,归档,分类等然后简单做一个系统分析:博客首页(无需认证,内容展示)博文内容博客作者标签流量管理页面(内容管理需要登录认证)动态页面(需要认证)评论(访客需要登录认证)接下来的工作就是根据功能需求设计前后端API。一般自己开发全栈的话,API形式可以随意,后期可以灵活调整。如果需要和前端同事合作,需要严格按照restful风格来写,接口的参数、命名、方法、结构返回体等都严格体现要求。API格式应最大限度地满足功能需求。前端和后端API的形式也取决于所使用的技术。Frodo的前端页面使用模板渲染,后台使用Vue。然后可以在页面上对模板进行编程,并且可以实时确定上下文。预先指定。后台APIurlmethodparamsresponseinfoapi/postsGETlimit:1page:带_tag的页数{'items':[post.*.,],'total':number}查询帖子需要登录api/posts/newPOSTFormDatatitleslugsummarycontentis_pagecan_commentauthor_idstatusxxapi/发布/GET/PUT/DELETEXiitems.*.created_atitems.*.author_iditems.*.slugitems.*.iditems.*.titleitems.*.typeitems.*._pageviewitems.*.summarystatusitems.*.can_commentitems.*.author_nameitems.*.tags.*total需要登录api/usersGETx{'items':[user.*.,],'total':num}需要登录api/user/newPOSTFormDataactivenameemailpasswordavatar:avatar.pngx需要登录api/user/GET/PUTxuser.created_atuser.avataruser.iduser.activeuser.emailuser.nameuser.url(/user/3/)ok(true)需要登录api/uploadPOST/OPTIONSxxnaapi/user/searchGETnameitems.*.iditems.*.name需要登录api/tagsGETxitems.*.name需要登录api/user/infoGETuser(token)user{'name','avartar'}等同于current_userapi/get_url_infoPOSTurlxnaapi/statusPOSTtext,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数据库的过期策略和定时存储策略,但是这些对于大型项目来说是需要考虑的。其实还应该考虑的是数据库并发访问的问题,这涉及到锁和同步机制。这部分我会在交流部分详细说明。思考完以上问题,大致有下图:上图中不同颜色的字段考虑到不同的特性,即:数据库存储,选择mysqlKV存储,选择redis高频字段项,选择redis或者memcachedORM设计用于缓存的模式ORM是简化SQL操作的产物。python能做的最好的事情就是Django框架。它主要做了两件事:类到数据库表的映射(创建这些类时通过转化元类实现建表)属性,注意不是类实例)提供简化的面向对象的sql操作接口表结构和表迁移表结构在类中都有体现,Frodo使用的sqlalchemy就是以Column()类的形式。类比较多的时候,建议先写一个_基类_,指定公共字段,会议中也指定公共方法。fromsqlalchemyimportColumn,Integer,String,DateTime,and_,desc@as_declarative()类Base():__name__:str@declared_attrdef__tablename__(cls)->str:returncls.__name__.lower()@propertydefurl(self):returnf'/{self.__class__.__name__.lower()}/{self.id}/'@propertydefcanonical_url(self):pass上面的基类指定表名是类名的小写.接下来,可以指定一些公共字段和方法:id=Column(Integer,primary_key=True,index=True)created_at=Column(DateTime)@classmethoddefto_dict(cls,results:Union[RowProxy,List[RowProxy]])->Union[List[dict],dict]:如果不是isinstance(results,list):return{col:valforcol,valinzip(results.keys(),results)}list_dct=[]forrowinresults:dct={col:valforcol,valinzip(row.keys(),row)}list_dct.append(dct)returnlist_dct比如id和created_at是公共字段,to_dict是很常见的序列化方式。接下来是一个单独的表,比如Post表:)=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)种类=在config.K_POST中,Column的类属性是数据库对应的属性,其他的都是为类的其他功能设置的。请注意,Post类会覆盖created_at字段,该字段指定默认创建日期。为什么要继承Basemodel?一些元类编程方法用于这一点。主要原因是异步。Basemodel类的设计在“异步篇”中有讲解。下一步是迁移到数据库。可以直接使用sqlalchemy的metadata.create(engine),但是这样不利于调试。Alembic单独进行数据库迁移管理。将所有编写的类导入模型/__init__.py:from.baseimportBasefrom.userimportUser,GithubUserfrom.postimportPost,PostTag,Tagfrom.commentimportCommentfrom.reactimportReactItem,ReactStatsfrom.activityimportStatus,ActivityimportBase在alembic的env.py文件中,然后指定迁移生成的行为。这样每次修改类(增加字段,更新字段属性等)时,都可以使用alembicmigrate自动迁移。类设计模式数据库表建立好了,最重要的就是写数据类,涉及到增删改查的基本操作和一些类特有的方法。这时候从“需求”到“接口”再到“类方法”的设计需要考虑以下两点:语言的特性能带来什么,比如@property、__get__、__call__等特殊函数在Python类中可以使用效果?考虑类设计、类方法、实例方法甚至虚拟方法?设计模式的使用,例如Frodo使用的Mixin模式。本文来自于类方法的功能设计。具体的实现细节,比如一些负责通信的方法的细节,会在“通信篇”中介绍。接下来,我们可以大致画一张图:上图选取了几个有代表性的类设计,不同的颜色代表不同的设计思路。当然,这些都是基于需求场景。这一步也可以在开发过程中完成。不断调整:Classmethod:类方法,不需要实例化的方法,因为数据库字段属性都是类属性,所以很多数据操作方法不需要实例化,适合设计成类方法Property:属性方法,适合对于场景可能是需要频繁访问但是需要数据io的情况。比如很多类依赖authorid:awaitcls.get_user_id()awaitcls.userCached装饰器:redis中需要缓存结果的方法就使用了这种装饰器,例如:@classmethod@cache(MC_KEY_ALL_POSTS%'{with_page}')asyncdefget_all(cls??,with_page=True):ifwith_page:posts=awaitPost.async_filter(status=Post.STATUS_ONLINE)else:posts=awaitPost.async_filter(status=Post.STATUS_ONLINE,type=Post.TYPE_ARTICLE)returnsorted(posts,key=lambdap:p['created_at'],reverse=True)@cache处理规则将在“通信篇”中介绍。CachedProperty:这种类型的装饰器用于需要在内存中缓存结果的方法。他的场景是在通话过程中需要反复使用的数据,但是获取成本很高。@cached_propertyasyncdeftarget(self)->dict:kls=Noneifself.target_kind==config.K_POST:kls=Postdata=awaitkls.cache(ident=self.target_id)elifself.target_kind==config.K_STATUS:kls=Statusdata=awaitkls.cache(id=self.target_id)ifklsisNone:returnreturnawaitkls(**data).to_async_dict(**data)比如awaitself.target需要多次使用在一个请求中获取目标是非常昂贵的,此时可以将其存储在程序内存中。当然这个特性已经进入了python标准库functools.lri_cached,只是还没有支持异步。@cached_property是参照他人项目创建的类装饰器。它的实现在models/utils.py中。总结:数据库设计是非常重要的第一步,后续API的开发效率很大程度上取决于此。但数据与具体的语言实现相关,需要综合考虑场景的各种特性。PS:在写这篇文章的时候,Frodo接下来的计划是用Golang重写后台API,也算是真正用到了Go。Frodo的整个前端我都没有手写,所以很难添加新的功能模块。毕竟我只是一个后端工程师-.-...,不过向全栈迈出一小步也算是进步了~