欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-04-30 16:34:59
...

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

一 框架和工具

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

  • Web框架:Tornado
  • ORM框架: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,如图所示:

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

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

点击Create,建立工程。

三 目录结构

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

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

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

四 开始写code

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

  1. 建立全局配置文件以及写好读取配置的util方法
  2. 数据库的util方法
  3. 使用Alembic建立三张表
  4. 写一个简单的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环境下:

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

然后让我们切到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

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

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

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

 其中,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笔记——用Tornado搭建假单统计系统

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

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