Classic ASP 中的简单参数化查询类





5.00/5 (8投票s)
本文讨论了一个封装 ADO Connection 对象并使其对开发人员更友好的类。
引言
本文为特定问题提供了一个通用的解决方案:通过简化参数化查询的创建来防止 SQL 注入。提供的类充当一个通用的数据库连接封装类,公开了几个关键方法来管理查询和事务。该类的这些方法比标准的参数对象方法更容易使用。
背景
最近,我不得不在 classic ASP 中进行大量开发,并且需要一种方法来管理参数化查询。一个主要concern 是防止 SQL 注入攻击。另一个concern 是将数据库访问代码整合到一个应用程序的集中点。历史上,classic ASP 应用程序在代码库中随处可见数据库访问代码,这使得维护比必要时更复杂。
ASP 中预处理语句/参数化查询的标准指南是发出 一系列语句来构造参数对象,将它们添加到命令中,然后执行命令。这个过程通常很麻烦,而且坦率地说并不愉快。它也相当容易出错,因为您有一个复杂的解决方案,而它通常不是必需的。
相反,我创建了一个简单的自定义 VBScript 类,它将所有必要的 *
数据库操作封装到一个对象中,该对象仅公开实际需要的项。ADO 是一个瑞士军刀,但实际上我从未用过它的一小部分功能,所以这个类只公开了我一直发现有用的那些功能。
*
当然是为我的需求而必需的,并且可能足以满足 80% 的使用场景,但您可以根据需要进行修改。
该类利用了参数创建的一个关键概念:在执行查询时可以传递参数数组,参数将在运行时动态为您构建。是的,这会产生微小的数据库命中,但我们应该记住,ASP 性能的指导方针大部分是 15 年前制定的,而服务器和数据库的性能和缓存已经取得了长足的进步。换句话说,为了获得清晰度和程序员时间,这种微小的数据库命中的权衡是值得的,尤其是当您的时间比您使用的硬件更昂贵时。
Using the Code
该类通过封装 ADO 连接对象并公开对开发人员友好的方法来工作。默认实现是将连接对象保持 private
并且不公开它,但如果您愿意,当然可以将其设为 public
,如果您需要直接访问底层连接。
注意:为简单起见,我们在 SQL Server 中使用 master.INFORMATION_SCHEMA
进行演示,因此只要您安装了 SQL Server,它们应该可以运行。
我们首先实例化该类并使用连接字符串对其进行初始化
dim DB : set DB = new Database_Class
DB.Initialize YOUR_CONNECTION_STRING_HERE
在内部,该类保留连接字符串,并通过一个名为 Connection
的 private
函数进行引用。此函数在连接不存在时创建连接。这使得连接可以在实际需要时延迟加载,这意味着您可以全局包含该类,并且仅在实际需要时创建连接。
在使用 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
来表示没有传递参数。这允许我们拥有相当灵活的方法,即使我们无法实现真正的函数重载。此方法的返回值是一个 recordset
,正如预期的那样。
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
也是一个返回参数,其中包含所有页面中的总记录数。
调用者还必须采取措施来确保在使用记录集进行迭代时使用了分页。以下是演示中用于显示分页记录集的粗略函数
Sub putpaged(rs, per_page)
put "<table border='1' cellpadding='5'>"
put "<tr>"
dim field
For Each field in rs.Fields
put "<th>" & field.Name & "</th>"
Next
put "</tr>"
dim x : x = 0
Do While x < per_page and Not rs.EOF
put "<tr>"
For Each field in rs.Fields
put "<td>" & rs(field.Name) & "</td>"
Next
put "</tr>"
x = x + 1
rs.MoveNext
Loop
put "</table>"
End Sub
在此演示中,put
方法只是一个 sub
,它封装了 response.write
以便于输入和阅读,但有一个转折:如果传入的变量是 recordset
,它会调用一个将 recordset
显示为表格的方法。因此,演示中的行如下所示
put "<p><strong>No parameters: All tables and columns</strong></p>"
set rs = DB.Query("select table_name, column_name, is_nullable from master.information_schema.columns", empty)
put rs
这种对开发人员友好的编码方式极大地简化了代码的可读性和理解性。它还使得在 HTML 输出命令中缩进代码成为可能,就像前面的示例一样,其中构造表行的代码在逻辑上嵌套在创建 <tr>
元素本身的 put
调用中。
事务
由于该类封装了连接对象,因此公开事务功能也是合乎逻辑的。我们可以公开连接对象本身,但如果我们不需要额外功能,为什么要这样做呢?以下是一个完全人为的示例来演示该过程。
dim sql, rs<br />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) 'bad programmer, you should close your recordsets...
DB.Execute "insert into order_status (order_id, status) values (?, ?)", Array(new_id, "N")
DB.CommitTransaction
我们可以随时调用 DB.RollbackTransaction
来取消事务,正如预期的那样。
开发者友好就是最好的友好
我认为这里的关键启示是,在 ASP 中编写非常干净的代码是可能的。也许现在它不再那么重要了,因为它在技术上已经过时很多年了…… :\
但有时我们仍然不得不处理它,而且有时是大量地处理。很多时候,我们继承的代码是使用不太理想的结构编写的,因此尽可能地控制和简化代码可以大大减轻我们的维护负担。而且,当我们必须在 ASP 中从头开始构建东西时(是的,这种情况确实会发生),这种方法可以从一开始就使我们的产品更加稳定。
最后,VBScript/ASP 还有许多其他方法可以通过此处所示的技术进行扩展和扩展,以简化开发并提供超出通常开箱即用体验的更多功能。事实上,虽然肯定不如 C#,但它的潜力仍然比许多人预期的要大。我希望在不久的将来写更多关于这些其他可能性的文章。我希望您喜欢本文并能从提供的代码中获得一些帮助。