在Delphi程序设计中,SQL编程是非常强大的一个方面。SQL指的是结构化查询语言。SQL的大部分实现都与该语言的某个特定定义相关。例如,许多数据库的SQL服务器支持ANSI-92 SQL定义。ANSI(美国国家标准局)由许多公司和个人组成,其既定兴趣是进行标准化。您所使用的SQL语言的语法依赖于所用的特定数据库。
这里您可能会问,为什么需要SQL,能否只用Delphi?答案是可能两个都需要。如果建立数据库应用程序,当然需要使用Delphi控件(与Delphi专业版和企业版一同发布),如TTable、TDatabase、或TClientDataSet来管理数据库中的数据,但确实有些操作使用SQL更为容易。对于使用SQL相对简单的情况,您可以使用TQuery等组件将SQL语句发送到数据库服务器。
例如,假定您有一个关于职员的数据库。进一步假定您非常高兴,要给所有的职员都加薪百分之十。您可以编写Delphi代码打开包含薪水的表,并逐个职员增加薪水。或者,您可以编写一个SQL语句来完成该工作。有些数据库服务器(服务器程序),像SQL Server、Oracle、或Interbase Server可以在较为健壮的服务器(硬件服务器)上运行,因此请求可以在硬件服务器上处理。最后结果是:更新操作可以在较为快速、健壮的服务器上运行,而无须用大量的数据阻塞网络。如果网络是内部网、Internet或外部网,两种处理方法的响应速度会有很大的差别。
这意味着,如果您开发数据库应用程序时不使用SQL,那么您和您的程序将处于非常不利的地位。不要担心。本章将示范SQL语言的一般形式,以及如何在Delphi中使用它。本章中的SQL语言与ANSI-92 SQL标准密切相关,可以在大多数SQL服务器上工作。本章中的一些较为高级的特征可能需要根据实际使用的数据库进行微小的改动。另外,本书的CD-ROM中包含了一个基本的SQL Builder工具,您可以使用SQL Builder为程序定义并测试SQL语句。
结构化查询语言包括很多语言,都使用SQL的名字。对于当前的ANSI标准,每个厂商都支持不同的兼容级别。例如,Oracle包括PL/SQL,它支持过程调用和参数传递,而Microsoft也提供了自己的版本,称为T-SQL。另外还有几种基本SQL的派生语言,但所有的语言在本质上都具有相当基本的语法、数目适当的关键字、以及对数据处理的一般性支持。
基本的SQL支持在数据库中选择、插入、更新和删除记录。用得不那么频繁,但同样重要的操作是创建与删除表。有些产品如SQL Server 2000、Access或Oracle提供了可视化的数据库建立工具,方便了数据库的管理。更好的选择是CASE工具,如DataArchitect和ERwin,这些工具提供与数据库之间的双向工程能力(双向工程是指创建数据库并通过检查数据库将数据库结构读回到CASE工具的能力。对于设计和建立数据库程序来说,CASE工具是必不可少的)。
本章示范了SQL语言的基本用法,其中包括最常用的一些命令,工具通常使用这些命令来完成工作。我们就从四个最基本的SQL命令开始。
数据库管理的最常见的任务包括数据的添加、删除和更新。如同26个字符的英文字母表一样令人迷惑,从这些支持基本任务的命令中可以演化出各种表达力非常强的语句。本节我们从最简单的例子开始,包括SELECT、INSERT、UPDATE和DELETE语句。如果您已经熟悉了基本的SQL语法,可以跳到下一节,其中示范了这些语句的一些高级用法。
当学习这些例子时,请记住,您需要利用工具对命令进行编辑并将其发送到数据库。可以使用与Delphi一同发布的SQL Explorer(或Database Explorer)、Database Desktop或本书CD-ROM上的SQL Builder示例程序。
SELECT语句用于以行为单位从一个或多个表获取数据。现在,我们把注意力集中到单表的select语句上。基本的select语句的规范形式如下。
SELECT fieldslist FROM tablename
语句以关键字SELECT开始。fieldslist可以是逗号分隔的字段名列表,或星号(*),后者意味着任意或所有。FROM子句表示了包含字段的表的名字。
这里引入了一些新的术语。我们将稍停一下来介绍这些术语。单个的表在逻辑上与由行和列组成的电子表格非常相似。在电子表格中行列的交称之为表元。在数据库用语中,术语“行”与电子表格中的行意义大致相同。电子表格中的术语列和表元合并为数据库中的字段。数据库中单个表的定义由其所有字段的定义组成,包括字段名、字段类型、字段大小等信息。当表中含有数据时,单个记录可称之为行。数据库由一个或多个表组成。
在任何时候,SELECT语句都可以用于获取数据库中一个或多个表的某些或全部行。字段列表由数据源表中的某些字段名组成。这里有几个SELECT语句的基本例子。
SELECT * FROM BIOLIFE // biolife.sql
SELECT WEIGHT, "SIZE" FROM ANIMALS.DBF // animals.sql
SELECT CustNo As Customer FROM ORDERS // customer.sql
SELECT O.CustNo As Customer FROM ORDERS O // customer2.sql
(忽略SQL语句末尾的Pascal风格的注释。这些只是用于方便在CD-ROM上查找相应的文件。)第一个语句读作“选择Biolife表中的所有字段”。该表实际上是Paradox表,即biolife.db文件,可以在前面提到过的示例数据库目录下找到。下一个语句只选择了两个字段:weight和size。而size是关键字。为避免这个问题,我们使用引号把size括起来,使得可以将其作为字段处理。还可以注意到animals.sql中的sql语句对表名使用了文件扩展名。在Paradox和dBase数据库中,表存储在单独的文件中。如果表只是整个数据库文件的一部分,则不能使用文件扩展名,例如Access中的.MDB文件。customer.sql示范了字段别名。假定您不需要返回实际的字段名,而只需要具有良好格式的字段名,那么可以使用As子句来创建字段的别名。第四个例子创建了ORDERS表的别名O。当在SQL语句中有多个表(这里并未演示)的时候,创建表的别名是很有用的。
可按照下列步骤,试一试下面的例子。
1.运行Database Desktop。
2.在Database Desktop中,选择SQL | Select Alias菜单项,指向DBDEMOS别名(如图19.1所示)。
图19.1 选择DBDEMOS别名,以指向示例文件
3.选择File | New | SQL File菜单项打开一个空白编辑窗口,然后选一个上面所示的SELECT语句键入。
4.选择SQL | Run SQL菜单项(或单击带有闪电符号的工具栏按钮)来运行查询。
如果一切工作正常,可以看到一个ANSWER.DB表(见图19.2),其中包含了SQL语句的结果集合。
这里新引入的概念是别名的思想。数据库可能位于其他的物理计算机上、跨越网络、也可能像示例表那样位于本地。别名是由Datasource Administrator或BDE Administrator管理的(创建别名的详细信息,可以参考第13章中关于打开数据库连接的有关章节)。
SELECT语句可能相当复杂,可以对结果集进行精确的控制。在本章稍后关于高级SQL编程的部分中,我们将继续讨论SELECT语句。现在我们开始学习DELETE语句。
图19.2 由customer2.sql语句所生成的结果集合。可以注意到
字段别名Customer位于ANSWER.DB表中列的顶部
DELETE语句非常简单。DELETE FROM tablename将删除tablename表中所有的行。更常见的情况需要删除数据的特定行。对这种情况,添加WHERE子句即可。这里有几个DELETE语句。在运行这些语句之前最好对表进行一下备份,但如果偶然删除了表,总可以从Delphi光盘上恢复。
DELETE FROM BIOLIFE // del_biolife.sql
DELETE FROM ANIMALS.DBF // del_animals.sql
.db和inserted.db表,可用于恢复所进行的删除和插入。请记住,每次运行INSERT或DELETE语句时,或者退出Database Desktop,临时的inserted.db和deleted.db总会被覆盖。
前面提到过,SQL语言的基本语法是非常强大而直接的。还有什么会比删除所有的行更为强大呢?但上面的SQL语句并不删除表,它只删除所有的行。
INSERT语句更为复杂。它用于向表添加行。INSERT语句可用于向所有的字段添加值,也可以只对某些字段添加值,这依赖于具体使用的INSERT语句。由于这个原因,INSERT语句需要表名、字段列表以及与每个字段相匹配的值。
INSERT INTO "ANIMALS.DBF" VALUES("Turtle", 7, 5, "Wetlands", NULL)
INSERT INTO ":DBDEMOS:animals.dbf"
(NAME, ":DBDEMOS:animals.dbf"."SIZE", WEIGHT, AREA)
VALUES('Turtle', 7.0, 5.0, 'Swamps')
上面给出了两个例子。第一个例子将值插入到animals.dbf表中。由于VALUES子句列出了所有字段的值,所以忽略了实际的字段名。第二个例子示范了关键字SIZE的用法,它既是关键字,也是animals表的一个字段名。“:DBDEMOS:anmimals.dbf”是该表的完整路径,其中包括了别名。将实际的路径作为所涉及的关键字的前缀,即可解决关键字用作字段名的问题。在第二个例子中,并未使用所有的字段(BMP没有用到),这样就需要在第一个括弧中给出所需的字段列表。
UPDATE语句用于修改现存的记录。UPDATE语句如果不使用WHERE子句,将更新所有的记录。大多数情况下都需要限制更新记录的条件,但所有的更新都从基本的语句开始。SQL语言中UPDATE的语法如下:
UPDATE tablename SET field1 = value1 [, field2 = value2,
fieldn = valuen]
该语句以关键字UPDATE开始,后接表名。SET子句后接逗号分隔的列表,包括所要更新的字段及其新值。下面的例子示范了基本的UPDATE语句:
UPDATE ANIMALS SET AREA = "WETLANDS" //upd_animals.sql
上面的语句修改了所有的记录,将每行AREA字段的值都更新为“WETLANDS”。
可以在UPDATE语句中进行计算。下面的UPDATE语句假定animals.dbf表中所有的动物都在圣诞那一天吃得太多,因而体重增长了50%。
UPDATE ANIMALS.DBF SET WEIGHT = WEIGHT * 1.5 //
upd_animals2.sql
UPDATE语句的威力在于,可以对特定的行进行更新,而且还可以根据外部原则定义条件更新。关于更多的UPDATE语句的例子,可以参考后面高级SQL编程章节中有关定义WHERE子句的部分。
在Delphi、SQL和数据库服务器之间,需要一些方法和技巧将三者结合起来。有几种组件可以连接到数据库(细节请阅读第13章),但TQuery组件专门用于使用SQL语言连接到数据库服务器。实际上,TQuery组件可用作客户/服务器、两层、以及使用MIDAS的分布式程序设计。
要向数据库服务器发送SQL语句,必须设置TQuery组件的几个特性。TQuery.Database特性设置为表示物理数据库位置的BDE或ODBC别名。DataSource特性可动态使用,向查询提供参数化值。Params特性用于定义查询的参数,SQL特性则包含了SQL语句的文本。Database特性不用去管,DatabaseName特性的编辑器将自动填写该特性。下面我们来看一下DataSource、Params和SQL特性如何进行工作。
SQL特性
当向SQL特性添加文本时,该文本将发送到SQL服务器。由于采用了TQuery组件,在后台将使用BDE数据库引擎作为中介。
当定义SQL语句时,如果向SQL语句添加参数,那么对语句进行语法分析时将创建TParam对象。SQL语句中的参数以冒号(:)为前缀。下面的例子示范了SQL语言中的参数。
UPDATE ANIMALS.DBF SET WEIGHT = WEIGHT * :WEIGHT
//upd_animals3.sql
如果把上述SQL语句添加到TQuery组件的SQL特性,那么在TQuery组件中将创建参数WEIGHT(见图19.3)。在SQL语句中定义参数时,将创建TParam对象并添加到TParams特性。在设计时,您可以指定参数的DataType和特性的ParamType。ParamType表示该参数将如何使用(使用ParamType的例子请参见第13章)。
图19.3 前台为参数编辑器,后台为Object Inspector,演示了将
SQL文本存储到TQuery.SQL时创建:WEIGHT参数的情景
DataSource特性
TQuery.DataSource用于指定数据源,该数据源对应的数据集将为SQL语句中的参数提供值。考虑下面的SQL代码:
SELECT * FROM Customer C WHERE C.State = :State
对包含上述SQL查询的TQuery组件的DataSource特性指定数据源之后,将自动填写语句中的:State参数。惟一的额外条件是,第二个数据集中的字段名要与参数名相同。
SQL代码可能会相当复杂。以Oracle和PL/SQL为例,可能出现可以生成SQL代码的SQL代码。本节将讨论一些较为高级的SQL编程概念,使得您可以创建实际的SQL解决方案。
WHERE子句可用作已示范过的SQL语句的附件。该子句的目的是对SQL语句的结果集进行限制,以得到更好的结果。最常见的情况是,WHERE子句定义了一个字段名与值对的列表,由布尔操作符分隔。下面举的例子示范了WHERE子句与四种SQL语句联用时的基本语法。
SELECT和WHERE
SELECT * FROM biolife.db WHERE Category="Ray"
// where_biolife.sql
图19.4 由SQL Builder示例程序创建的SQL SELECT语句。为简
明起见,使用星号(*)替换了程序所返回的字段名列表
上述SELECT语句(使用SQL Builder示例程序创建,相应的文件在本书光盘上)示范了一个使用相等测试的SELECT语句。当激活该查询时,会返回Category字段值为“Ray”的那些记录。
UPDATE和WHERE
本节将示范使用大于(>)操作符的UPDATE语句。假定您的公司增长速度非常快,因此需要调整统计程序中的雇员数目。下面的UPDATE语句的例子对所有大于50的雇员序号都乘以10。
UPDATE employee.db SET EmpNo=EmpNo * 10 WHERE EmpNo > 50
上述SQL代码保存在本书光盘的where_upd_employee.sql文件中。使用SQL Builder示例程序,执行下列步骤即可创建该SQL代码:
1.运行SQLBuilder.exe,该程序位于书后的光盘上。
2.参照图19.4,将Database Name设置为DBDEMOS。
3.从Table Name组合框中选择employee.db表。
4.在Field Names列表中将EmpNo字段的新值设置为EmpNo*10。
5.从SQL Type单选按钮组中选择UPDATE类型。
6.最后,在Design属性页底部的SQL文本编辑器中输入WHERE子句“WHERE EmpNo >
7.单击Tools | Run SQL菜单项运行该语句。
UPDATE语句在Data属性页上不会显示输出。另外,您也可以在SQL Builder示例程序中选择Database Name后,手工输入SQL语句来试一下UPDATE。
DELETE和WHERE
DELETE语句很强大,加上WHERE子句后就更加有用。可以在WHERE子句中使用另外一些操作符来扩展WHERE子句所能过滤的行。下面的例子示范了DELETE语句和WHERE子句,其中使用了LIKE操作符。
DELETE FROM parts.db WHERE Description LIKE "Dive%"
上述语句保存在where_del_parts.sql文件中,使用了LIKE操作符来过滤以Dive开头的DESCRIPTION字段。Paradox表将%(百分号)与LIKE操作符一同使用,其他的SQL服务器则用*(星号)。
利用WHERE子句和LIKE、<、>等操作符,可以为SQL语句创建很多过滤条件。例如,SQL Server等服务器支持CONTAINS和BETWEEN等操作符。下面的语句示范了一个使用BETWEEN操作符的SELECT语句,该操作符在UPDATE语句中同样容易使用。
SELECT * FROM PLAYER_STATISTICS WHERE GOALS BETWEEN 2 and 5
这是件可以节省时间的事情。每当SQL Builder生成了不合适的SQL语句就会发生EDBEngineError异常。这并不是源代码的错误,而是一个SQL错误,需要通过输入正确的SQL语句来解决。选择性地忽略一些异常,可以使得创建SQL例子更加简单。
上述例子来自于第13章创建的PLAYER_STATISTICS表。该语句保存在where_betw_renegades.sql文件中,将返回所有目标数在2至5之间(包含2和5)的玩家。对于特定的数据库服务器来说,您需要看一下参考手册来确定其ANSI_SQL兼容性以及实际支持的SQL语句。
INSERT和WHERE
INSERT语句中的WHERE子句的作用是同样的。在INSERT INTO语句中,WHERE子句可用于附加一个嵌套的SELECT语句,从而有效地从另一个数据集复制多行到当前表。例子可以参考嵌套查询部分。
使用WHERE的隐式表连接
隐式连接语句使用了WHERE子句,将多个表通过两个相同字段关联起来。这就是关系模型最强大的功能之一:使用不同的表来避免数据的复制,但仍然可以使用关键字段表示相互关系,从而给出数据的统一视图。下面的SELECT语句示范了一些使用WHERE进行连接的技术。
SELECT C.CustNo, C.Company, O.* FROM Customer C, Orders O
WHERE C.CustNo = O.CustNo
上面列出了两个表,并分别赋予别名。Customer表的别名为C,Orders表的别名为O。语句只返回Customer表的Company和CustNo字段,而使用O.*返回了Orders表的所有字段。两个表是通过C.CustNo = O.CustNo语句进行连接的。
该语句可以读作“在顾客记录与订单记录的顾客序号相等的情况下,返回所有的行,行的字段包括Customer表的CustNo和Company,以及Orders表的所有字段”。这是很符合逻辑的。
这种类型的连接有一些主要的缺点:首先,既不会返回孤立的订单,也不会返回没有订单的顾客。孤立的订单是指订单所关联的顾客已经被删除了。考虑顾客被删除的情况,程序可能并未同时删除一些相关的信息,如订单。您的数据库中可能多出一些垃圾。如果希望给出顾客列表,上面的WHERE语句可能就有些问题。连接关系表的更灵活的方式是使用JOIN子句。
JOIN子句是WHERE子句的近亲,但更为强大。当编写的SQL语句包含多个表,而WHERE子句需要对来自不同表的两个以上的字段进行相等测试时,您实际上进行了隐式连接。使用JOIN子句可以产生同样的结果,但效果更好。
SELECT DISTINCT C.CustNo, C.Company, O.*
FROM CUSTOMER C
LEFT JOIN ORDERS O on (C.CustNo = O.CustNo)
与上一节的隐式WHERE连接相比,上述的JOIN语句语义更为精确。它可以理解为“返回Customer中所有惟一的行以及Orders表中所有与之相匹配的行。”从技术上讲,LEFT JOIN(如果支持的话)表示,应返回FROM子句之后第一个表中的所有行,而无论是否存在右侧表中的行与之相匹配;本例中右侧的表是Orders。
RIGHT JOIN的效果刚好相反。在上述查询中进行右连接将返回所有的订单,不管它们是否与顾客相匹配,而只会返回与订单相匹配的顾客。
自连接
进行连接时一个有用的机制是将表连接到自身,可称之为自连接。自连接可在同一SQL语句中对同一表中的数据进行比较。假定您需要确定所有位于同一个州的顾客(使用CUSTOMER和ORDERS表进行示范)。可以使用自连接来比较是否顾客序号字段不等而州字段相等。相应的SQL语句如下。
SELECT C.COMPANY C.STATE, "=", C1.STATE, C1.COMPANY
FROM CUSTOMER C
JOIN CUSTOMER C1 on (C1.CustNo <> C.CustNo)
AND C1.STATE = C.STATE
ORDER BY COMPANY
该语句使用别名C1和C从CUSTOMER表中选择COMPANY和STATE字段。表是通过不匹配的CUSTNO字段和匹配的STATE字段进行连接的。结果列出了所有位于同一个州的顾客。在JOIN前添加关键字LEFT,则每个州只返回一个顾客。这里所示范的技术在确定顾客数目最多的州时非常有用。
交叉连接
要谨防交叉连接,交叉连接就是会产生表的笛卡尔积的连接。在笛卡尔积中,每一行都与另一个表中的每一行进行连接。如果使用自连接进行演示,当查询过于含糊时可能会产生交叉连接。
SELECT C.COMPANY, C.STATE, C1.COMPANY, C1.STATE
FROM CUSTOMER C
LEFT JOIN CUSTOMER C1 on (C1.CustNo <> C.CustNo)
上例对顾客序号不等的所有顾客记录进行连接。在示例数据库customer.db中,有56个记录。该查询的结果有3080个。这并不是我们所需要的结果。
数据可通过ORDER BY子句进行排序。可以选择字段名,后接所需的排序方式。默认的排序方式是升序的;如果不给出排序方式,结果将按升序排列。下面的代码示范了ORDER BY子句。
SELECT "parts.db".PartNo, "parts.db".VendorNo,
"parts.db".Description, "parts.db".OnHand,
"parts.db".OnOrder, "parts.db".Cost, "parts.db".ListPrice
FROM parts.db
ORDER BY Description, Cost Desc
上述冗长的SELECT语句返回的结果数据集是库存清单,Description字段默认按照升序排列,Cost字段按降序排序。在名字相同的情况下,较为昂贵的零件列在前面。
GROUP BY语句与SELECT语句联用。可以将WHERE和ORDER BY子句与GROUP BY子句同时使用。GROUP BY子句可用于进行摘要。例如,如果要对顾客的购物总量进行统计,编写下列语句即可,其中包括了顾客序号和名字。
SELECT C.CUSTNO, C.COMPANY, Sum(O.AMOUNTPAID) As Total
FROM CUSTOMER C
LEFT JOIN ORDERS O ON (C.CUSTNO = O.CUSTNO)
GROUP BY CUSTNO, COMPANY
该查询选择了CUSTNO和COMPANY字段,并对AMOUNTPAID字段调用Sum函数,而且将AMOUNTPAID字段的总和命名为Total。对表进行连接,只返回进行了购物的顾客。GROUP BY子句对记录进行聚合操作,在求和操作中不涉及的字段要在GROUP BY子句中列出,使用逗号分隔。
假定要列出购物金额最多的顾客,那么在查询的结尾添加ORDER BY Total Desc即可。修改后的查询如下。
SELECT C.CUSTNO, C.COMPANY, Sum(O.AmountPaid) As Total
FROM CUSTOMER C
LEFT JOIN ORDERS O ON (C.CUSTNO = O.CUSTNO)
GROUP BY CUSTNO, COMPANY
ORDER BY Total Desc