tornado开发学习之2.输入输出,数据库操作,内置模板,综合示例
使用python环境中的tornado进行web开发上篇已经解决了urlmap和基本运行机制的问题。接下来进行web编程就是一下几个问题
- 1.输入数据的获取和输出运算结果
- 2.数据库操作
- 3.自带模板的使用
当然还有cookies和session等,这些杂项,不在本篇进行学习。
1.输入数据的获取和输入运算结果
WEB作为一种UI表达方式,最重要的是获取数据和表达运算结果,所以如何在一个web开发框架中获取url或post传递过来的参数十分重要,我建议学习所有的Web开发语言或框架都从此开始比较好。在tornado中从我自己的理解来看有两种获取参数的方式
-
a.通过urlmap做正则表达式,通过抓取组来直接获取参数
这就需要我们在做URL配置时就想好要传递的参数如,我们需要给ArticleDetail类传递一个文章编号,我们知道这个编号只能是数字类型的。那么可以在配置时写成如下样子
handlers =[(r'/detail/(\d+)',ArticleDetail),]
其中的(d+)就是一个正则的抓取组,用于获取多位的数字。 那么在ArticleDetail类的get或post函数实现时就要写成
classArticleDetail(BlogHandler):defget(self, id):....具体的实现
入口时多了一个id参数,当然你也可以用别的变量名,都会将URL匹配的结果放进这个参数传入的。
-
b.通过在get或post中使用tornado的RequestHandler的get_argument函数来获取参数
为了同样获取到文章id这个参数,如果我们不在URL配置时做正则(一般我都不做,因为我对正则不很熟练),也可以在具体处理某个地址的类方法中使用get_argument来获得参数值,还是以获取文章编号为例,我们需要如下方式来编写Url配置
handlers =[(r'/detail',ArticleDetail),]
在地址定义中我们取消了正则匹配组,让地址直接交给ArticleDetail类全权负责。 接下来在ArticleDetail中get或post函数的入口就不能在有除self意外的参数了,当使用/detail?id=1访问系统时
classArticleDetail(BlogHandler):defget(self): id=self.get_argument('id')#获取到id....具体的实现def post(self): id=self.get_argument('id')#获取到id....具体的实现
对于输出,一般在任何WEB开发框架中都比较简单,tornado中就是简单的self.write(htmlsrc),其中htmlsrc就是即将交给浏览器显示的html文件的源码。一般是由模板引擎将数据与模板结合后字符串结果。
2.数据操作
WEB开发与其他开发一致,也需要涉及数据的持久化和数据的读取。WEB的UI是HTML解决软件的界面,参数的收集,事件的触发,结果的显示;开发语言负责解决运算逻辑,数据读取和保存;数据库和数据文件或其他方式的持久化解决对象持久化和数据源问题(当然还有一种叫oracle的数据库!!!!它太NB了,自己都能写WEB,也能写程序,与我理解上数据库应该干的活区别较大,请观众随意吐槽);所以解决参数获取和数据库操作,基本上就解决了WEB开发的最大问题了(jquery?html?这也是很大的问题,但属于前端开发的主要学习方向)。
tornado内置了一个简单封装了Mysql的操作,pypm install python-mysql需要事先安装好驱动哦。公布出来的函数比较少,与直接使用python的数据接口基本一致,稍稍简单写。
-
a.数据连接
#导入tornado的数据库包from tornado import database #创建数据库连接,db_host数据库主机名,db_port开放的端口号,db_user用户名,db_passwd登录密码,idle_time最大空闲时间默认是7小时以秒为单位,mysql has gone away一般跟这个有关系。这种连接方式不支持连接池。较原始,效率还可以。 db = database.Connection('%s:%s'%(db_host, db_port), db_name, db_user, db_passwd, idle_time)
创建数据库连接后就可以使用db来进行后边介绍的基本操作了。
-
b.查询 常用的查询方法如下
1). query(),用于执行select 语句,返回的是行集list,例如
users=db.query("select * from users") for u in users: print u.username,u.userpwd user=db.query("select * from users where id=%s",id) if user: print user[0].username,user[0].userpwd
2).get(),返回的是符合条件的结果集的第一行
user=db.get("select * from users where id=%s",id)if user:print user.username,user.userpwd
3).execute(),用于执行select以外的语句,返回值基本无视
rv=db.execute("update customer set sex='男' where id=10128") print rv
-
基本上如果你的语句没写错,得到的都是0
-
c.数据结果
知道如何查询后续就是如何利用查询得到的结果了。
对于query()操作返回的是[]数组(命名位items),
数组中每一个元素均是{}字典(每一个都是item)。
如果确切知道字段名(属性名)如确切知道查询结果中至少有一行数据,包含名为title的字段,可以直接使用items[0].title得到数据。
如果不确切知道字段名,或字段值可能为None,可使用items[0].get('title','')来获取值。 最常用的方法是循环
for item in items:print item.get('title','')
-
d.其他操作
-
execute_rowcount(sql)用于获取查询或更新得到的记录行集的行数,特别有意思的是如果你要更新的内容本来已经是那个样子,如
print db.execute_rowcount("update customer set sex='女' where id=10128")print db.execute_rowcount("update customer set sex='女' where id=10128")
执行两遍得到的结果是1,0。我原来一直以为数据库很弱智的,看来是我那时候不懂事,很天真。
- close()用于手动关闭数据连接,在小型应用中我们基本不会看到他的出现
-
3.自带的模板的使用
web编程其实可以不用模板的,但是那样不仅写的时候痛苦,写出来以后更痛苦
写的时候,html代码与python代码混合,连接字符串非常费力
改的时候很容易出现错误,python又不是编译执行的,上线了执行到某错误时,多恼火,低级愚蠢的错误容易犯
换web的UI时这种写法根本没办法操作,那就是重写一次程序啊,杀人的心都有的
所以如果你开发了一个Web系统,但是没有用模板技术,这个系统就只能给钱多人傻的单位做了。
mako是我最喜欢的模板,但是学习tornado么,而且不是那种排版敏感的,先来学习用用它自带的。
web使用开发使用模板引擎我么要主要解决一下几个主要的问题,在tornado中我们一个个破解
- 1.路径和模板存放方式
善用os.path.dirname(file)来解决路径问题,例如我们的模板准备放在工程的根目录下的/T/目录中,对于模板存放的目录我们可以这样定义
loader =template.Loader(os.path.dirname(__file__)+"/T/")
当然我更倾向直接定义绝对路径的方法来解决这个问题,因为使用上也特别简单,而且可以把模板放在与源代码无关的目录中,上线使用时也更加安全
TP="D:\\T\\" loader=template.Loader(TP)
模板一般就是普通的文本文件,方便起见一般以html扩展名存放比较好。比如index.html,这样我们把index.html放入我们定义的模板加载路径后使用以下代码加载模板引擎
t=loader.load("index.html")
其中t就是我们直接可以使用的模板了。
- 2.数据与模板如何结合
模板主要功能之一就是解决数据的显示方式,如何把我们的数据交给模板使用?首先我们来获取数据
cus1=db.query("select * from customers limit 3")#得到数据 cus2=db.query("SELECT * FROM customer LIMIT 5 OFFSET 5")#得到数据 htmlsrc=t.generate(datas1=cus1,datas2=cus2)#模板运算出的字符串作为html显示self.write(htmlsrc)#向客户端发送结果
代码第三行的datas1和data2是在tornado模板文件中将要引用的变量名我们的模板编写如下
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><body><!--这里从datas1中读取数据--><tablestyle="border:1px solid"> {% for u in datas1 %} <tr><td>{{ u.truename }}</td><td>{{ u.sex }}</td></tr> {% end %} </table><hr/><tablestyle="border:1px solid"> {% for u in datas2 %}<tr><td>{{ u.truename }}</td><td>{{ u.sex }}</td></tr> {% end %} </table></body></html>
可以高兴地讲:
1.tornado的模板不需要严格按照python的缩进来书写
2.tornado的模板使用{{变量或对象}}来获取具体的变量值或对象属性值
3.tornado的模板中使用{% for var in vars %}....{% end%}来实现循环
4.tornado的模板中使用{% if 条件 %}...{% elif 条件 %}...{% else %}...{% end %}方式实现模板内的判断
5.tornado的模板使用{% include 文件名 %}来包含子模板,同时也会运算子模板的运算逻辑
OK知道以上这些内容或许我们已经可以开始一个稍稍复杂一点儿程序了。下面我们来联系一下。
4.综合示例
制作的目标做一个对一个数据表进行增加、删除、修改和简单查询的程序数据表我们假定为通讯录数据为了简化程序的运行逻辑,尽可能不用ajax或其他需要一些javascript基础的写法数据库当然使用mysql啦
我按照一般习惯的开发过程来完成这个demo,综合以上的知识,我尽量少写代码以外的文字了,写尽可能细致的注释。功能虽然特别简单,都是按照平时实际项目的开发规范和架构来执行的。
a.数据结构及访问权限
我们使用root用户来访问数据库,假定访问的密码是root123456,使用以下语句来创建数据表:
CreateTable CREATE TABLE `d_contacts`(`id`int(11) NOT NULL AUTO_INCREMENT,`truename` varchar(20) DEFAULT NULL,`sex` varchar(2) DEFAULT NULL,`phoneno` varchar(20) DEFAULT NULL,`email` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8
b.规划目录结构
我们假定开发的项目文件存放在D:\projecting目录中,项目名称叫GingerTornadoDemo1
c.关键性源码解释
- 1).处理类的基类 basehandler.py
代码如下,通过定义基础类,可以让后续的开发变得更加简单,这里使用了一个技巧性的变化。 利用了Aaron Swartz的web.py框架中的Storage类来简化参数获取。以前需要一个个用get_argument()来获取的参数现在只要用i=self.input() 然后直接获取参数名如i.id就可以得到了。当然参数如果不存在仍然会报错的。获取后最好用utils包中的InitStorage做一次初始化(不会污染已有属性值),确保调用无误。
1 # -*- coding: utf-8 -*- 2 import json 3 __author__ = 'jy@cjlu.edu.cn' 4 from tornado.web import RequestHandler 5 from Storage import storage 6 class basehandler(RequestHandler): 7 """ 所有Handler基类 """ 8 def input(self): 9 """获取到所有的输入数据,将其转换成storage方便调用""" 10 i= storage()#初始化一个容器 11 #得到所有的输入参数和参数值 12 args=self.request.arguments 13 #将参数写入i的属性 14 for a in args: 15 i[a]=self.get_argument(a) 16 #获取file类型的参数 17 i["files"]=storage(self.request.files) 18 #获取path 19 i["path"]=self.request.path 20 #获取headers 21 i["headers"]=storage(self.request.headers) 22 return i
- 2).数据操作类 d_contacts.py
此文件保存于models目录中,用来做dcontacts数据表的基本操作,我简单封装了一些基础的函数。有些可能未在本demo中使用过,但我认为很重要也编写了出来。 在python中JAVA和C#中的失血模型由一个简单的Storage基本全部搞定,而且可以很方便的进行数据结构的调整和属性类型变化和增补属性。所以这个类也可以看出来Java和C#那种长时间对我编码风格的侵害有多深了。所有的程序都可以放一个文件中解决,但我仍然要把他们的功能按照多层的思想来细分。 这个类可以视作DAL类,如果有更复杂的业务逻辑操作还可以继承或调用dcontacts来编写更多的业务逻辑(至于是继承好还是调用好,我一般选择继承,虽然有违与封闭开发的面向对象接线的传统,但方便啊。谁用谁知道。)。
# -*- coding: utf-8 -*- __author__ = 'jy@cjlu.edu.cn' from config import sdb from Storage import storage def GC_set(obj): return "" class d_contacts: fields=["id","truename","sex","phoneno","email"] #定义好数据表有的数据 def getAll(self): """获取全部记录集合""" items=sdb.query("select * from d_contacts") if items: for i in items: i=storage(i) return items def delEntityById(self,id): """ 根据id来删除数据 """ return sdb.execute("delete from d_contacts where id=%s",id) def getEntityById(self,id): """ 根据id来或取对象实例""" if id: id=int(id) item=sdb.get("select * from d_contacts where id=%s",id) return storage(item) else: return None def getRowsBySqlwhere(self,sqlwhere): """ 根据sqlwhere来查询数据集合""" items=sdb.query("select * from d_contacts where id>0 "+sqlwhere) if items: for i in items: i=storage(i) return items def update(self,obj): """ 更新数据行集 """ obj=storage(obj) return sdb.execute("update d_contacts set truename=%s,sex=%s,phoneno=%s,email=%s where id=%s",obj.truename,obj.sex,obj.phoneno,obj.email,obj.id) def insert(self,obj): """ 写入数据 """ obj=storage(obj) return sdb.execute("insert into d_contacts (truename,sex,phoneno,email) values(%s,%s,%s,%s)",obj.truename,obj.sex,obj.phoneno,obj.email) def getRowBySqlwhere(self,sqlwhere): """ 根据sqlwhere来获取一个数据实例 """ item=sdb.get("select * from d_contacts where id>0 "+sqlwhere) if item: item=storage(item) return item
- 3).具体业务控制实现类 f1001.py
这是所有操作数据表dcontacts的具体业务逻辑的实现。此文件存放于f10目录中,同过它调用models包的dcontacts等来实现对d_contacts数据的操作和视觉表达。 既然是demo我就顺便使用了一下简单的ajax操作,来完成增、删、改的功能。这样就基本涵盖了大部分的web开发的基础性功能的实现方式了。
# -*- coding: utf-8 -*- __author__ = 'jy@cjlu.edu.cn' import os from tornado import template from Storage import storage #导入工具函数 from utils import * #导入基础类 from basehandler import basehandler #导出持久类 from models.d_contacts import d_contacts #导入模板 from config import TP #创建持久对象 optobj=d_contacts() #建立模板,指定模板路径 tl=template.Loader(os.path.join(TP,"f10/T")) class List(basehandler): """ 用于处理显示列表 """ def get(self): #获取全部输入参数 i=self.input() #获取全部数据,继续改下去一定要考虑分页 i.recs=optobj.getAll() #调用模板 t=tl.load("f1001_list.html") #把数据推给模板进行运算 htmlsrc=t.generate(i=i) #显示运算结果 self.write(htmlsrc) def post(self): """由于不需要处理POST,所以不实现 """ pass class Update(basehandler): """ 用于处理显示修改界面,处理ajax修改、新增、删除的请求 """ def get(self): i=self.input() #初始化i的rec属性,让Rec具有与数据表字段名的全部属性 i.rec=InitStorage(i,["id","truename","phoneno","email"]) #从数据库中获取数据 i.rec=optobj.getEntityById(i.id) #调用模板 t=tl.load("f1001_update.html") #把数据推给模板进行运算 htmlsrc=t.generate(i=i) #显示运算结果 self.write(htmlsrc) def post(self): """ 处理写入数据删除数据和保存数据等操作 """ def valid(v): """ 验证邮件是否正确 """ if IsEmail(v.email): return True else: return False #特别要注意,因为是响应的ajax的请求,返回数据类型需指定为json self.set_header("Content-Type", "application/json") #做一个容器变量 v=storage() #获取到全部的输入值 i=self.input() #将输入参数中与数据表字段名一致的值赋值给v对象的属性 v=CopyData_INC(v,i,optobj.fields) #根据操作类型来进行处理 if i.act=="add": vv=valid(i) if vv==True: optobj.insert(v) #返回处理结果JSON值 self.write(JsonResult("OK")) else: self.write(JsonResult("NOOK")) if i.act=="update": vv=valid(i) if vv==True: optobj.update(v) self.write(JsonResult("OK")) else: self.write(JsonResult("NOOK")) if i.act=="del": optobj.delEntityById(i.id) self.write(JsonResult("OK"))
- 4).其他
开发必然需要用到html和javascript,是难点但不是我们学习tornado的重点。具体看代码吧。 我只解释一下f1001_update.html模板中一段javascript
var rv={}; $("[rel='v']").each(function(){ rv[$(this).attr("id")]=$(this).val(); }); ....body中有如下元素.... <td><input type="text" rel="v" name="truename" id="truename" value=""/>*</td> <td><input type="text" rel="v" name="sex" id="sex" value=""/>*</td> <td><input type="text" rel="v" name="phoneno" id="phoneno" value=""/>*</td> <td><input type="text" rel="v" name="email" id="email" value=""/>*</td> ....
这是我几乎在所有项目的录入模板中都使用的一个小技巧,我额外地给所有需要获取值的控件增加了一个rel属性。 利用jquery的选择特性来遍历id后自动获取这些组件的值,再将值赋值给实现定义的字典rv。 这样做的好处是,你几乎从前台到后台都不需要在参数值的获取上再一个个地进行赋值了。rv对象可直接在ajax调用时使用,无需处理。ajax调用参数传给处理的类后,使用i=self.input(),又将所有的参数和值变成了i的属性和值。 接下来你要做的就是使用i这个已经赋值好的对象来做你的预算了,所有以往获取参数和传递参数的麻烦就全解决了。
简单的描述你肯定感受不到这样做的好处,但当你面对一个具有几十个参数需要录入,后台对应一个数据对象时,你就会深深地爱上我的这种写法。超级爽,不用写一个赋值和获取参数的代码啦。还是去体验源码吧。
-
5).效果和源码下载
列表功能:
修改功能:
最好的文档是源码:tornado综合示例
5.结论与其他
- 本来想写3~5篇的帖子,发现两篇就搞定了
- 肯定是疏漏了很多东西啊,权鉴、404、UI组件、分页等等,还有很多。但我认为那些不是主干。主干是我介绍的这些,有这些你就可以使用tornado来工作了
- 我学习到这里感受tornado挺不错的,但感觉在开发的效率上来看,与web.py还存在相当大的差距。我自己的想法是使用tornado来做web服务器,做urlmap。好了,其他的数据库组件还是继续使用web.py的,模板继续使用mako比较和我的胃口。因为我开始学习tornado仅是因为它的性能不错,名气不小。而且在Aaron Swartz自杀后,web.py后续的发展给我自己在组件的选择上撬开了一个小小的缺口。别无它。
- 喜欢的兄弟拿来学习用用,不喜欢也可以来吐槽。我感觉我基本说清楚了tornado的Web开发。言语有限,我对他的系统学习到此为止了,再有问题,个别看它的文档相信是小菜了。