WHILE(@LastPosition BEGIN SET@CharPosition=CHARINDEX(N'#',@FileIDsParam,@LastPosition) IF(@CharPosition<1) 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 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='SELECTLongitude,Latitude,[FSMLevel]FROMDAB_RSF_FileWHERE' SET@SQLString=@SQLString+'Longitude>='+CAST(@RectLeftParamASvarchar(10)) SET@SQLString=@SQLString+'ANDLongitude<='+CAST(@RectRightParamASvarchar(10)) SET@SQLString=@SQLString+'ANDLatitude>='+CAST(@RectBottomParamASvarchar(10)) SET@SQLString=@SQLString+'ANDLatitude<='+CAST(@RectTopParamASvarchar(10)) SET@SQLString=@SQLString+'AND('+@FileIDValue+')' EXEC(@SQLString) GO 在VC程序中执行: pRst=m_pConnection->Execute(_bstr_t(szSQL),NULL,adCmdStoredProc); 可以成功执行, 但pRst->GetState();的返回结果是0,表示adStateClosed, 然后对记录集进行操作报错: errorcode0x800A0E78对象关闭时,不允许操作 后来在网上查找解决方案,终于发现这种情况应该在存储过程的as语句后加上一句就可以了:setnocounton 现在的存储过程如下: SETNOCOUNTON SETNOCOUNTOFF 这个发现算得上是今天最大的收获!!! 基础的SQL语言对于每个从事SQL开发的人员来说都是非常重要的,而SQL注入是从SQL应用衍生出来的一种漏洞或攻击方法。本文在回顾基础的SQL语言的基础上,介绍了SQL注入和如何使用SQL注入法进行网络攻击。 网络的应用和普及产生了大量的数据信息,这些数据信息多被存在数据库中。与数据库交流主要是通过SQL语言,然而对它的不熟悉和使用的模糊限制了开发者的工作效率,更严重的是,这可能会导致系统的危机。SQL使用方便,应用广泛,并且主流的关系型数据库都支持SQL的执行。正是因为SQL的使用便捷和应用广泛导致了SQL注入一出世就造成了巨大的影响。任何允许执行人工输入SQL语句的地方,就存在SQL注入的危险。因此,深层理解SQL和SQL注入,掌握SQL注入的执行原理和方法是极为重要的。 一、SQL纵览 SQL(StructuredQueryLanguage)语言是一种结构化查询语言,它的功能不仅是查询,具体说,它是一种通用的、功能强的关系型数据库语言。SQL语言中完成核心功能的有以下9个关键词:SELECT(数据查询),CREAT、DROP、ALTER(数据定义),INSERT、UPDATE、DELETE(数据操纵),GRANT、REVOKE(数据控制)。 1、数据定义部分 (1)创建基本表: CREATTABLEemployee(EnoCHAR(6)NOTNULLUNIQUE, EnameCHAR(20)UNIQUE, E***CHAR(2), EageINT, EdeptCHAR(10), EspeCHAR(20)); 该语句创建了一个名为employee的数据表,它共有六列,分别为字符型(长度为6,非空,唯一)的雇员号Eno,字符型(长度为20,唯一)的雇员姓名Ename,字符型(长度为2)的雇员性别,整型的雇员年龄,字符型(长度为10)的雇员部门,字符型(长度为20)的雇员特长。 (2)删除基本表: DROPTABLEemployee; 删除表employee,数据也一并删除,一旦执行成功不可逆,因此使用该条语句时要格外注意,最好使用事务处理、事前备份和确认提醒等。 (3)更改基本表: ALTERTABLEemployeeADDEsalaryCHAR(5); 在雇员表中加入一列,字符型(长度为5)的雇员薪水 ALTERTABLEemployeeDROPUNIQUE(Ename); 去掉雇员表中雇员姓名列的唯一属性 ALTERTABLEemployeeMODIFYE***CHAR(1); 把雇员表中的性别列改为一位字符型。 2、数据查询部分 数据查询部分是SQL语句中最灵活、功能最强的部分,对于查询语句的使用熟练程度和对查询结构的优化能力最能体现SQL编程人员的基本功。因此,该部分必须要给予足够的重视。现详述如下: (1)基本查询语句: SELECTEno,Ename,E***FROMemployee; 查询employee表中的Eno,Ename,E***三列 SELECT*FROMemployee; 查询employee表中的所有列 SELECTDISTINCTEnoFROMemployee; 查询employee表中的Eno列,并去除重复行 (2)条件(WHERE)查询语句: 查询条件的连接词如下NOT,=,〉,〈,〉=,〈=,!=,〈〉,!〉,!〈(比较);BETWEENAND,NOTBETWEENAND(确定范围);IN,NOTIN(确定集合);LIKE,NOTLIKE(字符匹配);ISNULL,ISNOTNULL(空值);AND,OR(多条件连接) 1)比较 SELECTEnoFROMemployeeWHEREEage〈=25;; 列出雇员表中年龄小于25的雇员号 2)确定范围 SELECTEno,EnameFROMemployee WHEREEage[NOT]BETWEEN20AND30;; 列出雇员表中年龄(不)在20到30的雇员号和姓名 3)确定集合 WHEREEdept[NOT]IN(′SD′,′HD′);; 列出雇员表中(不)是软硬件开发部的雇员号和姓名 4)字符匹配 LIKE的用法如下: [NOT]LIKE′〈匹配模式〉′[ESCAPE′〈换码符〉′] 通配符号有%和_两种: %:匹配任意长度的字符串(长度可为0)。a%b可与ab,adfb等匹配。 _:匹配单个任意字符。a_b可与a#b,a@b等匹配。 如果有ESCAPE,则跟在换码符号后的%或_不再是通配符号,只是正常的%或_。 例如: SELECT*FROMemployeeWHEREEnameLIKE′刘%′; 查找雇员表中姓刘雇员的信息 SELECT*FROMemployeeWHEREEnameLIKE′刘__′; 查找雇员表中姓名为刘某(两个字)的雇员的信息(汉字占2个字符的位置) SELECT*FROMemployeeWHEREEspeLIKE′DB"_%t_′ESCAPE′"′; 查找雇员表中特长项为DB_开始,倒数第二个字符为t的雇员信息 5)空值 SELECT*FROMemployeeWHEREEspeIS[NOT]NULL; 查找雇员表中特长项(不)为空的雇员信息 6)多条件连接 SELECTEnameFROMemployeeWHEREEdept=′SD′ANDEage〈=30; 列出雇员表中软件开发部30岁以下雇员的姓名 (3)结果排序 对查询的结果进行排序使用ORDERBY,ASC(默认)为升序,DESC为降序。 SELECT*FROMemployeeORDERBYEdept,EageDESC; 把所有雇员按部门升序,年龄降序排列(缺省是升序) (4)结果分组 对查询结果的分组一般都要用到SQL的集函数,因此先介绍SQL的集函数。 SQL语言中的集函数主要有COUNT(统计总数),SUM(求总和),AVG(求均值),MAX(最大值),MIN(最小值)。 SELECTMAX(Eage)FROMemployeeWHEREEdept=′SD′; 列出软件开发部年纪最大雇员的姓名 SELECTEdeptFROMemployeeGROUPBYEdeptHAVINGCount(*)〉10; 统计各部门的雇员数,只显示雇员数大于10的部门 SELECTEdept,COUNT(Eno)FROMemployeeGROUPBYEdept; 统计各部门的雇员数,按部门分组列出各部门的雇员数 (5)连接查询 连接查询指的是查询涉及多个数据表,FROM后连接多个表的情况。假如我们要统计各个项目参加人的雇员号和姓名,涉及的表Eproject(雇员参加的项目)结构如下: Eproject(EnoCHAR(6),PnoCHAR(6),TimeBgnTIME, TimeEndTIME,RemarkCHAR(50)); 相应的查询语句为: SELECTEproject.Pno,employee.Eno,Ename, FROMemployee,Eproject WHEREemployee.Eno=Eproject.Eno ORDERBYEproject.Pno; 列出参加各项目的雇员号和姓名,并按项目号升序排列。 (6)集合查询 集合查询指的是多个SELECT查询结果间进行的集合操作,主要有UNION(并操作)、INTERSECT(交操作)、MINUS(差操作)。其中标准SQL中没有提供交操作和差操作,但它们可以使用联合查询实现。假如我们要查询硬件开发部年龄不大于25岁的雇员,可以用集合查询实现如下: SELECT*FROMemployeeWHEREEdept=′HD′ UNIONSELECT*FROMemployeeWHEREEage〈=25; 3、数据更新部分 SQL中的数据更新语句有INSERT,UPDATE和DELETE三种,用法如下: Zabcdefghijklmnopqrstuvwxy(1)插入数据 INSERTINTOemployee VALUES(′13253′,′王二′,′男′,23,′SD′,′DB_Project′);; 向雇员表中插入一条完整的数据 INSERTINTOemployee(Eno,Ename) VALUES(′13253′,′王二′);; 向雇员表中插入一条数据,只包含雇员号和姓名,其它列为空值 注意:以上情况,属性为非空的列一定不能为空值。 (2)修改数据 UPDATEemployeeSETEage=24WHEREEno=′13253′; 将雇员表中13253号雇员年龄改为24岁 (3)删除数据 DELETEFROMemployeeWHEREEno=′13253′; 将雇员表中13253号雇员信息删除4、数据控制部分 GRANTSELECTONTABLEemployeeTOusr1;; 允许用户usr1查询表employee GRANTALLPRIVILEGESONTABLEemployeeTOusr2;; 允许用户usr2对表employee的任何操作(查询,插入、更新、删除数据) (2)收回权限 SQL中收回用户权限使用REVOKE关键词,它的用法举例如下: REVOKEUPDATE(Eno)ONTABLEemployeeFROMusr3;; 收回用户usr3更新表employee中Eno列的权力 REVOKEINSERTONTABLEemployeeFROMPUBLIC;; 不允许所有用户在表employee中添加数据 二、SQL注入(SQLINJECTION)简介 自从SQLInjection被发现以来,人们通过大量的实验发现,它存在于任何允许执行SQL语句的地方。简单的说,SQLinjection是一种源于SQL的漏洞,也可以说成是一种攻击方法,它利用程序对用户输入数据的检验不足或程序自身对变量处理不当,把想要执行的SQL语句插入到实际的SQL语句中发送给服务器去执行,后果轻则导致敏感信息泄漏,重则使整个服务器受控。 SELECT*FROMmemberWHEREUID=′"&request("ID")&"′ ANDPasswd=′"&request("Pwd")&"′ 如果正常使用者的帐号user1,密码abcdefg12345,那么此时的SQL语句为: SELECT*FROMmemberWHEREUID=′user1′ANDPasswd=′abcdefg12345′;; 在这里举三个SQLInjection的实例简单说明一下这种漏洞的原理: 1、帐号输入user1′--,密码任意(如aaa),此时的SQL语句变为: SELECT*FROMmemberWHEREUID=′user1′--′ANDPasswd=′aaa′ 2、帐号输入′OR1=1--,密码任意(如aaa),此时的SQL语句变为: SELECT*FROMmemberWHEREUID=′′OR1=1--′ANDPasswd=′aaa′ 由于AND字句被作为说明而失去了作用,where字句返回为真,这个SQL语句就失去了鉴别作用。 3、帐号输入任意(如uuu),密码为aaa(任意)′OR1=1--,此时的SQL语句变为: SELECT*FROMmemberWHEREUID=′uuu′ANDPasswd=′aaa′OR1=1-- 由于--之后的语句被忽略,WHERE字句返回为真,该SQL语句就失去了鉴别作用。 三、SQL注入纵览 SELECT*FROMmemberWHEREUID=′′OR′′=′′ANDPasswd=′′OR′′=′′ 显然,该SQL语句失去了鉴别功能。 2、SELECT型SQL注入 (1)基本SELECT型 基本SELECT型SQL注入分为直接型和引用型。直接型SQL注入指的是用户提交的数据直接被用在SQL查询中。如果在某个合法输入值后添加一个空格和OR,系统返回了一个错误,那么就可能存在直接型SQL注入。直接值的位置可能存在于WHERE子句中,如: SQLString="SELECT*FROMmemberWHEREUID="&intUID 或者存在于某个SQL关键词中,如某个表名或列名: SQLString="SELECT*FROMmemberORDERBY"&strColumn 而引用型的SQL注入指的是用户提交的数据被放在引号中提交。如: SQLString="SELECT*FROMmemberWHEREUID=′"&strUID&"′" 此时要注入的部分要以单引号开始,与之前的单引号匹配,结尾在WHERE子句后加单引号,与之后的单引号匹配。 (2)基本集合(UNION)型 基本集合型SQL注入是在WHERE子句中插入一个UNIONSELECT语句,以达到执行注入部分的目的。例如目标SQL语句为: SQLString="SELECTName,Sex,TitleFROMmemberWHEREUID=′"&strColumn&"′" 使用的注入字串如下: ′UNIONSELECTotherfieldFROMothertableWHERE′′=′ 这样一来,提交的查询语句就成了: SELECTName,Sex,TitleFROMmemberWHEREUID=′′ UNIONSELECTotherfieldFROMothertableWHERE′′=′′ 结果就有以下操作:数据库首先检索member表查找UID为空的行,由于不存在UID为空的行,所以没有返回记录。返回的记录存在于注入部分的查询。有时使用空值会不起作用,可能是表中空值被使用或被用于实现其他的功能。这种情况下,你唯一要做的就是构造一个决不会出现在表中的字串,只要它能使UNIONSELECT之前不返回记录。 (3)语法错误型 对于某些数据库而言,返回的错误信息中包含了语法错误的部分,因此,通过制造语法错误(错误注入),可以得到很多有价值的信息。 构造的错误字符串有:′,错误值′,′错误值,′OR′,′OR,OR′,;等。 (4)圆括号型 如果返回的错误中包含圆括号,或者错误是丢失圆括号,那么就要在错误值和WHERE子句部分添加圆括号。例如目标SQL语句为 SQLStr="SELECTName,Sex,TitleFROMmemberWHERE(UID=′"&strID&"′)" 使用的注入字串就要变为: ′)UNIONSELECTotherfieldFROMothertableWHERE(′′=′ SELECTName,Sex,TitleFROMmemberWHERE(UID=′′) UNIONSELECTotherfieldFROMothertableWHERE(′′=′′) 由于不存在UID为空的行,所以第一部分没有返回记录,返回的记录存在于注入部分的查询。 (5)LIKE型 LIKE型的SQL注入也很常见。如果返回错误中含有%,_或者LIKE等字眼,说明该系统存在LIKE型注入漏洞,用户提交的数据会被送给LIKE子句去执行。例如目标SQL语句为 SQLStr="SELECTName,Sex,TitleFROMmemberWHERENameLIKE′%"&strColumn&"%′" 而使用的注入字串为: ′UNIONSELECTotherfieldFROMothertableWHERE′%37′=′ 得到提交的查询语句为: SELECTName,Sex,TitleFROMmemberWHERENameLIKE′%′ UNIONSELECTotherfieldFROMothertableWHERE′%′=′%′ 显然,第一部分返回为表member的所有记录,第二部分为用户想要得到的记录,最终得到就是用户想要的记录。 (6)错误结尾 有些时候,当尝试了许多注入方法后,返回依旧是错误。这说明目标SQL语句可能并不像所猜测的那样简单,它可能存在子查询或连接查询等复杂的情况。这时,对于SQLServer,由于;”之后的语句会被忽略,所以要在注入的SQL语句末尾,加上;;;--”。 (7)连接查询 如果目标SQL语句为 SQLStr="SELECTName,Sex,TitleFROMmember WHEREUID=′"&strID&"′ANDSex=′Female′" 如果使用的注入字串为: UNIONSELECTotherfieldFROMothertableWHERE′′=′′ANDSex=′Female′ 由于othertable中不一定存在名为Sex的列,所以可能会返回;InvalidcolumnnameSex”的错误。对于SQLServer而言,在系统表sysobjects中存有库中所有表的列名,所以,使用SELECTnameFROMsysobjectsWHERExtype=′U′可以返回库中所有用户定义表的表名。 在这种情况下,构造的SQL注入语句要成为以下结构: SELECTnameFROMsyscolumns WHEREid=(SELECTidFROMsysobjectsWHEREname=′TableName′) 3、INSERT型SQL注入 INSERT执行在数据库中增加列的功能,它用在用户注册,发表言论,网上购物等许多地方。由于它直接改变数据库的数据,所以使用INSERT型注入比SELECT型更危险。对于攻击者而言,如果使用INSERT型注入的语句出现错误,可能因为在数据库中产生一串单引号而被检测到。所以,使用INSERT型SQL注入要格外小心。 SQLStr="INSERTINTOTableName VALUES(′"&strName&"′,′"&strSex&"′,′"&strEmail&"′)" 如下填表: 姓名:′+SELECTTOP1FieldNameFROMTableName+′ 性别:Male 这样,提交的SQL语句为: INSERTINTOTableNameVALUES (′′+SELECTTOP1FieldNameFROMTableName+′′,′Male′,′aaa@yahoo.com′) 在返回的注册信息中,就可以找到表TableName中的FieldName的值。 由于SQL语言的设计思想就是要使用灵活,所以各种各样的SQL注入方法也会层出不穷。但是总的来说,只要对SQL语言足够熟悉,并且时刻注意SQL注入的危险,至少已经向安全迈出了第一步。 voidLog(char*szString,...) { TCHARszEntry[1024]; ZeroMemory(szEntry,sizeof(szEntry)); va_listargs; va_start(args,szString); vsprintf(szEntry,szString,args);//用vsprintf函数格式化,而不是vsprintf格式化 AfxMessageBox(szEntry); } voidCTestDlg::OnOK() //TODO:Addextravalidationhere floati=30; intj=30; Log("float=%f,int=%d",i,j); 表结构如下:idint4EntryIDint4BlogIDint4现在要求在插入时,不允许插入EntryID与BlogID都相同的记录,即表中不允许任意两条记录的EntryID与BlogID都相同,EntryID与BlogID构成记录的唯一标识。以前我的处理方法时,在插入之前,通过select检查是否存在相同的记录。现在我采用SQLServer唯一约束来实现,简单方便,效率又高。实现方法是:1、在数据库关系图中右击将包含约束的表,然后从快捷菜单中选择"属性"命令。-或-为将包含约束的表打开表设计器,在表设计器中右击,然后从快捷菜单中选择"属性"命令。2、选择"索引/键"选项卡。3、选择"新建"命令。系统分配的名称出现在"索引名"框中。4、在"列名"下展开列的列表,选择要将约束附加到的列(在这里我们选择列EntryID、BlogID)。5、选择"创建UNIQUE"复选框。6、选择"约束"选项。当保存表或关系图时,唯一约束即创建在数据库中。 现在我们再进行插入,就出现错误"违反了UNIQUEKEY约束..."。我们在程序中捕获这个错误,就知道插入了重复记录。 NoteThisdatatypeiscurrentlynotsupportedbyADO.Usagemaycauseunpredictableresults. ADO/WFCEquivalent Package:com.ms.wfc.data 关键字:正则表达式模式匹配Javascript 摘要:收集一些常用的正则表达式。 正则表达式用于字符串处理,表单验证等场合,实用高效,但用到时总是不太把握,以致往往要上网查一番。我将一些常用的表达式收藏在这里,作备忘之用。本贴随时会更新。 匹配中文字符的正则表达式:["u4e00-"u9fa5] 匹配双字节字符(包括汉字在内):[^"x00-"xff] 应用:计算字符串的长度(一个双字节字符长度计2,ASCII字符计1) String.prototype.len=function(){returnthis.replace([^"x00-"xff]/g,"aa").length;} 匹配空行的正则表达式:"n["s|]*"r 匹配HTML标记的正则表达式:/<(.*)>.*<"/"1>|<(.*)"/>/ 匹配首尾空格的正则表达式:(^"s*)|("s*$) String.prototype.trim=function(){returnthis.replace(/(^"s*)|("s*$)/g,"");} 利用正则表达式分解和转换IP地址: 下面是利用正则表达式匹配IP地址,并将IP地址转换成对应数值的Javascript程序: functionIP2V(ip){re=/("d+)".("d+)".("d+)".("d+)/g//匹配IP地址的正则表达式if(re.test(ip)){returnRegExp.$1*Math.pow(255,3))+RegExp.$2*Math.pow(255,2))+RegExp.$3*255+RegExp.$4*1}else{thrownewError("NotavalidIPaddress!")}} 不过上面的程序如果不用正则表达式,而直接用split函数来分解可能更简单,程序如下: varip="10.100.20.168"ip=ip.split(".")alert("IP值是:"+(ip[0]*255*255*255+ip[1]*255*255+ip[2]*255+ip[3]*1)) 匹配Email地址的正则表达式:"w+([-+.]"w+)*@"w+([-.]"w+)*"."w+([-.]"w+)* 利用正则表达式去除字串中重复的字符的算法程序:[注:此程序不正确,原因见本贴回复] vars="abacabefgeeii"vars1=s.replace(/(.).*"1/g,"$1")varre=newRegExp("["+s1+"]","g")vars2=s.replace(re,"")alert(s1+s2)//结果为:abcefgi我原来在CSDN上发贴寻求一个表达式来实现去除重复字符的方法,最终没有找到,这是我能想到的最简单的实现方法。思路是使用后向引用取出包括重复的字符,再以重复的字符建立第二个表达式,取到不重复的字符,两者串连。这个方法对于字符顺序有要求的字符串可能不适用。 得用正则表达式从URL地址中提取文件名的javascript程序,如下结果为page1 利用正则表达式限制网页表单里的文本框输入内容: 用正则表达式限制只能输入中文:onkeyup="value=value.replace(/[^"u4E00-"u9FA5]/g,'')"onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^"u4E00-"u9FA5]/g,''))" 用正则表达式限制只能输入全角字符:onkeyup="value=value.replace(/[^"uFF00-"uFFFF]/g,'')"onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^"uFF00-"uFFFF]/g,''))" 用正则表达式限制只能输入数字:onkeyup="value=value.replace(/[^"d]/g,'')"onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^"d]/g,''))" 用正则表达式限制只能输入数字和英文:onkeyup="value=value.replace(/["W]/g,'')"onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^"d]/g,''))" 读者层次:初学整理:VisionDeng(ZT) inti=100;longl=2001;floatf=300.2;doubled=12345.119;charusername[]="程佩君";chartemp[200];char*buf;CStringstr;_variant_tv1;_bstr_tv2;一、其它数据类型转换为字符串 二、字符串转换为其它数据类型strcpy(temp,"123"); 三、其它数据类型转换到CString使用CString的成员函数Format来转换,例如: 四、BSTR、_bstr_t与CComBSTR 五、VARIANT、_variant_t与COleVariant 六、其它一些COM数据类型 七、ANSI与UnicodeUnicode称为宽字符型字串,COM里使用的都是Unicode字符串。 八、其它 九、注意事项假如需要使用到ConvertBSTRToString此类函数,需要加上头文件comutil.h,并在setting中加入comsupp.lib或者直接加上#pragmacomment(lib,"comsupp.lib") 后记:本文匆匆写成,错误之处在所难免,欢迎指正. 关于把BSTR类型数据转换成CString类型数据时的问题当我在把BSTR类型数据转换成CString或“char*类型”数据时,发现在BSTR类型字符串较短的情况下没问题,当较长时就会出现内存读写错了。(在NT,2000下都测试是这样的。)根据你所说:1)字符串指针(char*)等已经被CString构造函数支持的数据类型可以直接赋值str=username;2)当b为BSTR类型时可以使用char*p=_com_util::ConvertBSTRToString(b);于是以下是对的:CStringcstr;BSTRbstr;....cstr=com_util::ConvertBSTRToString(bstr);...可是当bstr非常大时(其实,较大时就会)就会出现内存读写错,不知何故。此外我发现cstr=com_util::ConvertBSTRToString(bstr);可以简化为cstr=bstr;但当bstr较大时同样出现这个问题。请兄弟帮忙!急。谢谢! 关于把CString转化成LPCTSTR的问题作者:jakiesun发表日期:2001-9-520:08:48我记的我以前写过这样一段代码voidfunction(){CStringstr,str1,str2;function((char*)(LPCTSTR)str1);str=str1;...//调试道此发现str2的值随着str的改变而改变,请问能解释一下为什么,如有回答,请通知wangshaohong@sohu.com,tx先} 添加lib支持作者:磨刀霍霍发表日期:2001-9-1011:32:12如果不添加会产生错误,在setting中加入comsupp.lib或者直接#pragmacomment(lib,"comsupp.lib")微软认为缺省的设置callconvention如果不设置成__cdecl也会出现同样的错误。 /* LogFileLibrary(WIN98/NT/2000) Compileby:BC++5;C++BUILDER;VC++;VC.NET; */ Use: //这个代码我用工业现场24X7值守的程序纪录各种信息,简单易用; //一般用一个全局日志对象,有临界排斥可以多线程安全使用。 //有两个类 classLogFile;//用户定义日志文件名 classLogFileEx;//有日志文件名自动生成功能,可分年月日频率生成文件名,可指定日志存放的目录 LogFilegLog("My.Log"); gLog.Log("test",4);//记录日志 gLog.Log("系统启动"); LogFileExgLog(".",LogFileEx::YEAR);//一年生成一个日志文件 LogFileExgLog(".""Log",LogFileEx::MONTH);//一月生成一个日志文件 LogFileExgLog(".""Log",LogFileEx::DAY);//一天生成一个日志文件 //注意日志所属目录创建失败会自动退出,请注意目录的合法性,文件生成频率看情况掌握 //24小时运行的程序可以每天生成一个日志文件,以免内容过多 #ifndef_LOGFILE_H #define_LOGFILE_H #include #include #include #include classLogFile protected: CRITICAL_SECTION_csLock; char*_szFileName; HANDLE_hFile; boolOpenFile()//打开文件,指针到文件尾 if(IsOpen()) returntrue; if(!_szFileName) returnfalse; _hFile=CreateFile( _szFileName, GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if(!IsOpen()&&GetLastError()==2)//打开不成功,且因为文件不存在,创建文件 OPEN_ALWAYS, SetFilePointer(_hFile,0,NULL,FILE_END); returnIsOpen(); DWORDWrite(LPCVOIDlpBuffer,DWORDdwLength) DWORDdwWriteLength=0; WriteFile(_hFile,lpBuffer,dwLength,&dwWriteLength,NULL); returndwWriteLength; virtualvoidWriteLog(LPCVOIDlpBuffer,DWORDdwLength)//写日志,可以扩展修改 time_tnow; chartemp[21]; DWORDdwWriteLength; 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,""xd"xa",2,&dwWriteLength,NULL); FlushFileBuffers(_hFile); voidLock(){::EnterCriticalSection(&_csLock);} voidUnlock(){::LeaveCriticalSection(&_csLock);} public: LogFile(constchar*szFileName="Log.log")//设定日志文件名 _szFileName=NULL; _hFile=INVALID_HANDLE_VALUE; ::InitializeCriticalSection(&_csLock); SetFileName(szFileName); virtual~LogFile() ::DeleteCriticalSection(&_csLock); Close(); if(_szFileName) delete[]_szFileName; constchar*GetFileName() return_szFileName; voidSetFileName(constchar*szName)//修改文件名,同时关闭上一个日志文件 assert(szName); _szFileName=newchar[strlen(szName)+1]; assert(_szFileName); strcpy(_szFileName,szName); boolIsOpen() return_hFile!=INVALID_HANDLE_VALUE; voidClose() CloseHandle(_hFile); voidAddLog(LPCVOIDlpBuffer,DWORDdwLength)//追加日志内容 assert(lpBuffer); __try Lock(); if(!OpenFile()) return; WriteLog(lpBuffer,dwLength); __finally Unlock(); voidLog(constchar*szString,...) vsprintf(szEntry,szString,args); AddLog(szEntry,strlen(szEntry)); voidGetErrorMessage(LPCTSTRlpFunction,DWORDdwError) LPVOIDlpMsgBuf; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,NULL,dwError,MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),//Defaultlanguage (LPTSTR)&lpMsgBuf,0,NULL); sprintf(szEntry,"Function:%s,%s",lpFunction,(LPCTSTR)lpMsgBuf); LocalFree(lpMsgBuf); private://屏蔽函数 LogFile(constLogFile&); LogFile&operator=(constLogFile&); }; classLogFileEx:publicLogFile char*_szPath; char_szLastDate[9]; int_iType; voidSetPath(constchar*szPath) assert(szPath); WIN32_FIND_DATAwfd; chartemp[MAX_PATH+1]={0}; if(FindFirstFile(szPath,&wfd)==INVALID_HANDLE_VALUE&&CreateDirectory(szPath,NULL)==0) strcat(strcpy(temp,szPath),"CreateFail.ExitNow!ErrorID:"); ltoa(GetLastError(),temp+strlen(temp),10); MessageBox(NULL,temp,"ClassLogFileEx",MB_OK); exit(1); else GetFullPathName(szPath,MAX_PATH,temp,NULL); _szPath=newchar[strlen(temp)+1]; assert(_szPath); strcpy(_szPath,temp); enumLOG_TYPE{YEAR=0,MONTH=1,DAY=2}; LogFileEx(constchar*szPath=".",LOG_TYPEiType=MONTH) _szPath=NULL; SetPath(szPath); _iType=iType; memset(_szLastDate,0,9); ~LogFileEx() if(_szPath) delete[]_szPath; constchar*GetPath() return_szPath; voidAddLog(LPCVOIDlpBuffer,DWORDdwLength) chartemp[10]; staticconstcharformat[3][10]={"%Y","%Y-%m","%Y%m%d"}; time_tnow=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); LogFileEx(constLogFileEx&); LogFileEx&operator=(constLogFileEx&); #endif ActiveXDataObjects(ADO)enablesyoutowriteaclientapplicationtoaccessandmanipulatedatainadatabaseserverthroughaprovider.ADO'sprimarybenefitsareeaseofuse,highspeed,lowmemoryoverhead,andasmalldiskfootprint.ThissampleprojectisforADODB,animplementationofADOoptimizedforusewithMicrosoftOLEDBproviders,includingtheMicrosoftODBCproviderforOLEDB.Usingthiswecanexecutestoredprocedure,passargumentsandretrievevalue.Tousethissampleyouwillhavetocreatethetwostoredproceduresmentionedbelow.ForusingthisprojectyouneedMFC5.0ORabove+ADOinyourmachine. {CStringstrTmp; CStringm_sdatasource;//DatasourcenameCStringm_sUserID;//UserIdCStringm_sPassword;//Password //GETtheabovevaluesfromtheuser//WithoutcreatingDatasourcewecanusedatabasebythefollowingcode/*strTmp.Format("driver={sqlserver};""server=%s;""Database=%s;""UID=%s;""PWD=%s;",m_server,m_sdatabase,m_sUserID,m_sPassword);*/ }//weneedtocreatethestoredproceduresbelowbeforerunningtheapplication//CREATEPROCEDUREsp_AddAccountingInfo@nfinalint,@pcDatedatetime,//@pcURLvarchar(250),@pcTopvarchar(250),//@pcQueryStringvarchar(250),@pcBytesint,@pcRequestsintAS/*Doyouroperationhere*///CREATEPROCEDUREsp_AddAccountingInfo//@nfinalint,//@pcDatedatetime,//@pcURLvarchar(250),//@pcTopvarchar(250),//@pcQueryStringvarchar(250),//@pcBytesint,//@pcRequestsint//AS/*Putyourcodehere*/}vc下用ado调用存储过程 1_ConnectionPtrm_pConnection;2_CommandPtrm_pCommand;.cpp中在函数中执行//建立ado连接3HRESULThr;4hr=m_pConnection.CreateInstance(__uuidof(Connection));5try6{7if(SUCCEEDED(hr))8{9hr=m_pConnection->Open(_bstr_t(L"Provider=SQLOLEDB.1;PersistSecurityInfo=False;UserID=sa;InitialCatalog=Viper;DataSource=Viper"),_bstr_t(L"sa"),_bstr_t(L""),adModeUnknown);10}11}12catch(_com_error&err)13{14AfxMessageBox(err.Description(),MB_OK,0);15AfxMessageBox(err.ErrorMessage(),MB_OK,0);16AfxMessageBox("无法连接SQLSERVER服务器,程序将退出。请检查网络设备",MB_OK,0);17exit(0);18} //执行储存过程19CStringcvar1,cvar2;20intcvar3;21cvar1="ddd";22cvar2="";23cvar3=0;24try25{26m_pCommand.CreateInstance(__uuidof(Command));27m_pCommand->ActiveConnection=app->m_pConnection;28m_pCommand->CommandType=adCmdStoredProc;29m_pCommand->CommandText=_bstr_t("pr_zs_dzdy");3031_variant_tvvar1,vvar2,vvar3;32vvar1=_variant_t(_bstr_t(cvar1));33vvar2=_variant_t(_bstr_t(cvar2));34vvar3=_variant_t(cvar3);35_ParameterPtrmp_var1,mp_var2,mp_var3;36mp_var1.CreateInstance(__uuidof(Parameter));37mp_var2.CreateInstance(__uuidof(Parameter));38mp_var3.CreateInstance(__uuidof(Parameter)); 39mp_var1=m_pCommand->CreateParameter40(41_bstr_t("var1"),42adVarChar,43adParamInput,443,45vvar146);47m_pCommand->Parameters->Append(mp_var1);4849mp_var2=m_pCommand->CreateParameter50(51_bstr_t("var2"),52adVarChar,53adParamOutput,543,55vvar256);57m_pCommand->Parameters->Append(mp_var2);5859mp_var3=m_pCommand->CreateParameter60(61_bstr_t("var3"),62adIntger,63adParamOutput,649,65vvar366);67m_pCommand->Parameters->Append(mp_var3);686970_variant_tvNull;71vNull.vt=VT_ERROR;72vNull.scode=DISP_E_PARAMNOTFOUND;73m_pCommand->Execute(&vNull,&vNull,adCmdStoredProc);74cvar2=mp_var2->Value.bstrVal;75cvar3=mp_var3->Value;76}77catch(_com_error&error)78{79MessageBox(error.ErrorMessage(),"ADO错误!");80MessageBox(error.Description(),"ADO错误!");81} 【打印文档】【大中小】【关闭窗口】 关于存储过程的ADO调用的一些心得(输出参数,返回值) 在一个项目中,我需要用到存储过程来访问数据,为了提供一个比较一致的接口以便调用,我没有使用CreateParameter(),而是调用CommandPtr的Refresh()函数先从数据库中查询参数._ConnectionPtrm_pConn;m_pConn.CreateInstance(__uuidof(Connection));m_pConn->Open("driver={SQLServer};server=127.0.0.1;DATABASE=pub;UID=sa;PWD=","","",0);_CommandPtrm_pCommand;m_pCommand.CreateInstance(__uuidof(Command));_RecordsetPtrm_pRecordset;m_pRecordset.CreateInstance(__uuidof(Recordset)); m_pCommand->ActiveConnection=m_pConn;m_pCommand->CommandText="SP_XX";//存储过程名m_pCommand->PutCommandType(adCmdStoredProc);m_pCommand->Parameters->Refresh();//从数据库查询参数信息接下来就可以对每一个参数赋值了:longcnt=m_pCommand->Parameters->GetCount();//取得参数的个数for(longk=1;k 昨天做项目时发现此处不正确,m_pRecordset不能close。而且释放指针时要先释放m_pCommand,再释放m_pRecordset bisquedarkgray 很久没有翻译东西了,乘晚上整理资料的时候翻一篇。 OverthepastfewmonthsI'vehadtheopportunitytointerviewdozensofcandidates...... 过去的几个月里,我有幸面试了众多应聘DBA和DB开发岗位的求职者。我们希望开发人员能够创建存储过程,编写合理的复杂SQL语句,以及触发器。我喜欢问这些求职者一个问题: “假设我们使用SQLServer2000进行开发。现在我需要传递给存储过程两个变量:firstname和lastname。存储过程负责向名字为TEST的表插入这两个变量,其中TEST表有两个字段,名字就叫firstname和lastname。TEST表的主键是一个自增长类型的字段,名字叫ContactID。问题是我如何获取插入的那条数据的主键值。” 让我们想一下答案是什么。你是否知道如何创建存储过程,获取数值并返回给调用的应用程序? 有人会直接问我-这个问题重要吗?对于我来说,我问这个问题的目的是为了测试求职者是否有求解非常规需求的能力。设想一下标准的订单/订单明细表应用场景--你是否知道如何不知道订单主键的情况下插入订单的详细信息?当你因为没有使用主键时,可能会带来锁问题。这时,自增长字段加入是常用的一个手段。但是使用@@Identity来获取插入数据的标识,有可能会带来问题,比如在触发器内使用就会发生问题。这并不是一个可以给出唯一答案的问题,但是这个问题可以让我们对处理类似表的问题,来展开讨论。 我收到了很多不同的回答,但是绝大多数并非最优。几乎每个人都知道如何插入数据、如何返回值,但是几乎每个人都在获取自增长字段取值上回答得不是很好。 错误回答#1-Selectmax(contactid)fromTest.因为你无法避免别人也同时在插入数据,因此这个回答是错误的。当然,你可以通过提升隔离级别来达到目的,但是这将会大幅降低并发性能,因此不好。 错误回答#2-Selecttop1contactidfromtestorderbycontactiddesc.错误的原因和回答#1一样。 错误回答#3-通过插入的数据来组合成一个唯一的标识,从而获得自增长字段的值。如果插入的数据确实组合起来是唯一的,能达到目的,但是如果不唯一,怎么办呢?因此这也不是好办法。 正确答案-因为我们使用的是SQLServer2000,因此使用Scope_Identity(),如果用的是SQLServer7,那么只有只用@@Identity,并且以output参数方式传递(return值一般是用来作为错误代码用)。使用@@Identity意味着将来也许会发生错误,例如审核时使用自增长字段。 现在我们来做一系列的试验来验证: createdatabaseIdentityTestuseidentitytestcreatetableTEST(ContactIDintnotnullidentity(1,1),firstnamevarchar(100)null,lastnamevarchar(100)null)insertintoTESTDefaultValuesselect@@Identity 运行后会返回1。如果在此运行,则返回2。 insertintoTESTDefaultValuesselectScope_Identity() 运行后返回3。 现在我们来设计如何使@@Identity返回错误结果。我们先创建一个包含一个新的自增长字段的表TESTHISTORY,然后在TEST表上加触发器。 createtableTESTHISTORY(HistoryIDintnotnullidentity(1,1),ContactIDintnotnull,firstnamevarchar(100)null,lastnamevarchar(100)null)createtriggeri_TESTondbo.TESTforinsertassetnocountoninsertintoTESTHISTORY(ContactID,FirstName,LastName)selectContactID,FirstName,LastNamefromInserted 现在看看会发生什么: insertintoTESTDefaultValuesselect@@Identity 返回值为1。注意,此时TEST表最后插入的记录,主键值为4,而TESTHISTORY表作后插入记录的主键值=1。 返回值为5。TEST表最后插入记录的主键值为5,而且TESTHISTORY表的确也插入了第二条记录。现在我们再测试如果同时有别的连接来向插入TEST表插入数据,情况会如何。首先我们用当前的连接,运行: insertintoTESTDefaultValues 此时,TEST插入了第6条记录。然后新建一个连接,并运行相同的SQL语句: 此时,TEST表插入了第7条记录。然后我们在原先那个连接里,来获取“错误”的结果,值为3。 select@@Identity 现在我们用scope_identity()来测试。我们希望得到的结果是6,不是7! selectScope_Identity() 结果确实如此,证明使用scope_identity()是正确的。我知道,这样测试很麻烦,你也许不会去仔细探究。但是如果你准备使用SQLServer2000,你就必须知道它是如何工作的。有兴趣用这个问题来考考你的开发人员,可以教他们一些专业的开发技巧,这样也许会使你在将来省却很多本可以避免的麻烦。 数据库的最初雏形据说源自美国一个奶牛场的记账薄(纸质的,由此可见,数据库并不一定是存储在电脑里的数据^_^),里面记录的是该奶牛场的收支账目,程序员在将其整理、录入到电脑中时从中受到启发。当按照规定好的数据结构所采集到的数据量大到一定程度后,出于程序执行效率的考虑,程序员将其中的检索、更新维护等功能分离出来,做成单独调用的模块,这个模块后来就慢慢发展、演变成现在我们所接触到的数据库管理系统(DBMS)——程序开发中的一个重要分支。 一、树型关系的数据表不少程序员在进行数据库设计的时候都遇到过树型关系的数据,例如常见的类别表,即一个大类,下面有若干个子类,某些子类又有子类这样的情况。当类别不确定,用户希望可以在任意类别下添加新的子类,或者删除某个类别和其下的所有子类,而且预计以后其数量会逐步增长,此时我们就会考虑用一个数据表来保存这些数据。按照教科书上的教导,第二类程序员大概会设计出类似这样的数据表结构: 类别表_1(Type_table_1)名称类型约束条件说明type_idint无重复类别标识,主键type_namechar(50)不允许为空类型名称,不允许重复type_fatherint不允许为空该类别的父类别标识,如果是顶节点的话设定为某个唯一值 这样的设计短小精悍,完全满足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_idint无重复类别标识,主键type_namechar(50)不允许为空类型名称,不允许重复type_fatherint不允许为空该类别的父类别标识,如果是顶节点的话设定为某个唯一值type_layerchar(6)限定3层,初始值为000000类别的先序遍历,主要为减少检索数据库的次数 按照这样的表结构,我们来看看上面例子记录在表中的数据是怎样的: type_idtype_nametype_fathertype_layer1总类别00000002类别110100003类别1.120101004类别1.220102005类别210200006类别2.150201007类别310300008类别3.170301009类别3.2703020010类别1.1.13010101…… 现在按type_layer的大小来检索一下:SELECT*FROMType_table_2ORDERBYtype_layer 列出记录集如下: type_idtype_nametype_fathertype_layer1总类别00000002类别110100003类别1.1201010010类别1.1.130101014类别1.220102005类别210200006类别2.150201007类别310300008类别3.170301009类别3.27030200…… 现在列出的记录顺序正好是先序遍历的结果。在控制显示类别的层次时,只要对type_layer字段中的数值进行判断,每2位一组,如大于0则向右移2个空格。当然,我这个例子中设定的限制条件是最多3层,每层最多可设99个子类别,只要按用户的需求情况修改一下type_layer的长度和位数,即可更改限制层数和子类别数。其实,上面的设计不单单只在类别表中用到,网上某些可按树型列表显示的论坛程序大多采用类似的设计。 或许有人认为,Type_table_2中的type_father字段是冗余数据,可以除去。如果这样,在插入、删除某个类别的时候,就得对type_layer的内容进行比较繁琐的判定,所以我并没有消去type_father字段,这也正符合数据库设计中适当保留冗余数据的来降低程序复杂度的原则,后面我会举一个故意增加数据冗余的案例。 商品类型表(Wares_type)名称类型约束条件说明type_idint无重复类别标识,主键type_namechar(50)不允许为空类型名称,不允许重复type_fatherint不允许为空该类别的父类别标识,如果是顶节点的话设定为某个唯一值type_layerchar(6)限定3层,初始值为000000类别的先序遍历,主要为减少检索数据库的次数 供货厂商表(Wares_provider)名称类型约束条件说明provider_idint无重复供货商标识,主键provider_namechar(100)不允许为空供货商名称 你拿着这3个表给老板检查,老板希望能够再添加一个商品图片的字段,不过只有一部分商品有图片。OK,你在商品信息表(Wares_info)中增加了一个haspic的BOOL型字段,然后再建了一个新表——商品图片表(Wares_pic): 商品图片表(Wares_pic)名称类型约束条件说明pic_idint无重复商品图片标识,主键wares_idint不允许为空所属商品标识,和Wares_info.wares_id关联pic_addresschar(200)不允许为空图片存放路径 商品长度表(Wares_length)名称类型约束条件说明length_idint无重复商品图片标识,主键wares_idint不允许为空所属商品标识,和Wares_info.wares_id关联lengthchar(20)不允许为空商品长度说明 去掉商品信息表(Wares_info)中的haspic字段,添加商品额外属性表(Wares_ex_property)和商品额外信息表(Wares_ex_info)2个表来完成添加新属性的功能。 商品额外属性表(Wares_ex_property)名称类型约束条件说明ex_pidint无重复商品额外属性标识,主键p_namechar(20)不允许为空额外属性名称 商品额外信息表(Wares_ex_info)名称类型约束条件说明ex_iidint无重复商品额外信息标识,主键wares_idint不允许为空所属商品标识,和Wares_info.wares_id关联property_idint不允许为空商品额外属性标识,和Wares_ex_property.ex_pid关联property_valuechar(200)不允许为空商品额外属性值 在商品额外属性表(Wares_ex_property)中添加2条记录:ex_pidp_name1商品图片2商品长度 再在整个电子商务平台的后台管理功能中追加一项商品额外属性管理的功能,以后添加新的商品时出现新的属性,只需利用该功能往商品额外属性表(Wares_ex_property)中添加一条记录即可。不要害怕变化,被第一颗子弹击中并不是坏事,坏的是被相同轨道飞来的第二颗、第三颗子弹击中。第一颗子弹来得越早,所受的伤越重,之后的抵抗力也越强8) 三、多用户及其权限管理的设计开发数据库管理类的软件,不可能不考虑多用户和用户权限设置的问题。尽管目前市面上的大、中型的后台数据库系统软件都提供了多用户,以及细至某个数据库内某张表的权限设置的功能,我个人建议:一套成熟的数据库管理软件,还是应该自行设计用户管理这块功能,原因有二:1.那些大、中型后台数据库系统软件所提供的多用户及其权限设置都是针对数据库的共有属性,并不一定能完全满足某些特例的需求;2.不要过多的依赖后台数据库系统软件的某些特殊功能,多种大、中型后台数据库系统软件之间并不完全兼容。否则一旦日后需要转换数据库平台或后台数据库系统软件版本升级,之前的架构设计很可能无法重用。 下面看看如何自行设计一套比较灵活的多用户管理模块,即该数据库管理软件的系统管理员可以自行添加新用户,修改已有用户的权限,删除已有用户。首先,分析用户需求,列出该数据库管理软件所有需要实现的功能;然后,根据一定的联系对这些功能进行分类,即把某类用户需使用的功能归为一类;最后开始建表:功能表(Function_table)名称类型约束条件说明f_idint无重复功能标识,主键f_namechar(20)不允许为空功能名称,不允许重复f_descchar(50)允许为空功能描述 用户组表(User_group)名称类型约束条件说明group_idint无重复用户组标识,主键group_namechar(20)不允许为空用户组名称group_powerchar(100)不允许为空用户组权限表,内容为功能表f_id的集合 用户表(User_table)名称类型约束条件说明user_idint无重复用户标识,主键user_namechar(20)无重复用户名user_pwdchar(20)不允许为空用户密码user_typeint不允许为空所属用户组标识,和User_group.group_id关联 采用这种用户组的架构设计,当需要添加新用户时,只需指定新用户所属的用户组;当以后系统需要添加新功能或对旧有功能权限进行修改时,只用操作功能表和用户组表的记录,原有用户的功能即可相应随之变化。当然,这种架构设计把数据库管理软件的功能判定移到了前台,使得前台开发相对复杂一些。但是,当用户数较大(10人以上),或日后软件升级的概率较大时,这个代价是值得的。 四、简洁的批量m:n设计碰到m:n的关系,一般都是建立3个表,m一个,n一个,m:n一个。但是,m:n有时会遇到批量处理的情况,例如到图书馆借书,一般都是允许用户同时借阅n本书,如果要求按批查询借阅记录,即列出某个用户某次借阅的所有书籍,该如何设计呢?让我们建好必须的3个表先: 书籍表(Book_table)名称类型约束条件说明book_idint无重复书籍标识,主键book_nochar(20)无重复书籍编号book_namechar(100)不允许为空书籍名称…… 借阅用户表(Renter_table)名称类型约束条件说明renter_idint无重复用户标识,主键renter_namechar(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_idint无重复员工标识,主键clerk_namechar(10)不允许为空员工姓名 每餐总表(Eatdata1)名称类型约束条件说明totle_idint无重复每餐总表标识,主键personschar(100)不允许为空就餐员工的员工标识集合eat_datedatetime不允许为空就餐日期eat_typechar(1)不允许为空就餐类型,用来区分中、晚餐totle_pricemoney不允许为空每餐总花费persons_numint不允许为空就餐人数 就餐计费细表(Eatdata2)名称类型约束条件说明idint无重复就餐计费细表标识,主键t_idint不允许为空每餐总表标识,和Eatdata1.totle_id关联c_idint不允许为空员工标识标识,和Clerk_table.clerk_id关联pricemoney不允许为空每人每餐花费 其中,就餐计费细表(Eatdata2)的记录就是把每餐总表(Eatdata1)的一条记录按就餐员工平摊拆开,是个不折不扣的冗余表。当然,也可以把每餐总表(Eatdata1)的部分字段合并到就餐计费细表(Eatdata2)中,这样每餐总表(Eatdata1)就成了冗余表,不过这样所设计出来的就餐计费细表重复数据更多,相比来说还是上面的方案好些。但是,就是就餐计费细表(Eatdata2)这个冗余表,在做每月每人餐费统计的时候,大大简化了编程的复杂度,只用类似这么一条查询语句即可统计出每人每月的寄餐次数和餐费总帐: SELECTclerk_nameASpersonname,COUNT(c_id)aseattimes,SUM(price)ASptpriceFROMEatdata2JOINClerk_tabsleON(c_id=clerk_id)JOINeatdata1ON(totleid=tid)WHEREeat_date>=CONVERT(datetime,'"&the_date&"')ANDeat_date 想象一下,如果不用这个冗余表,每次统计每人每月的餐费总帐时会多麻烦,程序效率也够呛。那么,到底什么时候可以增加一定的冗余数据呢?我认为有2个原则:
EXEC执行TSQL动态语句无法获得记录集的解决leenshan
THE END