Tornado笔记——用Tornado搭建假单统计系统

    科技2024-06-08  77

    距离上一次写博客已经有些日子了,之前的阎王殿工程由于未能明确脑洞的方向无疾而终,但相关的技术已转为了我的技术储备。在这半年里,我使用Tornado搭建了工作中所用的内部网站,对于Tornado的相关开发也有了一定的体会。因此,我决定再写一个关于Tornado的系列建站教程,这次我们要搭建一个假单(出勤)统计系统,可以统计员工每周的请假和出勤情况。

    一 框架和工具

    在这个系列中,我将选择以下工具和框架作为后端开发工具:

    Web框架:TornadoORM框架:SQLAlchemy+Alembic数据库:sqlite3反向代理服务器:Nginx自己挑个顺手的IDE,我这里使用pycharm

    1 Tornado介绍

    与Django相同,Tornado是另一种流行的python框架。它的特点是在处理I/O时采用非阻塞异步方式,这使得它可以非常快地处理大量I/O操作。与Django相比,Tornado算是个轻量级框架,它本身并不提供像Django中的那些Form类以及ORM框架,而是单纯提供web服务以及服务器相关的模块。这使得我们可以快速地搭建起一个网站的架子,但同时我们也不得不去寻找其他的ORM框架,比如SqlAlchemy。

    2 SQLAlchemy

    使用ORM框架,我们可以将数据库中的表和对象建立映射,从而便于我们对表进行操作。在Django中,由于Django自己提供了一套ORM框架,因此我们无需使用其他的ORM框架。然而,Tornado是个轻量级的web框架,并没有提供自己的ORM框架。因此,我们选用SQLAlchemy作为我们的ORM框架。通过使用SQLAlchemy,我们可以很容易地实现对数据的增删改查以及建立表等操作。然而,SQLAlchemy没有提供修改表结构的操作,所以我们需要引入Alembic来做数据迁移。

    3 Alembic

    Alembic是一款搭配SQLAlchemy使用的数据迁移工具,与SQLAlchemy是同一作者。Alembic用于给已使用了SQLAlchemy建立映射的表添加新的字段,或直接使用其建立新表。它的感觉有点像git,每一次对数据库的改动都会自动产生一个版本号,在写好相应的升版操作后,将数据库升级到新版即可。

    4 Sqlite3和Nginx

    这两个是我们的老朋友了,在这里就不在赘述了。

    二 环境准备

    首先安装好python,这里选用python3.9。安装好后用pip将tornado和SQLAlchemy还有Alembic安装好。

    随后打开pycharm,建立我们的工程,我这里工程名叫LeaveManage,如图所示:

    这里大家可以选择建立一个虚拟环境,也可以选择直接使用本机的环境。使用虚拟环境的好处是,我们可以给每个不同的Python建立属于自己的环境,相对“干净”一些。由于我们之前已经安装了相关的库,这里可以把Inherit global site-packages勾上,让虚拟环境直接把我们安装好的库复制过来。

    点击Create,建立工程。

    三 目录结构

    我们在工程中新建一些包和文件夹,最终的目录结构如下所示:

    database包:在这个包中存放各种表的类以及CURD操作方法。migrate文件夹:我们将这个文件夹作为Alembic环境,之后所有对数据库的表结构修改都在这里进行。server文件夹:在这个文件夹下存放我们的主程序,在template文件夹下存放我们之后的各种前端模板。setting包:我们在这个文件夹下会建立一个配置文件,在里面存放一些配置信息;以及我们需要一些util方法来读取这些配置信息。venv:我们建立的虚拟环境,如果你没有建立虚拟环境,就不会有这个文件夹。

    四 开始写code

    在这篇博客中,我们先把整个网站的架子搭好,包括:

    建立全局配置文件以及写好读取配置的util方法数据库的util方法使用Alembic建立三张表写一个简单的index页面,配置好nginx,并将Tornado跑起来

    我们在setting里建立globalsettings.py文件,在这个文件里我们将存放一些配置以及读取配置文件的方法。

    # setting/globalsettings.py import os # 配置文件目录 SETTINGFILE_PATH = os.path.abspath('..\\setting\\globalsetting.ini') # 模板目录,考虑到模板目录不会经常变,就将其hardcode在这里 TEMPLATE_PATH = os.path.abspath('..\\server\\template') # 获得html文件的路径 def gettemplatepath(templatename): return os.path.join(TEMPLATE_PATH,templatename) # 从配置文件中读取配置的值 def getconfig(configname): with open(SETTINGFILE_PATH,'r') as f: lines = f.readlines() for line in lines: header = line.split('=')[0] if header == configname: value = line.split('=')[1] return value return ''

    然后我们再建立globalsetting.ini文件,设好以下配置:

    PORT=8000 DBPATH=D:\\LeaveManageDB\\LeaveManage.db

    port是Tornado稍后启动要监听的端口,而dbpath是我们DB文件存放的位置。

    接下来让我们进入database包,开始初始化数据库。

    我们建立3个python文件,分别命名为tablebase.py、dbcore.py和curd.py,这三个将是我们的核心文件。

    在tablebase.py中,我们将建立一个基类,之后所有表的类都将作为其子类,以便SQLAlchemy可以映射表和对象。

    # database/tablebase.py from sqlalchemy.ext.declarative import declarative_base Base = declarative_base()

    在dbcore.py中,我们将建立数据库会话(session),以便这个session可在其他地方使用。

    # database/dbcore.py from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from setting.globalsettings import getconfig conn_str = 'sqlite:///' + getconfig('DBPATH') engine = create_engine(conn_str) Session = sessionmaker(bind=engine) session = Session()

    session是SQLAlchemy管理数据库的工具,通过session,我们可以实现对数据库的query,commit和rollback。因此,我们需要建立一个全局的session来连接到数据库。

    在curd.py中,我们实现两个对数据库操作方法:insertdata和deletedata:

    # database/curd.py from database.dbcore import session from sqlalchemy.exc import DBAPIError,SQLAlchemyError def insertdata(dbobject): result = 'Fail' session.add(dbobject) try: session.commit() result = 'Success' except DBAPIError as e: print(e) session.rollback() except SQLAlchemyError as e: print(e) session.rollback() finally: return result def deletedata(dbobject): result = 'Fail' session.delete(dbobject) try: session.commit() result = 'Success' except DBAPIError as e: print(e) session.rollback() except SQLAlchemyError as e: print(e) session.rollback() finally: return result

    我们在这两个方法里使用了刚才建立的session,并且如果碰到了异常,则会rollback,防止出现脏写。

    接下来,让我们建立tbluser.py,tblusergroup.py和tblgroupprivilege.py,分别对应User表,UserGroup表和GroupPrivilege表。

    User表包含以下字段:

    id:用户ID,自增序列,唯一username:用户名,非空,唯一password:密码,非空email:email地址,非空,唯一usergroup:用户组,每个用户属于一个用户组,非空state:状态registerdate:注册日期lastlogintime:最后登录时间 # database/tbluser.py from database.tablebase import Base from sqlalchemy import Column,String,Integer,Date,DateTime class User(Base): __tablename__ = 'user' id = Column(Integer,autoincrement=True,primary_key=True) username = Column(String,unique=True,nullable=False) password = Column(String,nullable=False) email = Column(String,unique=True,nullable=False) usergroup = Column(String,nullable=False) state = Column(String) registerdate = Column(Date) lastlogintime = Column(DateTime) def __repr__(self): return '<user(username=%s,email=%s,registerdate=%s)>' % (self.username,self.email,self.registerdate)

    UserGroup表包含以下字段:

    id:用户组ID,自增序列,唯一groupname:用户组名,非空,唯一createdate:创建日期 # database/tblusergroup.py from database.tablebase import Base from sqlalchemy import Column,String,Integer,Date class UserGroup(Base): __tablename__ = 'usergroup' id = Column(Integer,autoincrement=True,primary_key=True) groupname = Column(String,unique=True,nullable=False) createdate = Column(Date) def __repr__(self): return '<usergroup(groupname=%s,createdate=%s)>' % (self.groupname,self.createdate)

    GroupPrivilege表包含以下字段:

    id:自增序列,唯一groupname:用户组名,非空,唯一funclist:这个用户组可以执行的功能名列表 # database/tblgroupprivilege.py from database.tablebase import Base from sqlalchemy import Column,String,Integer,Date class GroupPrivilege(Base): __tablename__ = 'groupprivilege' id = Column(Integer,autoincrement=True,primary_key=True) groupname = Column(String,unique=True,nullable=False) funclist = Column(String) def __repr__(self): return '<groupprivilege(groupname=%s,funclist=%s)>' % (self.groupname,self.funclist)

    在建立好这三个文件后,我们在工程目录里打开PowerShell,开始给数据库建表。

    注意,如果我们这里使用了虚拟环境,要首先以管理员模式打开PowerShell,执行Set-ExecutionPolicy RemoteSigned,选Y,否则PowerShell会无法激活venv环境。

    我们首先输入以下命令来激活虚拟环境:

    .\venv\Scripts\activate

    在输入之后,我们可以看到命令行前面有个前缀,表示我们在venv环境下:

    然后让我们切到migrate目录下,输入以下命令:

    alembic init alembic

     这样Alembic就会自己把环境准备好,并在migrate文件夹下新建一个叫alembic的文件夹以及一个alembic.ini的配置文件。

    我们打开alembic.ini,然后把里面的sqlalchemy.url=改成我们的数据库路径,如sqlalchemy.url = sqlite:///D:\\LeaveManageDB\\LeaveManage.db

    接下来,输入命令,开始建立第一个数据库版本:

    alembic revision -m "create user table"

    可以看到,这个命令和git也很像,意思是生成一个新版本,备注是create user table。

    然后我们会发现,在migrate/alembic/versions文件夹下会多出一个py文件,前面那一串乱码是Alembic自己生成的版本号(当然,每个人版本号不一样),而后面的就是我们刚才输入的comment.

    在这个py文件里有两个函数:upgrade和downgrade,分别对应数据库的升级和降级操作。我们要做的就是在upgrade里建立user表:

    # migrate/alembic/versions/xxx_create_user_table.py """create user table Revision ID: 0b427108a839 Revises: Create Date: 2020-10-07 16:45:26.729601 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '0b427108a839' down_revision = None branch_labels = None depends_on = None def upgrade(): op.create_table( 'user', sa.Column('id',sa.Integer,primary_key=True,autoincrement=True), sa.Column('username',sa.String,unique=True,nullable=False), sa.Column('password',sa.String,nullable=False), sa.Column('email',sa.String,unique=True,nullable=False), sa.Column('usergroup',sa.String,nullable=False), sa.Column('state',sa.String), sa.Column('registerdate',sa.Date), sa.Column('lastlogintime',sa.DateTime) ) def downgrade(): op.drop_table('user')

    相对应的,在downgrade里删除表,然而由于sqlite对drop的支持不是很好,所以这里的downgrade只是个摆设罢了。

    随后,我们回到PowerShell里,执行以下命令,将表跑进DB里:

    alembic upgrade head

    依次类推,我们可以把剩下的两张表一并跑进DB,可以使用sqlite3客户端查看:

     其中,alembic_version是alembic自己建的用于维护版本关系的表,剩下的三个是我们刚刚建立的表。

    接下来,我们可以开始搭tornado的架子了。我们在server下的main.py文件中建立tornado的主程序:

    # server/main.py import sys import platform import os import tornado.ioloop import tornado.web from tornado.web import url from setting.globalsettings import getconfig,gettemplatepath if platform.system() == 'Windows': import asyncio asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) class BaseHandler(tornado.web.RequestHandler): def get_current_user(self): return self.get_secure_cookie('currentuser') class Index(BaseHandler): def get(self): indexpath = gettemplatepath('index.html') self.render(indexpath) def make_app(): routelist = [ (r"/",Index) ] return tornado.web.Application(routelist,cookie_secret='12f6352#527') if __name__ == '__main__': app = make_app() port = int(getconfig('PORT')) app.listen(port) mainserver = tornado.ioloop.IOLoop.current() mainserver.start()

    在这个文件里,我们定义了一个Index作为主页,简单地渲染了template里的index.html页面,并将/这个路径映射到Index上。随后,我们使用配置文件中的端口启动tornado。然后,我们修改nginx的配置文件,使得我们访问nginx时能将流量转发到tornado中:

    # nginx配置文件 ... server { listen 9000; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { #root html; proxy_pass http://localhost:8000; #index index.html index.htm; } } ...

    这样,当我们访问localhost:9000时,其实会转发到localhost的8000端口,即tornado的端口上。

    我们启动nginx和tornado,然后访问localhost:9000,会看到hello world的主页:

    这样,我们就搭好了这个系统初步的架子。

    在后续的博客中,我将继续为大家介绍使用tornado搭建网站的步骤,希望大家关注~ 

    Processed: 0.012, SQL: 8