在进行经典ASP开发时,经常需要处理数据库查询和事务,同时还要考虑到安全性,特别是防止SQL注入攻击。本文将介绍一个自定义的VBScript类,它封装了数据库操作,简化了参数化查询的创建,并且提供了易于使用的接口。
在处理经典ASP项目时,需要一种管理参数化查询的方法。一方面,需要保护应用程序免受SQL注入攻击;另一方面,需要将数据库访问代码集中到应用程序的一个中心点,以简化维护。
在ASP中,创建预处理语句/参数化查询的标准指导是构建一系列语句来创建参数对象,然后将它们附加到命令上,最后执行命令。这个过程通常很繁琐,而且容易出错。
创建了一个简单的自定义VBScript类,它将所有必要的数据库操作封装到一个对象中,并且只暴露那些实际需要的项。ADO是一个多功能工具,但实际上从未使用过它的所有功能,所以类只暴露了实际发现有用的功能。
该类通过包装ADO连接对象并公开开发者友好的方法来工作。默认实现是保持连接对象私有,不公开,但如果愿意,当然可以将其公开,以便直接访问底层连接。
首先实例化类并用连接字符串初始化它:
dim DB
set DB = new Database_Class
DB.Initialize "YOUR_CONNECTION_STRING_HERE"
类内部保留了连接字符串,并通过一个名为Connection的私有函数引用连接。这个函数反过来在需要时创建连接。这允许在实际需要时才懒加载连接,这意味着可以全局包含这个类,连接只会在实际需要时创建。
使用ADO执行SQL时,通常有两种“模式”:执行返回数据的SQL或执行写入数据的SQL。这自然导致公开两种不同的方法,Query()和Execute()。每个方法都接受一个SQL语句和一个或多个参数(通过标量或数组参数传递的开发者友好用法),绑定参数并执行SQL。两者之间的唯一区别是Query()返回一个记录集,而Execute()不返回;事实上,Execute()内部调用Query()但丢弃结果。在SQL中使用?字符标识参数。
以下查询未使用参数:
set rs = DB.Query("select table_name, column_name, is_nullable from master.information_schema.columns", empty)
这里使用了VBScript关键字empty来表示没有传递参数。这使能够拥有相当灵活的方法,尽管不能有真正的函数重载。此方法的返回值是一个记录集,正如预期的那样。
Query()方法具有开发者友好的特性,它可以接受任意数量的SQL参数。它通过检查第二个参数的类型来实现这一点。如果第二个参数不是数组,那么Query()方法在执行前内部将这个单一参数转换为数组。这使能够避免在不需要时使用丑陋的Array(...)语法。
set rs = DB.Query("select table_name, column_name, is_nullable from master.information_schema.columns where column_name like ?", "x%")
这里是参数类型变化的地方:
set rs = DB.Query("select table_name, column_name, is_nullable from master.information_schema.columns where column_name like ? and is_nullable = ?", Array("x%", "YES"))
这里,将数组作为第二个参数传递给Query()。数组包含将绑定到SQL的每个参数,按照SQL字符串中问号出现的顺序。可以使用数组结构传递几乎无限数量的参数。
Execute()看起来与Query()完全相同,除了它是一个Sub,因此没有返回值:
DB.Execute "update foo set a = ?", "b"
DB.Execute "insert into foo values (?, ?, ?)", Array(1, "two", "three")
通过扩展Query()方法来接受更多参数,可以很容易地管理分页(即简单的记录集分页)。签名如下:
PagedQuery(sql, params, per_page, page_num, ByRef page_count, ByRef record_count)
前两个参数与前两种方法完全相同。per_page是要返回的每页记录数。page_num是想在记录集中查看的当前页码。page_count是一个返回参数,将包含记录集中的总页数。record_count也是一个返回参数,将包含所有页面中的总记录数。
由于该类包装了一个连接对象,因此很自然地也要公开事务功能。本可以公开连接对象本身,但如果不需要额外的功能,为什么要这样做呢?以下是一个完全虚构的例子,用于演示过程。
dim sql, rs
dim order_type = "some type"
dim order_name = "some name"
DB.BeginTransaction
sql = "insert into orders (order_type, order_name) values (?, ?)"
DB.Execute sql, Array(order_type, order_name)
sql = "select max(id) from orders where order_type = ? and order_name = ?"
dim new_id : new_id = DB.Query(sql, Array(order_type, order_name))(0)
DB.Execute "insert into order_status (order_id, status) values (?, ?)", Array(new_id, "N")
DB.CommitTransaction
可以随时调用DB.RollbackTransaction来取消事务,这是预期的。