今天就这个问题摸索了半天。
T-SQL语句如下:
@FileIDsParam varchar(160),
@RectLeftParam INT,
@RectTopParam INT,
@RectRightParam INT,
@RectBottomParam INT
AS
DECLARE @SQLString NVARCHAR(2048)
DECLARE @FileID varchar(10)
DECLARE @FileIDValue varchar(260)
DECLARE @LastPosition INT
DECLARE @CharPosition INT
DECLARE @Values NVARCHAR(160)
SET @LastPosition = 0
SET @CharPosition = 1
SET @FileIDValue = ''
WHILE(@LastPosition<LEN(@FileIDsParam))
BEGIN
SET @CharPosition = CHARINDEX(N'#',@FileIDsParam,@LastPosition)
IF(@CharPosition<1)
BEGIN
BREAK
END
SET @FileID = SUBSTRING(@FileIDsParam, @LastPosition, @CharPosition - @LastPosition )
SET @FileIDValue = @FileIDValue + ' LogFileID = '
SET @FileIDValue = @FileIDValue + @FileID
SET @FileIDValue = @FileIDValue + ' OR '
SET @LastPosition = @CharPosition+1
END
SET @FileIDValue = SUBSTRING(@FileIDValue,0,(LEN(@FileIDValue)-3))
SET @RectLeftParam = @RectLeftParam - 100000
SET @RectRightParam = @RectRightParam +100000
SET @RectBottomParam = @RectBottomParam - 100000
SET @RectTopParam = @RectTopParam + 100000
SET @SQLString = 'SELECT Longitude,Latitude,[FSM Level] FROM DAB_RSF_File WHERE '
SET @SQLString = @SQLString + ' Longitude>='+CAST(@RectLeftParam AS varchar(10))
SET @SQLString = @SQLString + ' AND Longitude<=' + CAST(@RectRightParam AS varchar(10))
SET @SQLString = @SQLString + ' AND Latitude>=' + CAST(@RectBottomParam AS varchar(10))
SET @SQLString = @SQLString + ' AND Latitude<=' + CAST(@RectTopParam AS varchar(10))
SET @SQLString = @SQLString + ' AND (' + @FileIDValue + ')'
EXEC(@SQLString)
GO
在VC程序中执行:
pRst = m_pConnection->Execute(_bstr_t(szSQL), NULL, adCmdStoredProc);
可以成功执行,
但pRst->GetState();的返回结果是0,表示adStateClosed,
然后对记录集进行操作报错:
error code 0x800A0E78 对象关闭时,不允许操作
后来在网上查找解决方案,终于发现这种情况应该在存储过程的as语句后加上一句就可以了: set nocount on
现在的存储过程如下:
@FileIDsParam varchar(160),
@RectLeftParam INT,
@RectTopParam INT,
@RectRightParam INT,
@RectBottomParam INT
AS
SET NOCOUNT ON
DECLARE @SQLString NVARCHAR(2048)
DECLARE @FileID varchar(10)
DECLARE @FileIDValue varchar(260)
DECLARE @LastPosition INT
DECLARE @CharPosition INT
DECLARE @Values NVARCHAR(160)
SET @LastPosition = 0
SET @CharPosition = 1
SET @FileIDValue = ''
WHILE(@LastPosition<LEN(@FileIDsParam))
BEGIN
SET @CharPosition = CHARINDEX(N'#',@FileIDsParam,@LastPosition)
IF(@CharPosition<1)
BEGIN
BREAK
END
SET @FileID = SUBSTRING(@FileIDsParam, @LastPosition, @CharPosition - @LastPosition )
SET @FileIDValue = @FileIDValue + ' LogFileID = '
SET @FileIDValue = @FileIDValue + @FileID
SET @FileIDValue = @FileIDValue + ' OR '
SET @LastPosition = @CharPosition+1
END
SET @FileIDValue = SUBSTRING(@FileIDValue,0,(LEN(@FileIDValue)-3))
SET @RectLeftParam = @RectLeftParam - 100000
SET @RectRightParam = @RectRightParam +100000
SET @RectBottomParam = @RectBottomParam - 100000
SET @RectTopParam = @RectTopParam + 100000
SET @SQLString = 'SELECT Longitude,Latitude,[FSM Level] FROM DAB_RSF_File WHERE '
SET @SQLString = @SQLString + ' Longitude>='+CAST(@RectLeftParam AS varchar(10))
SET @SQLString = @SQLString + ' AND Longitude<=' + CAST(@RectRightParam AS varchar(10))
SET @SQLString = @SQLString + ' AND Latitude>=' + CAST(@RectBottomParam AS varchar(10))
SET @SQLString = @SQLString + ' AND Latitude<=' + CAST(@RectTopParam AS varchar(10))
SET @SQLString = @SQLString + ' AND (' + @FileIDValue + ')'
EXEC(@SQLString)
SET NOCOUNT OFF
GO
这个发现算得上是今天最大的收获!!!
基础的SQL语言对于每个从事SQL开发的人员来说都是非常重要的,而SQL注入是从SQL应用衍生出来的一种漏洞或攻击方法。本文在回顾基础的SQL语言的基础上,介绍了SQL注入和如何使用SQL注入法进行网络攻击。
网络的应用和普及产生了大量的数据信息,这些数据信息多被存在数据库中。与数据库交流主要是通过SQL语言,然而对它的不熟悉和使用的模糊限制了开发者的工作效率,更严重的是,这可能会导致系统的危机。SQL使用方便,应用广泛,并且主流的关系型数据库都支持SQL的执行。正是因为SQL的使用便捷和应用广泛导致了SQL注入一出世就造成了巨大的影响。任何允许执行人工输入SQL语句的地方,就存在SQL注入的危险。因此,深层理解SQL和SQL注入,掌握SQL注入的执行原理和方法是极为重要的。
一、SQL纵览
SQL(Structured Query Language)语言是一种结构化查询语言,它的功能不仅是查询,具体说,它是一种通用的、功能强的关系型数据库语言。SQL语言中完成核心功能的有以下9个关键词:SELECT(数据查询),CREAT、DROP、ALTER(数据定义),INSERT、UPDATE、DELETE(数据操纵), GRANT、REVOKE(数据控制)。
1、数据定义部分
(1)创建基本表:
CREAT TABLE employee (Eno CHAR(6) NOT NULL UNIQUE,
Ename CHAR(20) UNIQUE,
E*** CHAR(2),
Eage INT,
Edept CHAR(10),
Espe CHAR(20));
该语句创建了一个名为employee的数据表,它共有六列,分别为字符型(长度为6,非空,唯一)的雇员号Eno,字符型(长度为20,唯一)的雇员姓名Ename,字符型(长度为2)的雇员性别,整型的雇员年龄,字符型(长度为10)的雇员部门,字符型(长度为20)的雇员特长。
(2)删除基本表:
DROP TABLE employee;
删除表employee,数据也一并删除,一旦执行成功不可逆,因此使用该条语句时要格外注意,最好使用事务处理、事前备份和确认提醒等。
(3)更改基本表:
ALTER TABLE employee ADD Esalary CHAR(5);
在雇员表中加入一列,字符型(长度为5)的雇员薪水
ALTER TABLE employee DROP UNIQUE(Ename);
去掉雇员表中雇员姓名列的唯一属性
ALTER TABLE employee MODIFY E*** CHAR(1);
把雇员表中的性别列改为一位字符型。
2、数据查询部分
数据查询部分是SQL语句中最灵活、功能最强的部分,对于查询语句的使用熟练程度和对查询结构的优化能力最能体现SQL编程人员的基本功。因此,该部分必须要给予足够的重视。现详述如下:
(1)基本查询语句:
SELECT Eno,Ename,E*** FROM employee;
查询employee表中的Eno,Ename,E***三列
SELECT * FROM employee;
查询employee表中的所有列
SELECT DISTINCT Eno FROM employee;
查询employee表中的Eno列,并去除重复行
(2)条件(WHERE)查询语句:
查询条件的连接词如下NOT,=,〉,〈,〉=,〈=,!=,〈〉,!〉,!〈(比较);BETWEEN AND,NOT BETWEEN AND (确定范围);IN,NOT IN(确定集合);LIKE,NOT LIKE(字符匹配);IS NULL,IS NOT NULL(空值);AND,OR(多条件连接)
1)比较
SELECT Eno FROM employee WHERE Eage 〈=25;;
列出雇员表中年龄小于25的雇员号
2)确定范围
SELECT Eno,Ename FROM employee
WHERE Eage [NOT]BETWEEN 20 AND 30;;
列出雇员表中年龄(不)在20到30的雇员号和姓名
3)确定集合
SELECT Eno,Ename FROM employee
WHERE Edept [NOT]IN(′SD′,′HD′);;
列出雇员表中(不)是软硬件开发部的雇员号和姓名
4)字符匹配
LIKE的用法如下:
[NOT]LIKE ′〈匹配模式〉′[ESCAPE ′〈换码符〉′]
通配符号有 % 和 _ 两种:
% :匹配任意长度的字符串(长度可为0)。a%b 可与ab,adfb等匹配。
_:匹配单个任意字符。a_b 可与a#b,a@b等匹配。
如果有ESCAPE,则跟在换码符号后的%或_不再是通配符号,只是正常的%或_。
例如:
SELECT * FROM employee WHERE Ename LIKE ′刘%′;
查找雇员表中姓刘雇员的信息
SELECT * FROM employee WHERE Ename LIKE ′刘_ _′;
查找雇员表中姓名为刘某(两个字)的雇员的信息(汉字占2个字符的位置)
SELECT * FROM employee WHERE Espe LIKE ′DB"_%t_′ESCAPE ′"′;
查找雇员表中特长项为DB_ 开始,倒数第二个字符为t的雇员信息
5)空值
SELECT * FROM employee WHERE Espe IS [NOT] NULL;
查找雇员表中特长项(不)为空的雇员信息
6)多条件连接
SELECT Ename FROM employee WHERE Edept=′SD′AND Eage 〈= 30;
列出雇员表中软件开发部30岁以下雇员的姓名
(3)结果排序
对查询的结果进行排序使用ORDER BY,ASC(默认)为升序,DESC为降序。
SELECT * FROM employee ORDER BY Edept,Eage DESC;
把所有雇员按部门升序,年龄降序排列(缺省是升序)
(4)结果分组
对查询结果的分组一般都要用到SQL的集函数,因此先介绍SQL的集函数。
SQL语言中的集函数主要有COUNT(统计总数),SUM(求总和),AVG(求均值),MAX(最大值),MIN(最小值)。
例如:
SELECT MAX(Eage) FROM employee WHERE Edept=′SD′;
列出软件开发部年纪最大雇员的姓名
SELECT Edept FROM employee GROUP BY Edept HAVING Count(*)〉10 ;
统计各部门的雇员数,只显示雇员数大于10的部门
SELECT Edept,COUNT(Eno) FROM employee GROUP BY Edept ;
统计各部门的雇员数,按部门分组列出各部门的雇员数
(5)连接查询
连接查询指的是查询涉及多个数据表,FROM后连接多个表的情况。假如我们要统计各个项目参加人的雇员号和姓名,涉及的表Eproject(雇员参加的项目)结构如下:
Eproject ( Eno CHAR(6),Pno CHAR(6),TimeBgn TIME,
TimeEnd TIME,Remark CHAR(50));
相应的查询语句为:
FROM employee, Eproject
列出参加各项目的雇员号和姓名,并按项目号升序排列。
(6)集合查询
集合查询指的是多个SELECT查询结果间进行的集合操作,主要有UNION(并操作)、INTERSECT(交操作)、MINUS(差操作)。其中标准SQL中没有提供交操作和差操作,但它们可以使用联合查询实现。假如我们要查询硬件开发部年龄不大于25岁的雇员,可以用集合查询实现如下:
SELECT * FROM employee WHERE Edept=′HD′
UNION SELECT * FROM employee WHERE Eage 〈= 25;
3、数据更新部分
SQL中的数据更新语句有INSERT,UPDATE和DELETE三种,用法如下:
Zabcdefghijklmnopqrstuvwxy(1)插入数据
INSERT INTO employee
VALUES (′13253′,′王二′,′男′,23,′SD′, ′DB_Project′);;
向雇员表中插入一条完整的数据
INSERT INTO employee (Eno ,Ename)
VALUES (′13253′,′王二′);;
向雇员表中插入一条数据,只包含雇员号和姓名,其它列为空值
注意:以上情况,属性为非空的列一定不能为空值。
(2)修改数据
UPDATE employee SET Eage=24 WHERE Eno=′13253′;
将雇员表中13253号雇员年龄改为24岁
(3)删除数据
DELETE FROM employee WHERE Eno=′13253′;
将雇员表中13253号雇员信息删除4、数据控制部分
GRANT SELECT ON TABLE employee TO usr1;;
允许用户usr1查询表employee
GRANT ALL PRIVILEGES ON TABLE employee TO usr2;;
允许用户usr2对表employee的任何操作(查询,插入、更新、删除数据)
(2)收回权限
SQL 中收回用户权限使用REVOKE关键词,它的用法举例如下:
REVOKE UPDATE(Eno) ON TABLE employee FROM usr3;;
收回用户usr3更新表employee中Eno列的权力
REVOKE INSERT ON TABLE employee FROM PUBLIC;;
不允许所有用户在表employee中添加数据
二、SQL注入(SQL INJECTION)简介
自从SQL Injection被发现以来,人们通过大量的实验发现,它存在于任何允许执行SQL语句的地方。简单的说,SQL injection是一种源于SQL的漏洞,也可以说成是一种攻击方法,它利用程序对用户输入数据的检验不足或程序自身对变量处理不当,把想要执行的SQL语句插入到实际的SQL语句中发送给服务器去执行,后果轻则导致敏感信息泄漏,重则使整个服务器受控。
SELECT * FROM member WHERE UID =′ "& request("ID") &" ′
AND Passwd =′ "& request("Pwd") & " ′
如果正常使用者的帐号user1,密码abcdefg12345,那么此时的SQL语句为:
SELECT * FROM member WHERE UID =′user1′ AND Passwd =′abcdefg12345′;;
在这里举三个SQL Injection的实例简单说明一下这种漏洞的原理:
1、帐号输入 user1′-- ,密码任意(如aaa),此时的SQL语句变为:
SELECT * FROM member WHERE UID =′user1′--′ AND Passwd =′aaa′
2、帐号输入′OR 1=1--,密码任意(如aaa),此时的SQL语句变为:
SELECT * FROM member WHERE UID =′′OR 1=1--′ AND Passwd =′aaa′
由于AND字句被作为说明而失去了作用,where字句返回为真,这个SQL语句就失去了鉴别作用。
3、帐号输入任意(如uuu) ,密码为aaa(任意)′OR 1=1--,此时的SQL语句变为:
SELECT * FROM member WHERE UID =′uuu′AND Passwd =′aaa′OR 1=1 --
由于--之后的语句被忽略,WHERE字句返回为真,该SQL语句就失去了鉴别作用。
三、SQL注入纵览
从最初的"1=1"型SQL注入,到现在的SQLServer存储过程和扩展存储过程注入,SQL注入迄今为止已经被发现数十种。总的来说,它的分类可以从SQL语言的自身进行,它可以分为授权旁路型、SELECT型、INSERT型、其它型(如SQLServer存储过程)。如前所述,SQL语言中的数据查询部分(SELECT语句)是SQL语句中最灵活、功能最强的部分,因此,该部分的SQL注入也种类繁多,主要有基本SELECT型、基本集合(UNION)型、语法错误型列举、匹配(LIKE)型、--结尾型等。现对于各种SQL注入分述如下:
这种类型的SQL注入是最简单、最易理解的一种SQL注入,它主要存在于表格式登录系统。除了简介中所列出的几种之外,还有一种更直接的SQL注入,登录帐号和密码都为 ′OR "= ′,此时SQL语句变为
SELECT * FROM member WHERE UID =′′OR ′′=′′ AND Passwd =′′OR ′′=′′
显然,该SQL语句失去了鉴别功能。
2、SELECT型SQL注入
(1)基本SELECT型
基本SELECT型SQL注入分为直接型和引用型。直接型SQL注入指的是用户提交的数据直接被用在SQL查询中。如果在某个合法输入值后添加一个空格和OR,系统返回了一个错误,那么就可能存在直接型SQL注入。直接值的位置可能存在于WHERE子句中,如:
SQLString=" SELECT * FROM member WHERE UID = "& intUID
或者存在于某个SQL关键词中,如某个表名或列名:
SQLString=" SELECT * FROM member ORDER BY " & strColumn
而引用型的SQL注入指的是用户提交的数据被放在引号中提交。如:
SQLString=" SELECT * FROM member WHERE UID =′"& strUID & "′"
此时要注入的部分要以单引号开始,与之前的单引号匹配,结尾在WHERE子句后加单引号,与之后的单引号匹配。
(2)基本集合(UNION)型
基本集合型SQL注入是在WHERE子句中插入一个UNION SELECT语句,以达到执行注入部分的目的。例如目标SQL语句为:
SQLString=" SELECT Name,Sex,Title FROM member WHERE UID =′"& strColumn & "′"
使用的注入字串如下:
′UNION SELECT otherfield FROM othertable WHERE ′′= ′
这样一来,提交的查询语句就成了:
SELECT Name,Sex,Title FROM member WHERE UID =′′
UNION SELECT otherfield FROM othertable WHERE ′′= ′′
结果就有以下操作:数据库首先检索member表查找UID为空的行,由于不存在UID为空的行,所以没有返回记录。返回的记录存在于注入部分的查询。有时使用空值会不起作用,可能是表中空值被使用或被用于实现其他的功能。这种情况下,你唯一要做的就是构造一个决不会出现在表中的字串,只要它能使UNION SELECT之前不返回记录。
(3)语法错误型
对于某些数据库而言,返回的错误信息中包含了语法错误的部分,因此,通过制造语法错误(错误注入),可以得到很多有价值的信息。
构造的错误字符串有:′,错误值′,′错误值,′OR′,′OR,OR′,;等。
(4)圆括号型
如果返回的错误中包含圆括号,或者错误是丢失圆括号,那么就要在错误值和WHERE子句部分添加圆括号。例如目标SQL语句为
SQLStr=" SELECT Name,Sex,Title FROM member WHERE(UID =′"& strID & "′)"
使用的注入字串就要变为:
′)UNION SELECT otherfield FROM othertable WHERE ( ′′= ′
这样一来,提交的查询语句就成了:
SELECT Name,Sex,Title FROM member WHERE(UID =′′)
UNION SELECT otherfield FROM othertable WHERE ( ′′= ′′)
由于不存在UID为空的行,所以第一部分没有返回记录,返回的记录存在于注入部分的查询。
(5)LIKE型
LIKE型的SQL注入也很常见。如果返回错误中含有%,_或者LIKE等字眼,说明该系统存在LIKE型注入漏洞,用户提交的数据会被送给LIKE子句去执行。例如目标SQL语句为
SQLStr=" SELECT Name,Sex,Title FROM member WHERE Name LIKE′%"& strColumn & "%′"
而使用的注入字串为:
′UNION SELECT otherfield FROM othertable WHERE ′%37′= ′
得到提交的查询语句为:
SELECT Name,Sex,Title FROM member WHERE Name LIKE′%′
UNION SELECT otherfield FROM othertable WHERE ′%′= ′%′
显然,第一部分返回为表member的所有记录,第二部分为用户想要得到的记录,最终得到就是用户想要的记录。
(6)错误结尾
有些时候,当尝试了许多注入方法后,返回依旧是错误。这说明目标SQL语句可能并不像所猜测的那样简单,它可能存在子查询或连接查询等复杂的情况。这时,对于SQLServer,由于;”之后的语句会被忽略,所以要在注入的SQL语句末尾,加上;;;- -”。
(7)连接查询
如果目标SQL语句为
SQLStr=" SELECT Name,Sex,Title FROM member
WHERE UID =′"& strID & "′AND Sex=′Female′"
如果使用的注入字串为:
′UNION SELECT otherfield FROM othertable WHERE ′′= ′
这样一来,提交的查询语句就成了:
SELECT Name,Sex,Title FROM member WHERE UID =′′
UNION SELECT otherfield FROM othertable WHERE ′′= ′′AND Sex=′Female′
由于othertable中不一定存在名为Sex的列,所以可能会返回;Invalid column name Sex”的错误。对于SQLServer而言,在系统表sysobjects中存有库中所有表的列名,所以,使用SELECT name FROM sysobjects WHERE xtype=′U′可以返回库中所有用户定义表的表名。
在这种情况下,构造的SQL注入语句要成为以下结构:
SELECT name FROM syscolumns
WHERE id=(SELECT id FROM sysobjects WHERE name=′TableName′)
3、INSERT型SQL注入
INSERT执行在数据库中增加列的功能,它用在用户注册,发表言论,网上购物等许多地方。由于它直接改变数据库的数据,所以使用INSERT型注入比SELECT型更危险。对于攻击者而言,如果使用INSERT型注入的语句出现错误,可能因为在数据库中产生一串单引号而被检测到。所以,使用INSERT型SQL注入要格外小心。
SQLStr="INSERT INTO TableName
VALUES(′"& strName & "′,′"& strSex & "′,′"& strEmail & "′)"
如下填表:
姓名:′+ SELECT TOP 1 FieldName FROM TableName+′
性别:Male
这样,提交的SQL语句为:
INSERT INTO TableName VALUES
在返回的注册信息中,就可以找到表TableName中的FieldName的值。
由于SQL语言的设计思想就是要使用灵活,所以各种各样的SQL注入方法也会层出不穷。但是总的来说,只要对SQL语言足够熟悉,并且时刻注意SQL注入的危险,至少已经向安全迈出了第一步。
void Log(char * szString, ...)
TCHAR szEntry[1024];
ZeroMemory(szEntry,sizeof(szEntry));
va_list args;
va_start(args, szString);
vsprintf(szEntry,szString,args); //用vsprintf函数格式化,而不是vsprintf格式化
AfxMessageBox(szEntry);
void CTestDlg::OnOK()
// TODO: Add extra validation here
float i=30;
int j=30;
Log("float=%f,int=%d",i,j);
请问:va_list(),va_start()是何意?(一)写一个简单的可变参数的C函数下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的C函数要在程序中用到以下这些宏:void va_start( va_list arg_ptr, prev_param );type va_arg( va_list arg_ptr, type );void va_end( va_list arg_ptr );va在这里是variable-argument(可变参数)的意思.这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.下面我们写一个简单的可变参数的函数,改函数至少有一个整数参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.void simple_va_fun(int i, ...){va_list arg_ptr;int j=0;va_start(arg_ptr, i);j=va_arg(arg_ptr, int);va_end(arg_ptr);printf("%d %d"n", i, j);return;}我们可以在我们的头文件中这样声明我们的函数:extern void simple_va_fun(int i, ...);我们在程序中可以这样调用:simple_va_fun(100);simple_va_fun(100,200);从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针.2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数.3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个参数是你要返回的参数的类型,这里是int型.4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数.如果我们用下面三种方法调用的话,都是合法的,但结果却不一样:1)simple_va_fun(100);结果是:100 -123456789(会变的值)2)simple_va_fun(100,200);结果是:100 2003)simple_va_fun(100,200,300);结果是:100 200我们看到第一种调用有错误,第二种调用正确,第三种调用尽管结果正确,但和我们函数最初的设计有冲突.下面一节我们探讨出现这些结果的原因和可变参数在编译器中是如何处理的.(二)可变参数在编译器中的处理我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下面以VC++中stdarg.h里x86平台的宏定义摘录如下(’"’号表示折行):typedef char * va_list;#define _INTSIZEOF(n) "((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )#define va_arg(ap,t) "( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )#define va_end(ap) ( ap = (va_list)0 )定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.C语言的函数是从右向左压入堆栈的,图(1)是函数的参数在堆栈中的分布位置.我们看到va_list被定义成char*,有一些平台或操作系统定义为void*.再看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆栈的地址,如图:高地址|-----------------------------||函数返回地址 ||-----------------------------||....... ||-----------------------------||第n个参数(第一个可变参数) ||-----------------------------|<--va_start后ap指向|第n-1个参数(最后一个固定参数)|低地址|-----------------------------|<-- &v图( 1 )然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我们看一下va_arg取int型的返回值:j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址(图2).然后用*取得这个地址的内容(参数值)赋给j.高地址|-----------------------------||函数返回地址 ||-----------------------------||....... ||-----------------------------|<--va_arg后ap指向|第n个参数(第一个可变参数) ||-----------------------------|<--va_start后ap指向|第n-1个参数(最后一个固定参数)|低地址|-----------------------------|<-- &v图( 2 )最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.关于va_start, va_arg, va_end的描述就是这些了,我们要注意的是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.(三)可变参数在编程中要注意的问题因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型.有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数printf是从固定参数format字符串来分析出参数的类型,再调用va_arg的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.如果simple_va_fun()改为:void simple_va_fun(int i, ...){va_list arg_ptr;char *s=NULL;va_start(arg_ptr, i);s=va_arg(arg_ptr, char*);va_end(arg_ptr);printf("%d %s"n", i, s);return;}可变参数为char*型,当我们忘记用两个参数来调用该函数时,就会出现core dump(Unix) 或者页面非法的错误(window平台).但也有可能不出错,但错误却是难以发现,不利于我们写出高质量的程序.以下提一下va系列宏的兼容性.System V Unix把va_start定义为只有一个参数的宏:va_start(va_list arg_ptr);而ANSI C则定义为:va_start(va_list arg_ptr, prev_param);如果我们要用system V的定义,应该用vararg.h头文件中所定义的宏,ANSI C的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以用ANSI C的定义就够了,也便于程序的移植.
表结构如下:id int 4EntryID int 4BlogID int 4现在要求在插入时,不允许插入EntryID与BlogID都相同的记录,即表中不允许任意两条记录的EntryID与BlogID都相同,EntryID与BlogID构成记录的唯一标识。以前我的处理方法时,在插入之前,通过select检查是否存在相同的记录。现在我采用SQL Server唯一约束来实现,简单方便,效率又高。实现方法是:1、在数据库关系图中右击将包含约束的表,然后从快捷菜单中选择"属性"命令。-或-为将包含约束的表打开表设计器,在表设计器中右击,然后从快捷菜单中选择"属性"命令。2、选择"索引/键"选项卡。3、选择"新建"命令。系统分配的名称出现在"索引名"框中。4、在"列名"下展开列的列表,选择要将约束附加到的列(在这里我们选择列EntryID、BlogID)。5、选择"创建 UNIQUE"复选框。6、选择"约束"选项。当保存表或关系图时,唯一约束即创建在数据库中。
现在我们再进行插入,就出现错误"违反了 UNIQUE KEY 约束..."。我们在程序中捕获这个错误,就知道插入了重复记录。
Note This data type is currently not supported by ADO. Usage may cause unpredictable results.
Note This data type is currently not supported by ADO. Usage may cause unpredictable results.
Note This data type is currently not supported by ADO. Usage may cause unpredictable results.
ADO/WFC Equivalent
关键字:正则表达式 模式匹配 Javascript
摘要:收集一些常用的正则表达式。
正则表达式用于字符串处理,表单验证等场合,实用高效,但用到时总是不太把握,以致往往要上网查一番。我将一些常用的表达式收藏在这里,作备忘之用。本贴随时会更新。
匹配中文字符的正则表达式: ["u4e00-"u9fa5]
匹配双字节字符(包括汉字在内):[^"x00-"xff]
应用:计算字符串的长度(一个双字节字符长度计2,ASCII字符计1)
匹配空行的正则表达式:"n["s| ]*"r
匹配HTML标记的正则表达式:/<(.*)>.*<"/"1>|<(.*) "/>/
匹配首尾空格的正则表达式:(^"s*)|("s*$)
利用正则表达式分解和转换IP地址:
下面是利用正则表达式匹配IP地址,并将IP地址转换成对应数值的Javascript程序:
不过上面的程序如果不用正则表达式,而直接用split函数来分解可能更简单,程序如下:
匹配Email地址的正则表达式:"w+([-+.]"w+)*@"w+([-.]"w+)*"."w+([-.]"w+)*
利用正则表达式去除字串中重复的字符的算法程序:[注:此程序不正确,原因见本贴回复]
得用正则表达式从URL地址中提取文件名的javascript程序,如下结果为page1
利用正则表达式限制网页表单里的文本框输入内容:
读者层次:初学 整理:Vision Deng(ZT)
int i = 100;long l = 2001;float f=300.2;double d=12345.119;char username[]="程佩君";char temp[200];char *buf;CString str;_variant_t v1;_bstr_t v2;一、其它数据类型转换为字符串
二、字符串转换为其它数据类型strcpy(temp,"123");
三、其它数据类型转换到CString使用CString的成员函数Format来转换,例如:
四、BSTR、_bstr_t与CComBSTR
五、VARIANT 、_variant_t 与 COleVariant
六、其它一些COM数据类型
七、ANSI与UnicodeUnicode称为宽字符型字串,COM里使用的都是Unicode字符串。
八、其它
后记:本文匆匆写成,错误之处在所难免,欢迎指正.
关于把BSTR类型数据转换成CString 类型数据时的问题?当我在把BSTR类型数据转换成CString 或 “char* 类型”数据时,发现在BSTR类型字符串较短的情况下没问题,当较长时就会出现内存读写错了。(在NT,2000下都测试是这样的。)根据你所说:1)字符串指针(char *)等已经被CString构造函数支持的数据类型 可以直接赋值 str = username;2)当b 为BSTR类型时可以使用char *p=_com_util::ConvertBSTRToString(b);于是以下是对的:CString cstr;BSTR bstr;....cstr=com_util::ConvertBSTRToString(bstr);...可是当bstr非常大时(其实,较大时就会)就会出现内存读写错,不知何故。此外我发现cstr=com_util::ConvertBSTRToString(bstr);可以简化为 cstr=bstr; 但当bstr较大时同样出现这个问题。请兄弟帮忙!急。谢谢!
/*
Log File Library(WIN98/NT/2000)
*/
/*
Use:
//这个代码我用工业现场24X7值守的程序纪录各种信息, 简单易用;
//一般用一个全局日志对象, 有临界排斥可以多线程安全使用。
//有两个类
class LogFile;//用户定义日志文件名
class LogFileEx;//有日志文件名自动生成功能 , 可分年月日频率生成文件名, 可指定日志存放的目录
LogFileEx gLog(".", LogFileEx :: YEAR);//一年生成一个日志文件
LogFileEx gLog(".""Log", LogFileEx :: MONTH);//一月生成一个日志文件
LogFileEx gLog(".""Log", LogFileEx :: DAY);//一天生成一个日志文件
//注意日志所属目录创建失败会自动退出, 请注意目录的合法性, 文件生成频率看情况掌握
//24小时运行的程序可以每天生成一个日志文件, 以免内容过多
*/
#ifndef _LOGFILE_H
#define _LOGFILE_H
#include <assert.h>
#include <time.h>
#include <stdio.h>
#include <windows.h>
class LogFile
protected:
CRITICAL_SECTION _csLock;
char * _szFileName;
HANDLE _hFile;
bool OpenFile()//打开文件, 指针到文件尾
if(IsOpen())
return true;
if(!_szFileName)
return false;
_hFile = CreateFile(
_szFileName,
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if(!IsOpen() && GetLastError() == 2)//打开不成功, 且因为文件不存在, 创建文件
_hFile = CreateFile(
_szFileName,
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if(IsOpen())
SetFilePointer(_hFile, 0, NULL, FILE_END);
return IsOpen();
DWORD Write(LPCVOID lpBuffer, DWORD dwLength)
DWORD dwWriteLength = 0;
if(IsOpen())
WriteFile(_hFile, lpBuffer, dwLength, &dwWriteLength, NULL);
return dwWriteLength;
virtual void WriteLog( LPCVOID lpBuffer, DWORD dwLength)//写日志, 可以扩展修改
time_t now;
char temp[21];
DWORD dwWriteLength;
if(IsOpen())
time(&now);
strftime(temp, 20, "%Y-%m-%d %H:%M:%S", localtime(&now));
WriteFile(_hFile, ""xd"xa#-----------------------------", 32, &dwWriteLength, NULL);
WriteFile(_hFile, temp, 19, &dwWriteLength, NULL);
WriteFile(_hFile, "-----------------------------#"xd"xa", 32, &dwWriteLength, NULL);
WriteFile(_hFile, lpBuffer, dwLength, &dwWriteLength, NULL);
WriteFile(_hFile, ""xd"xa", 2, &dwWriteLength, NULL);
FlushFileBuffers(_hFile);
void Lock() { ::EnterCriticalSection(&_csLock); }
void Unlock() { ::LeaveCriticalSection(&_csLock); }
public:
_szFileName = NULL;
_hFile = INVALID_HANDLE_VALUE;
::InitializeCriticalSection(&_csLock);
SetFileName(szFileName);
virtual ~LogFile()
::DeleteCriticalSection(&_csLock);
Close();
if(_szFileName)
delete []_szFileName;
const char * GetFileName()
return _szFileName;
void SetFileName(const char *szName)//修改文件名, 同时关闭上一个日志文件
assert(szName);
if(_szFileName)
delete []_szFileName;
Close();
_szFileName = new char[strlen(szName) + 1];
assert(_szFileName);
strcpy(_szFileName, szName);
bool IsOpen()
return _hFile != INVALID_HANDLE_VALUE;
void Close()
if(IsOpen())
CloseHandle(_hFile);
_hFile = INVALID_HANDLE_VALUE;
void AddLog(LPCVOID lpBuffer, DWORD dwLength)//追加日志内容
assert(lpBuffer);
__try
Lock();
if(!OpenFile())
return;
WriteLog(lpBuffer, dwLength);
__finally
Unlock();
void Log(const char * szString, ...)
TCHAR szEntry[1024];
va_list args;
va_start(args, szString);
vsprintf(szEntry,szString,args);
AddLog(szEntry, strlen(szEntry));
void GetErrorMessage(LPCTSTR lpFunction, DWORD dwError)
LPVOID lpMsgBuf;
TCHAR szEntry[1024];
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, dwError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
(LPTSTR)&lpMsgBuf, 0, NULL);
sprintf(szEntry, "Function: %s, %s", lpFunction, (LPCTSTR)lpMsgBuf);
AddLog(szEntry, strlen(szEntry));
LocalFree(lpMsgBuf);
private://屏蔽函数
LogFile(const LogFile&);
LogFile&operator = (const LogFile&);
};
class LogFileEx : public LogFile
protected:
char *_szPath;
char _szLastDate[9];
int _iType;
void SetPath(const char *szPath)
assert(szPath);
WIN32_FIND_DATA wfd;
char temp[MAX_PATH + 1] = {0};
if(FindFirstFile(szPath, &wfd) == INVALID_HANDLE_VALUE && CreateDirectory(szPath, NULL) == 0)
strcat(strcpy(temp, szPath), " Create Fail. Exit Now! Error ID :");
ltoa(GetLastError(), temp + strlen(temp), 10);
MessageBox(NULL, temp, "Class LogFileEx", MB_OK);
exit(1);
else
GetFullPathName(szPath, MAX_PATH, temp, NULL);
_szPath = new char[strlen(temp) + 1];
assert(_szPath);
strcpy(_szPath, temp);
public:
enum LOG_TYPE{YEAR = 0, MONTH = 1, DAY = 2};
LogFileEx(const char *szPath = ".", LOG_TYPE iType = MONTH)
_szPath = NULL;
SetPath(szPath);
_iType = iType;
memset(_szLastDate, 0, 9);
~LogFileEx()
if(_szPath)
delete []_szPath;
const char * GetPath()
return _szPath;
void AddLog(LPCVOID lpBuffer, DWORD dwLength)
assert(lpBuffer);
char temp[10];
static const char format[3][10] = {"%Y", "%Y-%m", "%Y%m%d"};
__try
Lock();
time_t now = time(NULL);
strftime(temp, 9, format[_iType], localtime(&now));
if(strcmp(_szLastDate, temp) != 0)//更换文件名
strcat(strcpy(_szFileName, _szPath), """");
strcat(strcat(_szFileName, temp), ".log");
strcpy(_szLastDate, temp);
Close();
if(!OpenFile())
return;
WriteLog(lpBuffer, dwLength);
__finally
Unlock();
void Log(const char * szString, ...)
TCHAR szEntry[1024];
va_list args;
va_start(args, szString);
vsprintf(szEntry,szString,args);
AddLog(szEntry, strlen(szEntry));
private://屏蔽函数
LogFileEx(const LogFileEx&);
LogFileEx&operator = (const LogFileEx&);
};
#endif
ActiveX Data Objects (ADO) enables you to write a client application to access and manipulate data in a database server through a provider.ADO's primary benefits are ease of use, high speed, low memory overhead, and a small disk footprint.This sample project is for ADODB, an implementation of ADO optimized for use with Microsoft OLE DB providers, including the Microsoft ODBC provider for OLE DB.Using this we can execute stored procedure, pass arguments and retrieve value. To use this sample you will have to create the two stored procedures mentioned below.For using this project you need MFC 5.0 OR above + ADO in your machine.
{CString strTmp;
CString m_sdatasource; // Data source nameCString m_sUserID; // User IdCString m_sPassword; // Password
}//we need to create the stored procedures below before running the application//CREATE PROCEDURE sp_AddAccountingInfo @nfinal int, @pcDate datetime,//@pcURL varchar (250), @pcTop varchar (250),//@pcQueryString varchar (250), @pcBytes int, @pcRequests int AS/*Do your operation here*///CREATE PROCEDURE sp_AddAccountingInfo//@nfinal int,//@pcDate datetime,//@pcURL varchar (250),//@pcTop varchar (250),//@pcQueryString varchar (250),//@pcBytes int,//@pcRequests int//AS/*Put your code here*/}vc下用ado调用存储过程
【打印文档】【大 中 小】【关闭窗口】
关于存储过程的ADO调用的一些心得(输出参数,返回值)
昨天做项目时发现此处不正确,m_pRecordset不能close。而且释放指针时要先释放m_pCommand,再释放m_pRecordset
bisquedarkgray
很久没有翻译东西了,乘晚上整理资料的时候翻一篇。
Over the past few months I've had the opportunity to interview dozens of candidates......
过去的几个月里,我有幸面试了众多应聘DBA和DB开发岗位的求职者。我们希望开发人员能够创建存储过程,编写合理的复杂SQL语句,以及触发器。我喜欢问这些求职者一个问题:
“假设我们使用SQL Server2000进行开发。现在我需要传递给存储过程两个变量:firstname和lastname。存储过程负责向名字为TEST的表插入这两个变量,其中TEST表有两个字段,名字就叫firstname和lastname。TEST表的主键是一个自增长类型的字段,名字叫ContactID。问题是我如何获取插入的那条数据的主键值。”
让我们想一下答案是什么。你是否知道如何创建存储过程,获取数值并返回给调用的应用程序?
有人会直接问我-这个问题重要吗?对于我来说,我问这个问题的目的是为了测试求职者是否有求解非常规需求的能力。设想一下标准的订单/订单明细表应用场景--你是否知道如何不知道订单主键的情况下插入订单的详细信息?当你因为没有使用主键时,可能会带来锁问题。这时,自增长字段加入是常用的一个手段。但是使用@@Identity来获取插入数据的标识,有可能会带来问题,比如在触发器内使用就会发生问题。这并不是一个可以给出唯一答案的问题,但是这个问题可以让我们对处理类似表的问题,来展开讨论。
我收到了很多不同的回答,但是绝大多数并非最优。几乎每个人都知道如何插入数据、如何返回值,但是几乎每个人都在获取自增长字段取值上回答得不是很好。
错误回答 #1 - Select max(contactid) from Test.因为你无法避免别人也同时在插入数据,因此这个回答是错误的。当然,你可以通过提升隔离级别来达到目的,但是这将会大幅降低并发性能,因此不好。
错误回答 #2 - Select top 1 contactid from test order by contactid desc.错误的原因和回答#1一样。
错误回答 #3 - 通过插入的数据来组合成一个唯一的标识,从而获得自增长字段的值。如果插入的数据确实组合起来是唯一的,能达到目的,但是如果不唯一,怎么办呢?因此这也不是好办法。
错误回答 #4 - 这个回答很接近正解了。这些回答者建议使用@@Identity,自然这是可以的 (小心,要知道如何正确使用@@Identity), 但是当我问他们关于@@Identity的相关技术细节时,我收到最多的答案如下:
正确答案 - 因为我们使用的是SQL Server 2000,因此使用Scope_Identity() , 如果用的是SQL Server 7,那么只有只用@@Identity,并且以output参数方式传递(return值一般是用来作为错误代码用)。使用@@Identity意味着将来也许会发生错误,例如审核时使用自增长字段。
现在我们来做一系列的试验来验证:
create database IdentityTestuse identitytestcreate table TEST (ContactID int not null identity (1, 1), firstname varchar(100) null, lastname varchar(100) null)insert into TEST Default Valuesselect @@Identity
运行后会返回1。如果在此运行,则返回 2。
insert into TEST Default Valuesselect Scope_Identity()
运行后返回 3。
现在我们来设计如何使@@Identity返回错误结果。我们先创建一个包含一个新的自增长字段的表TESTHISTORY,然后在TEST表上加触发器。
现在看看会发生什么:
insert into TEST Default Valuesselect @@Identity
返回值为1。注意,此时TEST表最后插入的记录,主键值为4,而TESTHISTORY表作后插入记录的主键值=1。
insert into TEST Default Valuesselect @@Identity
返回值为5。TEST表最后插入记录的主键值为5,而且TESTHISTORY表的确也插入了第二条记录。现在我们再测试如果同时有别的连接来向插入TEST表插入数据,情况会如何。首先我们用当前的连接,运行:
insert into TEST Default Values
此时,TEST插入了第6条记录。然后新建一个连接,并运行相同的SQL语句:
insert into TEST Default Values
此时,TEST表插入了第7条记录。然后我们在原先那个连接里,来获取“错误”的结果,值为3。
select @@Identity
现在我们用scope_identity()来测试。我们希望得到的结果是6,不是7!
select Scope_Identity()
结果确实如此,证明使用scope_identity()是正确的。我知道,这样测试很麻烦,你也许不会去仔细探究。但是如果你准备使用SQL Server 2000,你就必须知道它是如何工作的。有兴趣用这个问题来考考你的开发人员,可以教他们一些专业的开发技巧,这样也许会使你在将来省却很多本可以避免的麻烦。
数据库的最初雏形据说源自美国一个奶牛场的记账薄(纸质的,由此可见,数据库并不一定是存储在电脑里的数据^_^),里面记录的是该奶牛场的收支账目,程序员在将其整理、录入到电脑中时从中受到启发。当按照规定好的数据结构所采集到的数据量大到一定程度后,出于程序执行效率的考虑,程序员将其中的检索、更新维护等功能分离出来,做成单独调用的模块,这个模块后来就慢慢发展、演变成现在我们所接触到的数据库管理系统(DBMS)——程序开发中的一个重要分支。
下面进入正题,首先按我个人所接触过的程序给数据库设计人员的功底分一下类:1、没有系统学习过数据结构的程序员。这类程序员的作品往往只是他们的即兴玩具,他们往往习惯只设计有限的几个表,实现某类功能的数据全部塞在一个表中,各表之间几乎毫无关联。网上不少的免费管理软件都是这样的东西,当程序功能有限,数据量不多的时候,其程序运行起来没有什么问题,但是如果用其管理比较重要的数据,风险性非常大。2、系统学习过数据结构,但是还没有开发过对程序效率要求比较高的管理软件的程序员。这类人多半刚从学校毕业不久,他们在设计数据库表结构时,严格按照教科书上的规定,死扣E-R图和3NF(别灰心,所有的数据库设计高手都是从这一步开始的)。他们的作品,对于一般的access型轻量级的管理软件,已经够用。但是一旦该系统需要添加新功能,原有的数据库表差不多得进行大换血。3、第二类程序员,在经历过数次程序效率的提升,以及功能升级的折腾后,终于升级成为数据库设计的老鸟,第一类程序员眼中的高人。这类程序员可以胜任二十个表以上的中型商业数据管理系统的开发工作。他们知道该在什么样的情况下保留一定的冗余数据来提高程序效率,而且其设计的数据库可拓展性较好,当用户需要添加新功能时,原有数据库表只需做少量修改即可。4、在经历过上十个类似数据库管理软件的重复设计后,第三类程序员中坚持下来没有转行,而是希望从中找出“偷懒”窍门的有心人会慢慢觉悟,从而完成量变到质变的转换。他们所设计的数据库表结构有一定的远见,能够预测到未来功能升级所需要的数据,从而预先留下伏笔。这类程序员目前大多晋级成数据挖掘方面的高级软件开发人员。5、第三类程序员或第四类程序员,在对现有的各家数据库管理系统的原理和开发都有一定的钻研后,要么在其基础上进行二次开发,要么自行开发一套有自主版权的通用数据库管理系统。
我个人正处于第三类的末期,所以下面所列出的一些设计技巧只适合第二类和部分第三类数据库设计人员。同时,由于我很少碰到有兴趣在这方面深钻下去的同行,所以文中难免出现错误和遗漏,在此先行声明,欢迎大家指正,不要藏私哦8)
一、树型关系的数据表不少程序员在进行数据库设计的时候都遇到过树型关系的数据,例如常见的类别表,即一个大类,下面有若干个子类,某些子类又有子类这样的情况。当类别不确定,用户希望可以在任意类别下添加新的子类,或者删除某个类别和其下的所有子类,而且预计以后其数量会逐步增长,此时我们就会考虑用一个数据表来保存这些数据。按照教科书上的教导,第二类程序员大概会设计出类似这样的数据表结构:
类别表_1(Type_table_1)名称 类型 约束条件 说明type_id int 无重复 类别标识,主键type_name char(50) 不允许为空 类型名称,不允许重复type_father int 不允许为空 该类别的父类别标识,如果是顶节点的话设定为某个唯一值
这样的设计短小精悍,完全满足3NF,而且可以满足用户的所有要求。是不是这样就行呢?答案是NO!Why?
我们来估计一下用户希望如何罗列出这个表的数据的。对用户而言,他当然期望按他所设定的层次关系一次罗列出所有的类别,例如这样:总类别类别1类别1.1类别1.1.1类别1.2类别2类别2.1类别3类别3.1类别3.2……
看看为了实现这样的列表显示(树的先序遍历),要对上面的表进行多少次检索?注意,尽管类别1.1.1可能是在类别3.2之后添加的记录,答案仍然是N次。这样的效率对于少量的数据没什么影响,但是日后类型扩充到数十条甚至上百条记录后,单单列一次类型就要检索数十次该表,整个程序的运行效率就不敢恭维了。或许第二类程序员会说,那我再建一个临时数组或临时表,专门保存类型表的先序遍历结果,这样只在第一次运行时检索数十次,再次罗列所有的类型关系时就直接读那个临时数组或临时表就行了。其实,用不着再去分配一块新的内存来保存这些数据,只要对数据表进行一定的扩充,再对添加类型的数量进行一下约束就行了,要完成上面的列表只需一次检索就行了。下面是扩充后的数据表结构:
类别表_2(Type_table_2)名称 类型 约束条件 说明type_id int 无重复 类别标识,主键type_name char(50) 不允许为空 类型名称,不允许重复type_father int 不允许为空 该类别的父类别标识,如果是顶节点的话设定为某个唯一值type_layer char(6) 限定3层,初始值为000000 类别的先序遍历,主要为减少检索数据库的次数
按照这样的表结构,我们来看看上面例子记录在表中的数据是怎样的:
type_id type_name type_father type_layer1 总类别 0 0000002 类别1 1 0100003 类别1.1 2 0101004 类别1.2 2 0102005 类别2 1 0200006 类别2.1 5 0201007 类别3 1 0300008 类别3.1 7 0301009 类别3.2 7 03020010 类别1.1.1 3 010101……
现在按type_layer的大小来检索一下:SELECT * FROM Type_table_2 ORDER BY type_layer
列出记录集如下:
type_id type_name type_father type_layer1 总类别 0 0000002 类别1 1 0100003 类别1.1 2 01010010 类别1.1.1 3 0101014 类别1.2 2 0102005 类别2 1 0200006 类别2.1 5 0201007 类别3 1 0300008 类别3.1 7 0301009 类别3.2 7 030200……
现在列出的记录顺序正好是先序遍历的结果。在控制显示类别的层次时,只要对type_layer字段中的数值进行判断,每2位一组,如大于0则向右移2个空格。当然,我这个例子中设定的限制条件是最多3层,每层最多可设99个子类别,只要按用户的需求情况修改一下type_layer的长度和位数,即可更改限制层数和子类别数。其实,上面的设计不单单只在类别表中用到,网上某些可按树型列表显示的论坛程序大多采用类似的设计。
或许有人认为,Type_table_2中的type_father字段是冗余数据,可以除去。如果这样,在插入、删除某个类别的时候,就得对type_layer 的内容进行比较繁琐的判定,所以我并没有消去type_father字段,这也正符合数据库设计中适当保留冗余数据的来降低程序复杂度的原则,后面我会举一个故意增加数据冗余的案例。
二、商品信息表的设计假设你是一家百货公司电脑部的开发人员,某天老板要求你为公司开发一套网上电子商务平台,该百货公司有数千种商品出售,不过目前仅打算先在网上销售数十种方便运输的商品,当然,以后可能会陆续在该电子商务平台上增加新的商品出售。现在开始进行该平台数据库的商品信息表的设计。每种出售的商品都会有相同的属性,如商品编号,商品名称,商品所属类别,相关信息,供货厂商,内含件数,库存,进货价,销售价,优惠价。你很快就设计出4个表:商品类型表(Wares_type),供货厂商表(Wares_provider),商品信息表(Wares_info):
商品类型表(Wares_type)名称 类型 约束条件 说明type_id int 无重复 类别标识,主键type_name char(50) 不允许为空 类型名称,不允许重复type_father int 不允许为空 该类别的父类别标识,如果是顶节点的话设定为某个唯一值type_layer char(6) 限定3层,初始值为000000 类别的先序遍历,主要为减少检索数据库的次数
供货厂商表(Wares_provider)名称 类型 约束条件 说明provider_id int 无重复 供货商标识,主键provider_name char(100) 不允许为空 供货商名称
你拿着这3个表给老板检查,老板希望能够再添加一个商品图片的字段,不过只有一部分商品有图片。OK,你在商品信息表(Wares_info)中增加了一个haspic的BOOL型字段,然后再建了一个新表——商品图片表(Wares_pic):
程序开发完成后,完全满足老板目前的要求,于是正式启用。一段时间后,老板打算在这套平台上推出新的商品销售,其中,某类商品全部都需添加“长度”的属性。第一轮折腾来了……当然,你按照添加商品图片表的老方法,在商品信息表(Wares_info)中增加了一个haslength的BOOL型字段,又建了一个新表——商品长度表(Wares_length):
刚刚改完没多久,老板又打算上一批新的商品,这次某类商品全部需要添加“宽度”的属性。你咬了咬牙,又照方抓药,添加了商品宽度表(Wares_width)。又过了一段时间,老板新上的商品中有一些需要添加“高度”的属性,你是不是开始觉得你所设计的数据库按照这种方式增长下去,很快就能变成一个迷宫呢?那么,有没有什么办法遏制这种不可预见性,但却类似重复的数据库膨胀呢?我在阅读《敏捷软件开发:原则、模式与实践》中发现作者举过类似的例子:7.3 “Copy”程序。其中,我非常赞同敏捷软件开发这个观点:在最初几乎不进行预先设计,但是一旦需求发生变化,此时作为一名追求卓越的程序员,应该从头审查整个架构设计,在此次修改中设计出能够满足日后类似修改的系统架构。下面是我在需要添加“长度”的属性时所提供的修改方案:
去掉商品信息表(Wares_info)中的haspic字段,添加商品额外属性表(Wares_ex_property)和商品额外信息表(Wares_ex_info)2个表来完成添加新属性的功能。
商品额外属性表(Wares_ex_property)名称 类型 约束条件 说明ex_pid int 无重复 商品额外属性标识,主键p_name char(20) 不允许为空 额外属性名称
在商品额外属性表(Wares_ex_property)中添加2条记录:ex_pid p_name1 商品图片2 商品长度
再在整个电子商务平台的后台管理功能中追加一项商品额外属性管理的功能,以后添加新的商品时出现新的属性,只需利用该功能往商品额外属性表(Wares_ex_property)中添加一条记录即可。不要害怕变化,被第一颗子弹击中并不是坏事,坏的是被相同轨道飞来的第二颗、第三颗子弹击中。第一颗子弹来得越早,所受的伤越重,之后的抵抗力也越强8)
三、多用户及其权限管理的设计开发数据库管理类的软件,不可能不考虑多用户和用户权限设置的问题。尽管目前市面上的大、中型的后台数据库系统软件都提供了多用户,以及细至某个数据库内某张表的权限设置的功能,我个人建议:一套成熟的数据库管理软件,还是应该自行设计用户管理这块功能,原因有二:1.那些大、中型后台数据库系统软件所提供的多用户及其权限设置都是针对数据库的共有属性,并不一定能完全满足某些特例的需求;2.不要过多的依赖后台数据库系统软件的某些特殊功能,多种大、中型后台数据库系统软件之间并不完全兼容。否则一旦日后需要转换数据库平台或后台数据库系统软件版本升级,之前的架构设计很可能无法重用。
下面看看如何自行设计一套比较灵活的多用户管理模块,即该数据库管理软件的系统管理员可以自行添加新用户,修改已有用户的权限,删除已有用户。首先,分析用户需求,列出该数据库管理软件所有需要实现的功能;然后,根据一定的联系对这些功能进行分类,即把某类用户需使用的功能归为一类;最后开始建表:功能表(Function_table)名称 类型 约束条件 说明f_id int 无重复 功能标识,主键f_name char(20) 不允许为空 功能名称,不允许重复f_desc char(50) 允许为空 功能描述
用户组表(User_group)名称 类型 约束条件 说明group_id int 无重复 用户组标识,主键group_name char(20) 不允许为空 用户组名称group_power char(100) 不允许为空 用户组权限表,内容为功能表f_id的集合
采用这种用户组的架构设计,当需要添加新用户时,只需指定新用户所属的用户组;当以后系统需要添加新功能或对旧有功能权限进行修改时,只用操作功能表和用户组表的记录,原有用户的功能即可相应随之变化。当然,这种架构设计把数据库管理软件的功能判定移到了前台,使得前台开发相对复杂一些。但是,当用户数较大(10人以上),或日后软件升级的概率较大时,这个代价是值得的。
四、简洁的批量m:n设计碰到m:n的关系,一般都是建立3个表,m一个,n一个,m:n一个。但是,m:n有时会遇到批量处理的情况,例如到图书馆借书,一般都是允许用户同时借阅n本书,如果要求按批查询借阅记录,即列出某个用户某次借阅的所有书籍,该如何设计呢?让我们建好必须的3个表先:
书籍表(Book_table)名称 类型 约束条件 说明book_id int 无重复 书籍标识,主键book_no char(20) 无重复 书籍编号book_name char(100) 不允许为空 书籍名称……
借阅用户表(Renter_table)名称 类型 约束条件 说明renter_id int 无重复 用户标识,主键renter_name char(20) 不允许为空 用户姓名……
为了实现按批查询借阅记录,我们可以再建一个表来保存批量借阅的信息,例如:
这样的设计好吗?我们来看看为了列出某个用户某次借阅的所有书籍,需要如何查询?首先检索批量借阅表(Batch_rent),把符合条件的的所有记录的rent_id字段的数据保存起来,再用这些数据作为查询条件带入到借阅记录表(Rent_log)中去查询。那么,有没有什么办法改进呢?下面给出一种简洁的批量设计方案,不需添加新表,只需修改一下借阅记录表(Rent_log)即可。修改后的记录表(Rent_log)如下:
其中,同一次借阅的batch_no和该批第一条入库的rent_id相同。举例:假设当前最大rent_id是64,接着某用户一次借阅了3本书,则批量插入的3条借阅记录的batch_no都是65。之后另外一个用户租了一套碟,再插入出租记录的rent_id是68。采用这种设计,查询批量借阅的信息时,只需使用一条标准T_SQL的嵌套查询即可。当然,这种设计不符合3NF,但是和上面标准的3NF设计比起来,哪一种更好呢?答案就不用我说了吧。
五、冗余数据的取舍上篇的“树型关系的数据表”中保留了一个冗余字段,这里的例子更进一步——添加了一个冗余表。先看看例子:我原先所在的公司为了解决员工的工作餐,和附近的一家小餐馆联系,每天吃饭记账,费用按人数平摊,月底由公司现金结算,每个人每个月的工作餐费从工资中扣除。当然,每天吃饭的人员和人数都不是固定的,而且,由于每顿工作餐的所点的菜色不同,每顿的花费也不相同。例如,星期一中餐5人花费40元,晚餐2人花费20,星期二中餐6人花费36元,晚餐3人花费18元。为了方便计算每个人每个月的工作餐费,我写了一个简陋的就餐记账管理程序,数据库里有3个表:
员工表(Clerk_table)名称 类型 约束条件 说明clerk_id int 无重复 员工标识,主键clerk_name char(10) 不允许为空 员工姓名
每餐总表(Eatdata1)名称 类型 约束条件 说明totle_id int 无重复 每餐总表标识,主键persons char(100) 不允许为空 就餐员工的员工标识集合eat_date datetime 不允许为空 就餐日期eat_type char(1) 不允许为空 就餐类型,用来区分中、晚餐totle_price money 不允许为空 每餐总花费persons_num int 不允许为空 就餐人数
其中,就餐计费细表(Eatdata2)的记录就是把每餐总表(Eatdata1)的一条记录按就餐员工平摊拆开,是个不折不扣的冗余表。当然,也可以把每餐总表(Eatdata1)的部分字段合并到就餐计费细表(Eatdata2)中,这样每餐总表(Eatdata1)就成了冗余表,不过这样所设计出来的就餐计费细表重复数据更多,相比来说还是上面的方案好些。但是,就是就餐计费细表(Eatdata2)这个冗余表,在做每月每人餐费统计的时候,大大简化了编程的复杂度,只用类似这么一条查询语句即可统计出每人每月的寄餐次数和餐费总帐:
SELECT clerk_name AS personname,COUNT(c_id) as eattimes,SUM(price) AS ptprice FROM Eatdata2 JOIN Clerk_tabsle ON (c_id=clerk_id) JOIN eatdata1 ON (totleid=tid) WHERE eat_date>=CONVERT(datetime,'"&the_date&"') AND eat_date<DATEADD(month,1,CONVERT(datetime,'"&the_date&"')) GROUP BY c_id
想象一下,如果不用这个冗余表,每次统计每人每月的餐费总帐时会多麻烦,程序效率也够呛。那么,到底什么时候可以增加一定的冗余数据呢?我认为有2个原则:
1、用户的整体需求。当用户更多的关注于,对数据库的规范记录按一定的算法进行处理后,再列出的数据。如果该算法可以直接利用后台数据库系统的内嵌函数来完成,此时可以适当的增加冗余字段,甚至冗余表来保存这些经过算法处理后的数据。要知道,对于大批量数据的查询,修改或删除,后台数据库系统的效率远远高于我们自己编写的代码。2、简化开发的复杂度。现代软件开发,实现同样的功能,方法有很多。尽管不必要求程序员精通绝大部分的开发工具和平台,但是还是需要了解哪种方法搭配哪种开发工具的程序更简洁,效率更高一些。冗余数据的本质就是用空间换时间,尤其是目前硬件的发展远远高于软件,所以适当的冗余是可以接受的。不过我还是在最后再强调一下:不要过多的依赖平台和开发工具的特性来简化开发,这个度要是没把握好的话,后期维护升级会栽大跟头的。