C-5-数据库入门指南-全-
C#5 数据库入门指南(全)
一、获取和了解您的工具
本书旨在帮助您学习如何使用 C# 2012 编程语言和 SQL Server 2012 数据库服务器应用构建面向数据库的应用。本书使用的开发工具是 Microsoft Visual Studio 2012 和 Microsoft SQL Server 2012(代号 Denali) Express edition,两者都与微软合作。NET 框架 4.5。
注意对于本书来说,我使用的是可以从[
msdn.microsoft.com](http://msdn.microsoft.com)
下载的 Visual Studio 和 SQL Server 的免费版本。如果您正在使用这些工具的更全功能版本,您仍然可以遵循本书中的示例。
Visual Studio 2012 面向多个。NET Framework 版本,允许您为早期版本的。NET 框架,即。NET 2.0,。NET 3.0,。NET 3.5,还有。除了对. NET 4.0 的本地和默认支持之外。净 4.5。Visual Studio 集成开发环境(IDE)有助于开发人员提高工作效率,它提供了各种类型的应用模板和工具来执行大多数应用开发活动。
SQL Server 是目前最先进的关系数据库管理系统(RDBMSs)之一。SQL Server 继续提供并支持。NET 公共语言运行时(CLR)集成到 SQL Server 数据库引擎中,使得使用以. NET 语言(如 Visual C#)编写的托管代码实现数据库对象成为可能。除此之外,与以前的版本一样,SQL Server 附带了多种服务,如分析服务、数据转换服务、报告服务、通知服务、Service Broker、数据库邮件、PowerShell 支持等等。SQL Server 为数据库开发人员和数据库管理员(DBA)提供了一个通用的环境,即 SQL Server Management Studio (SSMS)。
SQL Server 2012 Express edition 是 SQL Server 2012 的关系数据库子集,提供 SQL Server 2012 Express 的几乎所有在线事务处理(OLTP)功能,支持最大 10GB 的数据库(每个 SQL Server 2012 Express 实例最多 32,767 个数据库),并且可以处理数百个并发用户。
现在您已经对这些开发工具有了一些了解,您将学习如何获得和安装它们,并且您将学习使用本书中的示例所需的示例数据库。本章将涵盖以下内容:
- 获取 Visual Studio 2012
- 安装 SQL Server 2012 Express
- Troubleshooting SQL Server services
- Install AdventureWorks sample database
获取 Visual Studio 2012
本书要求在您的计算机上安装 Visual Studio 2012。在撰写本文时,Visual Studio 的可用版本是 Visual Studio 2012 Developer Preview。要查找有关 Visual Studio 2012 的下载信息,请访问[
msdn.microsoft.com/vstudio](http://msdn.microsoft.com/vstudio)
。
您也可以从 MSDN 订阅网站[
msdn.microsoft.com](http://msdn.microsoft.com)
直接下载安装程序 ISO 镜像文件。通过单击开发人员中心中的 Visual Studio 链接来访问可下载的安装文件;然后解压下载的文件,运行Setup.exe
。
如果您有 Visual Studio 2012 的安装 DVD 或 CD,只需将 DVD 或 CD1 放入计算机的磁盘驱动器,并按照说明完成安装,确保 c 盘上有足够的磁盘空间。
Visual Studio 2012 有各种软件组件,所以您需要在安装 Visual Studio 时决定是否要安装它们。本书中的示例只需要 C# 语言组件,但您可能希望安装其他语言,如 VB。NET、VC++和 F# 等等,供你以后编程需要。
安装 SQL Server 2012 Express
为了完成本书中的示例,要安装 SQL Server 2012 Express,请按照下列步骤操作:
-
Go to
[www.microsoft.com/betaexperience/pd/SQLEXPCTAV2/enus/default.aspx](http://www.microsoft.com/betaexperience/pd/SQLEXPCTAV2/enus/default.aspx)
. According to your CPU architecture, decide which version you need, 32-bit or 64-bit, and select Express with Tools from the drop-down list of products. Then click Download. -
The download manager will start running. If the download manager is not already installed on your computer, it will prompt you to install it. Click install.
-
Depending on whether you choose the 32-bit or 64-bit version, you will be prompted to save the file
SQLEXPRWT_x86_ENU.exe
orSQLEXPRWT_x64_ENU.exe
, which is the installation utility of SQL Server 2012 Express. -
Save this file to a location on the host computer (such as the desktop). When the file download is complete, click Close.
-
Run the file to start the installation, and follow the steps to install.
-
When the Completing SQL Server Management Setup window appears, click the Finish button.
-
安装成功后,您会在开始所有程序 Microsoft SQL Server 2012 菜单中看到所有 SQL Server 组件已安装。确保您的 SQL Server 服务正在运行是很重要的,因此要验证这一点,您需要调用服务列表。转到开始。运行。 Services.msc 或控制面板管理工具服务。将加载一个服务窗口;向下滚动直到看到列出的 SQL Server 服务,如图图 1-1 所示。
图 1-1。服务窗口显示数据库服务正在运行
-
Pay attention to the name in brackets (your SQL instance name). This may vary from machine to machine; Therefore, it is very important to know the name of the SQL instance before connecting to it to continue using SQL Server. If you have multiple versions of SQL Server, multiple SQL Server services will be listed, and you need to know the SQL Server 2012 instance name to use.
-
If the SQL Server service is not running, you can start it manually by right-clicking and selecting Start. Then, your service should be listed as "Started" under "Status". This service must be running before any database-related operations can be performed.
同样,您需要记住在安装过程中使用的 SQL Server 实例名,以便能够顺利地连接和构建数据库应用。
因为 SQL Server 2012 没有附带示例数据库,所以您需要单独安装和配置示例数据库。下一节将介绍如何在 SQL Server Management Studio 中安装和配置 AdventureWorks 数据库。
安装并附加 AdventureWorks 示例数据库
出于数据库查询的目的,并且为了用 C# 构建数据库应用,您需要一个数据库。出于这些目的,本书将使用 AdventureWorks for SQL Server 2012 版本。
安装 AdventureWorks 数据库
要安装数据库,请按照下列步骤操作:
- 转到
[
msftdbprodsamples.codeplex.com/releases/view/4004](http://msftdbprodsamples.codeplex.com/releases/view/4004)
,点击链接 adventureworksdb。中规模集成电路(medium-scale integration 的缩写) - Click I agree to the license agreement; You will be prompted to run or save the
AdventureWorksDB.msi
file to your system. - Change the location where files are saved; You can save it anywhere in the computer system, but it is recommended that you save it with other database files under the instance of SQL Server, which will be located at
C:\Program Files\Microsoft SQL Server\MSSQL11.<your SQL Server 2012 instance name>\MSSQL\DATA
. You can verify the folder name of your SQL instance by looking at the name on your machine; As shown in Figure , Figure 1-1 , the name you see in system brackets will be the name of the folder where you might want to save the database files. - If you can't find the folder location mapped to the SQL Server instance, you can choose to save the file anywhere on the system.
- After selecting the file location, the setup wizard will take the
AdventureWorks_Data.mdf
andAdventureWorks_Log.ldf
files to the location you specified. After successfully installing the files, click Finish to close the wizard.
附加 AdventureWorks 示例数据库
Attach 是用于将.mdf
文件关联到数据库服务器的过程,这样您就可以开始处理与表相关联的数据库对象和数据。
您需要访问 SQL Server Management Studio 来附加 AdventureWorks2008 数据库。为此,请按照下列步骤操作:
-
Make sure you know the name of the instance of SQL Server running SQL Server; In my example, it is SQL2012, as you saw in Figure 1-1 earlier. You can check your instance name as described earlier.
-
从您安装的 SQL Server 2012 应用中打开 SQL Server Management Studio,在“连接到服务器”对话框中,输入 localhost\ <您的服务器名称> 作为服务器名称(参见图 1-2 )。在某些情况下,您可能会看到 localhost 被一个点(.)或者真机名。(您可以从计算机属性中查看机器名称。)
图 1-2。连接到服务器对话框
-
As shown in Figure Figure 1-2 , set the following options:
- Set the server type as the database engine.
- Set the server name to localhost \ . For me, as shown in Figure 1-1 , the name is SQL2012, then the server name is localhost\SQL2012. Also note that the server name is case insensitive; You can type any case (lowercase or uppercase).
- Set the authentication to Windows authentication. This is the default authentication type used when SQL Server is installed. This indicates that the login user name of the computer will be reserved to connect to SQL Server.
- Set the user name to the user credentials required to connect to SQL Server. Many SQL Server databases have Windows authentication installed, so you will see that the login user name of the same computer is added here by default. In many cases, it may be an administrator or a unique name, as you can see in Figure 1-2 , namely Redmond\v-vidyag.
-
点击连接按钮,你将被带到 SQL Server Management Studio,它看起来会像图 1-3 。
图 1-3。 SQL Server Management Studio 成功连接到数据库数据库引擎后
-
If an error occurs after clicking the "Connect" button in the "Connect to Server" dialog box, instead of the window shown in Figure , Figure 1-3 , it will look like , Figure 1-4 .
图 1-4。连接到服务器时出错
您收到此错误可能有以下几个原因:
- The SQL Server service instance name you provided (in this case, SQL2012) is not running.
- The machine name you used to specify the SQL instance is incorrect. The error in figure 1-3 shows that I used "local \ SQ2012" as "machine name \ instance name", which is incorrect unless the machine name is local (in this case, the SQL Server instance name is incorrect).
To fix the error, specify the correct parameters, check whether the SQL Server service is started, or pass the correct computer name.
成功加载 SSMS 后,下一步是附加您已经下载的示例数据库 AdventureWorks2008R2。为此,右击数据库节点并选择附加,如图图 1-5 所示。
图 1-5。准备连接数据库
Click Additional Options, and the Additional Database dialog box will appear, as shown in figure and figure 1-6 . T33 T36 】 Figure 1-6. Attach database dialog box
Click [Add], and a window will open to provide
.mdf
files for the database, as shown in figure and figure 1-7 . T45 T48 】 Figure 1-7. Locate database file dialog boxAs you can see, I was taken to the
DATA
folder of the SQL2012 instance of SQL Server 2012, because this is my connection. You can also see theAdventureWorks2008_Data.mdf
file listed underDATA
, because I saved it in this location.选择
AdventureWorks2008_Data.mdf
,点击确定;你会看到一个类似于图 1-8 所示的屏幕。图 1-8。选择
.mdf
数据库文件附加数据库可以看到,文件数据和日志都被选中;单击确定。如果要求您确认是否添加了全文目录,请单击“确定”。这将让你继续数据库连接过程,一个显示“正在执行”的窗口将会打开,如图 1-9 左下方所示。
图 1-9。正在附加数据库
Once this execution process is completed, you will be able to see AdventureWorks under the Databases node in SQL Server Management Studio, as shown in Figure 1-10. T83 T86 】 Figure 1-10 Listed in the database.
总结
在本章中,您通过安装 Visual Studio 2012、SQL Server 2012 和示例 AdventureWorks 数据库来准备开发环境。您还使用 SQL Server Management Studio 在 SQL Server 2012 中附加 AdventureWorks 数据库。
现在您已经有了工具,是时候熟悉它们了。
二、理解关系数据库
既然您已经了解了您将在本书中使用的工具,我将后退一步,简要介绍数据库领域的重要基础概念。
在本章中,我将介绍以下内容:
- What is a database?
- Choose between a spreadsheet and a database
- Why use the database?
- Benefits of using relational database management system
- Compare desktop and server RDBMS systems
- life cycle of database
- Mapping cardinality
- Understanding keywords
- Understanding data integrity
- Standardized concept
- Disadvantages of standardization
什么是数据库?
简单来说,数据库是结构化信息的集合。数据库是专门为管理大量信息而设计的,它们以一种有组织、有结构的方式存储数据,使用户在需要时可以方便地管理和检索数据。
数据库管理系统(DBMS)是一个软件程序,使用户能够创建和维护数据库。DBMS 还允许用户为单个数据库编写查询,以执行所需的操作,如检索数据、修改数据、删除数据等。
DBMSs 支持表(又名关系或实体)在行(又名记录或元组)和列(又名字段或属性)中存储数据,类似于数据在电子表格应用中的显示方式。
关系数据库管理系统(RDBMS)是一种以相关表的形式存储信息的 DBMS。RDBMS 基于关系模型。
在电子表格和数据库之间选择
如果数据库很像电子表格,为什么人们还在使用数据库应用?数据库设计用于以比电子表格应用更容易和更高效的方式执行以下操作:
- Retrieve all records that meet certain criteria.
- Update or modify a complete set of records at a time.
- Extract values from records distributed in multiple tables.
为什么要使用数据库?
以下是您使用数据库的一些原因:
- Compactness : The database helps you maintain a large amount of data, thus completely replacing the voluminous paper documents.
- Speed : Searching for a specific piece of data or information in a database is much faster than classifying it in a sheet piling.
- Less drudgery : Maintaining files manually is a boring job; Using the database completely eliminates this maintenance.
- Currency : The database system can be easily updated, so it can provide accurate information at any time as required.
使用关系数据库管理系统的好处
RDBMSs 通过控制以下各项提供了各种好处:
- Redundancy [ :RDBMS prevents you from having duplicate copies of the same data and occupying disk space unnecessarily.
- Inconsistent : The redundant data of each group may no longer be consistent with the same data of other groups. When RDBMS removes redundancy, inconsistency will not occur.
- Data integrity : The data values stored in the database must meet some kind of consistency constraint. (I will discuss this benefit in more detail in the section "Understanding Data Integrity" later in this chapter. )
- Data atomicity : When a fault occurs, the data is restored to the consistent state before the fault. For example, capital transfer activities must be atomic. (I will introduce the activity and atomicity of capital transfer in more detail in Chapter 6. )
- Access Exception [ :RDBMS prevents multiple users from updating the same data at the same time; This concurrent update may lead to data inconsistency.
- Data security : Not every user of the database system should be able to access all data. Security refers to protecting data from any unauthorized access.
- Transaction [: ] Transaction is a series of database operations, representing a logical work unit. In RDBMSs, the transaction either commits all the changes or rolls back all the executed operations until the failure occurs.
- Recovery : The recovery feature ensures that after the transaction fails, the data is reorganized into a consistent state.
- Storage management :RDBMS provides a mechanism for data storage management. The internal schema defines how data should be stored.
比较桌面和服务器 RDBMS 系统
在当今的行业中,您将主要使用两种类型的数据库:桌面数据库和服务器数据库。在这里,我会给你一个简单的介绍。
桌面数据库
桌面数据库被设计为服务于有限数量的用户,并且运行在桌面 PC 上,它们在任何需要数据库的地方提供了一个不太昂贵的解决方案。你有可能使用过桌面数据库程序;Microsoft SQL Server Express、Microsoft Access、Microsoft FoxPro、FileMaker Pro、Paradox 和 Lotus 都是桌面数据库解决方案。
桌面数据库在以下方面不同于服务器数据库:
- Not too expensive : Most desktop solutions cost only a few hundred dollars. In fact, if you have a licensed version of Microsoft Office Professional, you are already the licensed owner of Microsoft Access, which is one of the most commonly used and widely used desktop database programs.
- User-friendly : Desktop databases are quite user-friendly and easy to operate, because they do not need complex SQL queries to perform database operations (although some desktop databases also support SQL syntax if you want to write code). The desktop usually provides an easy-to-use graphical user interface.
服务器数据库
服务器数据库是专为同时为多个用户服务而设计的,它提供的功能允许您通过同时为多个用户请求服务来非常高效地管理大量数据。众所周知的服务器数据库示例包括 Microsoft SQL Server、Oracle、Sybase 和 DB2。为了在本书中构建数据库应用,我们将使用 SQL Server 2012 作为数据库应用。一旦你学会了如何用某个数据库构建一个应用,用其他数据库来构建你的应用也就不难了。
以下是服务器数据库区别于桌面数据库的一些其他特征:
- Flexibility : The server database is designed very flexibly, supports multiple platforms, responds to requests from multiple database users, and performs any database management tasks at the best speed.
- Availability : The server database is enterprise-oriented, so it needs to be available around the clock. In order to be always available, the server database comes with some high-availability functions, such as mirroring and log shipping.
- Performance : Server databases usually have huge hardware support, so the servers running these databases have a lot of RAM and multiple CPUs. This is why the server database supports rich infrastructure and provides the best performance.
- Scalability : This attribute allows the server database to expand its ability to process and store records, even though it has grown very fast.
数据库生命周期
数据库生命周期定义了从概念到实现的完整过程。这个周期的开发和实施过程可以分为几个小阶段;只有完成每个阶段后,你才能进入下一个阶段。
在开始开发任何系统之前,您需要有一个强大的生命周期模型来遵循。模型必须以正确的顺序定义所有的阶段,这将帮助开发团队构建问题更少、功能更全的系统。
数据库生命周期由以下阶段组成,从设计数据库全局模式的基本步骤到数据库的实现和维护:
Requirement analysis : Before starting the design and implementation, it is necessary to determine the requirements. Demand can be collected by interviewing producers and users of data; This process helps to create formal requirements specifications.
Logic design : After requirements are collected, conceptual data modeling techniques (such as entity-relationship (er) diagram) are needed to define data and relationships. This figure shows how one object will be connected to another object, and through what relationship (one-to-one or one-to-many). Relations will be explained later in this chapter.
物理设计:一旦逻辑设计就绪,下一步就是为数据库生成物理结构。物理设计阶段包括创建表和选择索引。对索引的介绍超出了本书的范围,但是索引基本上就像一本书的索引,它允许您根据您选择的主题跳转到特定的页面,并帮助您避免为了到达感兴趣的页面而翻动书的所有页面。数据库索引做类似的事情;它们管理和维护插入表中的行的顺序,这有助于 SQL 查询根据为索引列提供的值快速提取数据。
数据库实现:一旦设计完成,就可以通过使用关系型数据库管理系统的数据定义语言(DDL)实现正式模式来创建数据库 DDL。由在创建、修改和删除数据库或数据库对象中起关键作用的语句组成. . .
CREATE
、ALTER
和DROP
是数据定义语言的主要例子Data modification : A data modification language (DML) can be used to query and update databases, as well as to establish indexes, reference integrity and other constraints. DML consists of statements that play a key role in inserting, updating and deleting data in database tables.
INSERT
,UPDATE
andDELETE
are typical examples of DDL.Database monitoring : When the database starts running, whether the monitoring indication meets the performance requirements; If not, it should be modified to improve the database performance. Therefore, the database life cycle continues to be monitored, redesigned and modified.
映射基数
表是关系数据库的基本组成部分。事实上,数据和关系都只是作为数据存储在表中。表格由行和列组成。每一列代表一条信息。
映射基数,或基数比,表示另一个实体可以通过关系集关联的实体的数量。基数是指包含在数据库表的特定列中的数据值的唯一性。术语关系数据库指的是不同的表经常包含相关的数据。例如,一个公司的销售代表可能接受许多客户下的订单。订购的产品可能来自不同的供应商,并且每个供应商有可能供应一种以上的产品。所有这些关系几乎存在于每个数据库中,可以分类如下:
一对一(1:1) :对于表 A 中的每一行,在表 B 中最多只有一个相关行,反之亦然。这种关系通常用于按使用频率分隔数据,以优化数据的物理组织。例如,一个部门只能有一个部门负责人。
一对多(1:M) :对于表 A 中的每一行,表 B 中可以有零个或多个相关行;但是对于表 B 中的每一行,表 a 中最多有一行,这是最常见的关系。[图 2-1 显示了 Northwind 中表的一对多关系的例子。注意 Customers 表有一个 CustomerID 字段作为主键(由左边的钥匙符号表示),它与 Orders 表的 CustomerID 字段有关系;CustomerID 被视为 Orders 表中的外键。Customers 表和 Orders 表之间显示的链接表示一对多关系,因为许多订单可以属于一个客户。在这里,Customers 被称为父表,Orders 是关系中的子表。
图 2-1。一对多关系
多对多(M:M) :对于表 A 中的每一行,表 B 中有零个或多个相关行,反之亦然。多对多关系不是那么容易实现的,它们需要一种特殊的技术来实现。这种关系实际上是以一对多的格式实现的,所以它需要引入第三个表(通常称为连接表),作为相关表之间的路径。
这是一种很常见的关系。图 2-2 展示了一个来自 Northwind 的例子:一个订单可以有很多产品,一个产品可以属于很多订单。Order Details 表不仅表示 M:M 关系,还包含关于每个特定订单-产品组合的数据。
图 2-2。多对多关系
注虽然表之间的关系极其重要,但术语关系数据库与它们毫无关系。关系数据库(在不同程度上)基于 IBM 的 Edgar F. Codd 博士在 20 世纪 70 年代发明的数据关系模型。Codd 的模型基于一个关系的数学(集合论)概念。关系是元组的集合,可以用一组定义明确、行为良好的数学运算来操作——实际上是两组:关系代数和关系演算。使用关系数据库不需要知道或理解数学,但是如果你听到有人说数据库是关系型的,因为它“将数据联系起来”,你就会知道说这话的人并不了解关系型数据库。
了解钥匙
钥匙,整把钥匙,除了钥匙什么也没有,所以请帮我编码。
关系由表中的数据表示。要在两个表之间建立关系,需要在一个表中有数据,以便能够在另一个表中找到相关的行。这就是键的用武之地,RDBMS 主要处理两种类型的键,如前所述:主键和外键。
关键字是用于标识行的关系的一列或多列。
主键
主键是一个属性(列)或属性(列)的组合,其值唯一地标识实体中的记录。
在为实体选择主键之前,属性必须具有以下属性:
- Each record of the entity must have a non-null value.
- The value of each record entered in the entity must be unique.
- During the life cycle of each entity instance, these values cannot be changed or empty.
- An entity can only define one primary key.
除了有助于唯一标识记录之外,主键还有助于搜索记录,因为当您将主键分配给属性时,索引会自动生成。
一个实体将有多个属性可以作为主键。任何可能成为主键的键或最小键集被称为候选键。确定候选关键字后,为每个实体选择一个且仅一个主键。
有时需要不止一个属性来唯一标识一个实体。由多个属性组成的主键被称为组合键。一个实体中只能有一个主键,但是一个组合键可以有多个属性(换句话说,一个主键只会被定义一次,但是最多可以有 16 个属性)。主键代表父实体。主键通常用IDENTITY
属性定义,当您向表中插入一行时,该属性允许向表中插入一个自动递增的整数值。
当一个额外的属性是一个 identity 属性并被添加到一个列中时,它被称为代理键。这种列的值是在运行时插入记录之前生成的,然后存储到表中。
外键
外键是通过标识父实体来完成关系的属性。外键提供了一种方法来维护数据的完整性(称为参照完整性),并在一个实体的不同实例之间导航。模型中的每个关系都必须由外键支持。例如,在前面的图 2-1 中,“客户”和“订单”表有一个主键和外键关系,其中“订单”表的“客户 ID”字段是外键,它引用了“客户”表的“客户 ID”字段。
了解数据完整性
数据完整性是指数据库中的数据值是正确且一致的。数据完整性有两个方面:实体完整性和参照完整性。
实体完整性
我们之前在“主键”中提到过,主键的任何部分都不能为空。这是为了保证所有行都有主键值。主键值存在并且唯一的要求被称为实体完整性 (EI)。DBMS 通过不允许操作(INSERT
、UPDATE
)产生无效的主键来加强实体完整性。任何创建重复的主键或包含空值的操作都会被拒绝。也就是说,要建立实体完整性,您需要定义主键,这样 DBMS 就可以强制实现它们的唯一性。
参照完整性
一旦在具有外键的表之间定义了关系,就必须对键数据进行管理以维护正确的关系,也就是说,实施参照完整性 (RI)。RI 要求子表中的所有外键值要么匹配父表中的主键值,要么(如果允许的话)为 null。这也被称为满足外键约束。
标准化概念
规范化是一种避免潜在更新异常的技术,基本上是通过最小化逻辑数据库设计中的冗余数据。规范化设计在某种意义上是“更好”的设计,因为它们(理想地)将每个数据项保存在一个地方。规范化的数据库设计通常会降低更新处理成本,但会使查询处理变得更加复杂。必须根据数据库所需的性能来仔细评估这些权衡。通常,数据库设计需要非规范化以充分满足操作需求。
规范化逻辑数据库设计涉及一组正式的过程,将数据分成多个相关的表。每个过程的结果被称为范式。理论上已经确定了五种范式,但是大多数时候第三范式(3NF)是你在实践中需要走的最远的。要在 3NF 中,一个关系(SQL 称之为表的正式术语和规范化的数学理论所依赖的精确概念)必须已经处于第二范式(2NF),而 2NF 要求一个关系处于第一范式(1NF)。让我们简单看一下这些范式的含义:
第一范式(1NF) :在第一范式中,所有列值都是标量;换句话说,它们只有一个值,不能根据数据模型进一步分解。例如,虽然字符串的单个字符可以通过分解字符串的过程来访问,但是在 SQL 中只有完整的字符串可以通过名称来访问,所以就数据模型而言,它们不是模型的一部分。同样,对于一个包含一个经理列和一个包含雇员表中为给定经理工作的雇员列表的经理表,可以通过名称访问经理和列表,但不能访问列表中的单个雇员。根据定义,所有关系和 SQL 表都在 1NF 中,因为最低级别的可访问性(称为表的粒度)是列级别,列值是 SQL 中的标量。
第二范式(2NF) :第二范式要求不属于键的属性(SQL 列的正式术语)在功能上依赖于唯一标识它们的键。函数依赖基本上意味着对于给定的键值,一个表中的一列或一组列只存在一个值。例如,如果一个表包含雇员和他们的职位,并且不止一个雇员可能有相同的职位(很可能),唯一标识雇员的键不会唯一标识职位,因此职位在功能上不会依赖于表的键。要将表放入 2NF,您需要为标题创建一个单独的表——使用它自己的惟一键——并用新表的外键替换原始表中的标题。请注意这是如何减少数据冗余的。标题本身现在只在数据库中出现一次。只有它们的键出现在其他表中,键数据不被认为是冗余的(当然,它需要其他表中的列和数据存储)。
第三范式(3NF) :第三范式将函数依赖的概念扩展到了全函数依赖。本质上,这意味着表中的所有非键列都由整个主键唯一标识,而不仅仅是主键的一部分。例如,如果您将假设的 1NF manager-Employees 表修改为包含三列(ManagerName、EmployeeId 和 EmployeeName)而不是两列,并且将复合主键定义为 ManagerName + EmployeeId,则该表将位于 2NF 中(因为 EmployeeName 这一非键列依赖于主键),但不会位于 3NF 中(因为 EmployeeName 由定义为 EmployeeId 列的主键的一部分唯一标识)。为 employees 创建一个单独的表并从 Managers-Employees 中删除 EmployeeName 会将该表放入 3NF 中。注意,即使这个表现在被规范化为 3NF,数据库设计仍然没有达到应有的规范化程度。使用比经理姓名更短的 ID 为经理创建另一个表,虽然这不是这里的规范化所必需的,但绝对是一种更好的方法,对于现实世界的数据库来说可能是可取的。
规范化的弊端
数据库设计与其说是技术,不如说是艺术,明智地应用规范化总是很重要的。另一方面,规范化本质上增加了表的数量,因此也增加了检索数据所需的操作(称为连接)的数量。因为数据不在一个表中,所以具有复杂连接的查询会降低速度。这可能以 CPU 使用率的形式来消耗:查询越复杂,需要的 CPU 时间就越多。
通过有意提供冗余数据来减少连接的数量或复杂性以获得更快的查询响应时间,对一个或多个表进行反规范化可能是必要的。无论是规范化还是反规范化,目标都是控制冗余,以便数据库设计能够充分地(理想情况下是最佳地)支持数据库的实际使用。
总结
本章解释了基本的数据库概念。您还了解了桌面和服务器数据库、数据库生命周期的各个阶段、键的类型以及它们如何定义关系。您还了解了用于设计更好的数据库的规范化表单。
在下一章中,您将开始创建数据库并操作数据和对象。
三、创建数据库和表
开发应用时,通常需要创建一个数据库并向其中添加表,而不仅仅是使用现有的数据库和表对象。本章是关于创建一个新的数据库,然后创建它包含的表。
在本章中,我将介绍以下内容:
- 启动 SQL Server Management Studio
- Type of SQL Server database
- Schema of SQL Server database
- Create a database in a simple way.
- Create a database with your own settings
- Create table
启动 SQL Server Management Studio
自 2005 年以来,SQL Server Management Studio (SSMS)一直是 SQL Server 的主要开发工具。您可以使用 SSMS 来实现、开发和管理数据库。当然,因为这本书是给应用开发者看的,所以我们的重点将放在开发上。
正如在第一章的中所提到的,在成功连接 SSMS 的数据库引擎并开始创建自己的数据库和表之前,启动 SQL Server 服务是非常重要的。
要启动 SSMS,请选择所有程序 Microsoft SQL Server 2012 SQL Server Management Studio。确保“连接到服务器”对话框具有正确的值,然后单击“连接”。这将启动 SQL Server Management Studio。
SQL Server 数据库的类型
SQL Server 有两种类型的数据库:系统数据库和用户数据库。
- System databases are those pre-installed in all versions of SQL Server. When you perform tasks such as creating, maintaining and managing databases, they support SQL Server database systems. They are located in the system database folder and named as master, model, msdb and tempdb, as shown in Figure and Figure 3-1 .
图 3-1。 SQL Server 系统数据库
表 3-1 描述了 SQL Server 的各个系统数据库,如图图-3-1 所示。
- The user database can be either an example database such as AdventureWorks (which you downloaded and attached in Chapter 1 of ) or any SQL Server database created anywhere in any SQL Server system, including the database where you or your organization are building applications. Some sample databases provided by Microsoft are pubs, northwind and AdventureWorks. Since you have attached AdventureWorks to your SQL Server 2012, it will be listed in Object Explorer, as shown in Figure T2 and Figure 3-2 and T3.
图 3-2。 SQL Server 用户数据库
接下来,您创建的数据库将成为用户数据库,列在系统数据库文件夹之后。这里值得一提的是,您无法创建自己的系统数据库或创建一个数据库并将其放在 System Databases 文件夹中。
SQL Server 数据库的架构
SQL Server 2012 数据库由数据和日志信息组成。这些数据和日志信息存储在单独的文件中。
- Master data file : This is the master file for constructing SQL Server database, because it points to other files in the database. The file extension required for this file is
.mdf
, and a SQL Server database can only have one master data file.- Log data file : This is a recovery file that stores all log information and is used to recover the database when it fails. The file extension required for this file is
.ldf
. Any database must contain at least one log file, but a database can have multiple log files.
注意还有第三种类型的文件,称为二级数据文件,那也是用来存储数据信息的,由一级数据文件指向。一个数据库可以有多个数据文件,一个数据文件需要的扩展名是.ndf
。因为大多数工业数据库都是由主文件和日志文件组成的,所以我不包括关于辅助文件的更多细节,而是将重点放在主文件和日志文件上。你可能想在[
msdn.microsoft.com](http://msdn.microsoft.com)
了解更多。
每个数据库由.mdf
和.ldf
文件组成的事实适用于系统数据库和用户数据库。要看到这一点,去你的程序文件下的 SQL Server 文件夹(见第一章关于如何找到你的 SQL Server 文件夹路径的信息);到达 SQL Server 位置后,您将看到数据库文件。在我的例子中,我的 SQL 文件夹路径显示了图 3-3 中的文件。
图 3-3。 SQL Server 数据库文件
该文件夹列出了构成 SQL Server 数据库的所有.mdf
和.ldf
文件名,包括系统和用户数据库。例如,参考master.mdf
和master.ldf
并在右边的类型栏中查看这些文件的描述。此外,您可以看到您在第一章的中附加的 AdventureWorks 数据库也在此列出,并附有相关的.mdf
和.ldf
文件。现在,如果您继续使用 SQL Server 的这个特定实例,您的所有数据库都将位于同一位置。
注将图 3-3 所示的文件夹结构与你的文件夹进行比较。它可能会显示一组不同的数据库文件,尤其是用户数据库和一些 SQL Server 功能,如 Reporting Services。因此,您的 SQL Server 系统视图可能与图中不同。
以简单的方式创建数据库
要开始创建数据库,如果 SSMS 尚未打开,您需要将其打开。
然后点击新建查询,你应该会看到一个类似于图 3-4 的窗口。
图 3-4。 SQL Server Management Studio:新查询窗格
在 SQL Server 2012 的发布中,SSMS 有了新的外观和感觉;实际上,微软已经为 SQL Server 2012 使用了 Visual Studio IDE,所以如果你以前使用过 Visual Studio 2010,你会发现它很相似。如果你还没有使用过 Visual Studio 2010,那么当我们在本书后面开始使用 Visual Studio 2012 编写 C# 代码时,你会看到用户界面是相似的。
要以简单的方式创建数据库,请按照下列步骤操作:
- Make sure that your new query pane shows the main database just below the save icon on the toolbar.
- Go to the query pane and make sure that you are in the main database, type the following:
use master
- Select the statement, and then click Execute or press F5; You should see the message "Command completed successfully"
- Now press Enter to add a new line and type the following:
create database MySQLDb
- In this example, we are creating a new database named MySQLDb. Remember, SQL Server is case insensitive, so no matter how you type SQL statements, they will work normally.
- After entering the
create
statement, select this newly addedcreate
statement and press Execute or F5. When you look at the list of databases in the databases folder in Object Explorer, you should see the MySQLDb database just created, as shown in Figure T2 and Figure 3-5 T3.
图 3-5。以简单的方式创建 SQL Server 数据库
这个新创建的数据库与任何其他数据库没有什么不同,因为它将由.mdf
和.ldf
文件组成。通过使用这个一行 SQL 语句…
Create Database database-name
您已经将控制权交给 SQL Server,让它代表您使用自己的预定义设置来创建文件。但是这种方法有局限性:
- It stores data and log files in the same disk under Program Files \ Microsoft SQL Server; What should I do if you want to separate the log file from the data file and put it on another disk or folder?
- By default, data and log files are given the freedom to continue to grow until the disk is full; In other words, they have unlimited access to disk space. In real-world organizations, managing storage is very important, so there is a size limit for the database.
探索数据库属性
创建数据库后,有时您会想知道它的属性,如文件名、路径、大小等等。按照以下步骤浏览数据库属性:
-
Right-click the newly created database MySqlDb in Object Explorer and select Properties.
-
转到左侧的文件选项卡。在执行前面显示的一行数据库创建语句时,您将看到 SQL Server 为您选择的设置。数据库属性对话框将看起来像图 3-6 。
图 3-6。探索数据库属性??
用自己的设置创建数据库
有时,您希望能够控制数据库的创建,以便能够控制诸如数据库可以增长到的最大大小、初始数量被消耗后的增长量,甚至是数据和日志文件的位置/文件夹。表 3-4 显示了您可以使用的参数。
现在,让我们尝试在 SQL 查询语法中使用表 3-2 中讨论的参数,用您的参数值创建一个数据库。请遵循以下步骤:
-
Make sure that the new query pane displays the main database under the Save icon on the toolbar.
-
Go to the query pane and make sure that you are in the main database, type the following:
use master
-
Select the statement, and then click Execute or press F5; You should see the message "Command completed successfully"
-
现在回车添加一个新行,并键入
create database
语句,如下图所示。注意请注意下面代码中
FileName
参数中的粗体文本;这表示您的 SQL Server 2012 实例名称。建议你通过资源管理器浏览到你的数据库文件夹位置,如图图 3-3 所示,然后复制粘贴FileName
参数中的路径 -
CREATE DATABASE SQL2012Db ON PRIMARY ( NAME = Sql2012Data, FILENAME = ‘C:\Program Files\Microsoft SQL Server\**MSSQL11.SQL2012**\MSSQL\DATA\Sql2012Data.mdf', SIZE = 4MB, MAXSIZE = 15MB, FILEGROWTH = 20% ) LOG ON ( NAME = Sql2012Log, FILENAME =' C:\Program Files\Microsoft SQL Server\**MSSQL11.SQL2012**\MSSQL\DATA\Sql2012Log.ldf', SIZE = 1MB, MAXSIZE = 5MB, FILEGROWTH = 1MB )
-
Select the whole statement, click Execute or press F5.
-
After the successful execution of the statement, select the database node in Object Explorer, right-click, and select Refresh to list the recently created databases. You should see a list similar to Figure 3-7 . T30 T33 】 Figure 3-7. Create a database statement T36 with parameters
现在您已经知道了如何使用代码创建数据库,包括使用简单的技术和使用参数,所以是时候学习如何在这些数据库中创建表了。
了解表格基础知识
如果您之前创建的数据库中没有表,那么它将毫无用处,因为表提供了数据存储的基础结构。
在开始创建表之前,您应该了解它的基本知识。一个表格由列组成,然后数据作为行添加( inserted )到表格中。(在关系数据库理论中,一行也被称为记录或元组,一列被称为字段。)可以作为行输入的数据类型由表中各列的数据类型定义。大多数时候,你需要输入什么类型的数据是显而易见的;例如,姓名需要字符串或字符数据类型,而年龄需要整数数据类型。
此外,知道哪一列需要数据输入也很重要。Null 或 Not Null 关键字指定数据接受标准,换句话说,是必须输入还是可以跳过列值。
例如,假设您正在填写一份保险表格,并被要求输入您的名字,然后是您的地址和 SSN。在这种情况下,并不是每个人都有 SSN,但是名字对任何人来说都是必须的。
基于这两列,您有了一个业务场景,这个数据库方面的业务需求可以通过在创建表时将名字列指定为 Not Null 并将 SSN 列指定为 Null 来实现。
Null 表示未定义或未知的值,表示没有指定任何东西。例如,假设在名字字段中有人填写了空格;从技术上讲,这被认为是信息,因为空白是一个空格,是一个字符,因此是信息。
同样,如果一个没有 SSN 的人输入 000-00-0000 作为 SSN 会怎么样?这也不是一个未定义的值,因为他们输入了零,这是字符,因此是信息。
默认情况下,创建的列允许 Null,除非在创建列时显式指定了 Not Null。
表列的 SQL Server 数据类型
为了支持业务需求,SQL Server 提供了各种数据类型(参见表 3-3 )。
在 SQL Server 中创建表格
若要在 SQL Server 中创建表,需要建立数据库的上下文。换句话说,您需要进入一个想要创建表的数据库。在本章中,您创建了两个数据库:MySqlDb 和 Sql2012Db。您将使用 Sql2012Db 数据库来创建表。
在对象资源管理器中,展开“数据库”节点,选择 Sql2012Db 数据库,然后单击“新建查询”(在工具栏中或从上下文菜单中)。您将看到 Sql2012Db 数据库已被选择用于您打开的这个新查询窗格。
在此查询窗格中,键入以下代码:
CREATE TABLE MySqlTable ( Name varchar(10) Not Null, --Name value is a must and can’t be Null Age int, SSN varchar(11), Date datetime, Gender char(6) )
选择整个create
语句,点击执行或按 F5。在对象资源管理器中,展开创建此表的 Sql2012Db 数据库,然后展开“表”节点。您将看到新创建的表被列出。这是每个数据库存储表的方式。
此外,为了研究您创建的表,您可以展开它。在示例中,您将其命名为 MySqlTable,因此展开它,您将看到 Columns 节点。同样展开 Columns 节点,您将看到您在前面的create table
语句中创建的所有列。为了帮助您回忆,在创建表的过程中,任何没有在声明中明确包含 Not Null 的字段或列在默认情况下都是 Null;因此,除了姓名列,所有其他字段或列都被 SQL Server 设置为空,这一点你应该可以在图 3-8 中看到。
图 3-8。create table 语句和列属性
在表中添加标识列
有时,开发人员和数据库程序员需要在现有的表中创建新列。SQL Server 提供了一个ALTER
语句来修改/改变已经创建的对象。
在这种情况下,您创建的表缺少一个可以帮助唯一标识每个人的列,因为两个人可能共用一个姓名;例如,两者可能是相同的年龄和性别,并且没有 SSN。在这种情况下,拥有一个可以唯一标识每一行的列是一个很有价值的附加功能,这也是所有真实数据库的设计方式。
用不同的值或 ID 标识每个人的最佳方法是使用 SQL Server 的 IDENTITY 属性。IDENTITY 是一个自动递增的值,由 SQL Server 针对表中的每一个插入行自动插入。也就是说,您不负责指定标识列的值。
此身份属性需要一个整数类型的列与之相关联。IDENTITY 属性用于那些需要自动生成唯一系统值的列。该属性可用于生成序列号。这个的语法是IDENTITY (Seed,Increment)
。Seed
是标识列的起始值或初始值。Increment
是用于为列生成下一个值的步长值。
例如,int ColumnName IDENTITY(1,1)
表示第一行的值为 1,第二行的值为 2,依此类推。同样的身份属性定义可以写成int ColumnName IDENTITY
。因此,IDENTITY
和IDENTITY(1,1)
是相同的。
有两种方法可以将这个 ID 列添加到表中:使用alter table
语句,删除并重新创建表。
更改表格
向预先创建的列添加标识键基于一个alter table
语句。要进行尝试,请在查询窗格中键入以下语句,选择它,然后按 Execute。
ALTER TABLE MySqlTable ADD Id int IDENTITY(1,1)
该语句将向表中添加一个 Id 列。要查看这个新添加的列,您需要刷新表,方法是在对象资源管理器中选择它,右键单击并选择 refresh。然后展开表节点和列节点,您会看到 Id 列被添加到表中,如图图 3-9 所示。
图 3-9。查看现有表中新添加的列
现在,您已经学习了如何向已经创建的列中添加列。这种方法的唯一问题是,使用ALTER
添加的任何列总是被添加到列列表的最末尾,并且您无法以任何方式移动或切换它的位置。
考虑到常见的业务情况,Id 和类似的列通常被添加为大多数表中的第一列,如果您有类似的愿望,那么您需要遵循不同的方法,如下所述。
删除并重新创建该表
如前所述,如果您希望将 Id 列作为表中的第一列,那么您必须从头开始定义表。为此,您必须首先删除该表,然后重新创建它。
在查询窗格中,输入以下语句,选择它,然后按执行。
DROP TABLE MySqlTable
这将永久删除该表;如果您在对象资源管理器中选择“表”节点,右键单击并选择“刷新选项”,您将看到没有列出任何表,因为您刚刚删除了它。
现在,您将重新创建 MySqlTable,并在创建时将标识列添加到表定义中,如以下语句所示:
CREATE TABLE MySqlTable ( Id int IDENTITY (1,1), --Identity makes the column Not Null internally Name varchar(10) Not Null, --Name value is a must and can’t be Null Age int, SSN varchar(11), Date datetime, Gender char(6) )
选择此create table
语句并按执行;然后在对象资源管理器中选择 MySqlDb 数据库,右键单击并选择刷新。您将看到创建了一个新的 MySqlTable,它位于第一列,不像使用alter table
语句时添加的那样。
您也可以通过展开表节点,然后展开列节点来查看列列表,您将能够看到列列表,如图图 3-10 所示。
图 3-10。查看重新创建的表格和列列表
您可能想要比较图 3-9 和图 3-10 来了解差异,这分别通过使用ALTER
和CREATE
来实现。
总结
本章描述了如何创建数据库和表。您还了解了通过ALTER
语句向已经创建的表中添加列的技术,该语句主要修改预先创建的表。您还学习了使用DROP
语句,该语句从数据库中删除一个表对象。
在下一章,你将开始学习数据操作的概念。
四、操作数据库数据
现在您已经知道了如何创建数据库和表,是时候将注意力转向修改数据了,比如插入、更新和删除数据。
在本章中,我们将介绍以下内容:
- Insert data
插入数据
创建表后,您需要能够向表中添加数据,例如行。通过使用INSERT
语句可以做到这一点。
一个基本的INSERT
语句包含以下几个部分:
INSERT INTO <table> (<column1>, <column2>, ..., <columnN>) VALUES (<value1>, <value2>, ..., <valueN>)
使用这个语法,让我们向新创建的 SQL2012Db 数据库的 MySqlTable 表中添加一个新行。
试试看:插入新的一行
若要向表中插入新行,请在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击执行:
Use SQL2012Db Go insert into MySqlTable ( Name, Age, SSN, Date, Gender ) Values('Vidya Vrat',36,'111-20-3456',GetDate(),'Male') Go
在查询窗格中执行该语句应该会产生一个消息窗口,报告“(1 行受影响)”,如图 4-1 所示。
注意 GetDate()
是 SQL Server 内置的返回当前日期和时间的 datetime 函数,所以如果使用该函数,它会将当前日期和时间输入到列中。
图 4-1。在 MySqlTable 表中插入新的一行
它是如何工作的
第一列 ID 是一个标识列,您不能显式地向其中插入值,SQL Server 数据库引擎将确保为 ID 字段插入一个由 SQL Server 生成的唯一值。因此,INSERT
语句需要以这样的方式编写,即您指定想要为其显式插入值的列列表;虽然 MySqlTable 包含六个字段,但 ID 是一个标识列,它不期望从用户处插入任何值。SQL Server 可以在执行INSERT
语句时检测标识列。但是,最佳做法是指定列列表,然后将各自的值传递给这些字段,如以下查询所示:
Insert into MySqlTable (Name,Age,SSN,Date,Gender) Values('Rupali',31,'222-10-6789',GetDate(),'Female')
插入行后,在查询窗格中键入以下查询:
Select * from MySqlTable
选择该语句,然后单击执行或按 F5;你会看到新的行已经被添加,如图图 4-2 所示。
图 4-2。添加行后的 MySqlTable】
请注意插入正确数据类型的数据。在本例中,您已经看到了字符、整数和日期时间类型的列。
通过一条 INSERT 语句插入多行
通常单个INSERT
语句向表中添加一行,但是从 SQL Server 2008 开始,INSERT
语句能够通过单个INSERT
语句添加多行。您只需要用逗号分隔每一行数据,如下面的语句所示,然后单击 Execute 或按 F5。
Insert into MySqlTable (Name,Age,SSN,Date,Gender) Values('Vamika',6,'333-30-1234',GetDate(),'Female'), ('Arshika',1,'444-40-5678',GetDate(),'Female')
这应该显示成功执行,如图 4-3 中的所示。
图 4-3。用一条 INSERT 语句添加多行
现在,如果您想执行下面的SELECT
语句,您应该会看到带有自动递增的 ID 值的四行,从 1 到 4。换句话说,第一条记录将为 1,此后插入的每条记录都将增加 1。参见图 4-4 。
图 4-4。 SELECT 语句显示所有带有自动生成的 ID 值的插入行
更新数据
您可以使用UPDATE
语句修改数据。在编写UPDATE
语句时,必须小心包含一个WHERE
子句,否则会更新表中的所有行。因此,总是编写一个合适的WHERE
子句;如果您遗漏了一个WHERE
子句,如下面的UPDATE
语句所示,那么您将更改表中的所有记录,我确信没有业务案例需要这样做!
UPDATE <table> SET <columnl> = <valuel>, <column2> = <value2>, ..., <columnN> = <valueN>
现在你已经意识到了UPDATE
语句的含义,让我们好好看看它。本质上,这是一个简单的语句,允许您更新一个或多个行和列中的值。
UPDATE <table> SET <columnl> = <valuel>, <column2> = <value2>, ..., <columnN> = <valueN> WHERE <predicate>
试试看:更新一行
若要更改行的值,请在 SQL Server Management Studio Express 中打开一个新的查询窗口。输入以下查询,然后单击执行:
update MySqlTable set Name = 'Pearly' where Id = 3
它是如何工作的
ID 是 SQL 为 MySqlTable 表的行生成的惟一标识符,因此您可以使用它来定位我们想要更新的行。运行该查询应该会产生一个消息窗格,报告“(1 行受影响)”现在如果你执行Select * from MySqlTable
语句,你会看到修改后的记录,如图图 4-5 所示。
图 4-5。 SELECT 语句显示 UPDATE 语句后修改的行
当您更新多个列时,您仍然只使用一次SET
关键字,并且您用逗号分隔列名和它们各自的值。例如,以下语句将更改我们添加到表中的一个人的姓名和社会保险号:
update MySqlTable set Name = 'Sparkly', SSN = '444-50-9100' where Id = 4
如果您执行Select * from MySqlTable
,您会看到 person 的名字和 SSN 值已经改变,如图 4-6 中的所示。
图 4-6。 SELECT 语句显示多列更新语句后修改的行
删除数据
要删除数据,可以使用DELETE
语句。DELETE
声明与UPDATE
声明具有相同的含义。由于忘记了WHERE
子句,删除表中的每一行(不仅仅是错误的行)太容易了,所以要小心。DELETE
语句删除整行,所以没有必要(或不可能)指定列。它的基本语法如下(记住,WHERE
子句是可选的,但是没有它,所有行都将被删除):
DELETE FROM <table> WHERE <predicate>
如果您需要从 MySqlTable 表中删除一个或一组记录,那么您需要用某个惟一值(如标识键或主键)来确定您想要删除的记录,然后您用DELETE
语句的WHERE
条件来指定您想要删除的行的这个惟一值。
delete from MySqlTable where Id = 2
这将生成一个消息窗格,报告“(1 行受影响)”执行Select * from MySqlTable
语句,你会看到 ID 为 2 的行已经被删除,如图图 4-7 所示。
图 4-7。 SELECT 语句显示 DELETE 语句后的行
再次强调,在DELETE
语句中使用WHERE
子句很重要;如果您指定了一个没有它的DELETE
语句,那么您将删除指定表中的所有行。
总结
在本章中,您学习了如何使用以下 T-SQL 关键字对数据库执行数据操作任务:INSERT
、UPDATE
和DELETE
。
在下一章,你将学习如何查询数据库。
五、查询数据库
在本章中,您将学习在 SQL Server 2012 中编码查询。SQL Server 使用 T-SQL 作为它的语言,并且它有各种各样的用于查询的函数和构造。您将看到如何使用 SQL Server Management Studio 和 AdventureWorks 数据库提交针对各种数据查询场景的查询。本章包括以下内容:
- retrieve data
- Use the
GROUP BY
clause- pattern matching
- Use aggregate function
- Use the
DATETIME
function- Use list operators
- Use range operators
- Find null value
- Use join
检索数据
SQL 查询从数据库中检索数据。数据以行的形式存储在表中。行由列组成。最简单的形式是,查询由两部分组成:
- A
SELECT
list that specifies the columns to retrieve.- A
FROM
clause that specifies one or more tables to access.
提示我用大写字母写了SELECT
和FROM
只是为了表明它们是 SQL 关键字。SQL 不区分大小写,关键字在代码中通常用小写字母书写。在 T-SQL 中,查询被称为SELECT
语句,但是 ISO/ANSI 标准明确区分了“查询”和“语句”这种区别在概念上很重要。查询是对一个表的操作,产生一个表作为结果;语句可以(也可以不)对表进行操作,不产生表作为结果。此外,子查询可以在查询和语句中使用。因此,我们通常会调用查询查询而不是SELECT
语句。您可以随意调用查询,但是请记住查询是 SQL 的一个特殊特性。
使用两个关键字SELECT
和FROM
,下面是最简单的查询,它将从指定的表中获取所有数据:
Select * from <table name>
星号(*)表示您要选择表中的所有列。
在本章中,您将使用 SQL Server 2012 的一个实例。打开 SQL Server Management Studio,在连接服务器对话框中键入localhost<SQL Server 2012 实例名> 作为服务器名;然后单击连接。SQL Server Management Studio 将打开。展开“数据库”节点,并选择 AdventureWorks 数据库。你的屏幕应该类似于图 5-1 。
图 5-1。选择要查询的数据库
试试看:运行一个简单的查询
若要提交查询以检索所有员工数据,请在 SQL Server Management Studio 中打开一个新的查询窗口。在对象资源管理器中选择 AdventureWorks,然后单击工具栏上的“新建查询”按钮。这将打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-2 所示的结果。
Select * from Person.Address
图 5-2。查询结果窗格
它是如何工作的
您要求数据库返回所有列的数据,您得到的正是这些数据。如果您向右滚动,您会找到地址表中的所有列。
大多数情况下,您应该将查询限制在仅相关的列。当选择不需要的列时,会浪费资源。要显式选择列,请在关键字SELECT
后输入列名,如下面的查询所示,然后单击 Execute。图 5-3 显示了结果。
Select AddressID, AddressLine1, City from Person.Address
该查询选择地址表中的所有行,但只选择 AddressID、AddressLine1 和 City 列。
图 5-3。选择具体栏目
使用 WHERE 子句
查询可以有WHERE
子句。WHERE
子句允许您指定选择行的标准。这个子句可能很复杂,但是我们现在将坚持使用一个简单的例子。语法如下:
WHERE <columnl> <operator> <column2 / Value>
这里,<operator>
是比较运算符(例如=、<>
、>或<)。(本章后面的表 5-1 ,列出了 T-SQL 比较运算符。)
试试看:改进您的查询
在本练习中,您将看到如何优化您的查询。
- Add the following
WHERE
clause to the query in Figure 5-3 :Where City = 'Redmond'
- Press F5 to run the query, and you should see the results shown in Figure 5-4 .
图 5-4。使用WHERE
从句
注意 SQL Server 关键字不区分大小写。SQL Server 区分大小写取决于数据库的排序规则设置。但是,SQL Server 安装的默认排序规则 SQL_Latin1_General_CP1_CI_AS 不区分大小写。所以,大多数时候,开发人员不需要担心大小写的敏感性。但是 AdventureWorks 数据库是区分大小写的。因此,如果您尝试使用 redmond 运行图 5-4 中所示的查询,由于区分大小写,您将不会显示任何结果行,这将不会找到具有所提供名称的城市。
使用比较运算符
你可以在一个WHERE
子句中使用许多不同的比较运算符(见表 5-1 )。
提示如前所述,每个数据库供应商都有自己的 SQL 实现。这个讨论是专门针对 T-SQL 的;例如,标准 SQL 没有!=
操作符,而是调用<>
的不等于操作符。事实上,标准 SQL 调用WHERE
子句中的表达式谓词;我们将使用这个术语,因为谓词要么为真,要么为假,但其他表达式不必如此。如果您使用的是另一个版本的 SQL,请参考它的文档以了解具体信息。
你可能想测试一下例子中的比较运算符,如图 5-5 所示。
图 5-5。使用比较运算符
整理数据
筛选完所需的数据后,可以按一列或多列并按特定方向对数据进行排序。因为根据定义表是未排序的,所以查询检索行的顺序是不可预测的。要进行排序,可以使用ORDER BY
子句。
ORDER BY <column> [ASC | DESC] {, n}
<column>
是应该用于对结果进行排序的列。{, n}
语法意味着您可以指定任意数量的由逗号分隔的列。结果将按照您指定列的顺序进行排序。
以下是两种排序方向:
ASC
: Ascending order (1, 2, 3, 4, etc.)DESC
: descending order (10, 9, 8, 7, etc.)
如果省略ASC
或DESC
关键字,排序顺序默认为ASC
。以下是查询的基本语法:
SELECT <column> FROM <table> WHERE <predicate> ORDER BY <column> ASC | DESC
现在您已经看到了,您将在一个示例中使用这个语法。
试试看:编写一个增强的查询
在本例中,您将使用刚才显示的基本语法编写一个查询。您需要执行以下操作:
- Select all addresses in Redmond.
- 只显示地址 ID,地址行 1,城市.
- Sort addresses by AddressID.
在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-6 所示的结果。
Select AddressID, AddressLine1, City from Person.Address Where City= 'Redmond' Order By AddressID Asc
图 5-6。过滤和整理数据
工作原理
让我们单独看一下这些条款。SELECT
列表指定了您想要使用的列。
select AddressID, AddressLine1, City
FROM
子句指定您想要使用Address
表。
from Person.Address
WHERE
子句指定您想要 Redmond 的所有记录。
where City = ‘Redmond’
ORDER BY
子句指定行的排序顺序。这些行将按 AddressID 以升序排序。
order by AddressID asc
GROUP BY 子句
GROUP BY
子句用于将输出行组织成组。SELECT
列表可以包含聚合函数,并为每个组生成汇总值。通常,您会希望从数据库中生成报告,其中包含特定列或一组列的汇总数字。例如,您可能想从Person.Address
表中找出属于某个特定城市的地址总数。
试试看:使用 GROUP BY 子句
这个人。地址表包含地址详细信息。您想知道属于某个特定城市的地址总数。例如,如果您查看图 5-6 中的查询和记录数,您会注意到 Redmond 总共有 121 个地址条目。
在 SQL Server Management Studio Express 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-7 所示的结果。
Select City, Count(City) As 'Total Count' from Person.Address Group By City Order By City Asc
图 5-7。使用GROUP BY
来聚合值
如果你将结果集向下滚动到 Redmond,你会看到它显示了 121 行,如图 5-6 中的所示。
它是如何工作的
您指定 City 列,并使用COUNT
函数来计算地址表中每个地址所列出的城市总数。
Select City, count(City) AS 'Total Count' from Person.Address
GROUP BY
子句强制对指定的列进行分组,结果应该以城市列的分组形式显示。
group by City
ORDER BY
子句确保显示的结果将根据城市以适当的顺序组织。
order by City Asc
模式匹配
模式匹配是一种确定特定字符串是否匹配特定模式的技术。可以使用常规字符和通配符的组合来创建模式。在模式匹配过程中,常规字符必须与字符串中指定的完全匹配。LIKE
和NOT LIKE
(否定)是用于模式匹配的运算符。记住模式匹配是区分大小写的。SQL Server 支持以下通配符进行模式匹配:
- % (percent sign) : This wildcard represents zero to more than one character. For example,
WHERE title LIKE '%C# 5.0%'
finds all titles containing the text C # 5.0 , regardless of where the text appears in the title-beginning, middle or end. In this case, titles of C# 5.0 databases such as C # 5.0: Introduction , Acceleration C # 5.0 and Start will be listed.- _ (underline) : A single underline represents any single character. Using this wildcard, you can search the character length of the data you are looking for very specifically. For example,
WHERE au_fname LIKE '_ean'
Find all names (Dean, Sean, etc.) that consist of four letters and end with ean .WHERE aufname LIKE 'a____n'
Find all names that start with a , end with n and have any other three characters in the middle, for example, allan, amman, aryan, etc.- [] (square brackets) : These specify any single character within the specified range, such as
[a-f]
, or a set, such as[abcdef]
or even[adf]
. For example,WHERE aulname LIKE '[C-K]arsen'
Find the author's surname ending in arsen and beginning with any single character between c and k , such as Carsen, Darsen, Larsen, Karsen, etc.- [] (brackets and caret) : These specify any single character that is not within the specified range, such as
[^a-f]
, or a set, such as[^abcdef]
. For example,WHERE au_lname LIKE 'de[^]%'
retrieves all author surnames that begin with de , but the letters that follow cannot be L .
试试看:使用百分比(%)字符
要查看通配符%
的工作原理,请在 SQL Server Management Studio Express 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-8 所示的结果。
Select AddressID, AddressLine1, City from Person.Address where City like 'R%'
图 5-8。将LIKE
运算符与%
一起使用
它是如何工作的
您指定地址表的三列。
select AddressID, AddressLine1, City from Person.Address
使用LIKE
操作符指定带有模式的WHERE
子句,列出所有以字母 R 开头并在其后包含任意数量字母的城市。
where City like 'R%'
试试看:使用下划线(_)字符
若要查看下划线(_)
通配符的工作原理,请在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-9 所示的结果。
Select AddressID, AddressLine1, City from Person.Address where City like 'S______' –- S followed by 6 underscores
图 5-9。将LIKE
运算符与_
一起使用
它是如何工作的
您指定地址表的三列。
select AddressID, AddressLine1, City from Person.Address
使用LIKE
操作符指定带有模式的WHERE
子句,以列出所有以字母 S 开头并且其后最多包含六个字母的城市,例如西雅图、斯波坎、肖尼等等。
where City like 'S______'
试试看:使用方括号([])字符
要查看[]
字符如何在模式匹配中工作,请在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-10 所示的结果。
Select AddressID, AddressLine1, City from Person.Address where City like '[B,R,S]%' Order by City Asc
图 5-10。将LIKE
运算符与[ ]
一起使用
它是如何工作的
您指定地址表的三列。
select AddressID, AddressLine1, City from Person.Address
使用LIKE
操作符指定带有模式的WHERE
子句,以列出所有以字母 B 或 R 或 S 开头并在其后包含任意数量字母的城市,例如 Bellevue、Redmond、Seattle 等。
where City like '[B,R,S]%' Order by City Asc
试试看:使用方括号和脱字符([^ ])
要查看[^B,R,S]
字符如何在模式匹配中工作,请在 SQL Server Management Studio Express 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-11 所示的结果。
Select AddressID, AddressLine1, City from Person.Address where City like '[^B,R,S]%' Order by City Asc
图 5-11。将LIKE
运算符与[^]
一起使用
它是如何工作的
您指定地址表的三列。
select AddressID, AddressLine1, City from Person.Address
使用LIKE
操作符指定带有模式的WHERE
子句,以列出所有不以字母 B 或 R 或 S 开头并且其后包含任意数量字母的城市;例如,Bellevue、Redmond、Seattle 等都不会包含在结果集中。
where City like '[^B,R,S]%' Order by City Asc
聚合函数
SQL 有几个内置函数可以聚合一列的值。聚合函数应用于多组行,并返回单个值。例如,您可以使用聚合函数来计算所下订单的平均单价。你可以找到价格最低或最贵的订单。MIN
、MAX
、SUM
、AVG
、COUNT
在聚合函数中经常使用。
尝试一下:使用最小值、最大值、总和以及 AVG 函数
让我们从 SalesOrderDetail 表中找出每个销售订单(SalesOrderID)的单价(unit price)的最小值、最大值、总和以及平均值。
在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-12 所示的结果。
select SalesOrderID,min(UnitPrice)as "Min", max(UnitPrice) as "Max",Sum(UnitPrice) as "Sum", Avg(UnitPrice)as "Avg" from Sales.SalesOrderDetail where SalesOrderID between 43659 and 43663 group by SalesOrderID
图 5-12。使用聚合函数
它是如何工作的
使用MIN
和MAX
函数查找最小值和最大值,使用SUM
函数计算总值,使用AVG
函数计算平均值。
min(UnitPrice) as "Min", max(UnitPrice) as "Max", Sum(UnitPrice) as "Sum", Avg(UnitPrice)as "Avg"
因为您希望通过SalesOrderID
列出结果,所以使用了GROUP BY
子句。从结果集中,您看到订单 1 的最小单价为 5.1865,最大单价为 2039.994,总单价为 14323.7118,平均单价为 1193.6426。
试试看:使用计数功能
让我们找出这个人的记录数。联系表。
在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-13 所示的结果。
`Select count(*) as "Total Records"
from Person.Contact
Select count(Title)as "Not Null Titles"
from Person.Contact`
图 5-13。使用COUNT
聚合函数
工作原理
根据传递给函数的参数,COUNT
函数有不同的行为。如果尝试COUNT(*)
,查询将返回表中可用的记录总数,如最上面的结果所示:table Person。联系人总共包含 19972 条记录,而Count(*)
计数为空。
如果您将一个列名传递给COUNT
函数,它将再次返回记录总数,但是它将忽略该列中所有包含 null 值的行。在第二个查询中,您正在查询同一个表,该表列出了 19,972 条记录,但是因为您的第二个查询应用于Title
列,所以它只返回 1,009 条记录,因为这一次它忽略了所有的空值。换句话说,Count(ColumnName)
忽略了 null。
日期时间函数
尽管 SQL 标准定义了一个DATETIME
数据类型及其组成部分YEAR
、MONTH
、DAY
、HOUR
、MINUTE
和SECOND
,但它并没有规定 DBMS 如何使这些数据可用。每个 DBMS 都提供提取部分DATETIME
的函数。让我们看一些 T-SQL DATETIME
函数的例子。
试试看:使用 T-SQL 日期和时间函数
让我们用 T-SQL 日期和时间函数来练习一下。
在 SQL Server Management Studio Express 中打开一个新的查询窗口(数据库上下文不影响此查询)。输入以下查询,然后单击“执行”。您应该会看到如图 5-14 所示的结果。
select current_timestamp'standard datetime', getdate()'Transact-SQL datetime', datepart(year, getdate())'datepart year', year(getdate())'year function', datepart(hour, getdate())'hour'
图 5-14。使用日期和时间功能
它是如何工作的
您使用一个非标准版本的查询,省略了FROM
子句,以显示当前日期和时间以及它们的各个部分。SELECT
列表中的前两列给出了完整的日期和时间。
current_timestamp 'standard datetime', getdate() 'Transact-SOL datetime',
第一行使用标准 SQL 的CURRENTTIMESTAMP
value 函数;第二种使用 T-SQL 的GETDATE
函数。它们实际上是等效的,都返回完整的当前日期和时间。
接下来的两行各提供当前年份。第一种使用 T-SQL DATEPART
函数;第二个使用 T-SQL YEAR
函数。两者都采用DATETIME
参数并返回整数年。DATEPART
函数的第一个参数指定提取DATETIME
的哪一部分。注意,T-SQL 没有提供用于提取完整日期的date
说明符,也没有单独的DATE
函数。
datepart(year, getdate()) 'datepart year', year(getdate()) 'year function',
最后一行获取当前小时。这里必须使用 T-SQL DATEPART
函数,因为没有类似于YEAR
函数的HOUR
函数。注意,T-SQL 没有提供提取完整时间的时间说明符,也没有单独的TIME
函数。
datepart(hour, getdate()) 'hour'
您可以设置日期和时间的格式,以及用各种方式提取和转换它们的替代函数。日期和时间也可以增加和减少,增加和减少。虽然所有的 DBMS 都在一定程度上遵守 SQL 标准,但如何做到这一点是 DBMS 特有的。无论您使用什么 DBMS,您都会发现日期和时间是最复杂的数据类型。但是,在所有情况下,您都会发现函数(有时比 T-SQL 中的函数更丰富)是处理日期和时间的基本工具。
提示当提供日期和时间输入时,通常需要字符串值;例如,1/26/2012
是为示例中保存当前日期的列指定值的合适方式。但是,DBMSs 以特定于系统的编码存储日期和时间。当您使用日期和时间数据时,请仔细阅读数据库的 SQL 手册,了解如何最好地处理它。
列表运算符
IN
是 SQL Server 的列表操作符;它允许您指定条件所基于的选项列表。例如,您想要提取所需列表中的所有城市。SQL Server 也提供了它的否定,NOT IN
,所以你可以选择你不想包含在结果集中的值。
试试看:使用 IN 操作符
在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-15 所示的结果。
select AddressID, AddressLine1, City from Person.Address where City in ('Bellevue', 'Redmond', 'Seattle')
图 5-15。使用 IN 运算符
它是如何工作的
您指定地址表的三列。
select AddressID, AddressLine1, City from Person.Address
您可以使用您想要提取记录的城市名称来指定列表操作符IN
。因此,它只为所提供的城市过滤 303 条记录。如果你向下滚动,你会看到所有的记录将只属于提到的城市。
where City in ('Bellevue', 'Redmond', 'Seattle')
试试看:使用 NOT IN 运算符
在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-16 所示的结果。
select AddressID, AddressLine1, City from Person.Address where City not in ('Bellevue', 'Redmond', 'Seattle')
图 5-16。使用 NOT IN 运算符
它是如何工作的
您指定地址表的三列。
select AddressID, AddressLine1, City from Person.Address
您指定列表操作符NOT IN
,其中包含您不希望包含记录的城市名称。因此,它过滤了除了我们在NOT IN
列表中列出的城市之外的 19,311 条记录。如果向下滚动,您将看不到任何属于上述城市的记录。
where City not in ('Bellevue', 'Redmond', 'Seattle')
范围运算符
BETWEEN
是 SQL Server 的范围运算符;它允许您指定条件所基于的数据范围。例如,您想要提供您想要查看的数据范围。SQL Server 也提供了它的否定,NOT Between
,所以您可以选择不希望包含在结果集中的值。
试试看:使用 BETWEEN 运算符
在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-17 所示的结果。
select AddressID, AddressLine1, City from Person.Address where AddressID between 201 and 300
图 5-17。使用 Between 运算符
工作原理
您指定地址表的三列。
select AddressID, AddressLine1, City from Person.Address
您指定范围操作符BETWEEN
和您想要包含记录的范围AddressID
。因此,它为该地址过滤 100 条记录,范围在 201 和 300 之间,换句话说,总共 100 条记录。
where AddressID between 201 and 300
试试看:使用 NOT BETWEEN 运算符
在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-18 所示的结果。
select AddressID, AddressLine1, City from Person.Address where AddressID between 201 and 32521
图 5-18。使用 Not Between 运算符
工作原理
您指定地址表的三列。
select AddressID, AddressLine1, City from Person.Address
您指定范围操作符BETWEEN
和您想要包含记录的范围AddressID
。因此,它为该地址过滤了 200 条记录,范围从 201 到 32521 之间的而不是,换句话说,总共 200 条记录。
where AddressID between 201 and 32521
查找空值
空值是未定义和未知的值,由关键字NULL
表示。当执行查询时,有时分别提取NULL
和NOT NULL
行变得很重要。为了支持这一目的,SQL Server 提供了包含在WHERE
条件子句中的IS NULL
及其否定IS NOT NULL
。
试试看:使用 IS NULL 运算符
在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-19 所示的结果。
select Title, FirstName, MiddleName, LastName from Person.Contact where MiddleName is null
它是如何工作的
您指定联系人表的四列。
select Title, FirstName, MiddleName, LastName from Person.Contact
您根据MiddleName
是否为空来指定WHERE
条件。因此,它过滤了 8,499 条个人联系信息记录;所有列出的记录的MiddleName
都是NULL
。
where MiddleName is null
图 5-19。使用 IS NULL 运算符
试试看:使用 IS NOT NULL 操作符
在 SQL Server Management Studio 中打开一个新的查询窗口。输入以下查询,然后单击“执行”。您应该会看到如图 5-20 所示的结果。
select Title, FirstName, MiddleName, LastName from Person.Contact where MiddleName is not null
它是如何工作的
您指定联系人表的四列。
select Title, FirstName, MiddleName, LastName from Person.Contact
您根据MiddleName
是否不为空来指定WHERE
条件。因此,它过滤了 11,473 条关于此人联系方式的记录;所有列出的记录都有一个定义的MiddleName
。
where MiddleName is not null
图 5-20。使用 IS NOT NULL 运算符
加入
大多数查询需要来自多个表的信息。连接是一种关系操作,通过从两个(不一定是不同的)表中检索数据并根据连接规范匹配它们的行来生成一个表。
存在不同类型的连接,您将单独查看,但是请记住,每个连接都是一个二元操作;也就是说,一个表连接到另一个表,由于表之间可以相互连接,所以这两个表可能是同一个表。连接操作是一个丰富且有些复杂的主题。接下来的部分将涵盖基础知识。
内部联接
内部联接是最常用的联接。它只返回那些满足连接规范的行。尽管理论上任何关系操作符(如>
或<
)都可以在连接规范中使用,但等式操作符(=)几乎总是被使用。使用等式运算符的连接被称为自然连接。
内部连接的基本语法如下:
select <select list> from left-table INNER JOIN right-table ON <join specification>
注意,INNER JOIN
是一个二元操作,所以它有两个操作数,left-table
和right-table
,它们可能是基表或任何可以被查询的东西(例如,由子查询或另一个连接产生的表)。ON
关键字是连接规范的开始,它可以包含任何可以在WHERE
子句中使用的内容。
表格别名
表别名是一种技术,用于为任何 SQL 查询中需要的一个表或每个单独的表指定一个简短的昵称。尽管可以使用完整的表名,但在查询中反复重复表名是一个麻烦的过程。
因此,当您必须指定可能存在于多个表中的列名,或者您希望使用特定表中的不同列时,表别名使它变得非常容易;因此,指定<Table Name>.<Column Name>
变得非常重要。
参考下面的查询,其中我们将Production.Product
别名为 PP,将Production.ProductReview
别名为 PPR。接下来,当涉及到使用这些表中的列时,我们使用定义的别名。
此外,请记住这些别名是临时的,它们的生命周期是到查询被执行为止。执行查询后,不能对任何其他查询重复使用相同的别名。因此,别名的范围在已定义的查询内,生命周期在查询执行之前。
试试看:编写一个内部连接
让我们检索产品列表、产品 id 以及它们的ReviewerName
、Comments
和Rating
条目。
在 SQL Server Management Studio 中打开一个新的查询窗口(请记住将 AdventureWorks 作为您的查询上下文)。输入以下查询,然后单击“执行”。您应该会看到如图 5-21 所示的结果。
select PP.ProductID, PP.Name, PPR.ReviewerName, PPR.Comments, PPR.Rating from Production.Product PP inner join Production.ProductReview PPR on PP.ProductID = PPR.ProductID
图 5-21。使用内部连接
它是如何工作的
先说SELECT
清单。
select PP.ProductID, PP.Name, PPR.ReviewerName, PPR.Comments, PPR.Rating
因为要从两个表中选择列,所以需要识别列来自哪个表,这可以通过在表名前面加上一个点(.)添加到列名。这被称为歧义消除、或消除歧义,以便数据库管理器知道使用哪一列。尽管只需要对两个表中都出现的列执行此操作,但最佳实践是用它们的表名限定所有列。
下面的FROM
子句指定了您正在连接的表及其别名,以及您正在使用的连接类型:
from Production.Product PP inner join Production.ProductReview PPR
它指定了产品的内部联接。产品和生产。产品评论表。
它还指定了连接 Product 表的主键 ProductID 和 ProductReview 表的外键 ProductId 的条件。
on PP.ProductID = PPR.ProductID
ProductID
上的内部连接产生一个由五列组成的表:ProductID、Name、ReviewerName、Comments 和 Rating。数据是从生产中的行中检索的。产品和生产。ProductReview,其 ProductID 列具有相同的值。订单中任何与雇员中的行不匹配的行将被忽略,反之亦然。(这里不是这样的,但是你很快就会看到一个例子。)内部联接总是只生成满足联接规范的行。
提示用于加入的列不必出现在SELECT
列表中。如果您愿意,可以省略该列。
外部联接
外部连接返回(至少)一个连接表中的所有行,即使一个表中的行与另一个表中的行不匹配。存在三种类型的外部联接:左外部联接、右外部联接和完全外部联接。术语左和右指的是JOIN
运算符左边和右边的操作数。(参考内部连接的基本语法,你会明白为什么我们称操作数为left-table
和right-table
。)在左外连接中,将检索左表中的所有行,无论它们在右表中是否有匹配的行。相反,在右外连接中,将检索右表中的所有行,无论它们在左表中是否有匹配的行。在完全外部联接中,将返回两个表中的所有行。
提示左右外连接在逻辑上是等价的。通过改变操作符,将操作数或右连接转换为左连接,总是可以将左连接转换为右连接。因此,实际上只需要这些操作符中的一个。选择哪一个基本上是个人偏好的问题,但一个有用的经验法则是在同一个查询中使用 left 或 right,但不要同时使用两者。查询优化器不会在意,但是人们会发现如果连接总是朝着同一个方向,跟踪复杂的查询会容易得多。
这个什么时候有用?相当频繁。事实上,只要表之间存在父子关系,尽管保持了参照完整性,但某些父行在子表中可能没有相关的行,因为子行可能允许有空的外键值,因此不匹配父表中的任何行。
试试看:使用左外连接
若要列出所有 ProductID 和 ProductName 条目,甚至包括那些尚未审阅且没有关联的 ReviewerName、注释和评级的条目,请在 SQL Server Management Studio 中打开一个新的查询窗口(请记住将 AdventureWorks 作为您的查询上下文)。输入以下查询,然后单击“执行”。您应该会看到如图 5-22 所示的结果。
select PP.ProductID, PP.Name, PPR.ReviewerName, PPR.Comments, PPR.Rating from Production.Product PP left outer join Production.ProductReview PPR on PP.ProductID = PPR.ProductID
图 5-22。使用左外部连接
它是如何工作的
先说SELECT
清单。
select PP.ProductID, PP.Name, PPR.ReviewerName, PPR.Comments, PPR.Rating
因为要从两个表中选择列,所以需要识别列来自哪个表,这可以通过在表名前面加上一个点(.)添加到列名。这被称为歧义消除、或消除歧义,以便数据库管理器知道使用哪一列。尽管只需要对两个表中都出现的列执行此操作,但最佳实践是用它们的表名限定所有列。
下面的FROM
子句指定了要连接的表和使用的连接类型:
from Production.Product PP left outer join Production.ProductReview PPR
它指定了产品的左外连接。产品和生产。产品评论表。
它还指定了将Product
表的主键ProductID
与ProductReview
表的外键ProductId
连接起来的标准。
on PP.ProductID = PPR.ProductID
ProductID 的左外部连接生成一个由五列组成的表:ProductID、Name、ReviewerName、Comments 和 Rating。所有数据都是从生产中的行中检索的。Product,即左边的表,以及生产中匹配和不匹配的数据。ProductReview,其中它们的 ProductID 列具有匹配甚至不匹配的值。
试试看:使用右外连接
若要列出所有 ProductID 和 ProductName 列以及基于已检查列的详细信息,请在 SQL Server Management Studio 中打开一个新的查询窗口(请记住将 AdventureWorks 作为查询上下文)。输入以下查询,然后单击“执行”。您应该会看到如图 5-23 所示的结果。
select PP.ProductID, PP.Name, PPR.ReviewerName, PPR.Comments, PPR.Rating from Production.Product PP right outer join Production.ProductReview PPR on PP.ProductID = PPR.ProductID
图 5-23。使用左外部连接
它是如何工作的
先说SELECT
清单。
select PP.ProductID, PP.Name, PPR.ReviewerName, PPR.Comments, PPR.Rating
因为要从两个表中选择列,所以需要识别列来自哪个表,这可以通过在表名前面加上一个点(.)添加到列名。这被称为歧义消除、或消除歧义,以便数据库管理器知道使用哪一列。尽管只需要对两个表中都出现的列执行此操作,但最佳实践是用它们的表名限定所有列。
下面的FROM
子句指定了要连接的表和使用的连接类型:
from Production.Product PP right outer join Production.ProductReview PPR
它指定了产品的一个右外连接。产品和生产。产品评论表。
它还指定了连接 Product 表的主键 ProductID 和 ProductReview 表的外键 ProductId 的条件。
on PP.ProductID = PPR.ProductID
ProductID 的右外部连接生成一个由五列组成的表:ProductID、Name、ReviewerName、Comments 和 Rating。所有数据都是从生产中的行中检索的。ProductReview,这是正确的表,以及匹配和取消匹配生产中的数据。ProductReview,其中它们的 ProductID 列具有匹配甚至不匹配的值。
其他连接
SQL 标准还提供了FULL OUTER JOIN
、UNION JOIN
和CROSS JOIN
(甚至还有NATURAL JOIN
,基本上是一个使用等式谓词的内部连接),但是这些很少使用,超出了本书的范围。我们不会提供示例,但本节包含了对它们的简要总结。
一个FULL OUTER JOIN
就像是一个LEFT
和RIGHT OUTER
连接的组合。将检索两个表中的所有行,即使它们在另一个表中没有相关的行。
UNION JOIN
与外部连接不同,它不匹配行。相反,它创建一个包含两个表中所有行的表。对于两个表,它等效于以下查询:
select * from table1 union all select * from table2
这些表必须具有相同的列数,并且相应列的数据类型必须兼容(能够保存相同类型的数据)。
一个CROSS JOIN
组合两个表中的所有行。它没有提供连接规范,因为这是不相关的。它生成一个表,其中包含两个表中的所有列,行数与每个表中行数的乘积。结果也被称为笛卡尔积,,因为这是一个数学术语,用于将一个集合(表格)中的每个元素(行)与另一个集合中的所有元素相关联。例如,如果表 A 中有 5 行 5 列,表 B 中有 10 行 3 列,那么 A 和 B 的交叉连接将产生一个 50 行 8 列的表。这种连接操作不仅实际上不适用于任何真实世界的查询,而且对于甚至很小的真实世界的数据库来说,这也是一个潜在的非常昂贵的过程。(想象一下将它用于具有数千甚至数百万行的生产表。)
总结
在本章中,我们介绍了如何使用 SQL 特性构建数据库查询,例如范围、列表、I S NULL
操作符、聚合函数、DATETIME
函数、GROUP BY
子句、连接和模式匹配。
在下一章,你将学习如何创建存储过程。
六、使用存储过程
存储过程是 SQL 语句的集合,允许您重复执行任务。您可以创建一次该过程,并在程序中多次重用它。这可以提高应用的可维护性,并允许应用以统一和优化的方式访问数据库。本章的目标是通过在 SQL Server 2012 中创建和修改存储过程来让您熟悉存储过程,并解释 C# 程序如何与它们交互。本章包括以下内容:
- Create stored procedure
- Modify stored procedure
- Displays the definition of the stored procedure.
- Rename stored procedure
- Using stored procedures in C#
- Delete stored procedure
创建存储过程
存储过程可以有用于输入或输出的参数。存储过程可以有一个整数返回值(默认为零),并且可以返回零个或多个结果集。可以从客户端程序或其他存储过程中调用它们。它们确实很强大,并且正在成为许多数据库编程的首选模式,特别是对于多层应用和 web 服务,因为(在它们的众多优点中)它们可以大大减少客户机和数据库服务器之间的网络流量。
注意如果您使用的是 AdventureWorks2008R2 数据库,那么将应用一些表名更改。例如,在我的代码示例中,我使用了 Person。联系人表,实际上是人名。AdventureWorks2008R2 中的人物。如果你遇到任何这样的例子,替换掉那个人。与人联系。
尝试一下:在 SQL Server 中使用存储过程
让我们使用 SQL Server Management Studio 创建一个存储过程,该存储过程在 AdventureWorks 数据库中生成一个人的详细联系信息列表。它不需要任何输入,也不需要设置返回值。
-
Open SQL Server Management Studio and select localhost\SQL2012 as the server name in the Connect Server dialog box. Then click Connect.
-
在对象资源管理器中,展开“数据库”节点,选择 AdventureWorks 数据库,然后单击“新建查询”窗格。输入以下查询,然后单击“执行”。你应该在图 6-1 中看到结果。创建过程 sp_Select_All_PersonContact 作为选择联系人。头衔,联系人。名字,联系人。人的姓氏。联系
图 6-1。使用 SQL Server Management Studio 创建存储过程
-
要执行存储过程,输入如图图 6-2 所示的命令,点击执行。您应该会看到图 6-2 中所示的结果。
图 6-2。执行存储过程
它是如何工作的
您使用CREATE PROCEDURE
来创建存储过程。AS
关键字将存储过程的签名(过程的名称和参数列表,但是这里没有定义参数)与其主体(组成过程的 SQL)分开。
Create procedure sp_Select_All_PersonContact As select Contact.Title, Contact.FirstName, Contact.LastName from Person.Contact
SQL Server Management Studio 提交了CREATE PROCEDURE
语句,一旦创建了存储过程,您就可以通过编写以下语句从查询窗格运行它:
execute sp_Select_All_PersonContact
就这样!创建存储过程并不复杂。
注意前缀sp_
是 T-SQL 约定,通常表示存储过程是用 SQL 编写的。前缀xp_
(表示“扩展过程”)用于表示存储过程不是用 SQL 编写的。(但是,并不是所有 SQL Server 提供的sp_
存储过程都是用 SQL 编写的。)顺便说一下,SQL Server 2012 提供了数百个sp_
(和其他)存储过程来执行各种各样的常见任务。
尝试一下:创建一个带有输入参数的存储过程
让我们创建一个存储过程,为任何人的给定职务生成一个联系人列表。我们将把标题传递给存储过程,以便在查询中使用。
- Enter the following query and click Execute. You should see the message "Command completed successfully" in the result pane.
Create procedure sp_Contact_By_Title @Title nvarchar(8) As select Contact.Title, Contact.FirstName, Contact.LastName from Person.Contact where Contact.Title = @Title
- To execute the stored procedure, enter the command and parameter values, select the following statement, and then click Execute. You should see the results shown in Figure 6-3.
execute sp_Contact_By_Title 'Mr.'
图 6-3。使用输入参数
它是如何工作的
CREATE PROCEDURE
语句创建了一个有一个输入参数的存储过程。参数在过程名和AS
关键字之间指定。这里您只指定了参数名和数据类型,所以默认情况下它是一个输入参数。参数名以@
开头。
Create procedure sp_Contact_By_Title @Title nvarchar(8) As
该参数用在查询的WHERE
子句中。
where Contact.Title = @Title
尝试一下:创建一个带有输出参数的存储过程
输出参数通常用于在存储过程之间传递值。换句话说,它们像编程语言中的函数一样用于返回值,所以让我们编写一个带有输出参数的存储过程,这样我们以后就可以在 C# 程序中使用它了。我还将展示如何返回非零值。
3。输入以下查询,然后单击“执行”。您应该会在结果窗格中看到消息“命令已成功完成”。
`Create procedure sp_CountContacts_By_Title
@Title nvarchar(8),
@TitleCount int= 0 output
As
select Contact.Title, Contact.FirstName, Contact.LastName
from Person.Contact
where Contact.Title = @Title
Select @TitleCount = count(*)
from Person.Contact
where Title=@Title
return @TitleCount
You have made the stored procedure, and now you need to test it; in order to do so, enter the
following statements in the query pane, making sure you either replace the earlier statements
or select only these statements while executing.
Declare @return_value int,
@TitleCount int
Execute @return_value=sp_CountContacts_By_Title
@Title='Mr.',
@TitleCount=@TitleCount output
Select 'Total Title Count' =@return_value`
如果你看一下右下角的图 6-3 中的状态栏,你会注意到返回的总行数是 577;这在图 6-4 中反映为总标题数。
图 6-4。使用输出参数
它是如何工作的
您添加了一个输出参数@TitleCount
,如下所示:
Create procedure sp_CountContacts_By_Title @Title nvarchar(8), @TitleCount int= 0 output As
您给它分配了一个默认值零。关键字output
将其标记为输出参数。您还添加了一个额外的查询:
select Contact.Title, Contact.FirstName, Contact.LastName from Person.Contact where Contact.Title = @Title
您将新查询返回的标量分配给了SELECT
列表中的输出参数:
` @TitleCount = count(*)then you returned the same value
return @TitleCount`
COUNT
函数返回一个整数,所以这是演示如何使用RETURN
语句的一种便捷方式。
提示输入的参数也可以被赋予默认值。
修改存储过程
您可以使用Alter Procedure procedure_name
语句来修改现有的存储过程。
试试看:修改你的普通存储过程
-
这里你通过添加一个
Order by
子句来修改图 6-1 中显示的sp_Select_All_PersonContact
存储过程。(参见图 6-5 。)Alter procedure sp_Select_All_PersonContact As select Contact.Title, Contact.FirstName, Contact.LastName from Person.Contact Order by Contact.LastName
图 6-5。修改存储过程
-
Execute the stored procedure by writing a statement as shown in figure , figure 6-6 . Please note that the employee names are sorted now (and if you look at figure 6-2 , the records are not sorted).
图 6-6。执行修改后的存储过程
它是如何工作的
在执行完ALTER PROCEDURE
语句后,数据库中的存储过程被更新。
Alter procedure sp_Select_All_PersonContact
您还在修改过程时添加了Order by
子句。
order by Contact.LastName
显示存储过程的定义
SQL Server 提供了一种查看在数据库中创建的对象的定义的方法。这被称为元数据检索。关于对象的信息存储在一些预定义的系统存储过程中,需要时可以检索这些信息。
试试看:查看我们存储过程的定义
- Enter the following statement in the Query pane:
Execute sp_helptext 'sp_Select_All_Employees' sp_helptext 'sp_Select_All_PersonContact'
- Go to the Query menu, point to Results To, click Results to Text, and then click Go. You will see the same definition as the one you specified for the stored procedure. You can see the output of in Figure 6-7.
图 6-7。显示存储过程的定义
它是如何工作的
语句sp_helptext
是一个预定义的 SQL Server 存储过程,它接受对象名作为参数,并显示作为参数传递给sp_helptext
的对象的定义。
sp_helptext 'sp_Select_All_PersonContact'
注意 sp_helptext
对表格对象不起作用。换句话说,您看不到创建表对象时使用的CREATE TABLE
语句的定义。
重命名存储过程
SQL Server 允许您重命名对象。以下语句将允许您更改存储过程名称。sp_rename
是一个预定义的存储过程,允许重命名对象。
试试看:重命名存储过程
-
Enter the following statement in the query pane:
sp_rename 'sp_Select_All_PersonContact, 'sp_Select_All_ContactDetails'
-
点击执行,在结果窗格中会看到以下消息:
警告改变对象名的任何部分都可能破坏脚本和存储过程.
即使存储过程
sp_rename
已经成功执行,它仍然显示警告消息现在转到对象浏览器,展开 AdventureWorks 数据库节点,展开可编程性,右键单击存储过程节点,选择刷新。 -
Expand the stored procedures node and notice that sp_Select_All_PersonContact has been renamed sp_Select_All_ContactDetails. You should see the screen shown in Figure 6-8.
图 6-8。重命名存储过程
它是如何工作的
语句sp_rename
是一个预定义的 SQL Server 存储过程,它接受对象的旧名称和新名称作为参数。
sp_rename 'sp_Select_All_PersonContact, 'sp_Select_All_ContactDetails'
注意 sp_rename
在您想要重命名大多数对象时非常好用,比如表格、列和其他对象。
删除存储过程
一旦创建了存储过程,如果不需要它的功能,可以将其删除。Drop Procedure procedure_name
是基本语法。
注意因为我们将需要这些创建的存储过程,如果您在下面的练习中选择删除一个,请重新创建它,这样任何依赖关系都不会丢失。
试试看:删除存储过程
让我们删除第一个存储过程(sp_Select_All_PersonContact
),我们刚刚将其重命名为sp_Select_All_ContactDetails
。
- Enter the following statement in the query pane (replace the query with this statement), and click Execute:
Drop procedure sp_Select_All_ContactDetails
- You will see the following message:
Command(s) completed successfully.
- After deleting the stored procedure, navigate to Object Explorer and expand the AdventureWorks database node; Then expand Programmability, right-click the Stored Procedures node, and select Refresh. Please note that the program
sp_Select_All_ContactDetails
has been deleted (see Figure 6-9 ) and is no longer listed in the object browser, as you can see in Figure 6-8 .
图 6-9。删除存储过程
总结
在本章中,您创建了存储过程,并且了解了从 SSMS 创建、执行和修改存储过程所涉及的内容。您看到了调用存储过程与执行查询和语句并没有本质上的不同。您只需为需要使用的存储过程参数创建适当的命令参数。在下一章,你将学习如何使用 SQL 查询产生 XML 输出。
七、使用 XML
XML 已经存在很多年了;随着微软的发布。NET 技术,XML 变得更加流行。微软的开发工具和技术包含支持 XML 的内置特性。使用 XML 及其相关技术的优势在于它是 Internet 和. net 的主要基础。
我在本章的目标是向您介绍最基本的 XML 概念和术语,以及在 SQL Server 2012 中使用 XML 的基本技术。这将使您能够在编写软件应用时处理一些常见的编程任务。
本章将涵盖以下内容:
- Define XML
- Understand why XML is used
- Benefits of storing data as XML
- Understanding XML documents
- Understanding XML declarations
- Transforming relational data into XML
- Use
xml
data type.存储和检索 XML 文档
定义 XML
XML 代表可扩展标记语言。XML 源自标准通用标记语言(SGML)。XML 是一种元语言。元语言不是用于编程,而是用于定义其他语言,XML 定义的语言被称为标记语言。标记正是它所暗示的:一种“标记”某物的方式。XML 文档是文本文档的形式,人和计算机都可以阅读。
注本质上,每个 XML 文档都是由文档中使用的 XML 元素定义的语言的一个实例。特定的语言可能已被明确定义,也可能未被明确定义,但是 XML 的专业使用需要仔细规划自己的 XML 词汇表,并在一个模式中指定其定义,该模式可用于验证文档是否符合词汇表的语法和语义。XML 模式定义(XSD)是定义 XML 词汇表的语言。
万维网联盟(W3C)于 1996 年开发了 XML。为了支持各种各样的应用,W3C 使用 XML 来创建可扩展 HTML (XHTML),这是一种 XML 词汇表。自 1996 年以来,W3C 开发了各种其他面向 XML 的技术,包括可扩展样式表语言(XSL)和 XSL 转换(XSLT),XSL 为 XHTML 提供的功能与层叠样式表(CSS)为 HTML 提供的功能相同,XSL 转换是一种将 XML 文档转换为其他 XML 文档的语言。
为什么选择 XML
XML 是一种多用途、可扩展的数据表示技术。XML 增加了应用消费和操作数据的可能性。XML 不同于关系数据;XML 数据可以是结构化的、半结构化的或非结构化的。SQL Server 2012 中的 XML 支持与关系引擎和查询优化器完全集成,允许您检索和修改 XML 数据,甚至在 XML 和关系数据表示之间进行转换。
将数据存储为 XML 的好处
XML 是一种独立于平台的数据表示格式,它为特定的数据表示需求提供了优于关系格式的某些好处。
将数据存储为 XML 有很多好处:
- Because XML is self-describing, applications can use XML data without knowing the schema or structure. XML data is always hierarchically arranged in a tree structure. The XML tree structure must always have a root or parent node, which is the so-called XML document .
- Maintain XML document sorting. Because XML is arranged in a tree structure, it is easy to maintain the order of nodes.
- XML Schema is used to define a valid XML document structure.
- Because of the hierarchical structure of XML, you can search within the tree structure. XQuery and XPath are query languages for searching XML data.
- Data stored as XML is extensible. It is easy to manipulate XML data by inserting, modifying and deleting nodes.
注结构良好的 XML 是一种符合 W3C XML 1.0 推荐标准所规定的一组约束的 XML 文档。例如,格式良好的 XML 必须包含一个根级元素,并且任何其他嵌套元素必须正确打开和关闭,不能混淆。SQL Server 2005 验证一些格式良好的约束。一些规则,比如对根级元素的要求,没有被强制执行。关于格式良好性要求的完整列表,请参考位于[www.w3.org/TR/REC-xml](http://www.w3.org/TR/REC-xml)
的 W3C XML 1.0 推荐标准。
理解 XML 文档
XML 文档可以是计算机上的物理文件,也可以是网络上的数据流(理论上,它被格式化以便人们可以阅读,但实际上,它通常是压缩的二进制形式),或者只是内存中的一个字符串。然而,它本身必须是完整的,即使没有模式,它也必须遵守某些规则。
最基本的规则是,XML 文档必须是格式良好的。最简单地说,这意味着不允许元素重叠,所以必须在父元素的结束标签之前关闭所有的子元素。例如,这个 XML 文档是格式良好的:
<states> <state> <name>Delaware</name> <city>Dover</city> <city>Wilmington</city> </state> </states>
它有一个由开始标签<states>
和结束标签</states>
分隔的根(或文档)元素states
。根元素是state
元素的父元素,而后者又是一个name
元素和两个city
元素的父元素。一个 XML 文档只能有一个根元素。
元素可能有属性。例如,前面的文档可以重写如下,其中name
用作state
元素的属性:
<states> <state name="Delaware"> <city>Dover</city> <city>Wilmington</city> </state> </states>
它保留相同的信息,用一个name
属性替换只出现一次的name
元素,并将原始元素(Delaware
)的内容更改为属性("Delaware"
)的值。一个元素可以有任意数量的属性,但是它不能有重复的属性,所以city
元素不是替换的候选。
元素可能有内容(文本数据或其他元素),也可能是空。例如,如果您希望(只是为了便于讨论)跟踪文档中有多少个状态,您可以使用一个空元素来实现:
<states> <controlinfo count="1"/> <state name="Delaware"> <city>Dover</city> <city>Wilmington</city> </state> </states>
空元素controlinfo
有一个属性count
,但是没有内容。注意,它不是由开始和结束标记分隔的,而是存在于一个空元素标记中(以<
开始,以/>
结束)。
使用开始和结束标记的空元素的替代语法也是有效的:
<controlinfo count="1"></controlinfo>
许多生成 XML 的程序都使用这种形式。
注意虽然设计 XML 文档很容易,但是设计好它们就像设计数据库一样具有挑战性。许多有经验的 XML 设计者不同意属性的最佳用法,甚至不同意是否应该使用属性(如果没有属性,空元素实际上没有任何用处)。虽然元素可能在某些方面更理想地映射到关系数据,但这并不意味着属性在 XML 设计中没有位置。毕竟,XML 并不打算(原则上也不能)符合数据的关系模型。事实上,您将会看到,在 T-SQL 中,“纯”元素设计可能更难处理。
理解 XML 声明
除了元素和属性之外,XML 文档还可以有其他部分,但是只有当您真正需要深入研究 XML 时,大多数部分才是重要的。虽然是可选的,但是为了精确地符合 W3C 推荐标准, XML 声明是应该包含在 XML 文档中的一部分。如果使用,它必须出现在 XML 文档中的根元素之前。
XML 声明在格式上类似于元素,但是在尖括号旁边有问号。它总是有一个名为version
的属性;目前,这有两个可能的值:"1.0"
和"1.1"
。(定义了几个其他属性,但不是必需的。)因此,XML 声明的最简单形式如下:
<?xml version="1.0" ?>
XML 还有其他方面,但这是您开始学习的全部内容。事实上,这可能是你变得高效所需要的全部。正如您将看到的,我们在 XML 文档中不使用任何 XML 声明(或者更重要的东西,比如 XML 模式和名称空间),但是我们的小例子工作得很好,代表了基本的 XML 处理,并且可以扩展到更大的 XML 文档。
将关系数据转换为 XML
一个SELECT
查询将结果作为一个行集返回。通过在查询中指定FOR
XML 子句,您可以选择将 SQL 查询的结果检索为 XML。SQL Server 2005 使您能够通过在SELECT
语句中使用FOR
XML 子句将关系数据提取为 XML 形式。SQL Server 2005 扩展了FOR
XML 功能,使得表示复杂的层次结构和添加新的关键字来修改生成的 XML 结构变得更加容易。
注在第十三章中,我展示了如何从一个数据集中提取数据,将其转换成 XML,并用数据集的WriteXml
方法将其写入一个文件。
FOR
XML 子句将查询的结果集转换成 XML 结构,它提供了四种格式模式:
FOR XML RAW
FOR XML AUTO
FOR XML PATH
FOR XML EXPLICIT
我们将在示例中使用前两个来展示如何用查询生成 XML。
使用 FOR XML RAW
RAW
模式将查询结果集中的每一行都转换成一个 XML 元素,对于结果集中显示的每一行都标识为row
。在显示结果集时,SELECT
语句中的每个列名都作为属性添加到row
元素中。
默认情况下,行集合中不是NULL
的每个列值都被映射到row
元素的一个属性。
试试看:使用 FOR XML RAW(以属性为中心)
- Let's generate SQL query results in the original XML format.
FOR XML RAW
is the statement that we will use to generate output, which you will see in the following steps. In Object Explorer, expand the Databases node, select the AdventureWorks database, and then click the New Query window. Enter the following query and click execute:select Person.Contact.Title, Person.Contact.FirstName, Person.Contact.LastNamefrom Person.Contact where Person.Contact.Title ='Mr.' for xml raw
- You will see a link in the result pane of the query pane. Click on the link, and you should see the result as shown in Figure 7-1.
图 7-1。使用 FOR XML RAW
它是如何工作的
模式产生非常“原始”的 XML。它将结果集中的每一行转换成一个空的 XML row
元素,并为每个列值使用一个属性,使用我们在查询中指定的别名作为属性名。它产生一个由所有元素组成的字符串。
RAW
模式不会生成 XML 文档,因为它的根元素(raw
)与结果集中的行数一样多,而一个 XML 文档只能有一个根元素。
试试看:使用 FOR XML RAW(以元素为中心)
将格式从以属性为中心(如前面的示例所示)更改为以元素为中心意味着将为每一列创建一个新元素。为此,您需要在FOR XML RAW
子句后添加ELEMENTS
关键字,如下面的查询所示。
- Replace the existing query in the query pane with the following query, and click Go:
select Person.Contact.Title, Person.Contact.FirstName, Person.Contact.LastNamefrom Person.Contact where Person.Contact.Title ='Mr.' for xml raw, elements
- You will see a link in the result pane of the query pane. Click on the link, and you should see the result in Figure 7-2 .
图 7-2。使用 FOR XML 原始元素
它是如何工作的
RAW
ELEMENTS
模式产生非常“以元素为中心”的 XML。它将结果集中的每一行、每一列转换为一个属性。
RAW ELEMENTS
模式也不会生成 XML 文档,因为它的根元素(raw
)与结果集中的行数一样多,而一个 XML 文档只能有一个根元素。
试试看:重命名行元素
对于结果集中的每一行,RAW
模式生成一个名为row
的元素。您可以通过为RAW
模式指定一个可选参数来为该元素指定另一个名称,如该查询所示。为此,您需要在FOR XML RAW
子句后添加一个别名,如下面的查询所示。
- Replace the existing query in the query window with the following query, and click Go:
select Person.Contact.Title, Person.Contact.FirstName, Person.Contact.LastNamefrom Person.Contact where Person.Contact.Title ='Mr.' for xml raw ('PersonDetails'), elements
- You will see a link in the result pane of the query pane. Click on the link, and you should see the results in Figure 7-3.
图 7-3。重命名row
元素
它是如何工作的
RAW ('alias')
模式产生输出,其中row
元素被重命名为查询中指定的别名。
因为在查询中添加了ELEMENTS
指令,所以结果是以元素为中心的,这就是为什么用指定的别名重命名了row
元素。如果不在查询中添加ELEMENTS
关键字,那么输出将是以属性为中心的,并且row
元素将被重命名为查询中指定的别名。
关于 XML 原始格式的观察
以下是关于XML RAW
的小技巧:
XML RAW
does not provide the root node, which is why the XML structure is not a well-formed XML document.- Because
XML RAW
supports attribute and element-centered format, all columns must be formatted in the same way. Therefore, it is impossible to return the XML structure of XML attributes and XML elements at the same time.XML RAW
Generate a hierarchical structure in which all elements in the XML structure are at the same level.
使用 FOR XML AUTO
模式将查询结果作为嵌套的 XML 元素返回。这并不能很好地控制从查询结果生成的 XML 的形状。因此,如果您想要生成简单的层次结构,AUTO
模式查询非常有用。
在FROM
子句中的每个表,至少有一列在SELECT
子句中列出,被表示为一个 XML 元素。在SELECT
子句中列出的列被映射到属性或子元素。
尝试一下:使用 FOR XML AUTO
让我们将 SQL 查询结果生成为嵌套的 XML 元素。FOR XML AUTO
是我们将用来生成输出的语句,您将在以下步骤中看到:
- Replace the existing query in the query pane with the following query, and then click Go:
select Person.Contact.Title, Person.Contact.FirstName, Person.Contact.LastName from Person.Contact where Person.Contact.Title ='Mr.' for xml auto
- You will see a link in the result pane of the query pane. Click on the link, and you should see the results in Figure 7-4.
图 7-4。使用 FOR XML AUTO
它是如何工作的
三列,联系。头衔,联系人。名字和联系人。姓氏,引用这个人。联系表。所以,一个人。添加 Contact 元素,并添加三列作为 Person.Contact 的属性。
关于 XML 自动格式化的观察
以下是关于XML AUTO
的提示:
XML AUTO
No root node is provided, which is why the XML structure is not a well-formed XML document.- Because
XML AUTO
supports attribute and element-centered format, all columns must be formatted in the same way. Therefore, it is impossible to return the XML structure of XML attributes and XML elements at the same time.XML AUTO
does not provide a renaming mechanism likeXML RAW
, butXML AUTO
uses the names and aliases of tables and columns (if any).
使用 XML 数据类型
SQL Server 2012 有一个数据类型xml
,它不仅用于保存 XML 文档(本质上是字符串,可以存储在任何足够大的字符列中),还用于处理 XML 文档。当我讨论将 XML 文档解析成 DOM 树时,我没有提到一旦解析完成,XML 文档就可以更新。您可以更改元素内容和属性值,还可以在层次结构中添加和删除元素。
这里不会更新 XML 文档,但是xml
数据类型提供了更新 XML 文档的方法。它是一种非常不同的 SQL Server 数据类型,描述如何利用它需要一本自己的书——可能不止一本。这里的重点是每个数据库程序员需要知道的:如何使用xml
类型来存储和检索 XML 文档。
注意处理 XML 文档的方法太多了(即使在 ADO.NET 和 SQL Server 2000 的支持包 SQLXML 中也是如此),只有时间才能证明将这些特性集成到 SQL Server 数据类型中是否值得。因为 XML 是一项如此重要的技术,能够完全用 T-SQL 处理 XML 文档确实提供了许多可能性,但是现在还不清楚您还需要了解多少关于xml
数据类型的知识。无论如何,这一章将会给你开始实验所需要知道的东西。
试试看:创建一个表来存储 XML
要创建保存 XML 文档的表,请按照下列步骤操作:
-
在对象资源管理器中,选择之前创建的数据库 SQL2012Db,点击新建查询。
注意为了保持冒险工厂的整洁,我使用的是 SQL2012Db 数据库;您可能希望在同一个或另一个数据库中执行以下语句在查询窗格中,键入以下查询,然后单击执行:
create table xmltest ( xid int not null primary key, xdoc xml not null )
工作原理
这与没有xml
数据类型的CREATE TABLE
语句的工作方式相同。尽管我已经说过xml
数据类型不同于其他 SQL Server 数据类型,但是xml
类型的列的定义与其他任何列一样。(但是它们不能用在主键中。)
现在,您将把 XML 文档插入到xmltest
中,并查询它以查看它们是否被存储。
尝试一下:存储和检索 XML 文档
要插入 XML 文档,请按照下列步骤操作:
-
用清单 7-1 中的代码替换 SQL 编辑窗口中的代码。
清单 7-1。将可扩展置标语言文档插入 xmltest
`insert into xmltest
values(
1,
'
CA
California
Berkeley
Los Angeles
Wilmington
DE
Delaware
Newark
Wilmington
'
)insert into xmltest
values(
2,
'
'
)` -
Run two
INSERT
statements, and then display the table withselect * from xmltest
. You will see the two lines displayed. Click thexdoc
column in the first row, and you should see the XML as shown in figure and figure 7-5 .
图 7-5。查看 XML 文档
它是如何工作的
这和所有的工作一样。您只需提供整数形式的主键和字符串形式的 XML 文档。查询也像预期的那样工作。
总结
本章涵盖了每个 C# 程序员都需要了解的 XML 基础知识。它还向您展示了如何使用最常用的 T-SQL 特性从表中提取 XML 并像查询表一样查询 XML 文档。最后,我讨论了xml
数据类型,并给你一些使用它的练习。
关于使用 XML 文档的 XML 或 T-SQL 和 about 工具,您还需要了解多少取决于您需要做什么。对许多人来说,这一章可能是你真正需要了解的关于 XML 的全部内容。对于那些进行更复杂的 XML 处理的人来说,您现在已经有了自己进行实验的坚实基础。在下一章,你将学习事务。
八、了解事务
对于任何企业来说,事务都起着关键作用。它们可能包括许多单独的操作甚至其他事务。对于多个相关操作以及多个用户并发更新数据库的情况,事务对于维护数据完整性至关重要。
本章将讨论与事务相关的概念以及如何在 SQL Server 2012 中使用事务。
在本章中,我将介绍以下内容:
- What is a deal?
- When to use transactions
- Understanding ACID attributes
- Transaction design
- Transaction status
- Specify transaction boundary
- T-SQL statements allowed in transactions
- Local transactions in SQL Server 2012
- Distributed transactions in SQL Server 2012
- Guidelines for writing efficient transactions
- How to write transactions
什么是事务?
一个事务是一组被执行的操作,因此所有操作作为一个单元被保证成功或失败。
事务的一个常见例子是将钱从支票账户转移到储蓄账户的过程。这涉及到两个操作:从支票账户中扣款,并将其添加到储蓄账户中。两者必须同时成功并被提交到账户,或者两者必须同时失败并被回滚,以便账户保持一致的状态。在任何情况下,钱都不能从支票账户中扣除,而不能存入储蓄账户(反之亦然)。通过使用事务,可以保证借贷两种操作同时成功或失败。因此,两个帐户始终保持一致的状态。
何时使用事务
当几个操作必须作为一个整体成功或失败时,应该使用事务。以下是一些建议使用事务的常见场景:
- In batch processing, when changes to one table require other tables to be consistent, multiple rows must be inserted, updated or deleted as a unit.
- When data in two or more databases are modified at the same time
- In a distributed transaction
- To manipulate data in databases on different servers.
当您使用事务时,您将锁定数据,等待对数据库的永久更改。在释放锁之前,不能对锁定的数据进行其他操作。您可以锁定从单个行到整个数据库的任何内容。这被称为并发性,意味着数据库如何一次处理多个更新。
在银行示例中,锁确保两个独立的事务不会同时访问同一个帐户。如果他们这样做,存款或取款都可能丢失。
注意让事务保持最短的等待时间是很重要的。锁阻止其他人访问锁定的数据库资源。太多的锁或者频繁访问的资源上的锁会严重降低性能。
了解酸的特性
事务由四个属性来表征,通常称为 ACID 属性:原子性、一致性、隔离性和持久性。
注酸一词是安德烈·路透在 1983 年提出的。
原子性:如果一个事务被视为一个单独的动作,而不是一组独立的操作,那么它就是原子的。因此,只有当所有单独的操作都成功时,事务才会成功并提交给数据库。另一方面,如果单个操作在事务期间失败,则所有操作都被认为已经失败,如果已经发生,则必须撤消(回滚)。对于 Northwind 数据库的订单输入系统,当您在 Orders 和 Order Details 表中输入订单时,数据将一起保存在这两个表中,或者根本不保存。
一致性:事务应该使数据库保持一致的状态——不管它是否成功完成。事务修改的数据必须符合对列的所有约束,以维护数据完整性。对于 Northwind,如果 Orders 表中没有相应的行,则 Order Details 表中不能有行,因为这将使数据处于不一致的状态。
隔离:每个事务都有一个明确定义的边界——也就是说,它与另一个事务是隔离的。一个事务不应该影响同时运行的其他事务。一个事务所做的数据修改必须与所有其他事务所做的数据修改隔离开来。一个事务看到数据处于另一个并发事务修改它之前的状态,或者在第二个事务完成后看到数据,但看不到中间状态。
持久性:成功事务中发生的数据修改将永久保存在系统中,不管发生了什么。维护事务日志,以便在出现故障时,数据库可以恢复到故障前的原始状态。每个事务完成后,都会在数据库事务日志中记录一行。如果出现了需要从备份中恢复数据库的重大系统故障,那么可以使用这个事务日志来插入(前滚)已经发生的任何成功的事务。
为事务提供支持的每个数据库服务器都会自动实施这四个 ACID 属性。
事务设计
事务表示真实世界的事件,例如银行事务、机票预订、资金汇款等等。
事务设计的目的是定义和记录数据库系统所需的事务的高级特征,包括以下内容:
- Data to be used for the transaction
- The functional characteristics of the transaction
- Output of the transaction
- Importance to users
- Expected utilization rate
有三种主要的事务类型:
- Retrieval transaction : Retrieve data from the display screen.
- Update transaction : Insert new records, delete old records or modify existing records in the database.
- Mixed transaction : It involves retrieving and updating data.
事务状态
在没有失败的情况下,所有事务都成功完成。然而,事务可能不总是成功地完成其执行。这种事务被称为中止。
成功完成其执行的事务被称为提交。 图 8-1 显示如果一个事务已经被部分提交,它将被提交但仅当它没有失败;如果事务失败,它将被中止。
图 8-1。事务状态
指定事务边界
SQL Server 事务边界通过使用 API 函数和方法帮助您确定 SQL Server 事务的开始和结束时间:
- T7 Transact-SQL Statement T8: Use statements T0, T1, T2, T3, T4 and T5 to describe transactions. These are mainly used in DB-Library applications and T-SQL scripts, such as scripts that use the
osql
command to prompt the utility to run.- API functions and methods :ODBC, OLE DB, ADO, the and other database APIs. Namespaces contain functions or methods for describing transactions. These are the main mechanisms for controlling transactions in database engine applications.
每个事务只能由这些方法中的一种来管理。在同一事务中使用这两种方法会导致不确定的结果。例如,您不应该使用 ODBC API 函数启动事务,然后使用 T-SQL COMMIT
语句来完成事务。这不会通知 SQL Server ODBC 驱动程序事务已提交。在这种情况下,使用 ODBC SOLEndTran
函数来结束事务。
事务中允许的 T-SQL 语句
您可以在一个事务中使用所有 T-SQL 语句,但以下语句除外:ALTER DATABASE
、RECONFIGURE
、BACKUP
、RESTORE
、CREATE DATABASE
、UPDATE STATISTICS
和DROP DATABASE
。
此外,您不能使用spdboption
来设置数据库选项,也不能使用任何在显式或隐式事务中修改主数据库的系统过程。
SQL Server 2012 中的本地事务
所有数据库引擎都应该为事务提供内置支持。仅限于单个资源或数据库的事务被称为本地事务。本地事务可以是以下四种事务模式之一:
Auto-commit transaction : Auto-commit mode is the default transaction management mode of SQL Server. Every T-SQL statement will be submitted or rolled back after completion. If the statement completes successfully, submit the statement; If it encounters any errors, it will definitely roll back. As long as the default mode is not covered by any type of transaction, the SQL Server connection will run in auto-commit mode.
显式事务:显式事务是那些你显式控制事务何时开始,何时结束的事务。在 SQL Server 2000 之前,显式事务也被称为用户定义的或用户指定的事务。
这种模式的数据库备份与还原脚本使用了
BEGIN TRANSACTION
、COMMIT TRANSACTION
和ROLLBACK TRANSACTION
语句。显式事务模式仅在事务持续期间有效。当事务结束时,连接返回到显式事务开始之前的事务模式.隐式事务:当您使用 SQL Server Management Studio 连接到数据库并执行 DML 查询时,更改会自动保存。这是因为,默认情况下,连接处于自动提交事务模式。如果您不希望提交任何更改,除非您明确指出,则需要将连接设置为隐式事务模式。
您可以使用
SET IMPLICIT TRANSACTIONS ON|OFF
将数据库连接设置为隐式事务模式.将连接的隐式事务模式设置为
ON
后,SQL Server 会在首次执行以下语句时自动启动一个事务:ALTER TABLE``CREATE``DELETE``DROP``FETCH``GRANT``INSERT``OPEN``REVOKE``SELECT``TRUNCATE TABLE
和UPDATE
.事务保持有效,直到明确发布了
COMMIT
或ROLLBACK
语句。这意味着,当对数据库中的特定记录发出UPDATE
语句时,SQL Server 将保持对数据修改范围内的数据的锁定,直到发出COMMIT
或ROLLBACK
为止。如果这两个命令都没有发出,当用户断开连接时,事务将自动回滚。这就是为什么在高度并发的数据库上使用隐式事务模式不是最佳实践的原因Batch-scope transaction : If multiple active result sets (MARS) are enabled for the transaction running in it, the connection can be in batch-scope transaction mode. Basically, MARS has an associated batch execution environment, because it allows ADO.NET to take advantage of the ability of SQL Server 2012 to have multiple active commands on a single connection object.
当启用 MARS 时,您可以同时执行多个交错的批处理,因此对执行环境所做的所有更改都局限于特定的批处理,直到该批处理的执行完成。一旦批处理的执行完成,执行设置将被复制到默认环境中。因此,如果一个连接正在运行一个事务,在其上启用了 MARS,并且同时运行多个批处理,则称该连接正在使用批处理范围的事务模式。
MARS 允许执行多批交错的命令。但是,MARS 不允许在同一个连接上有多个事务;它只允许有多个活动的结果集。
SQL Server 2012 中的分布式事务
与局限于单个资源或数据库的本地事务相反,分布式事务跨越两个或多个服务器,它们被称为资源管理器。事务管理需要通过称为事务管理器或事务协调器的服务器组件在资源管理器之间进行协调。 SQL Server 可以作为由事务管理器(如 Microsoft 分布式事务协调器(MS DTC ))协调的分布式事务的资源管理器。
跨两个或更多数据库的单个 SQL Server 事务实际上是分布式事务。但是,SQL Server 在内部管理分布式事务。
在应用级别,分布式事务的管理方式与本地事务非常相似。在事务结束时,应用请求提交或回滚事务。事务管理器必须以不同的方式管理分布式提交,以最大限度地降低网络故障的风险,网络故障可能会导致由于各种原因导致的故障,导致其中一个资源管理器提交而不是回滚事务。这种危急情况可以通过分两个阶段管理提交过程来处理,也称为两阶段提交:
- Preparation stage : When the transaction manager receives the submission request, it sends a preparation command to all resource managers involved in the transaction. Then, each resource manager performs all operations required to persist the transaction and flushes all buffers holding any log images of other transactions to disk. When each resource manager completes the preparation phase, it returns the success or failure of the preparation phase to the transaction manager.
- Submission stage : If the transaction manager receives successful preparations from all resource managers, it sends a
COMMIT
command to each resource manager. If all resource managers report successful submission, the transaction manager will send a successful notification to the application. If any resource manager reports that the preparation fails, the transaction manager will send aROLLBACK
statement to each resource manager and indicate the submission failure to the application.
编码高效事务的指南
我建议您在编写事务代码时使用以下准则,以尽可能提高效率:
Do not require input from users during a transaction.
Get all necessary inputs from users before the transaction starts. If additional user input is needed during the transaction, roll back the current transaction and restart the transaction after providing user input. Even if users respond immediately, the reaction time of human beings is much slower than that of computers. All resources held by a transaction are held for a very long time, which may lead to blocking problems. If the user does not respond, the transaction will remain active and key resources will be locked until the user responds, which may take several minutes or even hours.
Do not open a transaction while browsing through data, if at all possible.
Trading should not be started until all preliminary data analysis is completed.
Keep the transaction as short as possible.
After you know the necessary modification, start a transaction, execute the modification statement, and then immediately commit or roll back. Don't open the transaction until you need it.
Make intelligent use of lower cursor concurrency options, Such as optimal concurrency options.
In a system with low probability of concurrent update, the cost of dealing with the occasional "after you read the data, others changed your data" error is much lower than that of always locking the data rows when reading the data.
Access the least amount of data possible while in a transaction.
The smaller the amount of data you access in a transaction, the smaller the number of rows locked, thus reducing the contention between transactions.
如何为事务编码
以下三个 T-SQL 语句控制 SQL Server 中的事务:
BEGIN TRANSACTION
: It marks the beginning of a transaction.COMMIT TRANSACTION
: marks the successful completion of the transaction. It will notify the database to save the job.ROLLBACK TRANSACTION
: This indicates that the transaction failed, and informs the database to roll back to the state before the transaction.
注意没有END TRANSACTION
语句。事务在(显式或隐式)提交和回滚时结束。
T-SQL 中的编码事务
您将使用存储过程来练习用 SQL 编写事务代码。这是一个有意人为的例子,但是代表了事务处理的基本原理。它让事情变得简单,这样你就可以专注于事务中可能发生的重要问题。这才是你真正需要理解的,尤其是在本书后面的 C# 应用中编写与事务相关的活动时。
![Image 警告在存储过程中使用ROLLBACK
和COMMIT
通常需要仔细考虑哪些事务可能已经在进行中并导致了存储过程调用。这个例子是自动运行的,所以在这里您不需要关心这个问题,但是您应该总是考虑它是否是一个潜在的问题。##### 试试看:创造亲子关系在编写事务代码之前,让我们创建两个表。1. Open SQL Server Management Studio, select the previously created database SQL2012Db in Object Explorer, right-click, and click New Query.2. 输入清单 8-1 中的 SQL 语句,创建带有主键和外键(换句话说,父子关系)的表。Person 表将有一个主键列,PersonDetails 表将通过外键列引用该主键列。 清单 8-1。创建父子关系创建表人 ( PersonID nvarchar(5) primary key not null, FirstName nvarchar(10) not null, Company nvarchar(15) ) create table PersonDetails ( PersonID nvarchar(5) foreign key references dbo.Person(PersonID), Address nvarchar(30) )
3. 现在点击执行。您将看到状态为“命令成功完成”,如图图 8-2 所示。
*图 8-2。执行创建表格语句(父子关系)*
-
接下来让我们将一些数据插入到 Person 和 PersonDetails 表中;执行清单 8-2 中的语句,并点击执行。
T31清单 8-2。创建父子关系 T35
Insert into Person values('Vidvr','Vidya Vrat','Lionbridge Inc'), ('Rupag','Rupali', 'Pearl Solutions')
该语句应显示状态“(2 行受影响)”
子级只能有那些映射到父级的记录;因此,我们可以在 PersonDetails 中只插入 Person 表中已经可用的 PersonIDs 的子记录。
Insert into PersonDetails values('Vidvr','Bellevue WA 98007'), ('Rupag', 'Bellevue WA 98007')
如您所见,子表的 PersonID 与父表匹配。
因此,现在我们有了一个完美的父子关系,在 Person 和 PersonDetails 表中有两个父记录和两个匹配的子记录,如图 8-3 所示。
图 8-3。显示 Person 和 PersonDetails 表之间的父子关系
试试看:用 T-SQL 编写事务代码
按照以下步骤对事务进行编码:
-
在这里,您将基于 Person 和 PersonDetails 表编写一个事务,我们将利用 SQL Server 的主键和外键规则来理解事务是如何工作的。Person 表有三列。PersonID 和 FirstName 两列不允许空值,PersonID 也是主键列;也就是说,只允许唯一的值。另外,最后一列 Company 允许空值。
同样,人员详细信息表是一个外键或子表;它有一个拟人列,这是一个外键列,引用人。PersonID .子表或外键表只能包含那些在父表或主键表中具有匹配主键列值的记录,如图 8-3 所示。如果插入的子记录没有匹配的父记录或主键值,那么它将导致错误,并且不会被插入到子表中在对象资源管理器中选择 SQL2012Db 数据库,点击新建查询按钮。使用清单 8-3 中的代码创建一个名为
sp_Trans_Test
的存储过程。清单 8-3。spTransTest
`create procedure sp_Trans_Test
@newpersonid nvarchar(5),
@newfirstname nvarchar(10),
@newcompanyname nvarchar(15),
@oldpersonid nvarchar(5)
as
declare @inserr int
declare @delerr int
declare @maxerr intset @maxerr = 0
begin transaction
-- Add a person
insert into person (personid, firstname, company)
values(@newpersonid, @newfirstname, @newcompanyname)-- Save error number returned from Insert statement
set @inserr = @@error
if @inserr > @maxerr
set @maxerr = @inserr-- Delete a person
delete from person
where personid = @oldpersonid-- Save error number returned from Delete statement
set @delerr = @@error
if @delerr > @maxerr
set @maxerr = @delerr
-- If an error occurred, roll back
if @maxerr <> 0
begin
rollback
print 'Transaction rolled back'
end
else
begin
commit
print 'Transaction committed'
end
print 'INSERT error number:' + cast(@inserr as nvarchar(8))
print 'DELETE error number:' + cast(@delerr as nvarchar(8)) -
Enter the following query in the query pane with the same code as Listing 8-3 . Select the statement, as shown in figure T3, figure 8-2 and T4, and then click Execute to run the query.
exec sp_Trans_Test 'Pearl', 'Vamika ', null,'Agraw'
-
The result pane should display the return value of 0, and you should see the same message as in Figure 8-4.
Select the statements as shown in figure , figure 8-3 , and then click the execute button. You will see that the person named Wamika has been added to the table, as shown in the result tab of T11 in Figure 8-3
Figure 8-5. Line inserted in the transaction
-
Add another person with the parameter value. Enter the following statement and execute it as you did for other similar statements.
exec sp_Trans_Test 'Spark', 'Arshika ', null,'Agarw'
-
In the message tab, you should get the same result as shown in the previous figure 8-4 .
-
Try the
SELECT
statement shown in Figure 8-4 again. You should see that Arshika has been added to the Person table. Vamika and Arshika have no child records in the PersonDetails table.
它是如何工作的
在存储过程中,您定义了四个输入参数:
`create procedure sp_Trans_Test
@newpersonid nvarchar(5),
@newfirstname nvarchar(10),
@newcompanyname nvarchar(15),
@oldpersonid nvarchar(5)
as`
您还声明了三个局部变量:
declare @inserr int declare @delerr int declare @maxerr int
这些局部变量将与存储过程一起使用,因此您可以捕获并显示从INSERT
和DELETE
语句返回的错误号(如果有的话)。
您用一个BEGIN TRANSACTION
语句标记事务的开始,然后用作为事务一部分的INSERT
和DELETE
语句跟随它。在每条语句之后,保存它的返回号。
`begin transaction
-- Add a person
insert into person (personid, firstname, company)
values(@newpersonid, @newfirstname, @newcompanyname)
-- Save error number returned from Insert statement
set @inserr = @@error
if @inserr > @maxerr
set @maxerr = @inserr
-- Delete a person
delete from person
where personid = @oldpersonid
-- Save error number returned from Delete statement
set @delerr = @@error
if @delerr > @maxerr
set @maxerr = @delerr`
在 SQL Server 中,错误处理始终非常重要,尤其是在事务性代码中。当您执行任何 T-SQL 语句时,总有可能会失败。T-SQL @@ERROR
函数返回最后执行的 T-SQL 语句的错误号。如果没有发生错误,@@ERROR
返回零。
在每一个 T-SQL 语句(甚至是SET
和IF
)被执行后,@@ERROR
被重置,所以如果你想保存一个特定语句的错误号,你必须在下一个语句执行前存储它。这就是为什么你要声明局部变量@inserr
、@delerr
和@maxerr
。
如果@@ERROR
返回除 0 之外的任何值,则发生了错误,并且您想要回滚事务。还包括PRINT
语句来报告是否发生了回滚或提交。
-- If an error occurred, roll back if @maxerr <> 0 begin rollback print 'Transaction rolled back' end else begin commit print 'Transaction committed' end
提示 T-SQL(和标准 SQL)支持关键字和短语的各种替代形式。这里你只使用了ROLLBACK
和COMMIT
。
然后添加更多的工具,这样就可以看到在事务处理过程中遇到了哪些错误号。
print 'INSERT error number:' + cast(@inserr as nvarchar(8)) print 'DELETE error number:' + cast(@delerr as nvarchar(8)) return @maxerr
现在让我们看看当您执行存储过程时会发生什么。您运行了两次,第一次添加了 Pearl,第二次添加了 Spark,但是每次都输入了同一个不存在的人 Agarw 来删除。如果一个事务中的所有语句作为一个单元应该成功或失败,为什么当DELETE
没有删除任何东西时INSERT
成功了?
图 8-4 应该把一切都说清楚了。INSERT
和DELETE
都返回错误号 0。即使没有删除任何行,DELETE
也返回错误号 0 的原因是,当DELETE
没有找到任何要删除的行时,T-SQL 不会将其视为错误。事实上,这就是为什么你用一个不存在的人。除去这些最近添加的人 Pearl 和 Spark,其他记录在 PersonDetails 表中都有子记录,如图图 8-3 所示;因此,您不能删除现有人员,除非您首先从 PersonDetails 表中删除他们的详细信息。
试试看:第一次操作失败时会发生什么
在本例中,您将尝试插入一个重复的人员并删除一个现有的人员。通过输入以下语句添加 Pearl 并删除 Spark,然后单击 Execute 按钮:
exec sp_Trans_Test 'Pearl', 'Vamika', null,'Spark'
结果应如图 8-6 中的所示。
图 8-6。第一次操作失败,第二次操作回滚
在图 8-6 所示的消息窗格中,注意整个事务被回滚,因为INSERT
失败并终止,错误号为 2627(其错误消息出现在窗口顶部)。DELETE
错误号为 0,意味着它成功执行但被回滚。(如果你查一下表,你会发现 Spark 仍然存在于 Person 表中。)
它是如何工作的
因为 Pearl 已经存在,并且 Person 表的 PersonID 列是主键,并且只能包含唯一值,所以 SQL Server 禁止插入重复值,所以第一个操作失败。执行事务中的第二个DELETE
语句,Spark 被删除,因为它在 PersonDetails 表中没有任何子记录;但是因为gmaxerr
不是 0(它是 2627,正如您在结果窗格中看到的),所以您通过撤销 Spark 的删除来回滚事务。因此,您可以看到表中的所有记录。
试试看:当第二次操作失败时会发生什么
在本例中,您将插入一个有效的新人员,并尝试删除在 PersonDetails 表中有子记录的人员。
通过输入以下语句添加 ag 并删除 Vidvr,然后单击 Execute 按钮:
exec sp_Trans_Test 'ag', 'Agarwal ',null, 'Vidvr'
结果应如图 8-7 中的所示。
图 8-7。第二次操作失败,第一次操作回滚
在图 8-7 所示的消息窗格中,注意事务被回滚,因为DELETE
失败并终止,错误号为 547(其消息出现在窗口顶部)。INSERT
错误号为 0,因此它显然执行成功,但被回滚。(如果你查一下表格,你会发现 ag 不是一个人。)
它是如何工作的
由于 ag 不存在,SQL Server 会插入该行,因此第一个操作会成功。当执行事务中的第二条语句时,SQL Server 阻止删除客户 Vidvr,因为它在 PersonDetails 表中有子记录,但是由于gmaxerr
不是 0(它是 547,正如您在结果窗格中看到的),整个事务被回滚。
试试看:当两个操作都失败时会发生什么
在本例中,您将尝试插入一个无效的新人员,换句话说,一个具有重复名称的人员,并尝试删除一个不可删除的人员,换句话说,一个在 PersonDetails 表中具有子记录的人员。
通过输入以下语句添加 Pearl 并删除 customer Rupag,然后单击 Execute 按钮:
exec sp_Trans_Test 'Pearl', 'Vamika', null,'Rupag'
结果应如图 8-8 所示。
图 8-8。两个操作都回滚
在图 8-8 的所示的消息窗格中,注意事务被回滚(尽管两个语句都没有成功,所以没有什么可以回滚),因为gmaxerr
为INSERT
返回 2627,为DELETE
返回 547。两个失败语句的错误消息都显示在窗口的顶部。
它是如何工作的
现在,您应该明白为什么这两条语句都失败了。这是因为第一条语句不能插入重复记录,第二条语句不能删除有关联子记录的记录。这就是为什么图 8-8 中的消息窗格显示了明确提到重复键的错误和与子记录冲突的引用。
总结
本章讲述了事务的基础知识,包括理解什么是事务、ACID 属性、本地和分布式事务、编写高效事务的指南以及用 T-SQL 编写事务代码等概念。尽管本章仅提供了事务的基础知识,但您现在已经对编码事务有了足够的了解,可以处理基本的事务处理,并在后面的章节中使用 C# 和 about 实现它。
在下一章,你将学习 Windows 窗体的基础知识。
九、构建 Windows 窗体应用
本章介绍 Windows 窗体以及如何使用 C# 2012 开发 Windows 窗体应用。
在本章中,我们将介绍以下内容:
- Understanding Windows forms
- User interface design principles
- User interface design best practices
- Use Windows forms
- Understanding design and code views
- Sort properties in the properties window.
- Set the properties of solutions, projects and Windows forms
- Using controls
- Set docking and anchor properties
- Add a new form to the project
- Implement MDD
了解 Windows 窗体
Windows 窗体,也称为 WinForms,是图形用户界面(GUI)应用编程接口(API)的名称,是微软的一部分。NET Framework,通过在托管代码中包装现有的 Windows API 来提供对本机 Microsoft Windows 界面元素的访问。
WinForms 是用户界面的基本构件。它们作为容器来承载允许您呈现应用的控件。WinForms 是应用开发中最常用的界面,尽管其他类型的应用也是可用的,如控制台应用和服务。但是 WinForms 提供了与用户交互的最佳方式,并接受按键或鼠标点击形式的用户输入。
用户界面设计原则
与任何应用交互的最佳机制通常是用户界面。因此,拥有易于使用的高效设计变得非常重要。在设计用户界面时,你首先要考虑的应该是使用这个应用的人。他们是你的目标受众,了解你的目标受众可以让你更容易地设计用户界面,帮助用户学习和使用你的应用。另一方面,一个设计糟糕的用户界面,如果导致目标受众回避甚至放弃你的应用,会导致沮丧和低效。
窗体是 Microsoft Windows 应用的主要元素。因此,它们为每个级别的用户交互提供了基础。可以将各种控件、菜单等添加到窗体中,以提供特定的功能。除了功能性之外,你的用户界面应该对用户有吸引力。
用户界面设计的最佳实践
用户界面为用户提供了与应用交互的机制。因此,易于使用的高效设计至关重要。下面是一些设计用户友好、优雅和简单的用户界面的指导原则。
简单
简单是用户界面的一个重要方面。视觉上“繁忙”或过于复杂的用户界面使得学习应用更加困难和耗时。用户界面应该允许用户快速完成程序所需的所有交互,但是它应该只暴露应用每个阶段所需的功能。在设计用户界面时,你应该记住程序的流程和执行,这样你的应用的用户会发现它很容易使用。显示相关数据的控件应该在表单上组合在一起。ListBox、ComboBox 和 CheckBox 控件可用于显示数据,并允许用户在预设选项之间进行选择。
使用 tab 键顺序(用户通过按 Tab 键在表单上的控件间循环的顺序)允许用户快速导航字段。
在设计用户界面时,试图重现真实世界的物体是一个常见的错误。例如,如果您想创建一个代替纸质表单的表单,那么很自然地会尝试在应用中复制纸质表单。这种方法可能适用于某些应用,但对于其他应用,它可能会限制应用,并且不会给用户带来真正的好处,因为复制纸质表单会限制应用的功能。设计应用时,考虑您的独特情况,并尝试使用计算机的功能来增强目标受众的用户体验。
默认值是简化用户界面的另一种方式。例如,如果您希望应用的 90%的用户在州字段中选择华盛顿,请将华盛顿作为该字段的默认选项。
设计用户界面时,来自目标受众的信息是最重要的。设计用户界面时最好的信息是来自目标受众的输入。定制您的界面,使频繁的任务易于执行。
控制位置
用户界面上控件的位置应该反映它们的相对重要性和使用频率。例如,如果你有一个既用于输入必需信息又用于输入可选信息的表单,那么必需信息的控件就更重要,应该得到更大的重视。在西方文化中,用户界面通常被设计成从左到右和从上到下阅读。最重要或最常用的控件最容易在窗体顶部访问。用户完成表单上的操作后将使用的控件,如提交按钮,应该遵循信息的逻辑流程,并放在表单的底部。
还需要考虑信息的相关性。相关信息应该显示在组合在一起的控件中。例如,如果您有一个显示有关客户、采购订单或雇员信息的窗体,您可以将每组控件分组到一个选项卡控件上,使用户可以轻松地在显示之间来回移动。
美观也是放置控件的一个重要考虑因素。您应该尽量避免显示比一目了然更多信息的表单。只要有可能,控件之间应该留有足够的空间,以创造视觉吸引力和易用性。
一致性
您的用户界面应该在应用中的每个表单上展示一致的设计。不一致的设计会让你的应用看起来杂乱无章,阻碍你的目标用户的采用。当用户在表单间导航时,不要要求他们适应新的视觉元素。
一致性是通过在整个应用中使用颜色、字体、大小和控件类型来实现的。在进行任何实际的应用开发之前,您应该决定一个在整个应用中保持一致的可视化方案。
美学
只要有可能,用户界面应该是吸引人的和令人愉快的。虽然清晰和简单不应该为了吸引人而牺牲,但是你应该努力创建一个不会阻止用户使用它的应用。
颜色
明智地使用颜色有助于让你的用户界面吸引目标受众,并吸引他们使用。然而,过度使用颜色是很容易的。鲜艳的颜色可能会吸引一些用户,但其他人可能会有负面反应。为应用设计背景配色方案时,最安全的做法是使用具有广泛吸引力的柔和颜色。
总是研究任何与颜色相关的特殊含义,这些含义可能会影响用户对你的应用的反应。如果你正在为一家公司设计应用,你可以考虑在你的应用中使用该公司的公司配色方案。为国际观众设计时,要意识到某些颜色可能具有文化意义。保持一致性,不要过度使用颜色。
总是考虑颜色如何影响可用性。例如,白色背景上的灰色文本可能难以阅读,从而削弱可用性。此外,请注意与色盲相关的可用性问题。例如,有些人不能区分红色和绿色。因此,这类用户看不到绿色背景上的红色文本。不要仅仅依靠颜色来传达信息。对比也能吸引人们对应用中重要元素的注意。
来源
可用性应该决定你为应用选择的字体。为了方便使用,避免使用难以阅读或修饰过的字体。坚持使用简单易读的字体,如 Palatino 或 Times New Roman。此外,与其他设计元素一样,字体应该在整个应用中保持一致。使用草书或装饰性字体只是为了视觉效果,比如在合适的时候用在扉页上,不要传达重要信息。
图像和图标
图片和图标增加了应用的视觉趣味,但是精心的设计对它们的使用是必不可少的。看起来“忙碌”或分散用户注意力的图像会阻碍你的应用的使用。图标可以传达信息,但同样,在决定使用它们之前,需要仔细考虑最终用户的反应。例如,您可以考虑使用类似于美国停止标志的红色八角形来表示用户可能不想在应用中超过该点。只要有可能,图标应该保持简单的形状,容易在 16x 16 像素的正方形中呈现。
使用 Windows 窗体
要使用 Windows 窗体,您需要使用 Visual Studio 2012 创建一个 Windows 窗体应用项目。为此,请单击开始所有程序 Visual Studio 2012,并从显示的列表中选择 Microsoft Visual Studio 2012。这将打开 Visual Studio 起始页。点击文件新建项目。现在你会看到新建项目对话框,你可以从中选择 Windows 窗体应用模板,如图图 9-1 所示。
图 9-1。选择 Windows 窗体应用项目模板
默认情况下,该项目被命名为 WindowsFormsApplication1(下一个是 WindowsFormsApplication2,依此类推)。选择项目模板时,您可以在“名称”文本框中为项目输入另一个名称,也可以在以后重命名项目。
一旦选择了 Windows 窗体应用模板以及所需的名称和位置,请单击“确定”。这将打开 Visual Studio 集成开发环境(IDE ),之所以这样称呼是因为它将所有与开发相关的工具、窗口、对话框、选项等嵌入(或集成)在一个公共窗口中,这使得开发过程更加容易。
在 IDE 中,当您打开项目时,您会看到一个名为Form1.cs
的 Windows 窗体已被添加,并且在右侧您还可以看到 Solution Explorer 窗口。您还需要了解另一个名为属性窗口的窗口。如果“解决方案资源管理器”窗口下的“属性”窗口不可用,可以通过单击“查看属性窗口”或按 F4 打开它。现在开发环境将看起来像图 9-2 。
图 9-2。带有解决方案资源管理器和属性窗口的 IDE
因为这是一个 Windows 窗体应用项目,所以您将使用允许您以 GUI 形式实现功能的控件或工具。您可以从工具箱中选取控件,工具箱显示在开发环境中 Windows 窗体的左侧。如果将鼠标指针悬停在工具箱选项卡上,将会打开工具箱窗口。展开所有 Windows 窗体工具集,如图图 9-3 所示。您可以从那里选取控件并将其放在 Windows 窗体的表面上。
图 9-3。带工具箱的 IDE
理解设计和代码视图
在 Visual Studio IDE 中,您主要处理两个视图:设计视图和代码视图。当你打开 Visual Studio IDE 时,默认情况下它会显示设计视图,如图图 9-3 所示。“设计”视图允许您将控件拖放到窗体上。您可以使用“属性”窗口设置对象和窗体或解决方案资源管理器中显示的其他文件的属性。解决方案资源管理器还允许您重命名项目、窗体甚至项目中包含的其他文件。通过选择这些对象,单击鼠标右键,然后从上下文菜单中选择“重命名”,可以重命名这些对象。
基本上,“设计”视图为您提供了一种处理控件、对象、项目文件等的可视化方式。当您使用代码来实现位于 Windows 窗体表面的可视化控件背后的功能时,您会希望使用 Visual Studio IDE 中的另一个可用视图“代码”视图。
若要从“设计”视图切换到“代码”视图,请单击“查看代码”,或者在“设计”视图中右击 Windows 窗体并选择“查看代码”。这两种方法都会为你打开代码视图,如图图 9-4 所示。
图 9-4。代码视图
代码视图显示所有代码功能。在图 9-4 中,注意Form1.cs
页签(你看到的是代码视图)在 Form1.cs【设计】页签旁边,它实际上是 Windows 窗体 Form1 的设计视图;这些选项卡允许您在“设计”视图的所有 GUI 元素和“代码”视图中帮助您实现功能的相关代码之间切换。有趣的是,如果您尝试在代码视图中访问工具箱,您会看到工具箱中没有任何控件。但是当您切换回设计视图时,您会发现工具箱已经完全加载了控件。
若要切换回“设计”视图,请在“代码”视图中右击窗体,然后选择“视图设计器”。您将会看到,现在您回到了设计视图,并且可以继续使用可视元素或控件。
您还可以使用解决方案资源管理器在“设计”和“代码”视图之间切换,方法是选择所需的 Windows 窗体(如果您打开了多个 Windows 窗体),右击并选择“查看代码”或“视图设计器”。这将打开所选 Windows 窗体的代码视图或设计视图。
在属性窗口中排序属性
每个对象(如表单控件)都有许多属性,您可能需要在使用任何应用时设置这些属性。为了帮助您浏览“属性”窗口中列出的许多属性,您可以按类别或字母顺序对它们进行排序。让我们来看看这些排序选项中的每一个。
分类视图
分类视图以属性集的形式组织属性,每个属性集都有一个名称来描述属性集合;例如,有名称为“外观”、“行为”、“数据”、“设计”、“焦点”等类别。通过单击显示在“属性”窗口顶部的工具栏最左边的图标,可以切换到分类视图。
在显示分类视图的图-9-5 中,在外观类别下,您将看到所有定义对象(在本例中是一个表单)外观和感觉的属性。注意其他类别也显示在图 9-5 中。
注在图 9-5 中,我们有意将其他类别保持在折叠模式,只是为了向您展示所有类别。当您切换到 Categorized 视图时,您会看到默认情况下所有的类别都是展开的。
图 9-5。房产分类视图
按字母顺序查看
字母视图按名称从 a 到z升序组织属性。您可以通过单击属性窗口顶部工具栏上左起第二个图标切换到字母视图。
在显示该视图的图 9-6 中,所有列出的属性按字母顺序排列。使用按字母顺序排列的视图,而不是按类别排列的视图,会使工作变得更加容易。例如,假设您正在寻找字体属性。在分类视图中,您必须知道该属性位于哪个类别下才能找到它。但是,如果您有按字母顺序组织的属性,您可以很容易地找到这个属性,因为它是以字母 F 开始的,所以您知道是否需要后退或前进来找到您的控件的这个属性。
图 9-6。按字母顺序排列的房产视图
设置解决方案、项目和 Windows 窗体的属性
在你开始在 Windows 窗体上放置控件之前,你需要学习如何修改你之前创建的解决方案、项目和窗体的一些属性值(如前面的图 9-2 所示)。
选择 WindowsFormsApplication1 解决方案,转到属性窗口,将其 Name 属性值设置为 Chapter9 。
注意在某些情况下,您可能无法在 Visual Studio 中看到解决方案(.sln
)文件。要列出一个解决方案文件,比如解决方案第九章(1 项目),如图图 9-7 所示,你必须点击工具选项,转到项目和解决方案选项卡,选择常规,勾选“总是显示解决方案”选项,然后点击确定。
在解决方案资源管理器中选择 WindowsFormsApplication1 项目,转到“属性”窗口,并修改定义项目文件名的项目文件属性值,使其显示为WinApp.csproj
。
现在更改 Windows 窗体的名称:在解决方案资源管理器中选择Form1.cs
,在属性窗口中将文件名属性从Form1.cs
修改为WinApp.cs
,并在出现的对话框中单击“是”。
现在单击位于解决方案资源管理器窗口中的 Form1。一旦选择了 Form1,您将在“属性”窗口中看到属性列表已经更改。选择 Text 属性,并将其值从 Form1 修改为 Windows 应用。Text 属性定义显示在窗体标题栏上的名称。
在设置了解决方案、项目和 Windows 窗体的属性后,IDE 将看起来像图 9-7 。
图 9-7。在设置了解决方案、项目和 Windows 窗体的属性后,使用 IDE
使用控件
现在已经有了 Windows 窗体应用,可以开始使用控件了。
任何 Windows 应用的基本元素都是控件,它通过提供嵌入在应用中的代码功能的视觉含义来发挥重要作用。
最常用的控件是标签、按钮、文本框、单选按钮、复选框、列表框、组合框、MenuStrip 和 ContextMenuStrip。没有这些控件,应用就无法存在,所以您将看到如何将其中一些控件合并到您的应用中。
试试看:使用标签、文本框和按钮控件
在本练习中,您将创建一个带有三个标签、两个文本框和一个按钮的 Windows 窗体应用。应用将接受你的名字作为输入,然后以对话框的形式闪现一条“欢迎”消息。
-
转到您之前创建的名为 Chapter9 的解决方案下名为 WinApp 的项目(参见图 9-7 )。确保您处于设计视图中。
-
Drag a Label control onto the form, and position it at the top middle of the form. Select this label, navigate to the Properties window, and set the following properties:
- 将 Name 属性设置为 lblWelcome。
- 将 Text 属性设置为 Welcome。
- 选择“字体”属性,单击省略号按钮,并在“大小”下拉列表中将 Label 控件的大小指定为 16 磅。
- 将 TextAlign 属性设置为 TopCenter。
提示你也可以双击工具箱中的任意控件将其添加到表单中。拖动控件和双击控件的区别在于,在拖动时,可以根据需要在窗体上定位控件。但如果你只是双击一个控件,它会被添加到左上角;所以,如果你更喜欢它在不同的位置,你还是要把它拖到那里。
-
将另外两个 Label 控件拖动到表单上,并将其放在“Welcome”文本的下方,稍微靠近表单的左侧。选择第一个标签,导航到“属性”窗口,将 Name 属性设置为 lblFirstName,将 Text 属性设置为 FirstName。
-
现在选择第二个标签,导航到“属性”窗口,将其 Name 属性设置为 lblLastName,将其 Text 属性设置为 LastName。
-
将两个 textBox 控件拖到窗体上,并将名为 textBox1 的 TextBox 放在名字标签的前面,将名为 textBox2 的 TextBox 放在姓氏标签的前面。
-
选择 textBox1,转到“属性”窗口,将其 Name 属性设置为 txtFname。选择 textBox2,并在“属性”窗口中将其 Name 属性设置为 txtLname。
-
Drag a Button control onto the form, and place it below the Label and TextBox controls. Select the Button control, go to the Properties window, change the Name property to btnSubmit, and then set its Text property to Submit.
现在,您已经准备好了应用的 GUI 设计;它应该类似于图 9-8 中所示的形式。
图 9-8。Windows 应用表单的 GUI 设计
是时候添加功能并切换到代码视图了。您将读入用户提供的名字和姓氏值,并在单击 Submit 按钮时显示一条消息,这意味着您需要将所有功能放在 Submit 按钮的 click 事件之后,该事件最终将从 TextBox 控件中读取值。为此,请继续执行以下步骤:
-
Double-click the Submit button. This will take you to the Code view, and you will see that the
btnSubmitClick
event template has been added to the Code view window. Now you will add the code to show a dialog box, with a greeting and welcome message for the entered first name and last name. To do so, you will useMessageBox
class; this class provides aShow()
function to display a dialog box with the provided information. Now let’s add the following code inside thisbtnSubmitClick
event to achieve the desired functionality of a dialog, with a message, a caption in dialog box’s title bar, an OK button, a Cancel button, and an information icon displayed:MessageBox.Show("Hello" + ' ' + txtFname.Text + ' ' + txtLname.Text + ' ' + "Welcome to the Windows Application","Welcome", MessageBoxButtons.OKCancel, MessageBoxIcon.Information);
现在你的代码视图将显示按钮的点击事件代码,如图图 9-9 所示。
图 9-9。用
MessageBox.Show
代码查看您的Button
点击事件 -
现在,单击 Build Build Solution,并确保在输出窗口中看到以下消息:
========== Build: 1 succeeded or up-to-date, 0 failed, 0 skipped ==========
-
现在是运行和测试应用的时候了。为此,请按 Ctrl+F5。Visual Studio 2012 将加载该应用。
-
在名字和姓氏文本框中输入值,然后单击提交按钮。您将看到类似于图 9-10 中所示的信息。
图 9-10。运行 Windows 应用表单
它是如何工作的
Visual Studio 附带了许多功能来帮助开发人员编写代码。其中一个特性是,您只需双击要添加代码的 GUI 元素,就会在代码视图中找到与该 GUI 元素相关的代码。例如,当您双击设计视图中的提交按钮时,您将被带到代码视图,并且自动生成btnSubmitClick
事件模板。
若要实现此控件的功能,请添加以下代码:
MessageBox.Show("Hello" + ' ' + txtFname.Text + ' ' + txtLname.Text + ' ' + "Welcome to the Windows Application","Welcome", MessageBoxButtons.OKCancel, MessageBoxIcon.Information);
MessageBox.Show()
是一个基于提供的参数弹出消息框的. NET Windows 窗体方法。要在消息框中显示带有用户指定的名字和姓氏的“欢迎”消息,您可以在编写代码时应用字符串串联方法。
在代码段中,您对消息“Hello Welcome to the Windows Application”进行了硬编码,但是用户的名字和姓氏出现在单词 Hello 之后,并与消息的其余部分“Welcome to the Windows Application”连接在一起
为了提高可读性,您还在从txtFnam
和txtLname
的Text
属性中读取的单词和值之间添加了由+
操作符的实例连接的单个空格字符(''
)。如果在字符串连接过程中不包括单个空格字符(''
),单词将会相互交错,消息框中显示的消息将难以阅读。
您得到的第二个参数是Caption
,它是对话框的标题。我们将其硬编码为“欢迎”,然后通过MessageBoxButtons.OKCancel
选择按钮集。我们传递的最后一个参数是MessageBoxIcon
,并使用了一个信息类型图标。
注意 MessageBox.Show()
是非常强大和方便的功能;您可能希望更多地使用智能感知为MessageBoxButtons
和MessageBoxIcon
类型参数显示的各种选择。
设置停靠和锚属性
在 Visual Studio 2005 之前,调整 Windows 窗体的大小需要您重新定位和/或调整这些窗体上的控件。例如,如果在窗体的左侧有一些控件,并且您试图通过向右侧拉伸或向左侧拉回来调整窗体的大小,这些控件不会根据调整后的窗体的宽度自动调整。开发人员必须编写相应的代码来改变控件,以适应用户调整表单的大小。这种技术代码量很大,不容易实现。
从 Visual Studio 2005 开始,出现了两个新的属性,Anchor 和 Dock,它们在设计时很容易设置。Visual Studio 2012 提供了相同的 Dock 和 Anchor 属性,它们解决了用户在调整窗体大小时面临的控件行为问题。
码头属性
Dock 属性允许您将控件附加到其父控件的一个边缘。术语 parent 适用于 Windows 窗体,因为 Windows 窗体包含您拖放到其上的控件。默认情况下,任何控件的 Dock 属性都设置为 None。
例如,停靠在窗体上边缘的控件将始终连接到窗体的上边缘,并且当调整其父控件的大小时,它将自动左右调整大小。
可以使用属性窗口中提供的图形界面来设置控件的 Dock 属性,如图图 9-11 所示。
图 9-11。设置 Dock 属性
锚属性
当用户调整窗体大小时,控件在 Anchor 属性的帮助下保持与其父窗体边缘的恒定距离。任何控件的 Anchor 属性的默认值都设置为 Top,Left,这意味着该控件将与窗体的上边缘和左边缘保持恒定的距离。可以使用属性窗口中提供的图形界面来设置 Anchor 属性,如图图 9-12 所示。
由于 Anchor 属性的默认设置为 Top,Left,如果您试图通过向右侧拉伸窗体来调整其大小,您将看到其控件仍然位于左侧,而不是在调整大小后移动到窗体的中心来调整窗体的大小。
如果在 Anchor 属性中同时设置了相对的边缘(例如左边缘和右边缘),则当调整窗体大小时,控件将拉伸。但是,如果在 Anchor 属性中没有设置相对的边缘,控件将在调整父级大小时浮动。
图 9-12。设置锚点属性
试试看:使用停靠和锚定属性
在本练习中,您将使用在本章前面创建的名为 WinApp 的现有 Windows 窗体应用。您将看到如何以这样的方式修改这个应用,即当您调整表单大小时,它的控件会相应地运行,并保持应用对用户的可呈现性。
转到解决方案资源管理器并打开 WinApp 项目。在设计视图中打开 WinApp 窗体。
-
通过单击窗体的标题栏来选择窗体;您将看到窗体边框周围的手柄,这些手柄允许您调整窗体的高度和宽度。
-
将光标放在右边框的手柄上,当鼠标指针变成双头时,单击并向右侧拉伸表单。您会看到窗体的宽度增加了,但是控件仍然连接在窗体的左上角。
-
Similarly, grab the handle located on the bottom of the form and try to increase the height of the form. You will notice that the controls are still attached to the top side of the form.
看一下图 9-13 ,它显示了调整后的(高度和宽度)表单和控件的位置。这些控件出现在左上角,因为它们的 Dock 属性值为 None,而它们的 Anchor 属性值为 top,left。
图 9-13。调整控件的形状和位置
现在,您将尝试设置控件的 Dock 和 Anchor 属性,然后重新测试应用。
-
选择名为 lbl Welcome 的标签控件,并将文本值设置为 Welcome。转到属性窗口。选择 AutoSize 属性,并将其值设置为 False(默认值为 True)。
-
将 Label 控件的宽度调整为窗体的宽度,并将 Label 控件调整为窗体的上边框。将该控件的 TextAlign 属性设置为顶部居中。
-
将 Label 控件的 Dock 属性从 None 设置为 Top,这意味着您希望标签始终贴在窗体的上边框上。
-
现在选择所有剩余的控件(两个标签、两个文本框和一个按钮),方法是在按住鼠标左键的同时滚动所有控件,或者在按住 Shift 或 Ctrl 键的同时单击选择每个控件。
-
选择所有控件后,转到“属性”窗口。您将看到列出了您在窗体上选择的控件共有的所有属性。
-
Select the Anchor property; modify its value from the default Top, Left to Top, Left, and Right. This will allow you to adjust the controls accordingly as soon as you resize the form. The controls will also grow in size accordingly to adjust to the width of the form, as you can see in Figure 9-14.
图 9-14。Anchor 属性设置 Top,Left,Right 对调整大小后的表单的影响
注意主播属性有非常有趣的行为;您可以尝试以各种组合设置该属性,并在调整窗体大小时查看效果。
-
将窗体恢复到原来的大小,这样就可以看到设置另一个 Anchor 属性的效果。
-
Select all the controls again as you did in step 8. Set the Anchor property to Top only and try resizing the form now. You will notice that the controls are floating in the middle of the form when you resize it, as you can see in Figure 9-15.
***图 9-15。**在调整大小后的窗体上设置 Top 的 Anchor 属性的效果*
- 点击文件全部保存,保存项目中的更改。
它是如何工作的
当您调整窗体大小时,它将根据 Dock 和 Anchor 属性的设置进行操作。
在第一个实例中,将 Label 控件的 Dock 属性设置为 Top,这允许将该 Label 控件附加到窗体的上边框,并跨越窗体的整个宽度。将其余控件的 Anchor 属性设置为 Top、Left 和 Right 可以移动控件,使它们与窗体的左右边框保持恒定的距离。
向项目添加新表单
任何现实世界或企业应用显然都需要多个 Windows 窗体来执行业务功能。默认情况下,每个项目只打开一个 Windows 窗体,但您可以自由添加更多窗体。
试试看:向 Windows 项目添加一个新窗体
在本练习中,您将向项目中添加另一个 Windows 窗体。您还将使用 ListBox、ComboBox、RadioButton 和 CheckBox 控件。在这个新表单中,您将分别向 ListBox 和 ComboBox 添加两个不同文本框中的数据。
- 导航到解决方案资源管理器并选择 WinApp 项目,右键单击,然后单击 Ad Windows 窗体。这将在项目中添加一个新的 Windows 窗体。
- 在显示的添加新项目对话框中,将表单名称从
Form1.cs
更改为UserInfo.cs
。单击添加。名为 UserInfo 的新表单将被添加到您的项目中。 - 确保新添加的表单 UserInfo 已在“设计”视图中打开。通过单击表单的标题栏选择 UserInfo 表单,导航到属性窗口,并将 Size 属性的宽度设置为 455,高度设置为 251。
- 将 Label 控件拖到窗体上;选择该控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblCountry。
- 将 AutoSize 属性设置为 false。
- 将位置属性的 X 设置为 12,Y 设置为 26。
- 将 Size 属性的宽度设置为 71,高度设置为 13。
- 设置 Text 属性以输入 Country。
- 将 TextBox 控件拖到 lblCountry 标签的前面。选择该控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtCountry。
- 将位置属性的 X 设置为 97,Y 设置为 19。
- 将 Size 属性的宽度设置为 129,高度设置为 20。
- 拖动 lblCountry 下的另一个标签,选择该控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblState。
- 将 AutoSize 属性设置为 false。
- 将位置属性的 X 设置为 12,Y 设置为 65。
- 将 Size 属性的宽度设置为 60,高度设置为 13。
- 将 Text 属性设置为输入状态。
- 将 TextBox 控件拖动到 lblState 标签的前面。选择该控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtState。
- 将位置属性的 X 设置为 97,Y 设置为 58。
- 将 Size 属性的宽度设置为 129,高度设置为 20。
- 将一个 ListBox 控件拖动到已添加的 TextBox 控件右侧的 UserInfoInfo 窗体上。选择该控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lstCountry。
- 将位置属性的 X 设置为 280,Y 设置为 12。
- 将 Size 属性的宽度设置为 129,高度设置为 82。
- 在刚刚添加的 ListBox 下面拖动一个 ComboBox。选择该控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 cboState。
- 将位置属性的 X 设置为 280,Y 设置为 117。
- 将 Size 属性的宽度设置为 129,高度设置为 21。
- 拖动标签控件下面的两个复选框,并将其命名为 chkPostalMail 和 chkEMail 将它们的 Text 属性分别设置为 Postal Mail 和 E-Mail。
- 将两个 RadioButtons 拖动到 TextBox 控件的下方,并命名为 rdbMale 和 rdbFemale。将它们的文本属性分别设置为男性和女性。
- 拖动一个按钮控件到 UserInfo 表单左侧的 CheckBox 控件下方;选择该控件,导航到“属性”窗口,并设置以下属性:
1. 将 Name 属性设置为 btnAdd。
2. 将位置属性的 X 设置为 12,Y 设置为 165。
3. 将 Size 属性的宽度设置为 75,高度设置为 23。
4. 设置要添加的文本属性。 - 拖动“添加”按钮旁边的按钮控件;选择该控件,导航到“属性”窗口,并设置以下属性:
1. 将 Name 属性设置为 btnRemoveCountry。
2. 将位置属性的 X 设置为 105,Y 设置为 165。
3. 将 Size 属性的宽度设置为 95,高度设置为 23。
4. 设置 Text 属性以删除 Country。 - 拖动“删除国家/地区”按钮旁边的按钮控件;选择该控件,导航到“属性”窗口,并设置以下属性:
1. 将 Name 属性设置为 btnRemoveState。
2. 将位置属性的 X 设置为 220,Y 设置为 165。
3. 将 Size 属性的宽度设置为 86,高度设置为 23。
4. 将 Text 属性设置为移除状态。 - Drag a Button control next to the Remove State button; select this control, navigate to the Properties window, and set the following properties:
1. 将 Name 属性设置为 btnShowDetails。
2. 将位置属性的 X 设置为 327,Y 设置为 165。
3. 将 Size 属性的宽度设置为 100,高度设置为 23。
4. 设置 Text 属性以显示详细信息。
现在你已经完成了 UserInfo 表单的设计部分;当拖放控件时,你应该放置控件来创建一个视觉上吸引人的布局,如图图 9-16 所示。
![Image](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs5-db/img/9781430242604_Fig09-16.jpg)
***图 9-16。**UserInfo 表单的 GUI 设计*
您希望用户在文本框中添加一个名称并单击 add 按钮,然后将 country 添加到 ListBox,将 state 添加到 ComboBox。相应地,Remove 按钮将删除国家或州,Show Details 将显示使用复选框和单选按钮做出的选择。为此,您需要逐一编写所有这些按钮的`click`事件背后的代码功能。
- 双击 Add 按钮并在
btnAdd_Click
事件中编写以下代码,它将读取文本框中输入的国家和州名,并将它们添加到 ListBox 和 ComboBox 中。lstCountry.Items.Add(txtCountry.Text); txtCountry.Clear(); cboState.Items.Add(txtState.Text); txtState.Clear();
- 双击 Remove Country 按钮,并在
btnRemoveCountry_Click
事件中编写以下代码,这将从名为 lstCountry 的列表框中删除选定的国家:lstCountry.Items.Remove(lstCountry.SelectedItem);
- 双击 Remove State 按钮,并在
btnRemoveState_Click
事件中编写以下代码,这将从名为 cboState 的 ComboBox 中删除选中的状态。cboState.Items.Remove(cboState.SelectedItem);
- 双击 Show Details 按钮,并在
btnShowDetails_Click
事件中编写以下代码,这将显示通过复选框和单选按钮选择的选项。if (chkEmail.Checked == true || chkPostalMail.Checked == true && rdbMale.Checked == true) {
- MessageBox。显示(“你好先生,你将通过美国邮政或电子邮件联系”,“信息”,MessageBoxButtons。确定取消,MessageBoxIcon。信息);
} else if (chkEmail.Checked == true || chkPostalMail.Checked == true && rdbFemale.Checked == true) { MessageBox.Show("Hello Mam, you will be contacted by either USPS or email", "Information", MessageBoxButtons.OKCancel, MessageBoxIcon.Information); }
- 转到“生成”菜单,选择“生成解决方案”。您应该会看到一条消息,表明构建成功。
- 保持当前项目打开,因为下一个练习会立即用到它。(不要担心,我们将在之后解释这个和下一个练习是如何工作的。)
试试看:设置启动表单
在一个 Visual C# 项目中设置启动窗体有点棘手,所以我想把它分解成自己的练习。要设置启动表单,您需要遵循以下步骤:
1.在您在前一个练习中修改的项目中,导航到解决方案资源管理器,打开
Program.cs
文件,并查找以下代码行:
Application.Run(new WinApp());
该代码行确保 WinApp 窗体将始终是第一个运行的窗体,因为这是第一个名为 Form1 的窗体,它已经被添加,并且在开始该项目时被重命名;为了将 UserInfo 表单设置为启动表单,您需要稍微修改一下这个语句,如下所示:
Application.Run(new UserInfo());
2.生成解决方案,并通过按 Ctrl+F5 运行和测试应用。将加载 UserInfo 应用表单。
3.在相应的文本框中输入国家和州名,然后单击“添加”按钮;你会看到你输入的名字已经被添加到列表框和组合框中,如图图 9-17 所示。
图 9-17。使用 ListBox 和 ComboBox withUserInfo Windows 窗体应用
4.选中两个复选框和一个单选按钮,然后单击 ShowDetails 按钮。你会看到一个消息框显示出来,如图图 9-18 所示。
图 9-18。在 UserInfo Windows 窗体应用中使用复选框和单选按钮
它是如何工作的
让我们先来看看“在 Windows 项目中添加一个新窗体”任务,一个按钮一个按钮一行行地理解代码。
首先有一个 Add 按钮,它将国家和州添加到 ListBox 和 ComboBox 中。ListBox 和 ComboBox 控件有一个名为Items
的集合,这个集合可以包含一个项目列表,这就是为什么在这里使用它。接下来调用Items
集合的Add
方法,最后将文本框中输入的值传递给列表框或组合框的Items
集合的Add
方法,如下所示:
lstCountry.Items.Add(txtCountry.Text);
此外,一旦添加了项目,为了更好的用户体验,建议清除文本框,以便用户可以轻松地键入新值。
txtCountry.Clear();
对组合框重复同样的操作:
cboState.Items.Add(txtState.Text); txtState.Clear();
对于移除国家和移除州按钮,您遵循与Items
集合类似的方法,但是这次您调用的不是Add()
而是Remove()
方法,如您所知,移除项的先决条件是必须首先在列表框或组合框中选择 and 项。因此,代码寻求将一个SelectedItem
传递给Remove()
方法。
`lstCountry.Items.Remove(lstCountry.SelectedItem);
cboState.Items.Remove(cboState.SelectedItem);`
现在,对于 Show Details 按钮,您已经使用了一些条件逻辑来根据您的选择生成不同的消息,特别是对于男性和女性单选按钮。
CheckBox 和 RadioButton 控件提供了一个名为 Checked 的属性,该属性可以是 true 或 false,即选中或不选中。你围绕这些建立一个条件,然后显示一个消息框。
if (chkEmail.Checked == true || chkPostalMail.Checked == true && rdbMale.Checked == true) { MessageBox.Show("Hello Mr, you will be contacted by either USPS or email", "Information",MessageBoxButtons.OKCancel, MessageBoxIcon.Information); } else if (chkEmail.Checked == true || chkPostalMail.Checked == true && rdbFemale.Checked == true) { MessageBox.Show("Hello Mam, you will be contacted by either USPS or email", "Information", MessageBoxButtons.OKCancel, MessageBoxIcon.Information); }
在“设置启动表单”任务中,您在Program.cs
文件中创建 AddName 表单的一个实例,如以下代码所示:
Application.Run(new UserInfo());
实施计量吸入器表单
术语多文档界面 (MDI)是指拥有一个 GUI 界面,允许在一个父表单或窗口下有多个文档或表单。
想象一下 Microsoft Word 的工作方式:你可以在一个父窗口中打开多个文档,所有的文档都将列在窗口菜单中,你可以从中选择你想阅读的任何一个,而不是在各自的窗口中打开各个文档,这样很难处理所有的文档,并且会在屏幕上覆盖许多打开的窗口。
同一应用的每个实例都有一个单独的窗口被称为单文档界面(SDI);记事本、MS Paint、计算器等应用都是 SDI 应用。SDI 应用只能在它们自己的窗口中打开,可能会变得难以管理,这与在一个 MDI 界面中打开多个文档或表单不同。
因此,MDI 应用遵循父窗体和子窗体的关系模型。MDI 应用允许您通过在 MDI 父窗体的上下文中打开文档来同时打开、组织和处理多个文档。所以,一旦打开,它们就不能像个体形态一样被拖出来。
父窗体(MDI)组织和排列当前打开的所有子窗体或文档。您可能已经在许多 Windows 应用的 Windows 菜单下看到过这样的选项,例如层叠、垂直平铺等等。
试试看:创建一个带有菜单栏的 MDI 父表单
在本练习中,您将在 WinApp 项目中创建一个 MDI 窗体。您还将看到如何为父窗体创建一个菜单栏,这将允许您导航到所有子窗体。为此,请按照下列步骤操作:
- 导航到解决方案资源管理器,选择 WinApp 项目,右键单击,并选择 Ad Windows 窗体。将名称值从
Form1.es
更改为ParentForm.es
,并点击添加。 - 在设计视图中选择新添加的 ParentForm。通过单击表单的标题栏选择 ParentForm 表单,导航到“属性”窗口,并设置以下属性:
- 将 IsMdiContainer 属性设置为 True(默认值为 False)。请注意,窗体的背景色已经变成了深灰色。
- 将 Size 属性的宽度设置为 546,高度设置为 411。
- 将 MenuStrip 控件拖动到 ParentForm。在左上角,您应该会看到一个显示文本类型的下拉列表。在下拉框中输入文本打开表单。这将是您的主要顶层菜单。
- 现在在打开的表单菜单下,通过输入文本 Win App 添加一个子菜单。
- 在 Win App 子菜单下,进入用户信息。
- 现在点击顶部菜单,打开表单,在它的右边,输入 Help 。在帮助菜单下,输入退出。
- 现在,点击顶部菜单,在帮助的右侧,键入 Windows 。
- 在 Windows 菜单下,添加以下选项作为单独的子菜单:层叠、水平平铺、垂直平铺和排列图标。这些将有助于安排子窗体。
- 现在是时候将代码附加到您在主菜单打开表单下添加的子菜单中了。首先,您将为子菜单 WinApp 添加代码,这基本上将打开 Win App 表单。在设计视图中,双击 Win 应用子菜单,这将带您到代码视图。在
click
事件下,添加以下代码:WinApp objWA = new WinApp(); objWA.Show();
- Now to associate functionality with the User Info submenu: double-click this submenu, and under the
click
event add the following code:UserInfo objUI = new UserInfo(); objUI.Show();
要将功能与位于帮助主菜单下的退出子菜单相关联,请双击退出,并在`click`事件下添加以下代码:
` Application.Exit();`
- 现在您已经有了表单打开代码功能,并且几乎可以运行应用了。但是首先需要将 ParentForm 设置为启动对象。为此,打开
Program.cs
,将Application.Run(new UserInfo());
语句修改为:Application.Run(new ParentForm());
- Now build the solution, and run the application by pressing F5; the MDI application will open and should look like Figure 9-19.
***图 9-19。**运行 MDI 表单应用*
- 现在,如果您单击 Win App,然后单击 User Info,这两个表单将依次打开。这些窗体可以被打开并拖动到 MDI 窗体之外。这不是 MDI 应用的预期行为,如图 9-20 所示。这个问题将在本章后面讨论。
图 9-20。运行 MDI 表单应用
它是如何工作的
每个 windows 窗体都是一个类,并通过为其创建的实例公开一个Show()
函数。您使用下面的代码,它创建一个对象,然后调用Show()
方法。这将打开 MDI 父窗体中的另一个窗体。
这将创建一个 WinApp 窗体实例,并为您打开它:
WinApp objWA = new WinApp(); objWA.Show();
以下代码创建了 UserInfo 表单的一个实例,并为您打开它:
UserInfo objUI = new UserInfo(); objUI.Show();
您使用以下代码关闭应用:
Application.Exit();
试试看:在 MDI 应用中打开 MDI 子窗体
如图 9-20 所述,问题是即使 MDI 表单显示了一个父菜单,这些表单仍然能够在外部打开,并且上下文从一个表单移动到另一个表单。您可以尝试单击每个打开的表单的标题栏,您将看到如何在这些打开的表单中来回移动。
在本练习中,您将克服这个问题,并将之前作为 MDI 子窗体创建的所有窗体与您在前一任务中创建的主 MDI 父窗体相关联。
-
In the project you modified in the previous exercise, you’ll first make the WinApp form an MDI child form. To do so, you need to set the
MdiParent
property of the child form’s object to the MDI parent form itself, but in the Code view. You have already added functionality in the previous task (opening the WinApp form); just before the line where you are calling theShow()
method, add the following code (this code can be found under Win App menu click event):objWA.MdiParent=this; After adding this line, the code will appear as follows: WinApp objWA = new WinApp(); objWA.MdiParent = this; objWA.Show();
注意
this
是一个 C# 语言关键字,表示类的当前实例。在这种情况下,它指的是 ParentForm。因为您是在 ParentForm 内部编写这段代码,所以您可以使用this
关键字来完成同样的工作。 -
Now you will make the UserInfo form an MDI child form. To do so, you need to set the
MdiParent
property to the name of the MDI parent form but in the Code view. Add the following code as you did in the previous step (this code can be found under the User Info menu click event):objUI.MdiParent=this;
添加这一行后,代码将如下所示:
UserInfo objUI = new UserInfo(); objUI.MdiParent=this; objUI.Show();
-
现在构建解决方案,并通过按 F5 运行应用;MDI 应用将会打开,并应如图 9-21 所示。
-
Click Open Form Win App; the WinApp form should open. Again, open the main menu and click User Info. Both the forms should now be open inside your main MDI parent form application, and unlike before, you will not be able to drag these out of your MDI parent form (as shown in Figure 9-20). Figure 9-21 shows the expected behavior of an MDI application with opened form(s).
图 9-21。在 MDI 表单应用中打开子表单
-
因为这两个窗体都是在一个 MDI 父窗体中打开的,所以使用它们变得更容易,并且它们不能被拖动到 MDI 父窗体之外。通过单击标题栏在这些表单之间来回切换。
-
完成表单后,选择 Help Exit 关闭应用。
它是如何工作的
正如您在前面的练习中注意到的,讨论的唯一问题是子窗体打开并能够被拖到外面。换句话说,它们并不真正属于父窗体。MDI 应用要求一个带有菜单栏的窗体作为 MDI 父窗体,这样所有的子窗体都可以在其中打开。
为此,首先需要创建子表单的一个对象:
WinApp objWA = new WinApp();
但是在我们真正调用objWA
上的Show()
方法之前,您需要告诉对象谁是它的父对象,这样它就可以在 MDI 父表单中操作。为此,您将使用代表当前表单类的this
关键字。
objWA.MdiParent = this;
您已经创建了对象,并将其上下文设置为 MDI 父表单,所以现在是调用Show()
方法的最佳时机,该方法将启动表单,以便您可以使用它。
objWA.Show();
wa.MdiParent=this;
行告诉子窗体哪个窗体是它的父窗体。因为您希望所有子窗体都出现在 ParentForm 中,并且您在 MDI 父窗体中编写代码,所以您可以使用this
关键字来表示当前对象。
也为 UserInfo 设置前面建议的更改。
UserInfo objUI = new UserInfo(); objUI.MdiParent=this; objUI.Show();
试试看:在 MDI 应用中排列 MDI 子窗体
多个窗体将在一个 MDI 窗口中打开,所以一旦你打开了几个窗体,你的 MDI 应用就会变得混乱。很难移动表格来将你的注意力从一个转移到另一个。因此,最重要的是要有一种机制,允许您以一种有组织的方式来安排表单。
例如,在大多数应用中,您可以排列表单,然后层叠它们,这样您就可以看到一堆打开的表单。或者您可以垂直或水平平铺它们,这样您就可以并排看到多个表单。你甚至可以最小化所有打开的表单,并将它们排列成一个图标。
为此,在本练习中,您将添加如图 9-22 所示的窗口菜单。
图 9-22。MDI 窗体应用的窗口菜单排列子窗体
。NET 的 Windows 窗体提供了 LayoutMdi 方法,该方法采用 MdiLayout 枚举来重新排列 Mdi 父窗体中的子窗体。您可以将表单排列成四种模式:层叠、水平平铺、垂直平铺和排列图标。
- 在设计视图中打开 ParentForm,点击窗口菜单,如图图 9-22 所示。
- 双击 Windows 下的第一个选项 Cascade,它会带你到它的
click
事件。添加以下代码:LayoutMdi(MdiLayout.Cascade);
- 双击水平平铺,在
click
事件下的代码视图中,添加以下代码:LayoutMdi(MdiLayout.TileHorizontal);
- 双击垂直平铺,在
click
事件下的代码视图中,添加以下代码:LayoutMdi(MdiLayout.TileVertical);
- 双击垂直平铺,在
click
事件下的代码视图中,添加以下代码:LayoutMdi(MdiLayout.ArrangeIcons);
- 现在构建解决方案,并通过按 F5 运行应用;MDI 应用将会打开。打开后,进入打开表单菜单,依次点击 Win App 和用户信息。在 MDI 父窗体中至少打开两个窗体是很重要的。
现在进入窗口菜单,点击层叠,垂直排列,水平排列,最后排列图标。当你尝试这些选项时,垂直平铺将显示如图图 9-23 排列的子表单。
图 9-23。在 MDI 表单应用中排列(垂直平铺)子表单
总结
在本章中,您了解了 Windows 窗体以及与图形用户界面设计相关的设计原则。您还了解了通常被忽略的特性的重要性,比如字体样式和颜色,以及它们对应用的影响和对大量用户的影响。您还使用了解决 Windows 窗体大小调整问题的最常用的 Windows 控件和属性。您了解了 MDI 应用的重要性,然后创建了一个带有菜单控件的 MDI 应用,在 MDI 应用中排列子窗体。
在下一章,你将学习 about 应用设计。
十、ADO.NET 简介
在工业界,如果没有与数据库的交互,大多数应用都无法构建。数据库服务于数据存储的目的,因此以后可以通过 SQL 查询或数据库应用检索数据。几乎每个运行的软件应用都与一个或多个数据库交互。因此,前端需要一种机制来连接数据库,而 ADO.NET 服务于这一目的。大多数的。需要数据库功能的. NET 应用依赖于 ADO.NET。在本章中,我们将介绍以下内容:
- 了解 ADO.NET
- ADO.NET
- The motivation behind it shifted from ADO to ADO.NET.
- Understanding ADO.NET architecture
- Understanding SQL Server data providers
- Understanding OLE DB data providers
- Understanding ODBC data providers
- Data provider as API
了解 ADO.NET
之前。NET 中,开发人员使用了 ODBC、OLE DB 和 ActiveX 数据对象(ADO)等数据访问技术。随着……的引入。微软创造了一种处理数据的新方法,叫做 ADO.NET。
ADO.NET 是一组向其公开数据访问服务的类。NET 程序员,为创建分布式数据共享应用提供了丰富的组件集。ADO.NET 是美国不可分割的一部分。NET Framework,并提供对关系数据、XML 数据和应用数据的访问。ADO.NET 类在System.Data.dll
中被发现。
这项技术支持各种开发需求,包括创建前端数据库客户端和应用、工具、语言和 Internet 浏览器使用的中间层业务对象。因此,ADO.NET 有助于将应用的 UI 或表示层与数据源或数据库连接起来。
ADO.NET 背后的动机
随着应用开发的发展,应用已经变得松散耦合,一种架构,其中组件更容易维护和重用,而不依赖于其他组件的实现细节。今天越来越多的应用使用 XML 来编码要通过网络连接传递的数据,这就是运行在不同平台上的不同应用如何进行互操作。
ADO.NET 旨在支持断开连接的数据架构、与 XML 的紧密集成、能够组合来自多个不同数据源的数据的通用数据表示,以及用于与数据库交互的优化工具,所有这些都是。NET 框架。
在 ADO.NET 的开发过程中,微软希望包括以下功能:
借力当前 ADO 知识 : ADO。NET 的设计解决了当今应用开发模型的许多需求。同时,编程模型尽可能与 ADO 相似,因此当前的 ADO 开发人员不必从头开始。ADO.NET 是美国固有的一部分。NET 框架,但 ADO 程序员对此并不陌生。
阿多。网也与麻烦共存。虽然是最新的。基于。网的应用将使用阿多。网编写,ADO 仍然可用 100 .净程序员通过. NET COM 互操作性服务.对 n 层编程模型的支持
: The concept of using disconnected recordset has become a focus in the programming model. ADO.NET provides first-class support for the unconnected N-tier programming environment. Addo. NET to build N-tier database application is
DataSet
.Integrated XML support : XML and data access are closely linked. XML is about data coding, and data access is increasingly related to XML. that The. NET Framework not only supports web standards, but also is completely built on these standards.
XML 支持是 ADO.NET 的基础。中的 XML 类。NET 框架和 ADO.NET 是同一个架构的一部分;它们在许多不同的层面上整合。因此,您不再需要在服务的数据访问集和它们的 XML 对应物之间进行选择;从一个跨越到另一个的能力是两者的设计中所固有的。
从阿多搬到 ADO.NET
ADO 是 ActiveX 对象的集合,这些对象被设计成在持续连接的环境中工作。它构建在 OLE DB 之上(我们将在“了解 OLE DB 数据提供程序”一节中讨论)。OLE DB 提供了对非 SQL 数据以及 SQL 数据库的访问,ADO 提供了一个接口,旨在简化 OLE DB 提供程序的使用。
然而,使用 ADO(和 OLE DB)访问数据意味着在到达数据源之前,您必须经过几层连接。正如 OLE DB 可以连接到大量的数据源一样,一种更老的数据访问技术——开放式数据库连接(ODBC)仍然可以连接到更老的数据源,如 dBase 和 Paradox。要使用 ADO 访问 ODBC 数据源,您可以使用 ODBC 的 OLE DB 提供程序(因为 ADO 只直接使用 OLE DB),从而向已经多层的模型中添加更多的层。
利用 ADO 的多层数据访问模型和连接特性,您可能很容易耗尽服务器资源并造成性能瓶颈。ADO 在它的时代服务得很好,但是 ADO.NET 有一些很棒的特性,使它成为一种优越得多的数据访问技术。
ADO.NET 不是 ADO 的新版本
ADO.NET 是一种全新的数据访问技术,具有完全从零开始构建的新设计。让我们先澄清一下:ADO.NET不代表 ActiveX 数据对象. NET。为什么?原因很多,但以下是两个最重要的原因:
- ADO.NET is an inseparable part. NET instead of an external entity.
- ADO.NET is not a collection of ActiveX components.
ADO.NET 这个名字类似于 ADO,因为微软希望开发人员在使用 ADO.NET 时有宾至如归的感觉,并且不希望他们认为他们需要“从头再学一遍”,如前所述,所以微软特意命名并设计了 ADO.NET,以提供以不同方式实现的类似功能。
在的设计过程中。微软意识到 ADO 不适合。ADO 是基于组件对象模型(COM)对象的外部包,需要。NET 应用显式包含对它的引用。相比之下,。NET 应用被设计为共享一个模型,其中所有的库被集成到一个框架中,被组织到逻辑命名空间中,并对任何想要使用它们的应用公开。明智的决定是。NET 数据访问技术应该遵守。NET 架构模型。于是,ADO.NET 诞生了。
ADO.NET 旨在适应连接和断开连接的访问。此外,ADO.NET 比 ADO 更多地采用了非常重要的 XML 标准,因为 XML 的使用是在 ADO 开发出来之后才出现的。使用 ADO.NET,您不仅可以使用 XML 在应用之间传输数据,还可以将应用中的数据导出到 XML 文件中,将其存储在本地系统中,并在以后需要时进行检索。
性能通常是有代价的,但在 ADO.NET 的情况下,价格肯定是合理的。与 ADO 不同,ADO.NET 不透明地包装 OLE DB 提供程序;相反,它使用专门为每种类型的数据源设计的托管数据提供者,从而充分利用它们的真正能力,提高应用的整体速度和性能。
ADO.NET 还可以在连接和断开的环境中工作。您可以连接到数据库,在简单地读取数据时保持连接,然后关闭连接,这是一个类似于 ADO 的过程。ADO.NET 真正开始发光的地方是在这个断开的世界。如果您需要编辑数据库数据,那么在服务器上维护连续连接的成本会很高。ADO.NET 通过提供一个复杂的分离模型来解决这个问题。数据从服务器发送,并在客户端本地缓存。当您准备好更新数据库时,您可以将更改后的数据发送回服务器,在服务器上为您管理更新和冲突。
在 ADO.NET,当你检索数据时,你使用一个叫做数据读取器的对象。当您处理断开连接的数据时,数据被缓存在本地的一个关系数据结构中,或者是一个数据表或者是一个数据集。
ADO.NET 和。NET 基础类库
一个数据集(DataSet
对象)可以在内存缓存中以表格(DataTable
对象)、它们的关系(DataRelation
对象)和约束(Constraint
对象)的形式保存大量数据,然后可以将这些数据导出到外部文件或另一个数据集。由于 XML 支持被集成到了 ADO.NET 中,所以您可以使用 XML 文档生成 XML 模式并传输和共享数据。表 10-1 描述了 ADO.NET 组件分组的名称空间。
因为 XML 支持已经紧密地集成到 ADO.NET 中,所以在System.Data
名称空间中的一些 ADO.NET 组件依赖于在System.Xml
名称空间中的组件。因此,有时需要在解决方案资源管理器中将这两个命名空间作为引用包含在内。
这些名称空间在物理上被实现为程序集,如果您在 VCSE 创建一个新的应用项目,对程序集的引用应该会自动创建,同时还会创建对System
程序集的引用。但是,如果它们不存在,只需执行以下步骤将名称空间添加到项目中:
- 在解决方案资源管理器中右击“引用”项;然后单击添加引用。
- 将显示一个包含可用参照列表的对话框。逐个选择
System.Data
、System.Xml
、System
(如果没有的话)(按住 Ctrl 键可多选);然后单击选择按钮。 - 单击 OK,引用将被添加到项目中。
提示虽然我们在本书中不使用,但是如果使用命令行 C# 编译器,可以使用以下编译器选项来包含所需程序集的引用:/r:System.dll /r:System.Data.dll /r:System.Xml.dll
。
从名称空间中可以看出,ADO.NET 可以使用 OLE DB 和 ODBC 等旧技术。但是,SQL Server 数据提供程序直接与 SQL Server 通信,无需添加 OLE DB 或开放式数据库连接(ODBC)层,因此这是最有效的连接形式。同样,Oracle 数据提供者直接访问 Oracle。
注各大 DBMS 厂商都支持自己的 ADO.NET 数据提供商。在本书中,我们将坚持使用 SQL Server,但是不管提供者是谁,都要编写相同类型的 C# 代码。
了解 ADO.NET 建筑
ADO.NET 提供了两种类型的架构组件来构建以数据为中心的应用:连接的和断开的。在微软内部。NET Framework 中,ADO.NET 位于名称空间System.Data
(程序集名称为System.Data.dll
)中,因此连接和断开连接的组件的所有类和函数都位于同一个名称空间中。因此,不管您已经选择或者以后将选择的是连接的还是断开的架构风格,在您的应用中添加一个System.Data
的引用是很重要的。
图 10-1 展示了 ADO.NET 最重要的建筑特色。我们将在后面的章节中更详细地讨论它们。
图 10-1。【ADO.NET 建筑
连接的数据对象
阿多。NET 的连接架构依赖于一致的数据库连接来访问数据并对检索到的数据执行任何操作。ADO.NET 提供了以下对象来帮助您使用连接架构构建应用:
Connection
: This is the main or core object of any database-oriented application. As you can imagine, if you don't know the statistical information of the data source, such as where it is located, what database you want to connect to, what user name and password it needs, etc., it is impossible to establish a connection and perform any data-related activities. Each. NET provider provides its ownConnection
object, which provides features for specific data sources.Command
: This object represents the processing of statements used by applications to perform data-oriented tasks, such as reading data, inserting or modifying data. Therefore, any SQL statement is actually executed through aCommand
object.DataReader
: DataReader involves creating an instance of Command object, and then creating DataReader by calling Command. ExecuteReader retrieves data from the data source, and the returned data can be obtained in a read-only manner through aDataReader
object. The data retrieval behavior ofDataReader
is also called Quick read-only fire hose cursor .Parameter
:Parameter
has always been an important part of any programming model. Similarly, in ADO.NET programming, it is important to pass the value toCommand
.Parameter
can be a value passed to or returned from the stored procedure, or a parameter passed to the SQL query.DataAdapter
:DataAdapter
is an object disclosed by ADO.NET, which is used to build a bridge between the connected and disconnected architectures, so that applications can establish connections and synchronize data with data sources.
断开的数据对象
阿多。NET 的连接架构依赖于一致的数据库连接来访问数据并对检索到的数据执行任何操作。然而,在当今复杂的分布式应用环境中,不可能依靠专用的数据库连接来检索和修改数据。
为了帮助您满足业务需求并在分布式环境中轻松工作,您可以利用 ADO。NET 的非连接体系结构;它提供灵活的应用设计,并帮助组织节省数据库连接。因此,可以检索数据,然后以DataSet
对象的形式存储在本地设备上。检索到的DataSet
可以由用户在他们的本地设备上修改,比如笔记本电脑、手持设备、平板电脑等等,一旦完成,他们就可以将更改同步到中央数据源。非连接架构以一种非常优化的方式利用了像Connection
这样的昂贵资源(也就是晚开早闭)。
ADO.NET 提供了以下对象来帮助您使用非连接架构构建应用:
DataSet
:DataSet
is the core architecture component of ADO.NET, which is used to make disconnected applications. A data set can be regarded as a subset of data. Datasets support disconnected and independent caching of data in a relational way, and update data sources as needed. A dataset contains one or more data tables.DataTable
:DataTable
is a row and column representation, which provides a logical view very similar to the physical table in the database. For example, you can store the data in the database table in ADO.NETDataTable
and manipulate the data as needed.DataRow
: As you know, tables are always made up of rows. In a similar way, ADO. TheDataTable
of. NET consists of rows of typeDataRowCollection
. ThisDataRowCollection
is an enumerable collection ofDataRow
objects. When new data is added toDataTable
, newDataRow
is added.DataColumn
: Like any other column in a database table, ADO. TheDataTable
of. NET is composed ofDataColumn
of typeDataColumnCollection
.DataView
:DataView
The role of ADO.NET is similar to the view in database. Typically, a view in a database provides a set of predefined, organized or filtered records. Similarly, a T2 provides filtered or sorted records from a T3. Just as a database table can have multiple views,DataTable
can also have multiple data views.
理解。NET 数据提供者
ADO.NET 由各种数据提供程序组成,这些数据提供程序允许一个简单的预定义对象模型与各种行业数据库(如 SQL Server、Oracle、Microsoft Access 等)进行通信。
有各种各样的数据库提供程序,所以每个数据提供程序都有自己的名称空间。事实上,每个数据提供者本质上都是在System.Data
名称空间中的接口的实现,专门用于特定类型的数据源。
例如,如果您使用 SQL Server,您应该使用 SQL Server 数据提供程序(System.Data.SqlClient
),因为这是访问 SQL Server 最有效的方式。
OLE DB 数据访问接口支持访问 SQL Server 的旧版本以及其他数据库,如 access、DB2、MySQL 和 Oracle。然而,本地数据提供程序(如System.Data.OracleClient
)在性能上更好,因为 OLE DB 数据提供程序在到达数据源之前通过另外两层工作,即 OLE DB 服务组件和 OLE DB 提供程序。
图 10-2 说明了使用 SQL Server 和 OLE DB 数据提供程序访问 SQL Server 数据库的区别。
图 10-2。 SQL Server 和 OLE DB 数据提供程序的差异
如果您的应用同时连接到较旧版本的 SQL Server (6.5 或更早版本)或多种数据库服务器(例如,Access 和 Oracle 数据库同时连接),则只有在这种情况下,您才应该选择使用 OLE DB 数据访问接口。
不存在严格的规则;如果您愿意,可以同时使用 OLE DB data provider for SQL Server 和 Oracle data provider ( System.Data.OracleClient
),但选择最适合您的用途的提供程序很重要。考虑到特定于服务器的数据提供程序的性能优势,如果您使用 SQL Server,99%的时间应该使用System.Data.SqlClient
类。
在我们了解每种数据提供程序的功能和使用方法之前,您需要清楚它们的核心功能。每个。NET 数据提供程序旨在很好地完成以下两件事:
- Provide access to data through active connection with data source.
- Provide data transmission between disconnected data sets and data tables.
数据库连接是通过使用数据提供者的Connection
类建立的(例如,System.Data.SqlClient.SqlConnection
)。数据读取器、命令和数据适配器等其他组件分别支持检索数据、执行 SQL 语句以及读取或写入数据集或数据表。
正如您所看到的,每个数据提供程序都以它所连接的数据源类型为前缀(例如,SQL Server 数据提供程序以Sql
为前缀),因此它的连接类被命名为SqlConnection
。OLE DB 数据提供程序的连接类被命名为OleDbConnection
。
让我们了解一下可以与 SQL Server 一起使用的三种数据提供程序。
了解 SQL Server 数据提供程序
那个。SQL Server 的. NET 数据提供程序位于System.Data.SqlClient
命名空间中。虽然您可以使用System.Data.OleDb
来连接 SQL Server,但是微软已经专门设计了用于 SQL Server 的System.Data.SqlClient
名称空间,并且它的工作方式比System.Data.OleDb
更加高效和优化。这种效率和优化方法的原因是,该数据提供者使用其本地网络协议直接与服务器通信,而不是通过多层。
表 10-2 描述了SqlClient
名称空间中的一些重要类。
另一个名称空间System.Data.SqlTypes
将 SQL Server 数据类型映射到。NET 类型,既提高了性能,又使开发人员的工作变得更加容易。
了解 OLE DB 数据提供程序
外面。NET,OLE DB 仍然是微软的高性能数据访问技术。OLE DB 数据提供程序已经存在了许多年。如果您过去曾为 Microsoft Access 编写过程序,您可能还记得使用 Microsoft Jet OleDb 3.5 或 4.0 来连接 Microsoft Access 数据库。您可以使用该数据提供程序来访问以任何格式存储的数据,因此即使在 ADO.NET,它在访问没有自己的 ADO.NET 数据提供程序的数据源时也发挥着重要作用。
的。OLE DB 的. NET Framework 数据提供程序位于命名空间System.Data.OleDb
中。表 10-3 描述了OleDb
名称空间中的一些重要类。
注意两个数据提供者SqlClient
和OleDb
之间的相似性。它们实现中的差异是显而易见的,用户界面基本上是相同的。
ADO.NET OLE DB 数据访问接口要求在连接字符串中指定 OLE DB 访问接口。表 10-4 描述了一些 OLE DB 提供者。
了解 ODBC 数据提供者
ODBC 是微软最初的通用数据访问技术。它仍然广泛用于没有 OLE DB 提供程序或。NET Framework 数据提供程序。ADO.NET 在名称空间System.Data.Odbc
中包含了一个 ODBC 数据提供者。
ODBC 体系结构本质上是一个三层过程。应用使用 ODBC 函数提交数据库请求。ODBC 将函数调用转换为特定于给定数据源的驱动的协议(调用级接口)。驱动程序与数据源通信,将任何结果或错误传递回 ODBC。显然,这比特定于数据库的数据提供者与数据库的直接通信效率要低,所以为了性能,最好避免使用 ODBC 数据提供者,因为它只是提供了一个更简单的 ODBC 接口,但仍然涉及所有的 ODBC 开销。表 10-5 描述了Odbc
名称空间中的一些重要类。
数据提供者是 API
那个。NET Framework 数据提供程序虽然复杂(您将在后面学到很多利用其复杂性的知识),但它们只是用于访问数据源的 API,最常见的是关系数据库。(ADO.NET 本质上是一个大 API,数据提供商是其中的主要部分。)
可以理解,ADO.NET 的新来者经常会对微软的文档感到困惑。他们读到了Connection
、Command
、DataReader
和其他 about 对象,但是他们没有在任何 about 名称空间中看到名为Connection
、Command
或DataReader
的类。原因是数据提供者类在System.Data
名称空间中实现了接口。这些接口定义了 ADO.NET API 的数据提供者方法。
概念很简单。数据提供者,比如System.Data.SqlClient
,由一些类组成,这些类的方法提供了访问特定类型数据源的统一方式。这适用于 ADO.NET 的所有设施,无论您需要访问哪种数据源。
SQL Server 数据提供程序针对访问 SQL Server 进行了优化,不能用于任何其他 DBMS。OLE DB 数据提供程序可以访问任何 OLE DB 数据源。ODBC 数据提供程序允许您使用一种更古老的数据访问技术,同样,您对此一无所知。在这样一个抽象的层次上工作能让你做得更多、更快。
ADO.NET 不仅是一种高效的数据访问技术,也是一种优雅的技术。数据提供者只是其中的一个方面。ADO.NET 编程的艺术更多的是建立在概念化而不是编码上。首先弄清楚 ADO.NET 能提供什么;然后在正确的类中寻找正确的方法,让想法变成现实。
由于概念清晰非常重要,您可以将连接、命令、数据读取器和其他 ADO.NET 组件主要视为抽象,而不仅仅是数据库程序中使用的对象。如果你专注于概念,学习何时以及如何使用相关的对象和方法将会很容易。
总结
在本章中,您看到了开发 ADO.NET 的原因以及它如何取代. NET 中的其他数据访问技术。我们概述了它的体系结构,然后重点介绍了它的核心组件之一,数据提供程序。您构建了三个简单的示例来练习基本的数据提供者用法,并体验了编写数据访问代码的统一方式,而不考虑数据提供者。最后,我们认为概念清晰是理解和使用数据提供者和 ADO.NET API 的关键。接下来,我们将研究 ADO.NET 的细节,从连接开始。
十一、处理异常
对于您编写的程序,您肯定会关心修复语言编译器引起您注意的任何错误或问题。然而,有一种特殊类型的错误在编译时不会发生;相反,它发生在运行时。随着您进行更复杂的应用开发,您有更多的机会出现这样的运行时错误,称为异常。发生这种情况的原因可能是应用试图打开一个与不存在的数据库的连接,打开一个不存在的文件,或者写入一个已经以只读模式打开的文件。本章将帮助你学习更多关于异常的知识,以及当异常发生时如何处理它们。
系统。异常类
英寸 NET 中,所有的异常都是从Exception
类派生的。Exception
类是在System
名称空间中定义的。其他派生的异常类分布在许多其他名称空间中,比如SQLException
、FileNotFoundException
、IndexOutOfRangeException
等等。
因此当你调用一些。NET 功能,并且在运行时出错,函数可能会抛出特定类型的异常。例如,如果您连接到一个不存在的数据库,您将收到一个运行时错误,换句话说,一个类型为SqlException
的异常。类似地,如果您试图打开一个不存在的文件进行读取,您将得到一个FileNotFound
异常。
重要的是要理解所有的异常都是从System.Exception
类派生的。例如,如果您捕捉到了System.Exception
,那么这将涵盖从System.Exception
派生的所有异常。我将在本章的后面演示这一点。表 11-1 显示了System.Exception
类暴露的属性。
什么原因导致异常发生
在我们深入了解如何处理异常的更多细节之前,让我们看看异常发生时是什么样子,以及应用在这种情况下如何表现。
如今,许多组织依靠日志文件来跟踪系统上发生的活动;你甚至可能已经看过或读过一些setup.log
文件。因此,文件处理是一个重要的概念,可以应用于许多情况。例如,您在文本框中输入的任何内容都可以记录到日志文件中,以后可以从磁盘上的存储文件中读取这些信息。
假设您有一个从用户那里读取文件路径和文件名并打开文件的应用。通常,应用工作正常,但是在这种情况下,提供了不正确的文件名或路径,因此会发生异常。
试试看:创建一个文件处理应用
在本练习中,您将创建一个带有四个标签、四个文本框和两个按钮的 Windows 窗体应用。应用将接受一些文本,然后将其保存/写入磁盘上的文件;它还将文件路径作为输入,并为您读取文件内容。
-
创建一个名为 Chapter11 的新 Windows 窗体应用项目。当解决方案资源管理器打开时,保存解决方案。
-
将 Chapter11 项目重命名为 FileHandling,然后将 Form1 重命名为 FileExceptionHandling。
-
将 FileExceptionHandling 窗体的 Text 属性更改为 File-Read/Write。
-
将 Label 控件拖到窗体上,并将其放在左上角。选择此标签控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblPathWrite。
- 设置 Text 属性以输入文件写入路径。
-
将一个 TextBox 控件拖动到刚刚拖动到窗体上的名为 lblPathWrite 的 Label 控件旁边。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtFileWritePath。
- 将 Size 属性设置为 301,20。
-
将一个 Button 控件拖动到刚刚拖动到窗体上的 TextBox 控件旁边。选择此按钮控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 btnWriteToFile。
- 设置 Text 属性以写入文件。
-
将一个 Label 控件拖到窗体上,并将其放在名为“输入文件写入路径”的 Label 控件下面。选择此标签控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblText。
- 设置 Text 属性以输入文本。
-
将一个 TextBox 控件拖动到刚刚拖动到窗体上的名为 lblText 的 Label 控件旁边。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtFileText。
- 将 Multiline 属性设置为 True。
- 将 Size 属性设置为 301,60。
-
Drag a Label control onto the form, and position it below the Label control named lblText. Select this Label control, navigate to the Properties window, and set the following properties:
- 将 Name 属性设置为 lblPathRead。
- 设置 Text 属性以输入文件读取路径。
将一个 TextBox 控件拖动到刚刚拖动到窗体上的名为 lblPathWrite 的 Label 控件旁边。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtFileReadPath。
- 将 Size 属性设置为 301,20。
-
将一个 Button 控件拖动到刚刚拖动到窗体上的 TextBox 控件旁边。选择此按钮控件,导航到“属性”窗口,并设置以下属性:
1. 将 Name 属性设置为 btnReadFile。
2. 将 Text 属性设置为 Read File。 -
将一个 Label 控件拖到窗体上,并将其放在名为“输入文件读取路径”的 Label 控件下面。选择此标签控件,导航到“属性”窗口,并设置以下属性:
1. 将 Name 属性设置为 lblFileContent。
2. 将 Text 属性设置为 File Content。 -
将一个 TextBox 控件拖动到刚刚拖动到窗体上的名为 lblText 的 Label 控件旁边。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
1. 将 Name 属性设置为 txtFileContent。
2. 将 Multiline 属性设为 True。
3. 将 Size 属性设置为 301,90。 -
Your FileExceptionHandling form will look like Figure 11-1.
***图 11-1。**设计视图中的文件处理表单*
- 现在是写代码的时候了。
- 双击“写入文件”按钮,开始添加代码。
- 首先为与文件相关的操作添加一个
using
语句,在顶部的代码编辑器中导航到using
语句,并在末尾添加以下语句:using System.IO;
- Now add the following code for writing text to the file from Listing11-1:
***列表 11-1。** btnWriteToFile_Click*
`StreamWriter sw = new StreamWriter(txtFileWritePath.Text, true);
sw.WriteLine(txtFileText.Text);
sw.Close();`
- 双击 Read File 按钮,并添加下面的代码来从清单 11-2 的文件中读取内容:
Listing11-2: btnReadFile_ClickStreamReader sr = new StreamReader(txtFileReadPath.Text); txtFileContent.Text = sr.ReadToEnd(); sr.Close();
- Save the changes and build the application. You should see the message “Build Succeeded.” Now it’s time to run the application and do a file read-write operation. Press Ctrl+F5 to run the program.
![Image](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs5-db/img/square.jpg) **注意**重要的是要明白`System.IO`只能读写那些扩展名为`.txt`或`.log file`的文件。此外,这种文件处理程序是基于文件路径和文件名的。出于演示的目的,我使用我的笔记本电脑特定的路径和文件名。这些文件路径和文件名与您系统上的不匹配,因此请相应地修改目录名和文件名。
- When the application launches, enter the file path in the first text box and then type the text you want to save in this file, as shown in Figure 11-2.
![Image](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs5-db/img/square.jpg) **注意**根据你的电脑使用文件读写路径很重要;此外,请确保您对指定的驱动器/文件夹具有写权限。
![Image](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs5-db/img/9781430242604_Fig11-02.jpg)
***图 11-2。**文件写入操作正在进行*
- Once you’re done, click the Write To File button. You should be able to see your file created, as shown in Figure 11-3.
***图 11-3。**应用创建的日志文件*
- Now switch back to the running application, type the path where your file just got created in the Enter File Read Path text box, and click the Read File button. You should see the output shown in Figure 11-4.
***图 11-4。**文件读写动作*
试试看:引起异常发生,观察行为
在本练习中,您将继续使用已创建的应用,然后创建一个会导致异常发生的场景,以便您可以观察该行为。
-
在 Visual Studio 2012 中打开 FileHandling 项目。
-
要引起异常,您只需对预先创建的文件执行文件读取操作。
-
This time, you will run the application from the file
Chapter11.exe
, which is located in the project’sbin\debug
folder, as shown in Figure 11-5.图 11-5。从其指定的
.exe
运行程序从bin\debug
运行到 -
When the application loads, enter the following path in the Enter File Read Path text box: c:\vidyavrat\MyLogFile.log. As you probably recall, you saved the file as
MyFile.log
(refer to the earlier Figure11-2), but here you are intentionally passing the wrong file name, as shown in Figure 11-6.图 11-6。提供错误的文件名导致异常
-
Now click the Read File button. Because this file name is incorrect, you will receive a strange-looking dialog with an unhandled exception, as shown in Figure 11-7.
图 11-7。通过
.exe
执行代码时出现异常对话框正如您所看到的,这将中止您的应用,并留给您一个不愉快的对话框,这当然不是用户友好的。
-
单击“退出”退出异常对话框。(如果您单击继续,您将切换回应用 UI。)
-
为了深入了解,让我们通过按 Ctrl+F5 从 Visual Studio 运行该项目。
-
When the application loads, repeat the previous steps to enter an incorrect file name, and click Read File. You will get an exception. The difference now by running the program through Visual Studio is that it points to the code and exact details of the exception so you can add exception-handling code, as shown in Figure 11-8.
图 11-8。通过 Visual Studio 执行代码时出现异常对话框
如您所见,这对开发人员来说是非常有用的。它明确指出您提供的文件名不存在或没有找到,并抛出一个FileNotFoundException
异常。
现在,一旦发生这种情况,就没有解决办法了。您必须通过按 Shift+F5 来中断应用,这将使您返回到代码视图,应用将停止运行。但是您现在知道发生了什么类型的异常,所以您可以添加代码来处理它。
注意本章的主要目的是异常处理;本章后面的练习将提供详细的操作步骤。现在,理解异常的一些更概念性的方面是很重要的。
探索异常的类型、消息和堆栈跟踪属性
任何。NET 异常中发生的或通过运行 EXE 文件而发生的异常包含大量信息,开发人员可以对其进行处理或进一步调查。Type、Message 和 StackTrace 属性服务于这个伟大的目的。
类型定义了类别,或者发生了哪种异常。每当发生. NET 异常时,它都会在对话框的标题栏中显示异常的类型。可以看到,图-11-8 中对话框的标题栏上提到了FileNotFoundException
。
通过点击对话框底部(在操作下)的查看详细信息,可以找到关于任何异常的更多信息,如图图 11-7 所示。这适用于您在. NET 中遇到的任何异常。
单击查看详细信息后,您将看到另一个窗口打开;它提供了异常快照。展开这个,你会发现很多信息。查找 Message 属性,它保存了发生异常时的确切消息,如图图 11-9 所示。
在大多数情况下,开发人员希望在这个系统生成的消息中添加一些额外的文本。我们将在本章后面介绍如何做到这一点。
图 11-9。异常消息属性
其他重要信息由 StackTrace 属性公开,该属性主要对希望调试代码并找出是哪一行代码导致了问题的人有用。此外,许多组织出于监控目的,将此类信息记录到事件查看器或日志文件中。
就在 Message 属性下面,您会发现 StackTrace 选项;选中它,然后点击其描述文本右侧向下的箭头,如图图 11-10 所示。
图 11-10。异常的堆栈跟踪属性
您会注意到,这个 StackTrace 属性是预先选定的,因此您可以轻松地复制和粘贴它。大多数 bug 分类会议都有很多关于 StackTrace 的讨论,每当一个手工测试人员谈到一些运行时错误时,大多数开发人员都会说,“请给我提供 StackTrace!”因为它可以准确地指出问题所在。
现在让我们将这个 StackTrace 复制并粘贴到一个记事本文件中并对其进行研究,如图图 11-11 所示。此图仅显示了堆栈跟踪的前半部分。当您复制和粘贴时,您将看到完整的详细信息。
图 11-11。正在调查异常的堆栈跟踪细节
如果您查找图中所示的选中行,您将会发现是哪个文件导致了这个异常的发生,以及问题出在哪个路径和行号上。
处理异常
现在您可能已经意识到,异常处理是一种用于避免任何运行时错误并优雅地处理它们的技术,而不是发出一些笨拙的消息或让应用在用户面前挂起。
异常处理主要基于三个关键字:try
、catch
、finally
。任何程序都可以有一个try
,后跟一个或多个catch
块,然后以一个finally
块结束。
try
块保存任何抛出或可能抛出异常的代码。catch
块作为一种防御机制,处理抛出的异常。finally
区块有独特的行为;它将在两种情况下执行:异常没有发生时和异常发生时。因此,finally
的最佳代码语句是关闭文件流、关闭数据库连接,或者甚至向客户告别,等等,但是现实世界的应用包括关闭流和连接。我将在下面的练习中演示这一点。
试试看:添加异常处理语句
在本练习中,您将继续使用创建的应用,然后添加异常处理代码块来处理此类异常,避免以不友好的方式向用户显示。
-
在 Visual Studio 2012 中打开 FileHandling 项目。
-
Now double-click the Read File button and replace the code with the one in Listing 11-3.
清单 11-3。 btnReadFile_Click
`StreamReader sr=null;
try
{ sr = new StreamReader(txtFileReadPath.Text);
txtFileContent.Text = sr.ReadToEnd();
}catch (FileNotFoundException ex)
{
MessageBox.Show(ex.Message + " " + "Please provide valid path and filename");
}catch (DirectoryNotFoundException ex)
{
MessageBox.Show(ex.Message + " " + "Please provide valid Directory
name", "File Read Error");
}finally
{
if (sr != null)
{
sr.Close();
}`
构建应用,并按 Ctrl+F5 运行它。如果你这次传递了错误的文件名,它实际上会抛出一个异常,但是我们正在处理,所以会显示一个用户友好的消息,如图 11-12 所示。
图 11-12。使用catch
程序块进行异常处理
如您所见,该对话框显示您输入了错误的路径或文件名。单击 OK,它将带您回到应用,让您正确地修改路径或文件名。
在这样的文件处理应用中,另一个场景是当用户传递错误的目录名时。为了处理这个问题,你需要一个单独的catch
块来处理DirectoryNotFoundException
。你已经添加了,如清单 11-3 所示。现在要测试它,请将路径更改为一个不存在的文件夹名,您将看到一个单独的对话框,提示“提供有效的目录名”
它是如何工作的
这个文件读取代码是基于Stream
对象的,所以您需要创建一个StreamReader
对象。
StreamReader sr=null;
现在您在try
块中使用这个对象来传递文件路径和文件名,以便读取内容。
try { sr = new StreamReader(txtFileReadPath.Text); txtFileContent.Text = sr.ReadToEnd(); }
如果提供了错误的文件路径或文件名,那么它将抛出一个FileNotFoundException
,因此您需要提供一个catch
块来处理这个异常。
catch (FileNotFoundException ex) { MessageBox.Show(ex.Message + " " + "Please provide valid path and filename"); }
如果提供了一个错误的目录名,那么它将抛出一个DirectoryNotFoundException
,所以您需要提供一个catch
块来处理这个异常。
catch (DirectoryNotFoundException ex) { MessageBox.Show(ex.Message + " " + "Please provide valid Directory name", "File Read Error"); }
在任何情况下,无论文件是否被读取,都需要关闭一个Stream
对象。执行这样的强制操作可能是一个finally
块的最佳候选。同样,你会注意到在异常情况下,Stream
对象不会被初始化,因为文件名或路径找不到,所以不能被关闭。
因此,您必须在关闭之前检查您创建的Stream
对象是否为空。
finally { if (sr != null) { sr.Close(); } }
总结
在本章中,你学习了异常处理以及如何处理 C# 文件 I/O 程序抛出的异常。在专门讨论 ADO.NET 的下一章中,您将在整个 ADO 中应用异常处理原则。NET 代码。
具体来说,在下一章中,您将了解如何创建一个到 SQL Server 2012 数据库的 ADO.NET 连接。
十二、建立连接
在对数据库做任何有用的事情之前,您需要与数据库服务器建立一个会话。您可以通过一个名为连接、的对象来实现这一点,该对象是一个类的实例,该类为特定的数据提供者实现了System.Data.IDbConnection
接口。在本章中,您将使用各种数据提供者来建立连接,并了解可能出现的问题以及如何解决这些问题。在本章中,我们将介绍以下内容:
- Introduce the data provider connection class
- Use
SqlConnection
- Connecting to SQL Server to Improve the Use of Connection Objects
- Use
OleDbConnection
连接到 SQL Server】
介绍数据提供者连接类
正如您在第十章中看到的,每个数据提供者都有自己的名称空间。每个都有一个实现System.Data.IDbConnection
接口的连接类。表 12-1 总结了微软提供的数据提供商。
如您所见,这些名称遵循一个约定,使用前缀为数据提供者标识符的Connection
。由于所有的连接类都实现了System.Data.IDbConnection
,所以每一个的用法都是相似的。每个都有额外的成员,提供特定于特定数据库的方法。
使用 SqlConnection 连接到 SQL Server 2012
在此示例中,您将连接到 SQL Server 连接到 SQL Server 2012 AdventureWorks 数据库。
尝试一下:使用 SqlConnection
您将编写一个非常简单的程序来打开和检查连接。
-
在 Visual Studio 2011 中,创建一个名为 Chapter12 的新 Windows 控制台应用项目。当解决方案资源管理器打开时,保存解决方案。
-
Rename the Chapter12 project to ConnectionSQL. Rename the
Program.cs
file toConnectionSql.cs
, and replace the generated code with the code in Listing 12-1.清单 12-1。T4
ConnectionSql.cs
`using System;
using System.Data;
using System.Data.SqlClient;namespace Chapter12
{
class ConnectionSql
{
static void Main(string[] args)
{
// Connection string (connection string key=value
//might be different for you based on your environment
string connString = @"server = .\sql2012; integrated security = true;";// Create connection
SqlConnection conn = new SqlConnection(connString);try
{
// Open connection
conn.Open();
Console.WriteLine("Connection opened.");
}catch (SqlException ex)
{
// Display error
Console.WriteLine("Error: " + ex.Message + ex.StackTrace);
}finally
{
// Close connection
Console.WriteLine("Connection closed.");
}Console.ReadLine();
}
}
}` -
Run the application by pressing Ctrl+F5. If the connection is successful, you’ll see the output in Figure 12-1.
图 12-1。打开和关闭数据库连接
如果连接失败,你会看到如图图 12-2 所示的错误信息。(您可以通过首先关闭 SQL Server 服务,在命令提示符下输入net stop mssql$<SQL Server instance name>
来实现这一点。如果你尝试这样做,记得用net start mssql$<SQL Server instance name>
重启它。)或者,简单的方法是尝试将错误的 SQL 实例名传递给连接字符串。
图 12-2。连接到 SQL Server 时连接失败的错误
现在不要担心这个消息的细节。连接失败的原因通常与您的代码无关。这可能是因为服务器没有启动,就像在这种情况下,或者因为密码是错误的,或者因为其他一些配置问题存在。您将很快看到建立数据库连接中的常见问题。
它是如何工作的
让我们检查一下清单 12-1 中的代码,以理解连接过程中的步骤。首先,指定 ADO.NET 和 SQL Server 数据提供程序命名空间,以便可以使用其成员的简单名称。
using System; using System.Data; using System.Data.SqlClient;
然后,创建一个连接字符串。一个连接字符串由参数组成——换句话说,由分号分隔的key=value
对——指定连接信息。尽管有些参数对所有数据提供程序都有效,但每个数据提供程序都有它将接受的特定参数,因此了解您所使用的数据提供程序的连接字符串中哪些参数是有效的非常重要;这将在本章后面详细解释。
// Connection string string connString = @"server = .\sql2012; integrated security = true;";
让我们简要地检查一下这个例子中的每个连接字符串参数。server 参数指定要连接的 SQL Server 实例。
server = .\sql2012;
在该语句中,.
(点)表示本地服务器,后面跟有\
(斜线)的名称表示数据库服务器上运行的 SQL Server 实例名称。因此,这里有一个名为 sql2012 的 SQL Server 2012 实例运行在本地服务器上。
提示 (local)
是.
(点号)的替代,用来指定本地机器,所以.\sqlexpress
可以用(local)\sql2012
代替,甚至可以写成localhost\sql2012
。
下一个子句指示您应该使用 Windows 身份验证(即任何有效登录的 Windows 用户都可以登录到 SQL Server)。
integrated security = true;
你也可以用sspi
代替true
,因为它们有相同的效果。其他参数也是可用的。稍后您将使用它来指定您想要连接的数据库。
接下来创建一个连接(一个SqlConnection
对象),向它传递连接字符串。这不会创建数据库会话。它只是创建一个对象,稍后您将使用它来打开一个会话。
// Create connection SqlConnection conn = new SqlConnection(connString);
现在您有了一个连接,但是您仍然需要通过调用连接上的Open
方法来建立与数据库的会话。如果试图打开一个会话失败,将会抛出一个异常,所以您使用一个try
语句来启用异常处理。您在调用Open
后会显示一条消息,但是这条线只有在连接成功打开时才会被执行。
try { // Open connection conn.Open(); Console.WriteLine("Connection opened."); }
在代码的这个阶段,您通常会通过打开的连接发出一个查询或执行一些其他数据库操作。然而,我们将把它留到后面的章节,在这里只关注连接。
接下来是一个异常处理程序,以防Open()
失败,如本章前面的图 12-2 所示。
catch (SqlException ex) { // Display error Console.WriteLine("Error: " + ex.Message + ex.StackTrace); }
每个数据提供程序都有一个特定的异常类用于其错误处理;SqlException
是 SQL Server 数据提供程序的类。异常中提供了关于数据库错误的特定信息,但是这里只显示了它的原始内容。
当您完成数据库时,您调用Close()
来终止会话,然后打印一条消息来显示调用了Close()
。
finally { // Close connection conn.Close(); Console.WriteLine("Connection closed."); }
你在finally
块中调用Close()
,以确保总是被调用。
控制台应用倾向于在短时间内加载带有输出的命令窗口,然后自动关闭。要在屏幕上保持窗口以便阅读和理解输出,调用Console
类的ReadLine()
方法。这将让窗口停留,直到你按下回车键。
Console.ReadLine();
注意建立连接(数据库会话)的成本相对较高。它们使用客户端和服务器上的资源。尽管连接最终可能会通过垃圾收集或超时而关闭,但在不再需要时让一个连接保持打开是一种不好的做法。太多打开的连接会降低服务器速度或阻止建立新的连接。
注意,可以在关闭的连接上调用Close()
,不会抛出异常。因此,如果连接早一点关闭,或者即使它从未打开过,您的消息也会显示出来。参见图 12-2 ,连接失败但仍显示关闭信息。
在一个典型的例子中,多次调用Open()
和Close()
是有意义的。ADO.NET 支持断开的数据处理,即使与数据提供程序的连接已经关闭。模式如下所示:
`try
{
// open connection
conn.Open();
// online processing (e.g., queries) here
//
conn.Close(); // close connection
//
// offline processing here
//
conn.Open(); // reopen connection
//
// online processing(e.g., INSERT/UPDATE/DELETE) here
//
conn.Close(); // reclose connection
}
catch(SqlException ex)
{
// error handling code here
}
finally
{
// close connection
conn.Close();
}`
finally
块仍然调用Close()
,如果没有遇到异常就不必要地调用它,但这不是问题,也不昂贵,而且它确保连接将被关闭。尽管许多程序员在程序终止前一直保持连接,但这通常会浪费服务器资源。有了连接池,根据需要打开和关闭连接实际上比一劳永逸地打开更有效。
就这样!您已经完成了第一个连接示例。但是,既然您看到了一个可能的错误,那么让我们来看看连接错误的典型原因。
调试与 SQL Server 的连接
编写使用连接的 C# 代码通常是让连接工作的简单部分。问题通常不在于代码,而在于客户机(您的 C# 程序)和数据库服务器之间的连接参数不匹配。必须使用所有适当的连接参数,并且必须具有正确的值。即使是经验丰富的数据库专业人员,在第一次连接时也经常会遇到问题。
除了这里显示的参数之外,还有更多可用的参数,但你已经明白了。墨菲定律的一个推论适用于关系:如果几件事都可能出错,那么其中肯定会有一件出错。您的目标是检查连接的两端,以确保您所有的假设都是正确的,并且客户端程序指定的所有内容都在服务器上正确匹配。
解决方案通常在服务器端。如果 SQL Server 实例没有运行,客户端将尝试连接到不存在的服务器。如果未使用 Windows 身份验证,并且客户端上的用户名和密码与有权访问 SQL Server 实例的用户的用户名和密码不匹配,则连接将被拒绝。如果连接中请求的数据库不存在,将会出现错误。如果客户端的网络信息与服务器的网络信息不匹配,服务器可能不会收到客户端的连接请求,或者服务器的响应可能不会到达客户端。
对于连接问题,使用调试器来定位发生错误的代码行通常没有帮助——问题几乎总是发生在对Open
方法的调用上。问题是,为什么?您需要查看错误消息。
典型的错误如下:
Unhandled Exception: System.ArgumentException: Keyword not supported...
这是因为使用了无效的参数或值,或者连接字符串中的参数或值拼写错误。确保您输入了您真正想要输入的内容。
图 12-2 早先显示了可能是连接到 SQL Server 时最常见的消息。在这种情况下,很可能 SQL Server 根本没有运行。使用net start mssql$<your sql instance name>
重新启动 SQL Server Express 服务。
此消息的其他可能原因如下:
- The SQL Server instance name is incorrect. For example, you used
.\sqlexpress
, but SQL Server was installed with a different name. It is also possible that SQL Server is installed as the default instance (without instance name) or installed on another computer (see the next section); If this is the case, please correct the instance name.- SQL Server has not been installed—Go back to chapter 1 of and install SQL Server 2012 Express according to the instructions therein.
- There is a security problem-your Windows login and password are invalid on the server. This is unlikely to be a problem when connecting to a local instance of SQL Server Express, but it may happen when trying to connect to an instance of SQL Server on another server. There is a hardware problem-this is also unlikely if you try to connect to a server on the same machine.
SqlConnection 中的安全性和密码
SQL Server 2012 中有两种用户身份验证。首选方法是使用 Windows 身份验证(集成安全性),正如您在遵循本书中的示例时所做的那样。SQL Server 使用您的 Windows 登录名来访问该实例。您的 Windows 登录名必须存在于运行 SQL Server 的计算机上,并且您的登录名必须有权访问 SQL Server 实例,或者是具有访问权限的用户组的成员。
如果在连接字符串中不包含Integrated Security = true
(或Integrated Security = sspi
)参数,则连接默认为 SQL Server 安全,在 SQL Server 中使用单独的登录名和密码。
如何使用 SQL Server 安全性
如果您确实打算使用 SQL Server 安全性,因为您的公司或部门就是这样设置对您的 SQL Server 的访问权限的(可能因为某些客户端是非 Microsoft 的),您需要在连接字符串中指定用户名和密码,如下所示:
thisConnection.ConnectionString = @"server = sqlexpress; user id = sa; password = xly2z3";
用户名sa
是 SQL Server 的默认系统管理员帐户。如果已经设置了特定用户,如george
或payroll
,请指定该名称。sa
的密码在安装 SQL Server 时设置。如果您使用的用户名没有密码,您可以完全省略 password 子句或指定一个空密码,如下所示:
password =;
然而,空白密码是不好的做法,即使在测试环境中也应该避免。
【SqlConnection 的连接字符串参数
表 12-2 总结了 SQL Server 数据提供程序连接字符串的基本参数。
表 12-2 中的别名栏给出了替代参数名称。例如,您可以使用以下任一选项来指定服务器:
data source = .\<sql instance name> server = .\ <sql instance name> address = .\ <sql instance name> addr = .\ <sql instance name> network address = .\ <sql instance name>
连接池
一个值得注意的底层细节是连接池,尽管您不应该改变它。回想一下,就内存和时间而言,创建连接是非常昂贵的。使用连接池,关闭的连接不会立即被销毁,而是保存在内存中未使用的连接池中。如果新的连接请求与池中某个未使用的连接的属性相匹配,则该未使用的连接将用于新的数据库会话。
在网络上创建一个全新的连接可能需要几秒钟,而重用一个池化的连接可能需要几毫秒;使用池连接要快得多。连接字符串具有可以改变连接池大小甚至关闭连接池的参数。默认值(例如,缺省情况下连接池是打开的)适用于大多数应用。
提高对连接对象的使用
第一个示例程序中的代码很简单,所以您可以专注于连接如何工作。让我们增强一下。
在连接构造函数中使用连接字符串
在 ConnectionSql 项目中,您在单独的步骤中创建了连接并指定了连接字符串。因为您总是必须指定连接字符串,所以您可以使用将连接字符串作为参数的构造函数的重载版本。
// create connection SqlConnection conn = new SqlConnection(@"server = (local)\sqlexpress; integrated security = true; ");
这个构造函数在创建SqlConnection
对象时设置ConnectionString
属性。您将在下一个示例中尝试它,并在后面的章节中使用它。
显示连接信息
连接有几个提供连接信息的属性。这些属性大多数是只读的,因为它们的目的是显示而不是设置信息。(您可以在连接字符串中设置连接值。)这些属性在调试时通常很有用,可以验证连接属性是否是您所期望的。
这里,我们将描述大多数数据提供者共有的连接属性。
试试看:显示连接信息
在这个例子中,您将看到如何编写一个程序来显示连接信息。
-
将名为 ConnectionDisplay 的 C# 控制台应用项目添加到 Chapter12 解决方案中。
-
Rename
Program.cs
toConnectionDisplay.es
. When prompted to rename all references toProgram
, you can click either Yes or No. Replace the code with that in Listing 12-2.清单 12-2。T4
ConnectionDisplay.es
using System; using System.Data; using System.Data.SqlClient; namespace Chapterl2 { class ConnectionDisplay { static void Main() {
` // Create connection
SqlConnection conn = new SqlConnection(@” server = .\sql2012;
integrated security = true; ");try
{
// Open connection
conn.Open();
Console.WriteLine("Connection opened.");// Display connection properties
Console.WriteLine("Connection Properties:");
Console.WriteLine("\tConnection String: {0}",
conn.ConnectionString);
Console.WriteLine( "\tDatabase: {0}", conn.Database);
Console.WriteLine( "\tDataSource: {0}", conn.DataSource);
Console.WriteLine("\tServerVersion: {0}", conn.ServerVersion);
Console.WriteLine( "\tState: {0}", conn.State);
Console.Writel_ine("\tWorkstationld: {0}", conn.Workstationld);}
catch (SqlException ex)
{
// Display error
Console.WriteLine("Error: " + ex.Message + ex.StackTrace);
}finally
{
// Close connection
conn.Close();
Console.WriteLine("Connection closed.");
}Console.ReadLine();
}
}
}` -
To set ConnectionDisplay as the startup project, select the project in Solution Explorer, right-click and select Set as StartUp Project, and run it by pressing Ctrl+F5. If the connection is successful, you’ll see output like that shown in Figure 12-3.
图 12-3。显示连接信息
它是如何工作的
ConnectionString
属性可以读写。在这里你只是展示它。
Console.WriteLine("\tConnection String: {0}", conn.ConnectionString);
您可以在逐字字符串中看到您赋予它的值,包括空格。
有什么意义?嗯,在调试连接时,验证连接字符串是否真的包含您认为已经赋值的值是很方便的。例如,如果您尝试不同的连接选项,程序中可能会有不同的连接字符串参数。你可能注释掉了一个,打算以后用,但是忘了。显示ConnectionString
属性有助于查看参数是否丢失。
下一条语句显示了Database
属性。由于每个 SQL Server 实例都有几个数据库,因此该属性显示您在连接时最初使用的数据库。
Console.WriteLine("\tDatabase: {0}",conn.Database);
在这个程序中,它显示
Database: master
由于您没有在连接字符串中指定数据库,所以您连接到了 SQL Server 的默认数据库主服务器。如果您想连接到 AdventureWorks 或您选择的数据库,您需要指定Database
参数,例如:
// connection string string connString = new SqlConnection(@"server = .\sqlexpress; database = northwind "; integrated security = true;)
还可以通过执行以下语句将默认数据库从 master 数据库更改为其他数据库,例如 AdventureWorks:
exec sp_defaultdb 'sa'/adventureworks'
同样,这是一个方便显示的属性,用于调试目的。如果您得到一个错误,说某个特定的表不存在,通常问题不是这个表不存在,而是它不在您所连接的数据库中。显示Database
属性有助于您快速找到这种错误。
提示如果您在连接字符串中指定了一个服务器上不存在的数据库,您可能会看到以下错误消息:“System .Data.SqlClient.SqlException:无法打开登录名请求的数据库“database”。登录失败。
您可以使用ChangeDatabase
方法更改连接上当前使用的数据库,如下所示:
Conn.ChangeDatabase("AdventureWorks");
下一条语句显示了DataSource
属性,该属性给出了 SQL Server 数据库连接的服务器实例名称。
Console.WriteLine( "\tDataSource: {0}", conn.DataSource);
这将显示与您的 SQL 实例名称相同的 SQL Server 实例名称;例如,在我的例子中,它将显示以下内容:
DataSource: .\sql2012
同样,这主要是为了调试的目的。
ServerVersion
属性显示服务器版本信息。
Console.WriteLine("\tServerVersion: {0}",conn.ServerVersion);
它显示了您在第一章中安装的 SQL Server Express 版本。(您的版本可能有所不同。)
ServerVersion: 11.00.1750
版本号对调试很有用。该信息实际上来自服务器,因此它表明连接正在工作。
注意 SQL Server 2008 是版本 10,SQL Server 2005(和 SSE)是版本 9。SQL Server 2000 是版本 8。
**State
属性表示连接是打开的还是关闭的。
Console.WriteLine( "\tState: {0}", conn.State);
因为您在调用Open()
之后显示这个属性,所以它表明连接是打开的。
State: Open
您已经显示了您自己的消息,表明连接是打开的,但是该属性包含当前状态。如果连接关闭,State
属性将是Closed
。
然后显示工作站 ID,它是标识客户端计算机的字符串。Workstationld
属性是特定于 SQL Server 的,可以方便地进行调试。
Console.WriteLine("\tWorkstationId: {0}",conn.WorkstationId);
它默认为计算机名。我的电脑命名为 VIDYAVRAT,但你的,当然,会有所不同。
Workstationld: <YourComputerName>
这对于调试非常有用,因为服务器上的 SQL Server 工具可以显示哪个工作站 ID 发出了特定的命令。如果您不知道是哪台机器导致了问题,您可以修改您的程序来显示Workstationld
属性,并将它们与服务器上显示的工作站 id 进行比较。
您还可以使用 workstation ID 连接字符串参数设置该属性,如下所示,因此,如果您希望建筑物 B 中的所有工作站都显示服务器上的该信息,您可以在程序中指明:
// Connection string string connString = @" server = \sql2012; workstation id = Building B; integrated security = true; ";
关于使用SqlClient
连接到 SQL Server 的基础讨论到此结束。现在让我们看看如何连接另一个数据提供者。
使用 OleDbConnection 连接到 SQL Server
正如您在第十章中看到的,您可以使用 OLE DB 数据提供程序来处理任何 OLE DB 兼容的数据存储。Microsoft 为 Microsoft SQL Server、Microsoft Access (Jet)、Oracle 以及各种其他数据库和数据文件格式提供 OLE DB 数据提供程序。
如果本机数据提供程序可用于特定的数据库或文件格式(如用于 SQL Server 的SqlClient
数据提供程序),通常使用它比使用通用 OLE DB 数据提供程序更好。这是因为 OLE DB 在 C# 程序和数据源之间引入了一个额外的间接层。没有本地数据提供程序的一种常见数据库格式是 Microsoft Access 数据库(.mdb
文件)格式,也称为 Jet 数据库引擎格式,因此在这种情况下,您需要使用 OLE DB(或 ODBC)数据提供程序。
我们不假设您有要连接的 Access 数据库,所以您将在 SQL Server 中使用 OLE DB。
试试看:使用 OLE DB 数据提供程序连接到 SQL Server
若要使用 OLE DB 数据提供程序连接到 SSE,请按照下列步骤操作:
-
添加一个名为 ConnectionOleDb 的 C# 控制台应用项目,并将
Program.cs
重命名为Connection01eDb.cs
。 -
Replace the code in
Connection01eDb.cs
with that in Listing 12-3. This is basically the same code asConnection.cs
, with the changed code in bold.清单 12-3。T4
Connection01eDb.cs
`using System;
using System.Data;
using System.Data.OleDb;namespace Chapter12
{
class ConnectionOleDb
{
static void Main()
{
// Create connection
OleDbConnection conn = new OleDbConnection(@"provider = sqloledb;
data source = .\sql2012; integrated security = sspi;");try
{
// Open connection
conn.Open();
Console.WriteLine("Connection opened.");// Display connection properties
Console.WriteLine("Connection Properties:");
Console.WriteLine("\tConnection String: {0}",conn.ConnectionString);
Console.WriteLine("\tDatabase: {0}",conn.Database);
Console.WriteLine("\tDataSource: {0}",conn.DataSource);
Console.WriteLine("\tServerVersion: {0}",conn.ServerVersion);
Console.WriteLine("\tState: {0}",conn.State);
}
catch (OleDbException ex)
{
// Display error
Console.WriteLine("Error: " + ex.Message + ex.StackTrace);
}
finally
{
// Close connection
conn.Close();
Console.WriteLine("Connection closed.");
}Console.ReadLine();
}
}
}` -
Make ConnectionOleDb the startup project, and run it by pressing Ctrl+F5. If the connection is successful, you’ll see output like that shown in Figure 12-4.
图 12-4。显示 OLE DB 连接信息
它是如何工作的
我们将只讨论这个例子和前一个例子的不同之处。第一步是引用 OLE DB 数据提供程序命名空间。
System.Data.OleDb. using System.Data.OleDb;
接下来,创建一个OleDbConnection
对象,而不是一个SqlConnection
对象。请注意连接字符串的更改。您可以使用Provider
和Data Source
来代替server
参数。注意Integrated Security
参数的值必须是sspi
,而不是true
。
// create connection OleDbConnection conn = new 01eDbConnection(@"provider = sqloledb;data source = .\sql2012; integrated security = sspi;" );
最后,请注意,您在显示中省略了Workstationld
属性。OLE DB 数据提供程序不支持它。
这是使用任何访问任何数据源的模式。NET 数据提供程序。使用特定于数据提供程序的参数指定连接字符串。使用数据提供程序命名空间中的适当对象。仅使用该数据提供程序提供的属性和方法。
总结
在本章中,您使用两个数据提供程序及其相应的连接字符串、参数和值创建、打开和关闭了连接。使用连接属性创建连接后,您显示了有关连接的信息。您还看到了如何处理与连接相关的各种异常。
在下一章,你将看到 ADO.NET 命令,并了解如何使用它们来访问数据。**
十三、执行 ADO.NET 命令来检索数据
一旦建立了与数据库的连接,您就想开始与它交互,让它为您做一些有用的事情。您可能需要检索、添加、更新或删除一些数据,或者以其他方式修改数据库,通常是通过运行查询。无论什么任务,都不可避免地会涉及到一个命令。
在这一章中,我们将解释命令,这些命令是封装您想要执行的操作的 T-SQL 的对象,并提供将它提交给数据库的方法。每个数据提供者都有一个实现System.Data.IDbCommand
接口的命令类。
在本章中,我们将介绍以下内容:
- Create command
- executive order
- Execute commands with multiple results.
- execute statement
- Use stored procedures
在我们的示例中,我们将使用 SQL Server 数据提供程序(System.Data.SqlClient
)。它的命令命名为SqlCommand
。其他数据提供程序的命令工作方式相同。
创建命令
对于要对数据库执行的命令,每个命令都必须与到数据库的连接相关联。通过设置命令的Connection
属性可以做到这一点,为了节省资源,多个命令可以使用同一个连接。您可以使用SqlCommand
构造函数创建一个命令。一旦创建了一个命令,就可以执行与已建立的连接相关联的 SQL 语句了。您将在下一节的语句中看到命令的执行。
给命令分配文本
每个命令都有一个属性CommandText
,它保存您创建的命令对象将执行的 SQL 语句。您可以直接分配给此属性,也可以在构造命令时指定它。让我们看看这些替代方案。
试试看:设置 CommandText 属性
下面的 Windows 应用展示了如何使用SqlCommand
遍历结果集并检索行。
-
创建一个名为 Chapter13 的新 Windows 窗体应用项目。当解决方案资源管理器打开时,保存解决方案。
-
将第十三章项目重命名为 ADO。NET_Command 。将
Form1.cs
文件重命名为CommandText.cs
。 -
通过单击窗体的标题栏选择 CommandText 窗体,并将 Size 属性的宽度设置为 287,高度设置为 176。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtCommandText。
- 将位置属性的 X 设置为 12,Y 设置为 12。
- 将 Multiline 属性设置为 True。
- 将 Size 属性的宽度设置为 244,高度设置为 106。
- 将文本属性留空。
-
Now your CommandText form in the Design view should like Figure 13-1.
图 13-1。CommandText 表单的设计视图
-
Double-click the empty surface of the
CommandText.cs
form, and it will open the code editor window, showing theCommandText_Load
event. Modify theCommandText_Load
event to look like Listing 13-1.清单 13-1。
CommandText.cs
` Using System.Data.SqlClient;
private void CommandText_Load(object sender, EventArgs e)
{
// Create connection
SqlConnection conn = new SqlConnection(@"server = .\sql2012;
integrated security = true; database = AdventureWorks");// Create command
SqlCommand cmd = new SqlCommand();try
{
// Open connection
conn.Open();txtSQL.AppendText("Connection opened \n" );
txtSQL.AppendText("Command created.\n");
// Setting CommandText
cmd.CommandText = @"select Name,ProductNumber
from Production.Product";txtSQL.AppendText("Ready to execute SQL Statement: \n\t\t\t" +
cmd.CommandText);}
catch (SqlException ex)
{
MessageBox.Show(ex.Message + ex.StackTrace,"Exception Details");
}
finally
{
conn.Close();
txtSQL.AppendText("\nConnection Closed.");
}
}` -
Build the project, and run it by pressing Ctrl+F5. You should see the results in Figure 13-2.
图 13-2。使用
CommandText
显示 SQL 语句
它是如何工作的
属性返回一个字符串,所以您可以像显示任何其他字符串一样显示它。当您最终执行分配给CommandText
属性的 SQL 语句时,它将返回AdventureWorks
产品表中产品的名称和产品编号值。
注意在命令可以执行之前,你必须设置命令的Connection
和CommandText
属性。
` // Create command
SqlCommand cmd = new SqlCommand();
// Setting CommandText
cmd.CommandText = @"select Name,ProductNumber
from Production.Product";`
当您使用其构造函数的另一种变体创建命令时,可以设置这两个属性,如下所示:
`// create command (with both text and connection)
String sql = @"select Name,ProductNumber from Production.Product";
SqlCommand cmd = new SqlCommand(sql, thisConnection);`
这相当于前面显式分配每个属性的代码。这是最常用的SqlCommand
构造函数的变体,您将在本章的剩余部分使用它。
执行命令
除非您可以执行命令,否则命令没有多大用处,所以现在让我们来看看。命令有几种不同的方法来执行 SQL。这些方法之间的差异取决于您对 SQL 的预期结果。查询返回数据行(结果集),但是INSERT
、UPDATE
和DELETE
语句不返回。你通过考虑你期望返回的内容来决定使用哪种方法(见表 13-1 )。
您刚刚在示例中使用的 SQL 应该返回一个值,即雇员人数。查看表 13-1 ,可以看到应该使用SqlCommand
的ExecuteScalar()
方法返回这一个结果。让我们试试。
用标量查询执行命令
ExecuteScalar
是用于执行由标量函数组成的 SQL 语句的方法。标量函数是从表中的整组行中只返回一个值的函数。例如,Min( )
、Max( )
、Sum( )
、Count( )
等等,都是标量函数的几个例子。如果从 Employee 执行一个查询,比如Select Min(Salary)
,那么不管表中有多少行,都只会返回一行。现在让我们看看ExecuteScalar( )
方法如何处理这样的 SQL 查询。
尝试一下:使用 ExecuteScalar 方法
要使用ExecuteScalar
方法,请遵循以下步骤:
-
选择 ADO。NET_Command 项目,右击并选择“添加 Windows 窗体”。从打开的对话框中,确保选择了 Windows 窗体,并将
Form1.cs
重命名为CommandScalar.cs
。单击“确定”将该表单添加到 ADO。NET_Command 项目。 -
通过单击窗体的标题栏选择 CommandScalar 窗体,并将 Size 属性的宽度设置为 385,高度设置为 126。
-
将 Label 控件拖到窗体上,并将其放在窗体的左侧。选择此标签控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblRowCount。
- 将位置属性的 X 设置为 4,Y 设置为 35。
- 将 Size 属性的宽度设置为 87,高度设置为 13。
- 将 Text 属性设置为 Total Row Count。
-
将 TextBox 控件拖到窗体上,并将其放置在 Label 控件旁边。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtScalar。
- 将位置属性的 X 设置为 97,Y 设置为 12。
- 将 Multiline 属性设置为 True。
- 将 ScrollBars 属性设置为 Both。
- 将 Size 属性的宽度设置为 164,高度设置为 65。
- 将文本属性留空。
-
将 Button 控件拖到窗体上,并将其放在 TextBox 旁边。选择此按钮控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 btnRowCount。
- 将位置属性的 X 设置为 269,Y 设置为 30。
- 将 Size 属性的宽度设置为 88,高度设置为 23。
- 设置 Text 属性来计算行数。
-
Now your CommandScalar form in the Design view should like Figure 13-3.
图 13-3。命令缩放表单的设计视图
-
Double-click the Button control; it will open the code editor window, showing the
btnRowCount_Click
event. Place the code into theclick
event code template so it looks like Listing 13-2.清单 13-2。
CommandScalar.cs
`using System.Data.SqlClient;
private void btnRowCount_Click(object sender, EventArgs e)
{
// Create connection
SqlConnection conn = new SqlConnection(@"server = .\sql2012;
integrated security = true; database = AdventureWorks");// Create Scalar query
string sql = @"select count(*)
from Production.Product";// Create command
SqlCommand cmd = new SqlCommand(sql, conn);
txtScalar.AppendText("Command created and connected.\n");try
{
// Open connection
conn.Open();txtScalar.AppendText("Number of Product is 😊;
// Execute Scalar query with ExecuteScalar method
txtScalar.AppendText(cmd.ExecuteScalar().ToString());
txtScalar.AppendText("\n");
}catch (SqlException ex)
{
MessageBox.Show(ex.ToString());
}finally
{
conn.Close();
txtScalar.AppendText("Connection Closed.");
}
}` -
To set the CommandScalar form as the start-up form, modify the
Program.cs
statement:Application.Run(new CommandText ());
表现为:
Application.Run(new CommandScalar());
构建项目,并通过按 Ctrl+F5 运行它。
-
When the form loads, click the button Count Rows. The result should look like Figure 13-4.
图 13-4。执行标量命令
它是如何工作的
您所做的就是在对 TextBox 的AppendText
方法的调用中添加对ExecuteScalar()
的调用:
` txtScalar.AppendText("Number of Product is 😊;
// Execute Scalar query with ExecuteScalar method
txtScalar.AppendText(cmd.ExecuteScalar().ToString());`
ExecuteScalar()
获取CommandText
属性,并使用命令的Connection
属性将其发送到数据库。它将结果作为单个对象返回,您可以用 TextBox 的AppendText
方法显示该对象。
ExecuteScalar()
方法的返回类型是object
,它是。NET Framework,当您记住数据库可以保存任何类型的数据时,这是非常有意义的。所以,如果你想把返回的对象赋给一个特定类型的变量(例如 ??),你必须把对象转换成特定的类型。如果类型不兼容,系统将生成一个运行时错误,指示无效的强制转换。
下面是一个演示这一思想的例子。在其中,您将来自ExecuteScalar()
的结果存储在变量count
中,并将其转换为特定的类型int
。
int count = (int) cmd.ExecuteScalar(); txtScalar.AppendText ("Number of Products is: "+ count);
如果你确定结果的类型总是一个int
(与COUNT(*)
的安全赌注),那么前面的代码是安全的。但是,如果您将int
保留在原位,并将命令的CommandText
更改为以下内容:
select Name from Production.Product where ProductNumber='BA-8327'
然后ExecuteScalar()
将返回字符串“Bearing Ball
”而不是一个整数,您将得到这个异常:
Unhandled Exception: System.InvalidCastException: Specified cast is not valid.
因为你不能把一个string
投射到一个int
。
如果一个查询实际上返回了多行,而您认为它只会返回一行,那么可能会出现另一个问题。在这种情况下,ExecuteScalar( )
只返回结果的第一行,忽略其余的行。如果你使用ExecuteScalar( )
,确保你不仅期望而且实际上得到一个返回值。
执行有多个结果的命令
对于希望返回多行和多列的查询,使用命令的ExecuteReader()
方法。
ExecuteReader()
返回一个数据读取器,这是一个SqlDataReader
类的实例,我们将在下一章学习。数据读取器具有允许您读取结果集中的连续行并检索单个列值的方法。
我们将把数据读取器的细节留到下一章,但是为了便于比较,我们将在这里给出一个简单的例子,使用ExecuteReader()
方法从一个命令创建一个SqlDataReader
来显示查询结果。
试试看:使用 ExecuteReader 方法
要使用ExecuteReader
方法,请遵循以下步骤:
-
选择 ADO。NET_Command 项目,右击,选择【添加 窗口窗体。从打开的对话框中,确保选择了 Windows 窗体,并将
Form1.cs
重命名为CommandReader.cs
。单击“确定”将该表单添加到 ADO。NET_Command 项目。 -
通过单击窗体的标题栏选择 CommandReader 窗体,并将 Size 属性的宽度设置为 455,高度设置为 283。
-
将 TextBox 控件拖动到窗体上。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtReader。
- 将位置属性的 X 设置为 12,Y 设置为 12。
- 将 Multiline 属性设置为 True。
- 将 ScrollBars 属性设置为垂直。
- 将 Size 属性的宽度设置为 415,高度设置为 223。
- 将文本属性留空。
-
Now your CommandReader form in the Design view should like Figure 13-5.
图 13-5。命令阅读器表单的设计视图
-
Now double-click the empty surface of the
CommandReader.cs
form, and it will open the code editor window, showing theCommandReader_Load
event. Modify theCommandReader_Load
event to look like Listing 13-3.清单 13-3。T4
CommandReader.cs
`Using System.Data.SqlClient;
private void CommandReader_Load(object sender, EventArgs e)
{
// Create connection
SqlConnection conn = new SqlConnection(@"
server = .\sql2012;
integrated security = true;
database = AdventureWorks");// Create command
string sql = @"select Name,ProductNumber
from Production.Product";SqlCommand cmd = new SqlCommand(sql, conn);
txtReader.AppendText("Command created and connected.\n\n");try
{
// Open connection
conn.Open();
SqlDataReader rdr = cmd.ExecuteReader();while (rdr.Read())
{
txtReader.AppendText("\nProduct: ");
txtReader.AppendText(rdr.GetValue(1) + "\t\t" + rdr.GetValue(0));
txtReader.AppendText("\n");
}
}catch (SqlException ex)
{
MessageBox.Show(ex.Message + ex.StackTrace, "Exception Details");
}finally
{
conn.Close();
txtReader.AppendText("Connection Closed.");
}
}` -
To set the CommandReader form as the start-up form, modify the
Program.cs
statement:Application.Run(new CommandScalar ());
表现为:
Application.Run(new CommandReader());
构建项目,并通过按 Ctrl+F5 运行它。
-
When the form loads, the result should look like Figure 13-6.
图 13-6。使用数据阅读器
它是如何工作的
在本例中,您使用ExecuteReader()
方法检索并输出生产中所有产品的名称和产品编号值。产品表。与ExecuteScalar()
一样,ExecuteReader()
获取CommandText
属性,并使用来自Connection
属性的连接将其发送到数据库。
当您使用ExecuteScalar
方法时,您只产生一个标量值。相反,使用ExecuteReader()
会返回一个SqlDataReader
对象。
// execute query SqlDataReader rdr = cmd.ExecuteReader(); while (rdr.Read()) { txtReader.AppendText(rdr.GetValue(1) + "\t\t" + rdr.GetValue(0)); }
SqlDataReader
对象有一个依次获取每一行的Read()
方法和一个获取行中某一列的值的GetValue
方法。它检索其值的特定列由指示该列索引的整数参数给出。注意GetValue
使用的是从零开始的索引,所以第一列是第 0 列,第二列是第 1 列,依此类推。由于查询要求两列,Name 和 ProductNumber,所以在这个查询结果中这两列编号为 0 和 1。
执行非查询语句
ExecuteNonQuery
是用于执行由 DML 语句组成的 SQL 语句的方法。这些语句由 SQL Server 的INSERT
、UPDATE
和DELETE
功能组成。因此,ExecuteNonQuery()
用于向命令提供 DML 语句并执行它。正如您在前面的章节中可能已经注意到的那样,INSERT
、UPDATE
和DELETE
语句不会返回任何记录。现在让我们看看ExecuteNonQuery( )
方法如何处理这样的 SQL 查询。
尝试一下:使用 ExecuteNonQuery 方法
要使用ExecuteNonOuery
方法,请遵循以下步骤:
-
选择 ADO。NET_Command 项目,右击,选择【添加 窗口窗体。从打开的对话框中,确保选择了 Windows 窗体,并将
Form1.cs
重命名为CommandNonQuery.cs
。单击“确定”将该表单添加到 ADO。NET_Command 项目。 -
通过单击窗体的标题栏选择 CommandNonQuery 窗体,并将 Size 属性的宽度设置为 297,高度设置为 277。
-
将 GroupBox 控件拖到窗体上,并将其放在窗体的左侧。选择 GroupBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 gbInsertCurrency。
- 将位置属性的 X 设置为 21,Y 设置为 22。
- 将 Size 属性的宽度设置为 240,高度设置为 201。
- 将 Text 属性设置为插入货币。
-
将 Label 控件拖到名为 gbInsertCurrency 的 GroupBox 中,并将其放在 GroupBox 的左侧。选择此标签控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblCurrencyCode。
- 将位置属性的 X 设置为 16,Y 设置为 30。
- 将 Size 属性的宽度设置为 77,高度设置为 13。
- 将 Text 属性设置为货币代码。
-
将一个 TextBox 控件拖到窗体上,并将其放置在名为 Currency Code 的 Label 控件旁边。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtCurrencyCode。
- 将位置属性的 X 设置为 99,Y 设置为 30。
- 将 Size 属性的宽度设置为 128,高度设置为 20。
- 将文本属性留空。
-
将另一个 Label 控件拖动到名为 gbInsertCurrency 的 GroupBox 中,并将其放置在货币代码标签的下方,靠近 GroupBox 的左侧。选择此标签控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblName。
- 将位置属性的 X 设置为 19,Y 设置为 64。
- 将 Size 属性的宽度设置为 35,高度设置为 13。
- 将 Text 属性设置为 Name。
-
将 TextBox 控件拖到窗体上,并将其放置在名为 Name 的 Label 控件旁边。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtName。
- 将位置属性的 X 设置为 99,Y 设置为 64。
- 将 Size 属性的宽度设置为 128,高度设置为 20。
- 将文本属性留空。
-
Right now your CommandNonQuery form in the Design view should look like Figure 13-7.
图 13-7。command non query 表单的设计视图
-
继续设计表单,将另一个名为 gbInsertCurrency 的 Label 控件拖动到 GroupBox 上,并将其放置在 GroupBox 左侧的 Name 标签下方。选择此标签控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblModifiedDate。
- 将位置属性的 X 设置为 19,Y 设置为 97。
- 将 Size 属性的宽度设置为 73,高度设置为 13。
- 将 Text 属性设置为 Modified Date。
-
将一个 DateTimePicker 控件拖到窗体上,并将其放在修改后的日期标签控件旁边。选择此 DateTimePicker 控件,导航到“属性”窗口,并设置以下属性:
* 将 Name 属性设置为 dtpModifiedDate。
* 将 Format 属性设置为 Short。
* 将位置属性的 X 设置为 99,Y 设置为 97。
* 将 Size 属性的宽度设置为 128,高度设置为 20。 -
将 Button 控件拖到名为 gbInsertCurrency 的 GroupBox 中,并将其放置在 Label 和 TextBox 控件的下方。选择此按钮控件,导航到“属性”窗口,并设置以下属性:
* 将 Name 属性设置为 btnInsertCurrency。
* 将位置属性的 X 设置为 56,Y 设置为 133。
* 将 Size 属性的宽度设置为 128,高度设置为 23。
* 将 Text 属性设置为插入货币。 -
将另一个 Label 控件拖动到名为 gbInsertCurrency 的 GroupBox 中,并将其放置在“插入货币”按钮的下方。选择此标签控件,导航到“属性”窗口,并设置以下属性:
* 将 Name 属性设置为 lblInsertStatus。
* 将 AutoSize 属性设置为 False。
* 将位置属性的 X 设置为 22,Y 设置为 168。
* 将 Size 属性的宽度设置为 205,高度设置为 21。 -
现在你在设计视图中的 CommandNonQuery 表单应该类似于图 13-8 。
-
Double-click the Insert Currency button, and it will open the code editor window, showing the
btnInsertCurrency_Click
event. Modify thebtnInsertCurrency_Click
event to look like Listing 13-4.
***图 13-8。**命令非查询表单的设计视图*
***清单 13-4。**T4`CommandNonOuery.cs`*
`Using System.Data.SqlClient;
private void btnInsertCurrency_Click(object sender, EventArgs e)
{
// Create connection
SqlConnection conn = new SqlConnection(@"server = .\sql2012;
integrated security = true;
database = AdventureWorks");
// Insert Query
string sqlIns = "Insert Into Sales.Currency(CurrencyCode,Name,ModifiedDate)" +
"Values(" + "'" + txtCurrencyCode.Text + "','" +
txtName.Text + "','" + dtpModifiedDate.Value.ToString() + "')";
// Create command
SqlCommand cmd = new SqlCommand(sqlIns, conn);
try
{
// Open connection
conn.Open();` ` cmd.ExecuteNonQuery();
lblInsertStatus.Text = "New Currency Added Successfully!!";
}
catch (SqlException ex)
{
MessageBox.Show(ex.Message + ex.StackTrace, "Exception Details");
}
finally
{
conn.Close();
}
}`
- To set the CommandReader form as the start-up form, modify the
Program.cs
statement:Application.Run(new CommandReader ());
表现为:
`Application.Run(new CommandNonQuery());`
构建项目,并通过按 Ctrl+F5 运行它。
- When the form loads and you are ready to enter currency details, you need to be careful because the table Sales.Currency used in the exercise has the column CurrencyCode defined as a primary key. Hence, if you try entering a currency that might already exist in the table (which will be the case when you try entering most of the well-known currencies), then you will get a primary key violation. For example, when you try to enter USD as the currency code, the moment you click the button Insert Currency, an exception occurs, as shown in Figure 13-9.
***图 13-9。**插入显示主键冲突的语句*
- Trying a successful entry in the table will become a possibility only when you enter a unique key, in other words, a CurrencyCode that doesn’t exist. To access the form again, click OK in the Exception dialog. Modify the Currency Code USD to US (I know this is not a real currency code, but for the sake of our example, it’s worth trying), and click Insert Currency button. You will see a successful insertion, as shown in Figure 13-10.
***图 13-10。**成功插入货币*
它是如何工作的
在这个程序中,您使用一个非查询将货币插入到销售额中。货币表。正如您在 CommandNonQuery 表单的设计视图中看到的,我们有两个 TextBox 控件和一个 DateTimePicker 控件,因此在这些控件中输入的值将通过 SqlCommand 对象提供给 SQL 表。因此,INSERT
查询将如下所示:
// Insert Query string sqlIns = "Insert Into Sales.Currency(CurrencyCode,Name,ModifiedDate)" + "Values(" + "'" + txtCurrencyCode.Text + "','" + txtName.Text + "','" + dtpModifiedDate.Value.ToString() + "')";
然后创建一个封装了INSERT
查询的命令。
// Create command SqlCommand cmd = new SqlCommand(sqlIns, conn); // Execute the SQL statements with a call to the following: cmd.ExecuteNonQuery(); ExecuteNonOuery() executes the INSERT statement, and if executed successfully, it will show the success message in the lblResultStatus control. lblInsertStatus.Text = "New Currency Added Successfully!!";
注意使用ExecuteNonOuery()
,您几乎可以提交任何 SQL 语句,包括数据定义语言(DDL)语句,来创建和删除数据库对象,如表和索引。但是在工业界,开发人员最常用的是插入、更新和删除行。
正如您到目前为止所看到的,所有执行任务的 SQL 语句,如SELECT
、INSERT
、UPDATE
或DELETE
,都被硬编码到 C# 代码中。但大多数时候,开发人员编写存储过程来执行 SQL 操作;存储过程就像函数一样工作,可以接受参数来执行任务。存储过程的优势之一是在 SQL Server 级别创建了一个统一的 SQL 语句,因为您的 ADO.NET 代码只是调用它。
使用存储过程
正如您在第六章中所学的,存储过程是允许您重复执行某项任务的 SQL 语句的集合。建议使用存储过程,而不是在 C# 代码中硬编码 SQL 语句,因为它利用了 SQL Server 的编译并提供了基于性能的重用。存储过程只是替换执行插入、更新和删除等操作的硬编码代码。
创建存储过程来执行删除操作
在前面的 CommandNonQuery 练习中,您看到了 Sales 上的插入操作。AdventureWorks2008
数据库的货币表。现在让我们在同一个表上做一个删除操作,但是通过一个存储过程。
试试看:创建一个与 C# 一起使用的存储过程
让我们使用 SQL Server Management Studio 创建一个存储过程,该存储过程将 CurrencyCode 值作为参数,从销售中删除一种货币。AdventureWorks
数据库中的货币表。它只需要一个输入参数。
- 打开 SQL Server Management Studio。在“连接到服务器”对话框中,选择 localhost\
作为服务器名称,然后单击“连接”。 - 在对象资源管理器中,展开“数据库”节点,选择 AdventureWorks 数据库,然后单击“新建查询”窗口。输入以下查询,然后单击执行。您应该会看到如图 13-11 所示的结果。
Create procedure sp_DeleteCurrency @currCode nvarchar(3) As Delete From Sales.Currency Where CurrencyCode = @currCode
图 13-11。创建存储过程删除货币
它是如何工作的
CREATE PROCEDURE
语句创建了一个有一个输入参数的存储过程。参数在过程名和AS
关键字之间指定。这里您只指定了参数名和数据类型,所以默认情况下它是一个输入参数。参数名以@
开头。
Create procedure sp_DeleteCurrency @currCode nvarchar(3) As Delete From Sales.Currency
该参数用在查询的WHERE
子句中。
Where CurrencyCode = @currCode
试试看:使用带有 ExecuteNonQuery 方法的存储过程
要使用ExecuteNonQuery
方法,请遵循以下步骤:
-
选择 ADO。NET_Command 项目,右击,选择【添加 窗口窗体。从打开的对话框中,确保选择了 Windows 窗体,并将
Form1.cs
重命名为CommandStoredProcedure.cs
。单击“确定”将该表单添加到 ADO。NET_Command 项目。 -
通过单击窗体的标题栏选择 CommandStoredProcedure 窗体,并将 Size 属性的宽度设置为 288,高度设置为 267。
-
将一个 GroupBox 控件拖到窗体上,并将其放在窗体的左侧。选择 GroupBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 gbDeleteCurrency。
- 将位置属性的 X 设置为 12,Y 设置为 12。
- 将 Size 属性的宽度设置为 248,高度设置为 201。
- 将 Text 属性设置为删除货币。
-
将一个 ListBox 控件拖到名为 gbDeleteCurrency 的 GroupBox 中,并将其放在 GroupBox 的左侧。选择此列表框控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblCurrencyCode。
- 将位置属性的 X 设置为 19,Y 设置为 29。
- 将 Size 属性的宽度设置为 85,高度设置为 121。
-
将 Button 控件拖动到名为 gbLoadCurrency 的 GroupBox 上,并将其放置在 ListBox 控件的右侧。选择此按钮控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 btnLoadCurrency。
- 将位置属性的 X 设置为 118,Y 设置为 45。
- 将 Size 属性的宽度设置为 116,高度设置为 23。
- 设置 Text 属性以加载货币列表。
-
拖动 btnLoadCurrency 正下方的另一个 Button 控件。选择此按钮控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 btnDeleteCurrency。
- 将位置属性的 X 设置为 118,Y 设置为 100。
- 将 Size 属性的宽度设置为 116,高度设置为 23。
- 将 Text 属性设置为删除货币。
-
将 Label 控件拖到名为 gbDeleteCurrency 的 GroupBox 中,并将其放置在 lstCurrency ListBox 的下方。选择此标签控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblDeleteStatus。
- 将 AutoSize 属性设置为 False。
- 将位置属性的 X 设置为 16,Y 设置为 165。
- 将 Size 属性的宽度设置为 218,高度设置为 21。
- 将“文本”属性留空。
-
Now your CommandStoredProcedure form in the Design view should like Figure 13-12.
图 13-12。命令存储过程表单的设计视图
-
Double-click the Load Currency List button, and it will open the code editior window, showing the
btnLoadCurrency_Click
event. Modify thebtnLoadCurrency_Click
event to look like Listing 13-5.清单 13-5。T4
CommandStoredProcedure.cs
Using System.Data.SqlClient; private void btnLoadCurrency_Click(object sender, EventArgs e) { // Create connection SqlConnection conn = new SqlConnection(@"server = .\sql2012; integrated security = true;
` database = AdventureWorks");// Select query
string sqlSelect = @"select CurrencyCode
from Sales.Currency";SqlCommand cmd = new SqlCommand(sqlSelect, conn);
try
{
// Open connection
conn.Open();
// Execute query via ExecuteReader
SqlDataReader rdr = cmd.ExecuteReader();while (rdr.Read())
{
lstCurrency.Items.Add(rdr[0]);
}
}catch (SqlException ex)
{
MessageBox.Show(ex.Message + ex.StackTrace, "Exception Details");
}
finally
{
conn.Close();
}
}` -
Now it’s time to add functionality for the Delete Currency button. Double-click the Delete Currency button, and it will open the code editior window, showing the
btndeleteCurrency_Click
event. Modify thebtnDeleteCurrency_Click
event to look like Listing 13-6.
***清单 13-6。**T4`CommandStoredProcedure.cs`*
`private void btnDeleteCurrency_Click(object sender, EventArgs e)
{
// Create connection
SqlConnection conn = new SqlConnection(@"server = .\sql2012;
integrated security = true;
database = AdventureWorks");
// Create command object with Stored Procedure name
SqlCommand cmd = new SqlCommand("sp_DeleteCurrency", conn);
//Set command object for Stored Procedure execution
cmd.CommandType = CommandType.StoredProcedure;` ` cmd.Parameters.Add(new SqlParameter("currCode", SqlDbType.NVarChar, 3));
cmd.Parameters["currCode"].Value = lstCurrency.SelectedItem.ToString();
try
{
// Open connection
conn.Open();
// Delete Query
if (lstCurrency.SelectedIndex == -1)
{
MessageBox.Show("Please Select a Currency before performing Delete
action",
"Information");
}
else
{
cmd.ExecuteNonQuery();
lblDeleteStatus.Text = "Currency is Deleted Successfully!!";
}
}
catch (SqlException ex)
{
MessageBox.Show(ex.Message + ex.StackTrace, "Exception Details");
}
catch (NullReferenceException ex)
{
MessageBox.Show("Load the Currency List first" + ex.StackTrace, "Exception
Details");
}
finally
{
conn.Close();
}
}`
- To set the CommandStoredProcedure form as the start-up form, modify the
Program.cs
statement.Application.Run(new CommandNonQuery ());
表现为:
`Application.Run(new CommandStoredProcedure());`
构建项目,并通过按 Ctrl+F5 运行它。
- 当窗体加载时,首先单击名为“加载货币列表”的按钮控件;这将从销售中加载货币。货币表。
- Now, select a currency (the one that you have not added), and click the Delete Currency button. As you may remember, the Sales.Currency table uses CurrencyCode as a primary key, so this key column is referenced by the Sales.CountryRegionCurrency table, and so a reference constraint–related exception will occur, as shown in Figure 13-13.
***图 13-13。**删除显示引用约束冲突的语句*
- Click OK. As you have seen, you can’t delete the currencies that have referenced entries in the Sales.CountryCurrencyRegion table. Hence, you can only delete the entry that is not related to this referencing table. In the CommandNonQuery exercise, you inserted a CurrencyCode called US, as shown in Figure 13-9. Scroll the currency list until you see US, and then click the Delete Currency button. You will see the successful deletion of the currency, as shown in Figure 13-14.
***图 13-14。**成功删除货币*
- 如果您再次点击加载货币列表,您会看到货币代码 US 并未列出,因为它已被删除。
它是如何工作的
在这个程序中,您使用一个存储过程从销售中删除货币。货币表。正如您在 CommandStoredProcedure 窗体的设计视图中所看到的,您有一个 ListBox 和两个 Button 控件。在列表框中选择的值将被传递给一个存储过程,该存储过程有一个删除查询。因此,通过以下语句为将值传递给存储过程做准备:
` // Create command object with Stored Procedure name
SqlCommand cmd = new SqlCommand("sp_DeleteCurrency", conn);
//Set command object for Stored Procedure execution
cmd.CommandType = CommandType.StoredProcedure;`
一旦指定了命令对象将使用存储过程,就该准备存储过程执行所需的参数了。
cmd.Parameters.Add(new SqlParameter("currCode", SqlDbType.NVarChar, 3));
如您所见,我们希望 ListBox 的选择被删除,所以我们将数据作为ListBox.SelectedItem
传递给存储过程参数。
cmd.Parameters["currCode"].Value = lstCurrency.SelectedItem.ToString();
一旦参数准备好了,我们就打开连接并使用ExecuteNonQuery
执行存储过程。
` // Open connection
conn.Open();
// Execute command associated with StoredProcedure
cmd.ExecuteNonQuery();`
总结
在本章中,我介绍了什么是 ADO.NET 命令以及如何创建一个Command
对象。我还讨论了将命令与连接相关联、设置命令文本以及使用ExecuteScalar()
、ExecuteReader()
和ExecuteNonOuery()
语句。您还了解了如何在 C# 代码中使用存储过程来执行 DML 操作。在下一章,你将会看到数据阅读器。
十四、使用数据读取器
在第十三章中,您使用数据读取器从多行结果集中检索数据。在本章中,我们将更详细地了解数据读取器。您将看到它们是如何使用的,以及它们在 ADO.NET 编程中的重要性。
在本章中,我们将介绍以下内容:
- Understand the general situation of data readers.
- Get data about data
- Get data about the table
- Using a data reader using multiple result sets
从总体上了解数据阅读器
在连接和命令之后,数据提供者的第三个组件是数据读取器。一旦连接到数据库并进行查询,就需要某种方法来访问结果集。这就是数据读取器的用武之地。
数据读取器是实现System.Data.IDataReader
接口的对象。数据读取器是一个快速、无缓冲、只进、只读的连接的流,它以每行为基础检索数据。它在结果集中循环时一次读取一行。
不能直接实例化数据读取器;相反,您可以使用命令的ExecuteReader
方法创建一个。例如,假设cmd
是一个查询的SqlClient
命令对象,下面介绍如何创建一个SqlClient
数据读取器:
SqlDataReader rdr = cmd.ExecuteReader();
现在,您可以使用这个数据读取器来访问查询的结果集。
提示我们将在下一章进一步讨论的一点是选择数据读取器还是数据集。一般规则是总是使用数据读取器来简单地检索数据。如果你需要做的只是显示数据,那么在大多数情况下你需要使用的就是数据阅读器。
我们将通过几个例子演示基本的数据读取器用法。第一个例子是最基本的;它只是使用一个数据读取器来遍历结果集。
假设您已经成功地与数据库建立了连接,执行了一个查询,一切似乎都很顺利——现在该怎么办?下一个明智的做法是检索行并处理它们。
试试看:遍历一个结果集
下面的 Windows 应用展示了如何使用SqlDataReader
遍历结果集并检索行:
-
创建一个名为 Chapter14 的新 Windows 窗体应用项目。当解决方案资源管理器打开时,保存解决方案。
-
将 Chapter14 项目重命名为 DataReader。将 Form1.cs 文件重命名为 DataLooper.cs。
-
通过单击窗体的标题栏选择 DataLooper 窗体,并将 Size 属性的宽度设置为 346,高度设置为 476。
-
将 ListBox 控件拖到窗体上,并将其放在窗体的中央。选择此列表框,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lbxProduct。
- 将位置属性的 X 设置为 21,Y 设置为 22。
- 将 Size 属性的宽度设置为 281,高度设置为 394。
-
Now your DataLooper form in the Design view should like Figure 14-1.
图 14-1。数据活套表单的设计视图
-
Double-click the empty surface of the DataLooper.cs form, and it will open the code editor window, showing the DataLooper_Load event. Modify the DataLooper_Load event to look like Listing 14-1.
清单 14-1。T4
DataLooper.cs
`Using System.Data.SqlClient;
private void DataLooper_Load(object sender, EventArgs e)
{
// Connection string
string connString = @"server=.\sql2012;database=AdventureWorks;
Integrated Security=SSPI";// Query
string sql = @"select Name from Production.Product";
SqlConnection conn = new SqlConnection(connString);try
{
// Open connection
conn.Open();// Create command
SqlCommand cmd = new SqlCommand(sql, conn);// Create data reader
SqlDataReader rdr = cmd.ExecuteReader();// Loop through result set
while (rdr.Read())
{
// Add to listbox - one row at a time
lbxProduct.Items.Add(rdr[0]);
}// Close data reader
rdr.Close();
}catch (SqlException ex)
{
MessageBox.Show(ex.Message + ex.StackTrace);
}finally
{
conn.Close();
}
}` -
Build the project, and run the DataLooper form by pressing Ctrl+F5. Your results should look like Figure 14-2.
图 14-2。使用
DataReader
循环结果集
它是如何工作的
SqlDataReader
是抽象类,不能显式实例化。出于这个原因,您通过执行SqlCommand
的ExecuteReader
方法来获得一个SqlDataReader
的实例。
// Create data reader SqlDataReader rdr = cmd.ExecuteReader();
ExecuteReader()
不仅仅是创建一个数据阅读器;它将 SQL 发送到连接以供执行,因此当它返回时,您可以遍历结果集的每一行并逐列检索值。为此,您调用SqlDataReader
的Read
方法,如果有一行可用,该方法将返回true
,并使光标前进(指向结果集中下一行的内部指针),如果另一行不可用,该方法将返回false
。由于Read()
将光标移动到下一个可用行,您必须为结果集中的所有行调用它,所以您在while
循环中调用它作为条件。
// Loop through result set while (rdr.Read()) { // Add to listbox - one row at a time lbxProduct.Items.Add(rdr[0]); }
一旦调用了Read
方法,下一行将作为集合返回并存储在SqlDataReader
对象中。要访问特定列中的数据,可以使用许多方法(我们将在下一节中介绍这些方法),但是对于这个应用,您使用序号索引器查找方法,将列号提供给读取器以检索值(就像您为数组指定索引一样)。因为在这种情况下,您在查询数据库时从 Customers 表中选择了一个单独的列,所以只有“zeroth”索引器是可访问的,所以您将该索引硬编码为rdr[0]
。
为了将连接用于其他目的或者在数据库上运行另一个查询,调用SqlDataReader
的Close
方法来显式关闭阅读器是很重要的。一旦读取器被连接到活动连接,该连接就保持忙于为读取器获取数据,并且保持不可用于其他用途,直到读取器被从其分离。这就是为什么您在try
块中关闭阅读器,而不是在finally
块中(即使这个简单的程序不需要将连接用于其他目的)。
// Close data reader rdr.Close();
使用序数索引器
使用序号索引器从结果集中检索列数据。让我们了解更多关于序数索引。以下代码:
rdr[0]
是对数据读取器的Item
属性的引用,并返回为当前行指定的列中的值。该值作为对象返回。
试试看:使用序数索引器
在此示例中,您将向 DataReader 项目添加一个 Windows 窗体,然后您将使用一个序号索引器。
-
选择 DataReader 项目,右键单击,然后选择“添加 Windows 窗体”。在打开的对话框中,确保选择了 Windows 窗体,并将 Form1.cs 重命名为 OrdinalIndexer.cs 然后单击“确定”将该窗体添加到 DataReader 项目中。
-
通过单击窗体的标题栏选择 OrdinalIndexer 窗体,并将 Size 属性的宽度设置为 289,高度设置为 351。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtValues。
- 将位置属性的 X 设置为 12,Y 设置为 12。
- 将 Multiline 属性设为 True。
- 将 ScrollBars 属性设置为垂直。
- 将 Size 属性的宽度设置为 249,高度设置为 287。
- 将文本属性留空。
-
Now your OrdinalIndexer form in the Design view should like Figure 14-3.
图 14-3。普通型设计视图
-
Double-click the empty surface of the DataLooper.cs form, and it will open the code editor window, showing the DataLooper_Load event. Modify the DataLooper_Load event to look like Listing 14-2.
清单 14-2。T4
0rdinallndexer.cs
`Using System.Data.SqlClient;
private void OrdinalIndexer_Load(object sender, EventArgs e)
{
// Connection string
string connString = @"server=.\sql2012;database=AdventureWorks; Integrated Security=SSPI";
string sql = @" select FirstName,LastName
from Person.Contact
where FirstName like 'M%'";// Create connection
SqlConnection conn = new SqlConnection(connString);try
{
// Open connection
conn.Open();// Create command
SqlCommand cmd = new SqlCommand(sql, conn);// Create data reader
SqlDataReader rdr = cmd.ExecuteReader();// Print headings
StringBuilder sb=new StringBuilder();
txtValues.AppendText("First Name".PadRight(25));
txtValues.AppendText("Last Name".PadLeft(20));
txtValues.AppendText(Environment.NewLine);
txtValues.AppendText("-----------------------------------------
--------------------------");
txtValues.AppendText(Environment.NewLine);// Loop through result set
while (rdr.Read())
{
txtValues.AppendText(rdr[0].ToString());
txtValues.AppendText("\t\t\t");
txtValues.AppendText(rdr[1].ToString());
txtValues.AppendText(Environment.NewLine);
}// Close reader
rdr.Close();
}catch (SqlException ex)
{
MessageBox.Show(ex.Message + ex.StackTrace,"Exception Details");
}finally
{
// Close connection
conn.Close();
} -
To set the OrdinalIndexer form as the start-up form, modify the Program.cs statement:
Application.Run(new DataLooper());
表现为:
Application.Run(new OrdinalIndexer());
-
Build the project, and run it by pressing Ctrl+F5. You should see the results in Figure 14-4.
图 14-4。使用 OrdinalIndexer 显示多列
它是如何工作的
你询问这个人。FirstName 和 LastName 列的联系人表,其中联系人姓名以字母 M 开头。
// Query string sql = @" select FirstName,LastName from Person.Contact where FirstName like 'M%'";
由于查询选择了两列,返回的数据也只包含这两列中的行集合,因此只允许访问两个可能的顺序索引器,即 0 和 1。
出于格式化的目的,我们希望将列标题 FirstName 和 LastName 显示为第一个标题行,然后我们希望将所有其他值行添加到它的下面。因此,您可以使用PadeRight
和PadLeft
方法来格式化输出,通过在指定的总长度的左侧/右侧填充空格,使所有字符左右对齐。
// Print headings StringBuilder sb=new StringBuilder(); txtValues.AppendText("First Name".PadRight(25)); txtValues.AppendText("Last Name".PadLeft(20)); txtValues.AppendText(Environment.NewLine); txtValues.AppendText("--------------------------------------------- --------------------"); txtValues.AppendText(Environment.NewLine);
现在你在一个while
循环中读取每一行,用索引器获取两列的值并将这两列追加到文本框中,这样所有的名字都显示为一个列表,如图 14-2 所示。
// Loop through result set while (rdr.Read()) { txtValues.AppendText(rdr[0].ToString()); txtValues.AppendText("\t\t\t"); txtValues.AppendText(rdr[1].ToString()); txtValues.AppendText(Environment.NewLine); }
处理完结果集中的所有行后,显式关闭读取器以释放连接。
// Close reader rdr.Close();
使用列名索引器
大多数时候,我们并不真正跟踪列号,而更喜欢通过各自的列名来检索值,只是因为通过它们的名称来记住它们要容易得多,这也使得代码更加自文档化。
通过指定列名而不是序号索引号来使用列名索引。这有它的好处。例如,添加或删除一个或多个列可能会更改表,打乱列的顺序,并在使用序号索引器的旧代码中引发异常。使用列名索引器可以避免这个问题,但是顺序索引器更快,因为它们直接引用列,而不是通过名称来查找。
下面的代码片段使用列名索引器检索与上一示例相同的列(FirstName 和 LastName)。
// Loop through result set while (rdr.Read()) { txtValues.AppendText(rdr["FirstName"].ToString()); txtValues.AppendText(rdr["LastName"].ToString()); }
用列名索引器替换OrdinalIndexer.cs
中的序数索引器,重新运行项目;你会得到与图 14-2 中相同的结果。下一节讨论大多数情况下的更好的方法。
使用类型化访问器方法
当数据读取器从数据源返回值时,将检索结果值并以. NET 类型而不是原始数据源类型存储在本地。这种就地类型转换功能是一致性和速度之间的一种折衷,因此为了对正在检索的数据进行一些控制,数据读取器公开了类型化访问器方法,如果您知道所返回值的特定类型,就可以使用这些方法。
类型化访问器方法都以Get
开头,采用序号索引进行数据检索,并且是类型安全的;C# 不允许你逃脱不安全的强制转换。这些方法比序号和列名索引器方法都要快。比列名索引更快似乎是合乎逻辑的,因为类型化的访问器方法采用序数进行引用;然而,我们需要解释它为什么比顺序索引快。这是因为即使这两种技术都接受列号,传统的顺序索引方法也需要查找结果的数据源数据类型,然后进行类型转换。使用类型化访问器可以避免查找模式的开销。
。NET 类型和类型化访问器方法可用于 SQL Server 和 OLE DB 数据库支持的几乎所有数据类型。
表 14-1 应该给你一个什么时候使用类型化访问器和使用什么数据类型的简单概念。它列出了 SQL Server 数据类型及其对应的。网络类型,。NET 类型访问器,以及专门为返回类型为System.Data.SqlTypes
的对象而设计的特定于 SQL Server 的特殊类型访问器。
表 14-2 列出了一些可用的 OLE DB 数据类型,它们对应的。NET 类型以及它们的。NET 类型的访问器。
要查看运行中的类型化访问器,您将构建一个使用它们的控制台应用。对于这个例子,您将使用来自Northwind
数据库的 Products 表。
表 14-3 显示了表格的数据设计。请注意,表中给定的数据类型将在表 14-1 中查找其对应的类型化访问器方法,以便您可以在您的应用中正确使用它们。
试试看:使用类型化访问器方法
在此示例中,您将向 DataReader 项目添加一个 Windows 窗体,然后使用类型化访问器方法。
-
选择 DataReader 项目,右键单击,然后选择“添加 Windows 窗体”。在打开的对话框中,确保选择了 Windows 窗体,并将 Form1.cs 重命名为 TypedAccessor.cs 单击“确定”将该窗体添加到 DataReader 项目中。
-
选择 TypedAccessor 窗体,并将 Size 属性的宽度设置为 476,高度设置为 353。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtTypeAccess。
- 设置位置属性的 X 为 12,Y 为 12。
- 将 ScrollBars 属性设置为垂直。
- 将 Size 属性的宽度设置为 437,高度设置为 290。
- 将 Multiline 属性设置为 True。
-
Now your TypedAccessor form in the Design view should look like Figure 14-5.
图 14-5。typed accessor 表单的设计视图
-
Now double-click the empty surface of the TypedAccessor.cs form, and it will open the code editor window, showing the TypedAccessor_Load event. Modify the TypedAccessor _Load event to look like Listing 14-3.
清单 14-3。T4
TypedAccessors.cs
`Using System.Data.SqlClient;
private void TypedAccessors_Load(object sender, EventArgs e)
{
// Connection string
string connString = @"server=.\sql2012;database=AdventureWorks;
Integrated Security=SSPI";// Query
string sql = @"select CardType, CardNumber,ExpMonth,ExpYear from
Sales.CreditCard";
SqlConnection conn = new SqlConnection(connString);try
{
// Open connection
conn.Open();// Create command
SqlCommand cmd = new SqlCommand(sql, conn);// Create data reader
SqlDataReader rdr = cmd.ExecuteReader();
// Fetch data
while (rdr.Read())
{// CardType
txtTypeAccess.AppendText(rdr.GetString(0).PadRight(30));
txtTypeAccess.AppendText("\t");
// CardNumber
txtTypeAccess.AppendText(rdr.GetString(1));
txtTypeAccess.AppendText("\t\t");
// ExpMonth
txtTypeAccess.AppendText(rdr.GetByte(2).ToString());
txtTypeAccess.AppendText("\t\t");
// ExpYear
txtTypeAccess.AppendText(rdr.GetInt16(3).ToString());
txtTypeAccess.AppendText("\n");
}
// Close data reader
rdr.Close();
}
catch (SqlException ex)
{
MessageBox.Show(ex.Message + ex.StackTrace,"Exception Details");
}finally
{// Close connection
conn.Close();
}
}` -
To set the TypedAccessor form as the start-up form, modify the Program.cs statement.
Application.Run(new OrdinalIndexer ());
出现为:
Application.Run(new TypedAccessor());
-
Build the project, and run it by pressing Ctrl+F5. You should see the results in Figure 14-6.
图 14-6。使用类型化访问器
它是如何工作的
你查询销售情况。CardType、CardNumber、ExpMonth 和 ExpYear 的信用卡表。
// Query string sql = @"select CardType, CardNumber,ExpMonth,ExpYear from Sales.CreditCard";
我们让您选择这些列的原因是为了处理不同种类的数据类型,并展示如何使用相关的类型化访问器来获得正确的结果。
`// Fetch data
while (rdr.Read())
{
// CardType
txtTypeAccess.AppendText(rdr.GetString(0).PadRight(30));
txtTypeAccess.AppendText("\t");
// CardNumber
txtTypeAccess.AppendText(rdr.GetString(1));
txtTypeAccess.AppendText("\t\t");
// ExpMonth
txtTypeAccess.AppendText(rdr.GetByte(2).ToString());
txtTypeAccess.AppendText("\t\t");
// ExpYear
txtTypeAccess.AppendText(rdr.GetInt16(3).ToString());
txtTypeAccess.AppendText("\n");
}`
查看表 14-1 ,您可以看到您可以分别使用GetString
、GetByte
和Getlntl6
访问器方法访问 SQL Server 中的 nvarchar、tinyint 和 smallint 数据类型。
这项技术速度很快,而且完全是类型安全的。我们的意思是,如果从本机数据类型到。NET 类型失败,则会因无效的强制转换而引发异常。例如,如果您尝试对一个bit
数据类型使用GetString
方法,而不是使用GetBoolean
方法,将会抛出一个“指定的强制转换无效”异常。
获取关于数据的数据
到目前为止,您所做的只是从数据源中检索数据。一旦你有了一个填充的数据阅读器,你可以做更多的事情。有许多有用的方法可以检索模式信息或与结果集直接相关的信息。表 14-4 描述了数据读取器的一些元数据方法和属性。
试试看:用数据阅读器获取结果集的信息
在本练习中,您将使用其中的一些方法和属性。
-
选择 DataReader 项目,右键单击,然后选择“添加 Windows 窗体”。在打开的对话框中,确保选择了 Windows Form,并将 Form1.cs 重命名为 ResultSetInfo.cs 然后单击“确定”将该窗体添加到 DataReader 项目中。
-
选择 ResultSetInfo 表单,并将 Size 属性的宽度设置为 462,高度设置为 460。
- 将一个 Label 控件拖到窗体上,选择该控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblDataType。
- 将 AutoSize 属性设置为 false。
- 将位置属性的 X 设置为 3,Y 设置为 31。
- 将 Size 属性的 X 设置为 57,Y 设置为 13。
- 将 Text 属性设置为数据类型。
-
将 Label 控件拖到窗体上。选择该控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblType1。
- 将 AutoSize 属性设置为 false。
- 将位置属性的 X 设置为 80,Y 设置为 21。
- 将 Size 属性的 X 设置为 101,Y 设置为 34。
- 将文本属性留空。
- 将 Label 控件拖到窗体上。选择该控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblType2。
- 将 AutoSize 属性设置为 false。
- 将位置属性的 X 设置为 222,Y 设置为 21。
- 将 Size 属性的 X 设置为 101,Y 设置为 34。
- 将文本属性留空。
- 将 TextBox 控件拖动到 Label 控件下方,并导航到“属性”窗口以配置以下属性:
- 将 Name 属性设置为 txtResultSet。
- 将 Multiline 属性设置为 True。
- 将位置属性的 X 设置为 32,Y 设置为 58。
- 将 ScrollBars 属性设置为垂直。
- 将 Size 属性的宽度设置为 341,高度设置为 234。
- 将 Label 控件拖动到 TextBox 下方,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblType3。
- 将 AutoSize 属性设置为 false。
- 将位置属性的 X 设置为 38,Y 设置为 317。
- 将 Size 属性的宽度设置为 335,高度设置为 13。
- 将文本属性留空。
- 将另一个标签控件拖到刚刚添加的标签下方。选择该控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblType4。
- 将 AutoSize 属性设置为 false。
- 将位置属性的 X 设置为 38,Y 设置为 352。
- 将 Size 属性的宽度设置为 335,高度设置为 13。
- 将文本属性留空。
- 将另一个标签控件拖到刚刚添加的标签下方。选择该控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblType5。
- 将 AutoSize 属性设置为 false。
- 将位置属性的 X 设置为 38,Y 设置为 381。
- 将 Size 属性的宽度设置为 335,高度设置为 13。
- 将文本属性留空。
-
Now your ResultSetInfo form in the Design view should like Figure 14-7.
图 14-7。ResultSetInfo 表单的设计视图
-
Double-click the empty surface of the
TypedAccessor.cs
form, and it will open the code editor window, showing theTypedAccessor_Load
event. Modify theTypedAccessor_Load
event to look like Listing 14-4.清单 14-4。T4
ResultSetInfo.cs
`Using System.Data.SqlClient;
private void ResultSetInfo_Load(object sender, EventArgs e)
{
// Connection string
string connString = @"server=.\sql2012;database=AdventureWorks;
Integrated Security=SSPI";// Query
string sql = @" select FirstName,LastName from Person.Contact
order by LastName";
SqlConnection conn = new SqlConnection(connString);try
{
conn.Open();SqlCommand cmd = new SqlCommand(sql, conn);
SqlDataReader rdr = cmd.ExecuteReader();
// Get column names
lbltype1.Text = rdr.GetName(0);
lblType2.Text = rdr.GetName(1);//Get column data types
lbltype1.Text += "\n"+ rdr.GetDataTypeName(0).ToString();
lblType2.Text += "\n"+ rdr.GetDataTypeName(1).ToString();// Get number of columns
lblType3.Text = "Number of columns in a row::" + rdr.FieldCount.ToString();// Get info about each column
lblType4.Text = rdr.GetName(0).ToString() + " is at index::" +
rdr.GetOrdinal("FirstName").ToString() +
" and its type is::" + rdr.GetFieldType(0).ToString();lblType5.Text = rdr.GetName(1).ToString() + " is at index:: "+
rdr.GetOrdinal("LastName").ToString() +
" and its type is::" + rdr.GetFieldType(1).ToString();while (rdr.Read())
{
// Get column values for all rows
txtResultSet.AppendText("\t");
txtResultSet.AppendText(rdr[0].ToString());
txtResultSet.AppendText("\t\t\t");
txtResultSet.AppendText(rdr[1].ToString() );
txtResultSet.AppendText("\n");
}//Close reader
rdr.Close();}
catch (SqlException ex)
{
MessageBox.Show(ex.Message + ex.StackTrace,"Exception Details");
}finally
{
conn.Close();
}
}` -
To set the ResultSetInfo form as the start-up form, modify the Program.cs statement.
Application.Run(new TypedAccessor ());
表现为:
Application.Run(new ResultSetInfo());
-
Build the project, and run it by pressing Ctrl+F5. Your results should look like Figure 14-8.
图 14-8。显示结果集元数据
它是如何工作的
GetName
方法通过索引获取列名。这个方法返回关于结果集的信息,所以它可以在第一次调用Read()
之前被调用。
// Get column names lbltype1.Text = rdr.GetName(0); lblType2.Text = rdr.GetName(1);
GetDataTypeName
方法返回列的数据库数据类型。它也可以在第一次调用Read()
之前被调用。
//Get column data types lbltype1.Text += "\n"+ rdr.GetDataTypeName(0).ToString(); lblType2.Text += "\n"+ rdr.GetDataTypeName(1).ToString();
数据读取器的FieldCount
属性包含结果集中的列数。这对于在不知道列名或其他属性的情况下遍历列很有用。
// Get number of columns lblType3.Text = "Number of columns in a row::" + rdr.FieldCount.ToString();
最后,您将看到如何使用GetOrdinal
和GetFieldType
方法。前者返回基于其名称的列索引;后者返回 C# 类型。这些分别是GetName()
和GetDataTypeName()
的计数器类型。
` // Get info about each column
lblType4.Text = rdr.GetName(0).ToString() + " is at index::" +
rdr.GetOrdinal("FirstName").ToString() +
" and its type is::" + rdr.GetFieldType(0).ToString();
lblType5.Text = rdr.GetName(1).ToString() + " is at index:: "+
rdr.GetOrdinal("LastName").ToString() +
" and its type is::" + rdr.GetFieldType(1).ToString();`
关于获取结果集的信息就说到这里。现在您将学习如何获取关于模式的信息。
获取关于表格的数据
术语模式对于关系数据库有多种含义。这里,我们用它来表示数据结构的设计,尤其是数据库表。表格由行和列组成,每一列都可以有不同的数据类型。列及其属性(数据类型、长度等)构成了表的模式。
为了方便地检索模式信息,可以在数据读取器上调用GetSchemaTable
方法。顾名思义,这个方法返回一个System.Data.DataTable
对象,它是被查询的表的一个表示(模式),并且以DataRow
和DataColumn
对象的形式包含行和列的集合。这些行和列由DataTable
类的属性Rows
和Columns
作为集合对象返回。
然而,这里通常会出现轻微的混淆。数据列对象不是列值;相反,它们是表示和控制单个列的行为的列定义。它们可以通过使用列名索引器来循环,并且可以告诉您许多关于数据集的信息。
尝试一下:获取模式信息
这里你会看到一个GetSchemaTable
方法的实际演示。
-
选择 DataReader 项目,右键单击,然后选择“添加 Windows 窗体”。在打开的对话框中,确保选择了 Windows 窗体,并将 Form1.cs 重命名为 SchemaTable.cs,然后单击 OK 将该窗体添加到 DataReader 项目中。
-
选择 SchemaTable 表单,并将 Size 属性的宽度设置为 378,高度设置为 459。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中间。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtSchema。
- 将位置属性的 X 设置为 12,Y 设置为 12。
- 将 Multiline 属性设置为 True。
- 将 ScrollBars 属性设置为垂直。
- 将 Size 属性的宽度设置为 392,高度设置为 333。
-
Now your SchemaTable form in the Design view should look like Figure 14-9.
图 14-9。可图式的设计视图
-
Double-click the empty surface of the SchemaTable.cs form, and it will open the code editor window, showing the SchemaTable_Load event. Modify the SchemaTable _Load event to look like Listing 14-5.
清单 14-5。T4
SchemaTable.cs
`Using System.Data.SqlClient
private void SchemaTable_Load(object sender, EventArgs e)
{
// Connection string
string connString = @"server=.\sql2012;
database=AdventureWorks;Integrated Security=SSPI";// Query
string sql = @"select * from Person.Address";// Create connection
SqlConnection conn = new SqlConnection(connString);try
{
conn.Open();SqlCommand cmd = new SqlCommand(sql, conn);
SqlDataReader rdr = cmd.ExecuteReader();// Store Employees schema in a data table
DataTable schema = rdr.GetSchemaTable();// Display info from each row in the data table.
// Each row describes a column in the database table.foreach (DataRow row in schema.Rows)
{
foreach (DataColumn col in schema.Columns)
{
txtSchema.AppendText(col.ColumnName + " = " + row[col]);
txtSchema.AppendText("\n");
}
txtSchema.AppendText("-----------------");
}//Close reader
rdr.Close();}
catch (Exception err)
{
MessageBox.Show(err.ToString() );
}
finally
{
conn.Close();
}
}` -
To set the SchemaTable form as the start-up form, modify the Program.cs statement.
Application.Run(new ResultSetInfo ());
表现为
Application.Run(new SchemaTable());
-
Build the project, and run it by pressing Ctrl+F5. You should see the results in Figure 14-10.
图 14-10。显示模式元数据
它是如何工作的
这段代码与您之前编写的代码有些不同。当调用GetSchemaTable
方法时,返回一个数据表的填充实例。
// Store Person’s schema in a data table DataTable schema = rdr.GetSchemaTable();
您可以使用数据表来表示数据库中的完整表,可以是表示其架构的表的形式,也可以是保存所有原始数据以供脱机使用的表的形式。
在这个例子中,一旦获得了一个模式表,就可以通过DataTable
的Rows
属性检索一个行集合,通过DataTable
的Columns
属性检索一个列集合。(您可以使用Rows
属性向表中添加一个新行或删除一行,并且您可以使用Columns
属性添加或删除一个现有列——我们将在第十五章中对此进行介绍。)表返回的每一行都描述了原始表中的一列,因此对于这些行中的每一行,都要使用嵌套的foreach
循环逐个遍历列的模式信息。
// Display info from each row in the data table. // Each row describes a column in the database table. foreach (DataRow row in schema.Rows) { foreach (DataColumn col in schema.Columns) { txtSchema.AppendText(col.ColumnName + " = " + row[col]); txtSchema.AppendText("\n"); } txtSchema.AppendText("-----------------"); }
注意如何在循环中使用DataColumn
对象的ColumnName
属性来检索当前的模式列名,然后通过使用熟悉的索引器风格的方法来检索与该列的定义相关的值,该方法使用了一个DataRow
对象。有许多重载的索引器,这只是其中的一种方法。
通过数据读取器使用多个结果集
有时,您可能真的希望快速完成一项工作,并且还希望同时使用两个或更多查询来查询数据库。此外,您不希望通过实例化多个命令或数据读取器,或者通过一次又一次地使用相同的对象,并在运行过程中添加代码,以任何方式影响应用的整体性能。
那么,有没有办法让一个数据读取器遍历多个结果集呢?是的,数据读取器有一个方法NextResult()
,它将读取器推进到下一个结果集。
尝试一下:处理多个结果集
在这个例子中,您将使用NextResult()
来处理多个结果集。
-
选择 DataReader 项目,右键单击,然后选择“添加 Windows 窗体”。在打开的对话框中,确保选择了 Windows 窗体,并将 Form1.cs 重命名为 MultipleResults.cs,单击“确定”将该窗体添加到 DataReader 项目中。
-
选择 MultipleResults 表单,并将 Size 属性的宽度设置为 358,高度设置为 516。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中间。选择此文本框,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtResult。
- 将位置属性的 X 设置为 12,Y 设置为 12。
- 将 Multiline 属性设置为 True。
- 将 ScrollBars 属性设置为垂直。
- 将 Size 属性的宽度设置为 318,高度设置为 454。
-
Now your MultipleResults form in the Design view should like Figure 14-11.
图 14-11。多结果表单的设计视图
-
Now double-click the empty surface of the MultipleResults.cs form, and it will open the code editor window, showing the SchemaTable_Load event. Modify the MultipleResults_Load event to look like Listing 14-6.
清单 14-6。T4
MultipleResults.cs
`using System.Data.SqlClient;
private void MultipleResults_Load(object sender, EventArgs e)
{
// Connection string
string connString = @"server=.\sql2012;database=AdventureWorks;
Integrated Security=SSPI";// Query1
string sql1 = @"select CountryRegionCode,Name
from Person.CountryRegion
where Name like 'A%' ";//Query2
string sql2 = @"select FirstName, LastName
from Person.Contact";//Combining queries to produce multiple result set
string sql = sql1 + sql2;// Create connection
SqlConnection conn = new SqlConnection(connString);
try
{
// Open connection
conn.Open();// Create command
SqlCommand cmd = new SqlCommand(sql, conn);// Create data reader
SqlDataReader rdr = cmd.ExecuteReader();// Loop through result sets
do
{
txtResult.AppendText(rdr.GetName(0));
txtResult.AppendText("\t\t");
txtResult.AppendText(rdr.GetName(1));
txtResult.AppendText("\n");
txtResult.AppendText("".PadLeft(30, '='));
txtResult.AppendText("\n");while (rdr.Read())
{
// Print one row at a time
txtResult.AppendText("\t\t\t");
txtResult.AppendText(rdr[1].ToString());
txtResult.AppendText("\n");
}
}
while (rdr.NextResult());// Close data reader
rdr.Close();
}
catch (SqlException ex)
{
MessageBox.Show(ex.Message + ex.StackTrace,"Exception Details");
}
finally
{
// Close connection
conn.Close();
}` -
To set the MultipleResults form as the start-up form, modify the Program.cs statement.
Application.Run(new SchemaTable ());
表现为:
Application.Run(new MultipleResults());
-
Build the project, and run it by pressing Ctrl+F5. You should see the results in Figure 14-12.
图 14-12。处理多个结果集
它是如何工作的
这个程序本质上与第一个相同,DataLooper.cs
( 清单 14-1 )。这里,您定义了两个独立的查询,然后将它们组合起来。
`// Query1
string sql1 = @"select CountryRegionCode,Name
from Person.CountryRegion
where Name like 'A%' ";
//Query2
string sql2 = @"select FirstName, LastName
from Person.Contact";
//Combining queries to produce multiple result set
string sql = sql1 + sql2;`
唯一的另一个变化是循环遍历结果集。将检索行的循环嵌套在遍历结果集的循环中。
` // Loop through result sets
do
{
txtResult.AppendText(rdr.GetName(0));
txtResult.AppendText("\t\t");
txtResult.AppendText(rdr.GetName(1));
txtResult.AppendText("\n");
txtResult.AppendText("".PadLeft(30, '='));
txtResult.AppendText("\n");
while (rdr.Read())
{
// Print one row at a time
txtResult.AppendText(rdr[0].ToString());
txtResult.AppendText("\t\t\t");
txtResult.AppendText(rdr[1].ToString());
txtResult.AppendText("\n");
}
}
while (rdr.NextResult());`
为了简化,我们让您在每个查询中只选择两个字符串列。扩展它来处理具有不同列数和列数据类型的结果表非常简单。
总结
在本章中,您使用了数据读取器来执行各种常见任务,从简单地遍历单个结果集到处理多个结果集。您了解了如何通过列名和索引来检索列的值,还了解了可用于处理不同数据类型的值的方法。您还了解了如何获取关于结果集的信息和模式信息。
在下一章,我们将讨论 ADO 真正有趣的方面。NET:使用数据集和数据适配器在与数据库断开连接时处理数据库数据。
十五、使用数据集和数据适配器
在第十四章中,您看到了如何使用数据读取器以连接的、只进、只读的方式访问数据库数据。通常,这就是您想要做的,而数据阅读器完全符合您的目的。
在这一章中,你将看到一个新的数据访问对象,即数据集。与数据读取器不同,数据读取器是实现System.Data.IDataReader
接口的特定于数据提供者的类的对象,数据集是类System.Data.DataSet
的对象,这是一个由所有数据提供者使用的独特的 ADO.NET 组件。数据集完全独立于数据源,既可以连接到数据源,也可以从数据源断开。它们的基本目的是提供存储在内存缓存中的数据的关系视图。
注意在另一个有点令人困惑的术语中,这个类被命名为DataSet
,但是通用术语被拼写为数据集
那么,如果数据集不必连接到数据库,如何用数据填充它并将它的数据保存到数据库呢?这就是数据适配器的用武之地。将数据适配器视为数据集和数据源之间的桥梁。没有数据适配器,数据集就不能访问任何类型的数据源。数据适配器负责数据集的所有连接细节,用数据填充数据集,并更新数据源。
在本章中,我们将介绍以下内容:
- Understand the object model
- Working with datasets and data adapters
- Propagate changes to data sources
- be complicated by
- Working with datasets and XML
- Understanding typed and untyped datasets
了解对象模型
本章开始时,我们将快速介绍使用数据集和数据适配器需要了解的所有新对象。您将从查看数据集和数据读取器之间的差异开始,然后更详细地查看数据在数据集中的结构以及数据集如何与数据适配器协作。
数据集与数据读取器
如果你只是想读取和显示数据,那么你只需要使用一个数据读取器,就像你在上一章看到的那样,特别是当你处理大量数据的时候。在需要遍历数千或数百万行的情况下,您需要一个快速的顺序读取器(一次从结果集中读取一行),数据读取器以一种高效的方式完成这项工作。
如果您需要以任何方式操作数据,然后更新数据库,您需要使用数据集。数据适配器使用数据读取器填充数据集;需要额外的资源来保存数据以供离线使用。你需要思考你是否真的需要一个数据集;否则,你只是在浪费资源。除非您需要更新数据源或使用其他数据集功能,如读写 XML 文件、导出数据库架构和创建数据库的 XML 视图,否则应该使用数据读取器。
数据集简介
ADO.NET 的数据集概念是多层数据库应用开发世界中的一大步。当检索或修改大量数据时,在等待用户发出请求的同时保持与数据源的开放连接是对宝贵资源的巨大浪费。
数据集在这里非常有用,因为它们使您能够在本地缓存中存储和修改大量数据,以表格的形式查看数据,并以离线模式(换句话说,与数据库断开连接)处理数据。
我们来看一个例子。假设您试图通过互联网连接到远程数据库服务器,以获取一些商业事务的详细信息。您在特定日期搜索所有可用的事务,并显示结果。在后台,您的应用创建一个与数据源的连接,连接两个表,并检索结果。假设您现在想要编辑这些信息,并添加或删除细节。不管是什么原因,您的应用都会一遍又一遍地经历相同的循环:创建新的连接、连接表和检索数据。不仅每次创建一个新的连接会有开销,而且您可能会做许多其他多余的工作,尤其是当您处理相同的数据时。如果您可以连接到数据源一次,将数据本地存储在类似于关系数据库的结构中,关闭连接,修改本地数据,然后在适当的时候将更改传播到数据源,这样不是更好吗?
这正是数据集的设计目的。数据集将关系数据存储为数据表的集合。在上一章中,当一个System.Data.DataTable
对象用来保存模式信息时,你简要地见过数据表。然而,在这种情况下,数据表只包含模式信息,但是在数据集中,数据表包含描述数据结构和数据本身的元数据。
图 15-1 显示了数据集架构。
图 15-1。数据集架构
该体系结构反映了关系数据库的逻辑设计。在本章中,您将看到如何使用数据表、数据行和数据列。
数据适配器简介
当您第一次实例化数据集时,它不包含任何数据。您可以通过将填充的数据集传递给数据适配器来获得它,数据适配器负责连接细节,并且是数据提供程序的一个组件。数据集不是数据提供程序的一部分。就像一个水桶,准备装水,但是需要一个外接管道让水进来。换句话说,数据集需要一个数据适配器来填充数据并支持对数据源的访问。
每个数据提供程序都有自己的数据适配器,就像它有自己的连接、命令和数据读取器一样。图 15-2 描述了数据集、数据适配器和数据源之间的交互。
图 15-2。数据集、数据适配器和数据源交互
数据适配器构造函数被重载。您可以使用以下任何一种方法来获取新的数据适配器。我们使用 SQL Server 数据提供程序,但是其他数据提供程序的构造函数是类似的。
SqlDataAdapter da = new SqlDataAdapter(); SqlDataAdapter da = new SqlDataAdapter(cmd); SqlDataAdapter da = new SqlDataAdapter(sql, conn); SqlDataAdapter da = new SqlDataAdapter(sql, connString);
因此,您可以用四种方式创建数据适配器:
- You can use its parameterless constructor (assign SQL and join later).
- You can pass a command to its constructor (here,
cmd
is aSqlCommand
object).- You can pass an SQL string and a connection.
- You can pass an SQL string and a connection string.
您将很快看到所有这些都在起作用。现在,我们将继续介绍如何使用数据表、数据列和数据行。您将在接下来的章节中使用这些内容。
数据表、数据列和数据行的简要介绍
数据表是类System.Data.DataTable
的一个实例。它在概念上类似于关系表。如图图 15-1 所示,一个数据表有数据行和数据列的集合。您可以通过数据表的Rows
和Columns
属性来访问这些嵌套集合。
一个数据表可以代表一个独立的表,或者在一个数据集内,正如你将在本章中看到的,或者作为一个由另一个方法创建的对象,正如你在上一章中看到的,当一个数据表通过调用数据读取器上的GetSchemaTable
方法被返回时。
数据列表示数据表中列的模式,然后可用于设置或获取列属性。例如,您可以使用它通过为数据列的DefaultValue
属性赋值来设置列的默认值。
您可以使用数据表的Columns
属性获取数据列的集合,该属性的索引器接受列名或从零开始的索引,例如(其中dt
是数据表):
DataColumn col = dt.Columns["ContactName"];
DataColumn col = dt.Columns[2];
数据行代表一行中的数据。您可以以编程方式添加、更新或删除数据表中的行。要访问数据表中的行,可以使用它的Rows
属性,该属性的索引器接受从零开始的索引,例如(其中dt
是数据表):
DataRow row = dt.Rows[2];
这是足够的理论了。是时候做一些编码了,看看这些对象在实践中是如何协同工作的!
使用数据集和数据适配器
数据集构造函数被重载。
DataSet ds = new DataSet(); DataSet ds = new DataSet("MyDataSet");
如果使用无参数构造函数,数据集名称默认为NewDataSet
。如果需要多个数据集,最好使用另一个构造函数并显式命名它。但是,您总是可以通过设置其DataSetName
属性来更改数据集名称。
您可以通过多种方式填充数据集,包括以下方式:
- Use data adapter
- Read from XML document
在本章中,我们将使用数据适配器。但是,在“使用数据集和 XML”一节中,您将快速浏览一下第二种方法的相反情况,您将从数据集编写 XML 文档。
试试看:用数据适配器填充数据集
在本例中,您将创建一个数据集,用数据适配器填充它,然后显示它的内容。
-
创建一个名为 Chapter15 的新 Windows 窗体应用项目。当解决方案资源管理器打开时,保存解决方案。
-
将 Chapter15 项目重命名为 DataSetandDataAdapter。
-
重命名
Form1.cs
文件以弹出DataSet.cs
。 -
通过单击窗体的标题栏选择 PopDataSet 窗体,并将 Size 属性的宽度设置为 301,高度设置为 342。
-
将 GridView 控件拖到窗体上,并将其放在窗体的左上角。选择此 GridView,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 gvProduct。
- 对于 Location 属性,将 X 设置为 12,将 Y 设置为 12。
- 将 ScrollBars 属性设置为 Both。
- 对于 Size 属性,将 Width 设置为 263,Height 设置为 282。
-
Now your PopDataSet form in the Design view should look like Figure 15-3.
图 15-3。pop dataset 表单的设计视图
-
Double-click the empty surface of the
PopDataSet.cs
form, and it will open the code editor window, showing thePopDataSet_Load
event. Modify thePopDataSet_Load
event to look like Listing 15-1.清单 15-1。
PopDataSet.cs
`Using System.Data.SqlClient;
private void PopDataSet_Load(object sender, EventArgs e)
{
// Connection string
string connString = @" server=.\sql2012;database=AdventureWorks;
Integrated Security=true";// Query
string sql = @"select Name,ProductNumber
from Production.Product
where SafetyStockLevel > 600";// Create connection
SqlConnection conn = new SqlConnection(connString);
{
// Open connection
conn.Open();// Create Data Adapter
SqlDataAdapter da = new SqlDataAdapter(sql, conn);// Create Dataset
DataSet ds = new DataSet();// Fill Dataset
da.Fill(ds, "Production.Product");// Display data
gvProduct.DataSource = ds.Tables["Production.Product"];
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + ex.StackTrace);
}
finally
{
//Connection close
conn.Close();
}
}` -
Build the project, and run the DataLooper form by pressing Ctrl+F5. Your results should look like 15-4.
图 15-4。填充数据集
它是如何工作的
定义查询并打开连接后,创建并初始化数据适配器。
// Create Data Adapter SqlDataAdapter da = new SqlDataAdapter(sql, conn);
然后你创建一个数据集。
// Create Dataset DataSet ds = new DataSet();
在这个阶段,你所拥有的只是一个空的数据集。关键是在数据适配器上使用Fill
方法来执行查询、检索数据和填充数据集。
// Fill Dataset da.Fill(ds, "Production.Product");
Fill
方法在内部使用数据读取器来访问表模式和数据,然后使用它们来填充数据集。
注意,这个方法不仅仅用于填充数据集。它有许多重载,如果需要,还可以用于填充没有数据集的单个数据表。
如果您没有向Fill
方法提供表的名称,它将被自动命名为TableN
,其中N
以空字符串开始(第一个表名就是Table
),并在每次向数据集中插入新表时递增。更好的做法是显式命名数据表,但这并不重要。
如果对已经包含数据的数据集多次运行相同的查询,Fill()
会更新数据,跳过根据模式重新定义表的过程。
这里值得一提的是,下面的代码会产生相同的结果。除了将 SQL 和连接传递给数据适配器的构造函数之外,您还可以使用使用适当的 SQL 和连接创建的命令来设置它的SelectCommand
属性。
// Create data adapter SqlDataAdapter da = new SqlDataAdapter(); da.SelectCommand = new SqlCommand(sql, conn);
有了填充的数据集,您现在可以访问各个数据表中的数据。(这个数据集只包含一个数据表。)
// get data table DataTable dt = ds.Tables["Production.Product"];
最后,使用嵌套的foreach
循环来访问每行中的列,并将它们的数据值输出到屏幕上。
// display data gvProduct.DataSource = ds.Tables["Production.Product"];
数据集中的过滤和排序
在前面的例子中,您看到了如何从数据集中提取数据。然而,如果您正在处理数据集,很可能您想要对数据做更多的事情,而不仅仅是显示它。通常,您会希望对数据进行动态过滤或排序。在下面的示例中,您将看到如何使用数据行来实现这一点。
试试看:对数据集中的数据进行动态过滤和排序
我们将从 Customers 表中获取所有的行和列,只过滤德国客户的结果,并按公司排序。我们将使用一个单独的查询来查找产品,并在同一个数据集中填充两个数据表。
-
选择 DataSetandDataAdapter 项目,右键单击,然后选择添加 Windows 窗体。从打开的对话框中,确保选择了 Windows 窗体,并将
Form1.cs
重命名为FilterSort.cs
。单击“确定”将该表单添加到 DataSetandDataAdapter 项目中。 -
通过单击窗体的标题栏选择 FilterSort 窗体,并将 Size 属性的宽度设置为 350,高度设置为 489。
-
将文本框控件拖到窗体上,并将其放在窗体的中央。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtSort。
- 对于 Location 属性,将 X 设置为 12,将 Y 设置为 8。
- 将 Multiline 属性设置为 True。
- 将 ScrollBars 属性设置为垂直。
- 对于“大小”属性,将“宽度”设置为 312,将“高度”设置为 435。
- 将文本属性留空。
-
Now your FilterSort form in the Design view should look like Figure 15-5.
图 15-5。FilterSort 表单的设计视图
-
Double-click the empty surface of the
FilterSort.cs
form, and it will open the code editor window, showing theFilterSort_Load
event. Modify theFilterSort_Load
event to look like Listing 15-2.清单 15-2。T4
FilterSort.cs
`Using System.Data.SqlClient;
private void FilterSort_Load(object sender, EventArgs e)
{
// Connection string
string connString = @"server=.\sql2012; database=AdventureWorks;
Integrated Security=true";// Query
string sql1 = @" select *
from Production.Product
where Name Like 'Mountain%'";string sql2 = @" select *
from Production.Location// Combine queries
string sql = sql1 + sql2;// Create connection
SqlConnection conn = new SqlConnection(connString);try
{
// Create Data Adapter
SqlDataAdapter da = new SqlDataAdapter();
da.SelectCommand = new SqlCommand(sql, conn);// Create and Fill Data Set
DataSet ds = new DataSet();
da.Fill(ds, "Production.Product");// Get the data tables collection
DataTableCollection dtc = ds.Tables;// Display data from first data table
//
// Display output header
txtSort.AppendText("Results from Product table:\n");
txtSort.AppendText("*******************************\n");
txtSort.AppendText("Name\t\t\t\tProductNumber\n");txtSort.AppendText("_________________________________________\n");
// set display filter
string fl = "Color = 'Black'";// Set sort
string srt = "ProductNumber asc";// display filtered and sorted data
foreach (DataRow row in dtc["Production.Product"].Select(fl, srt))
{
txtSort.AppendText(row["Name"].ToString().PadRight(25));
txtSort.AppendText("\t\t");
txtSort.AppendText(row["ProductNumber"].ToString());
txtSort.AppendText(Environment.NewLine);
}txtSort.AppendText("============================================\n");
// Display data from second data table// Display output header
txtSort.AppendText("Results from Location table:\n");
txtSort.AppendText("***********************************\n");
txtSort.AppendText("__________________________________________\n");// Display data
foreach (DataRow row in dtc[1].Rows)
{
txtSort.AppendText(row["Name"].ToString().PadRight(25));
txtSort.AppendText("\t");
txtSort.AppendText(row["CostRate"].ToString() );
txtSort.AppendText(Environment.NewLine);
}
}catch (Exception ex)
{
MessageBox.Show(ex.Message + ex.StackTrace);
}finally
{
// Connection close
conn.Close();
}
}` -
To set the FilterSort form as the start-up form, modify the
Program.cs
statement.Application.Run(new PopDataSet());
表现为:
Application.Run(new FilterSort());
构建项目,并通过按 Ctrl+F5 运行它。你的结果应该看起来像图 15-6 。
图 15-6。过滤整理数据表
它是如何工作的
您编码并组合两个查询,以便在同一个连接上执行。
`// Query1
string sql1 = @" select *
from Production.Product
where Name Like 'Mountain%'";
// Query2
string sql2 = @" select *
from Production.Location
where CostRate > 10.0 ";
// Combine queries
string sql = sql1 + sql2;
// Create connection
SqlConnection conn = new SqlConnection(connString);`
您创建一个数据适配器,为其属性SelectCommand
分配一个封装查询和连接的命令(供数据适配器的Fill
方法内部使用)。
// Create Data Adapter SqlDataAdapter da = new SqlDataAdapter(); da.SelectCommand = new SqlCommand(sql, conn);
然后创建并填充一个数据集。
// Create and Fill Data Set DataSet ds = new DataSet(); da.Fill(ds, "Production.Product");
每个查询返回一个单独的结果集,每个结果集存储在一个单独的数据表中(按照指定查询的顺序)。第一个表被明确命名为 Product 第二个被赋予默认名称 Location。
您从数据集的Tables
属性中获取数据表集合,以便于以后引用。
// get the data tables collection DataTableCollection dtc = ds.Tables;
作为显示第一个数据表的一部分,您声明了两个字符串。
// Set display filter string fl = "Color = 'Black'"; // Set sort string srt = "ProductNumber asc";
第一个字符串是一个指定行选择标准的过滤器表达式。它在语法上与 SQL WHERE
子句谓词相同。您只需要颜色列等于黑色的那些行。第二个字符串指定您的排序标准,在语法上与 SQL ORDER BY
子句相同,给出数据列名和排序顺序。
您使用一个foreach
循环来显示从数据表中选择的行,将过滤器和排序字符串传递给数据表的Select
方法。这个特定的数据表是数据表集合中一个名为 Product 的数据表。
// display filtered and sorted data foreach (DataRow row in dtc["Production.Product"].Select(fl, srt)) { txtSort.AppendText(row["Name"].ToString().PadRight(25)); txtSort.AppendText("\t\t"); txtSort.AppendText(row["ProductNumber"].ToString()); txtSort.AppendText(Environment.NewLine); }
使用创建数据集时指定的表名,从数据表集合(dtc
对象)中获取对单个数据表的引用。重载的Select
方法在数据表上进行内部搜索,过滤掉不满足选择标准的行,按照规定对结果进行排序,最后返回一个数据行数组。您可以使用索引器中的列名来访问行中的每一列。
值得注意的是,如果您只是对客户数据使用不同的查询,您可以获得相同的结果,而且效率会高得多。
Select * From Production.Productionwhere Color= ‘Black’ order by ProductNumber asc
从性能的角度来看,这是理想的,但是只有当您需要的数据仅限于这个特定序列中的这些特定行时,这才是可行的。然而,如果您正在构建一个更复杂的系统,那么最好是从数据库中一次提取所有数据(就像您在这里所做的那样),然后以不同的方式对其进行过滤和排序。阿多。NET 丰富的操作数据集及其组件的方法套件为您提供了以最佳方式满足特定需求的广泛技术。
提示一般来说,尽量利用 SQL,而不是编写 C# 程序,从数据库中获取你需要的数据。数据库服务器被优化来执行选择和排序,以及其他事情。查询可以比你在这本书里玩的那些更复杂更强大。通过仔细地(创造性地)对查询进行编码,以准确地返回您需要的内容,您不仅最小化了资源需求(内存、网络带宽等),而且减少了您必须编写的用于操作和格式化结果集数据的代码。
*第二个数据表中的循环有趣之处主要在于它的第一行,其中使用了序号索引:
foreach (DataRow row in dtc[l].Rows)
不要重命名第二个数据表(可以用它的TableName
属性来重命名),最好使用索引而不是名称位置,因为在Fill()
调用中更改名称需要您在这里进行更改,如果出现这种情况,这是不太可能的。
比较 FilterSort 和 PopDataSet
在第一个例子中,PopDataSet
( 清单 15-1 ),您看到了将数据放入数据集是多么简单。第二个例子,FilterSort
( 清单 15-2 ),只是一个变体,演示了如何处理多个结果集以及如何过滤和排序数据表。然而,这两个程序有一个主要的区别。你注意到了吗?
FilterSort
不显式打开连接!事实上,这是您编写的第一个(但不会是最后一个)不支持这一点的程序。为什么不呢?
答案很简单,但是非常重要。当调用Fill()
时,如果连接没有打开,则Fill
方法自动打开连接。然后,它在填充数据集后关闭连接。然而,如果一个连接在Fill()
被调用时是打开的,它会使用那个连接,而不会在之后关闭它。
因此,尽管数据集完全独立于数据库(和连接),但仅仅因为您正在使用数据集并不意味着您正在脱离数据库运行。如果要在断开连接的情况下运行,请使用数据集,但在填充数据集之前不要打开连接(或者,如果连接是打开的,请先将其关闭)。换句话说,数据集本质上是与数据库断开的。但是,这并不意味着使用数据集的应用已断开连接。
您将标准conn.Close();
留在finally
块中。因为Close()
可以在关闭的连接上被正确调用,如果被不必要的调用,它不会出现问题,但是它肯定保证连接将被关闭,不管在try
块中可能发生什么。
注意如果你想自己证明这一点,只需在调用Fill()
之前打开FilterSort
中的连接,然后显示连接的State
属性的值。会是Open
。注释掉这个Open()
调用,并再次运行它。State
即将关闭。
使用数据视图
在前面的例子中,您看到了如何使用Select
方法对数据表中的数据进行动态过滤和排序。然而,ADO.NET 有另一种方法来做同样的事情,而且做得更多:数据视图。一个数据视图(类System.Data.DataView
的一个实例)使您能够创建存储在底层数据表中的数据的动态视图,反映对其内容和顺序所做的所有更改。这与Select
方法不同,后者返回一个数据行数组,其内容反映数据值的变化,但不反映数据顺序。
注意数据视图是数据表内容的动态表示。像 SQL 视图一样,它实际上并不保存数据。
试试看:用数据视图提炼数据
我们不会在这里涵盖数据视图的所有方面,因为它们超出了本书的范围。然而,为了展示如何使用它们,我们将给出一个简短的例子,使用数据视图来动态排序和过滤底层数据表。
-
选择 DataSetandDataAdapter 项目,右键单击,然后选择添加 Windows 窗体。从打开的对话框中,确保选择了 Windows 窗体,并将
Form1.cs
重命名为DataViews.cs
。单击“确定”将该表单添加到 DataSetandDataAdapter 项目中。 -
通过单击窗体的标题栏选择 DataViews 窗体,并将 Size 属性的宽度设置为 304,高度设置为 359。
-
将 GridView 控件拖到窗体上,并将其放在窗体的中央。选择此 GridView 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 gvContact。
- 对于 Location 属性,将 X 设置为 12,将 Y 设置为 12。
- 将 ScrollBars 属性设置为垂直。
- 对于“大小”属性,将“宽度”设置为 262,将“高度”设置为 298。
-
Now your FilterSort form in the Design view should look like Figure 15-7.
图 15-7。数据视图表单的设计视图
-
Double-click the empty surface of the
DataViews.cs
form, and it will open the code editor window, showing theDataViews_Load
event. Modify theDataViews_Load
event to look like Listing 15-3.清单 15-3。T4
DataViews.cs
`Using System.Data.SqlClient;
private void DataViews_Load(object sender, EventArgs e)
{
// Connection string
string connString = @"server=.\sql2012;database=AdventureWorks;Integrated
Security=true";// Query
string sql = @"select FirstName, MiddleName
from Person.Contact";// Create connection
SqlConnection conn = new SqlConnection(connString);try
{
// Create Data Adapter
SqlDataAdapter da = new SqlDataAdapter();// Create and Fill Dataset
DataSet ds = new DataSet();da.Fill(ds, "Person.Contact");
// Get Data Table reference
DataTable dt = ds.Tables["Person.Contact"];// Create Data View
DataView dv = new DataView(dt,
"MiddleName = 'J.'",
"MiddleName",
DataViewRowState.CurrentRows);// Display data from data view
gvContact.DataSource = dv;
}catch (Exception ex)
{
MessageBox.Show(ex.Message + ex.StackTrace);
}finally
{
//Connection close
conn.Close();
}
}` -
To set the FilterSort form as the start-up form, modify the
Program.cs
statement.Application.Run(new FilterSort());
表现为:
Application.Run(new DataView());
构建项目,并通过按 Ctrl+F5 运行它。您应该在图 15-8 中看到结果。
图 15-8。使用数据视图
它是如何工作的
这个程序与其他示例基本相同,所以我们将重点关注它对数据视图的使用。创建一个新的数据视图,并通过向其构造函数传递四个参数来初始化它。
// Create Data View DataView dv = new DataView(dt, "MiddleName = 'J.'", "MiddleName", DataViewRowState.CurrentRows);
第一个参数是数据表,第二个参数是数据表内容的筛选器,第三个参数是排序列,第四个参数指定要包含在数据视图中的行的类型。
System.Data.DataViewRowState
是数据视图的底层数据表中行可以具有的状态的枚举。表 15-1 总结了这些状态。
每次添加、修改或删除一行时,它的行状态都会改变到表 15-1 中相应的行状态。如果您对基于特定行的状态(例如,数据表中的所有新行或所有已修改的行)检索、排序或筛选特定行感兴趣,这将非常有用。
然后,将数据视图作为数据源绑定到网格视图。
// display data from data view gvContact.DataSource = dv;
就像数据行代表数据表中的一行一样,数据行视图(也许称之为数据视图行会更好)代表数据视图中的一行。您为每个数据行视图检索经过筛选和排序的列数据,并将其输出到控制台。
正如这个简单的例子所表明的,数据视图提供了一种强大而灵活的方法来动态地改变数据表中的数据。
修改数据集中的数据
在接下来的几节中,您将通过一个实际的例子来演示以编程方式更新数据表中的数据的多种方法。请注意,这里您将只修改数据集中的数据,而不更新数据库中的数据。在“将更改传播到数据源”一节中,您将看到如何持久化对数据集所做的原始数据源更改。
注意你对数据集的更改不会自动传播到数据库。要保存数据库中的更改,您需要再次连接到数据库并显式执行必要的更新。
试试看:修改数据集中的数据表
让我们在数据表中更新一行并添加一行。
-
选择 DataSetandDataAdapter 项目,右键单击,然后选择添加 Windows 窗体。从打开的对话框中,确保选择了 Windows 窗体,并将
Form1.cs
重命名为ModifyDataTable.cs
。单击“确定”将该表单添加到 DataSetandDataAdapter 项目中。 -
通过单击表单的标题栏选择 ModifyDataTable 表单,并将 Size 属性的宽度设置为 371,高度设置为 348。
-
将 GridView 控件拖到窗体上,并将其放在窗体的中央。选择此 GridView 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 gvAddress。
- 对于 Location 属性,将 X 设置为 12,将 Y 设置为 12。
- 将 ScrollBars 属性设置为垂直。
- 对于“大小”属性,将“宽度”设置为 331,将“高度”设置为 287。
-
Now your ModifyDataTable form in the Design view should look like Figure 15-9.
图 15-9。修改后的数据表的设计视图
-
Double-click the empty surface of the
ModifyDataTable.cs
form, and it will open the code editor window, showing theModifyDataTable _Load
event. Modify theModifyDataTable _Load
event to look like Listing 15-4.清单 15-4。T4
ModifyDataTable.cs
`Using System.Data.SqlClient;
private void ModifyDataTable_Load(object sender, EventArgs e)
{
// Connection string
string connString = @"server=.\sql2012;database=AdventureWorks;Integrated
Security=true";// Query
string sql = @"select AddressLine2,City,StateProvinceID,PostalCode
from Person.Address
where City = 'Bothell'";// Create connection
SqlConnection conn = new SqlConnection(connString);try
{
// Create Data Adapter
SqlDataAdapter da = new SqlDataAdapter();
da.SelectCommand = new SqlCommand(sql, conn);// Create and Fill Dataset
DataSet ds = new DataSet();
da.Fill(ds, "Person.Address");// Get data table reference
DataTable dt = ds.Tables["Person.Address"];// FirstName column should be nullable
dt.Columns["AddressLine2"].AllowDBNull = true;// Modify City in first row
dt.Rows[0]["City"] = "Wilmington";// add a row
DataRow newRow = dt.NewRow();newRow["PostalCode"] = "111111";
newRow["StateProvinceID"] = "80";
newRow["City"] = "Birmingham";
dt.Rows.Add(newRow);// Display Rows
gvAddress.DataSource = dt;
gvAddress.Columns[0].Visible = false;
gvAddress.Rows[0].DefaultCellStyle.BackColor = Color.Red;
catch (Exception ex)
{
MessageBox.Show(ex.Message + ex.StackTrace);
}finally
{
// Connection close
conn.Close();
}
}` -
To set the ModifyDataTable form as the start-up form, modify the
Program.cs
statement.Application.Run(new DataView());
表现为:
Application.Run(new ModifyDataTable());
构建项目,并通过按 Ctrl+F5 运行它。你的结果应该看起来像图 15-10 。
图 15-10。修改数据表
它是如何工作的
和以前一样,您在数据集中使用单个数据表。
// Get data table reference DataTable dt = ds.Tables["Person.Address"];
接下来,您可以看到一个如何更改模式信息的示例。您选择 FirstName 列,它的AllowNull
属性在数据库中被设置为false
,并且您将它更改为true
。
// AddressLine2 column should be nullable dt.Columns["AddressLine2"].AllowDBNull = true;
请注意,如果您知道列的索引是什么,您可以使用序号索引(例如,dt.Columns[ l ]
),但是使用*
选择所有列会降低可靠性,因为如果数据库表模式改变,列的位置可能会改变。
您可以使用相同的方法修改行。您只需选择适当的行,并将其列设置为您想要的任何值,当然,与列数据类型一致。下面一行显示数据集第一行的 City 列被更改为 Wilmington:
// Modify City in first row dt.Rows[o]["city"] = "Wilmington";
接下来,向数据表中添加一个新行。
`// Add a row
DataRow newRow = dt.NewRow();
newRow["PostalCode"] = "111111";
newRow["StateProvinceID"] = "80";
newRow["City"] = "Birmingham";
dt.Rows.Add(newRow);`
NewRow
方法创建一个数据行(一个System.Data.DataRow
实例)。使用数据行的索引器为其列赋值。最后,将新行添加到数据表中,调用数据表的Rows
属性上的Add
方法,该属性引用 rows 集合。
更新数据源需要了解有关数据适配器方法和属性的更多信息。现在让我们来看看这些。
将更改传播到数据源
您已经看到了数据适配器如何填充数据集的数据表。您还没有看到的是数据适配器如何更新和同步数据源与数据集中的数据。它有三个属性支持这一点(类似于它的SelectCommand
属性,它支持查询)。
InsertCommand
UpdateCommandDeleteCommand
我们将简单描述一下InsertCommand
,然后将其付诸实践。
插入命令属性
数据适配器使用InsertCommand
属性将行插入到表中。在调用Update
方法时,将搜索添加到数据表中的所有行,并将其传播到数据库。
尝试一下:向数据源传播新的数据集行
让我们向数据库传播一个新行,这是清单 15-5 中的另一个变体。
-
选择 DataSetandDataAdapter 项目,右键单击,然后选择“添加 Windows 窗体”。从打开的对话框中,确保选择了 Windows 窗体,并将
Form1.cs
重命名为PersistAdds.cs
。单击“确定”将该表单添加到 DataSetandDataAdapter 项目中。 -
通过单击窗体的标题栏选择 PersistAdds 窗体,并将 Size 属性的宽度设置为 452,高度设置为 163。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将“名称”属性设置为 txtDepartment。
- 对于 Location 属性,将 X 设置为 12,将 Y 设置为 12。
- 对于“大小”属性,将“宽度”设置为 412,将“高度”设置为 95。
- 将文本属性留空。
-
Now your PersistAdds form in the Design view should look like Figure 15-11.
图 15-11。持久添加表单的设计视图
-
Double-click the empty surface of the
PersistAdds.cs
form, and it will open the code editor window, showing thePersistAdds _Load
event. Modify thePersistAdds _Load
event to look like Listing 15-5.清单 15-5。
PersistAdds.cs
`Using System.Data.SqlClient;
private void PersistAdds_Load(object sender, EventArgs e)
{
// Connection string
string connString = @" server=.\sql2012;database=AdventureWorks;Integrated
Security=true";// Query
string qry = @" select *
from HumanResources.Department
where GroupName = 'Sales'";// SQL to insert employees
string ins = @"insert into HumanResources.Department
(Name,GroupName, ModifiedDate)
values(@Name, @GroupName, @ModifiedDate)";// Create connection
SqlConnection conn = new SqlConnection(connString);try
{
// Create data adapter
SqlDataAdapter da = new SqlDataAdapter();
da.SelectCommand = new SqlCommand(qry, conn);// Create and fill data set
DataSet ds = new DataSet();
da.Fill(ds, "HumanResources.Department");// Get data table reference
DataTable dt = ds.Tables["HumanResources.Department"];// Add a row
DataRow newRow = dt.NewRow();
newRow["Name"] = "Microsoft Development";
newRow["GroupName"] = "Global Development";
newRow["ModifiedDate"] = "2012-04-28";
dt.Rows.Add(newRow);// Display rows
foreach (DataRow row in dt.Rows)
{txtDepartment.AppendText(row["Name"].ToString());
txtDepartment.AppendText("\t");
txtDepartment.AppendText(row["GroupName"].ToString());
txtDepartment.AppendText("\t");
txtDepartment.AppendText(row["ModifiedDate"].ToString());
}// Create command
SqlCommand cmd = new SqlCommand(ins, conn);
//
// Map parameters
cmd.Parameters.Add("@Name", SqlDbType.NVarChar, 50,"Name");
cmd.Parameters.Add("@GroupName",SqlDbType.NVarChar,50,"GroupName");
cmd.Parameters.Add("@ModifiedDate",SqlDbType.DateTime,25,"ModifiedDate");// Insert department
da.InsertCommand = cmd;
da.Update(ds, "HumanResources.Department");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message + ex.StackTrace);
}finally
{
//Connection close
conn.Close();
}
}` -
To set the PersistAdds form as the start-up form, modify the
Program.cs
statement.Application.Run(new ModifyDataTable());
表现为:
Application.Run(new PersistAdds());
构建项目,并通过按 Ctrl+F5 运行它。您应该在图 15-12 中看到结果。
图 15-12。增加一行
它是如何工作的
你添加一个INSERT
语句。
string ins = @"insert into HumanResources.Department (Name,GroupName, ModifiedDate) values(@Name, @GroupName, @ModifiedDate)";
为INSERT
查询创建一个命令。
// create command SqlCommand cmd = new SqlCommand(ins, conn);
然后配置命令参数。您将为其提供值的三列分别映射到一个命名的命令参数。您不需要提供主键值,因为它是由 SQL Server 生成的。
// Map parameters cmd.Parameters.Add("@Name", SqlDbType.NVarChar, 50,"Name"); cmd.Parameters.Add("@GroupName",SqlDbType.NVarChar,50,"GroupName"); cmd.Parameters.Add("@ModifiedDate",SqlDbType.DateTime,25,"ModifiedDate");
最后,用插入到 Department 表中的命令设置数据适配器的InsertCommand
属性,这样当您调用它的Update
方法时,它将是数据适配器执行的 SQL。然后调用数据适配器上的Update
,将更改传播到数据库。这里只添加了一行,但是由于 SQL 是参数化的,数据适配器将在 HumanResources 中查找所有新行。部门数据表,并向数据库提交所有这些数据表的插入内容。
// Insert department da.InsertCommand = cmd; da.Update(ds, "HumanResources.Department");
图 15-12 显示了新行,如果您使用数据库浏览器或 SQL Server 2012 的 SSMS 进行检查,您会看到该行已被传播到数据库。微软开发现在在部门表中。
指挥建设者
虽然很简单,但是为UpdateCommand
、InsertCommand
和DeleteCommand
属性编写 SQL 语句有点麻烦,所以每个数据提供者都有自己的命令构建器。如果一个数据表对应一个数据库表,您可以使用命令生成器自动为数据适配器生成适当的UpdateCommand
、InsertCommand
和DeleteCommand
属性。当调用数据适配器的Update
方法时,这一切都是透明的。
为了能够动态生成INSERT
、DELETE
和UPDATE
语句,命令生成器使用数据适配器的SelectCommand
属性来提取数据库表的元数据。如果在调用Update
方法后对SelectCommand
属性进行了任何更改,您应该调用命令构建器上的RefreshSchema
方法来相应地刷新元数据。
要创建命令生成器,需要创建数据提供程序的命令生成器类的实例,将数据适配器传递给其构造函数。例如,以下代码创建了一个 SQL Server 命令生成器:
SqlDataAdapter da = new SqlDataAdapter(); SqlCommandBuilder cb = new SqlCommandBuilder(da);
注意为了使命令生成器工作,SelectCommand
数据适配器属性必须包含一个查询,该查询返回数据库表的主键或唯一键。如果不存在,就会生成一个InvalidOperation
异常,并且不会生成命令。
尝试一下:使用 SqlCommandBuilder
在这里,您将使用 SqlCommand Builder 向数据库中插入一行。
-
选择 DataSetandDataAdapter 项目,右键单击,然后选择添加 Windows 窗体。从打开的对话框中,确保选择了 Windows 窗体,并将
Form1.cs
重命名为PersistAddsBuilder.cs
。单击“确定”将该表单添加到 DataSetandDataAdapter 项目中。 -
通过单击窗体的标题栏选择 PersistAddsBuilder 窗体,并将 Size 属性的宽度设置为 483,高度设置为 151。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtDepartment。
- 对于 Location 属性,将 X 设置为 12,将 Y 设置为 12。
- 对于 Size 属性,将 Width 设置为 441,Height 设置为 89。
- 将文本属性留空。
-
Now your PersistAddsBuilder form in the Design view should look like Figure 15-13.
图 15-13。PersistAddsBuilder 表单的设计视图
-
Double-click the empty surface of the
PersistAddsBuilder.cs
form, and it will open the code editor window, showing thePersistAddsBuilder _Load
event. Modify thePersistAddsBuilder _Load
event to look like Listing 15-6.清单 15-6。
PersistAddsBuilder.cs
`Using System.Data.SqlClient;
private void PersistAddsBuilder_Load(object sender, EventArgs e)
{
// Connection string
string connString = @"server=.\sql2012;database=AdventureWorks;Integrated
Security=true";// Query
string qry = @" select *
from HumanResources.Department
where GroupName = 'Research and Development' ";// Create connection
SqlConnection conn = new SqlConnection(connString);try
{
// Create Data Adapter
SqlDataAdapter da = new SqlDataAdapter();
da.SelectCommand = new SqlCommand(qry, conn);// Create command builder
SqlCommandBuilder cb = new SqlCommandBuilder(da);// Create and Fill Dataset
DataSet ds = new DataSet();
da.Fill(ds, "HumanResources.Department");// Get Data Table reference
DataTable dt = ds.Tables["HumanResources.Department"];// Add a row
DataRow newRow = dt.NewRow();
newRow["Name"] = "Language Design";
newRow["GroupName"] = "Research and Development";
newRow["ModifiedDate"] = "2012-04-29";dt.Rows.Add(newRow);
// Display rows
foreach (DataRow row in dt.Rows)
{
txtDepartment.AppendText(row["Name"].ToString());
txtDepartment.AppendText("\t\t");
txtDepartment.AppendText(row["GroupName"].ToString());
txtDepartment.AppendText("\t");
txtDepartment.AppendText(row["ModifiedDate"].ToString());
txtDepartment.AppendText("\n");
}
da.Update(ds, "HumanResources.Department");
}catch (Exception ex)
{
MessageBox.Show(ex.Message + ex.StackTrace);
}finally
{
//Connection close
conn.Close();
}
}` -
To set the PersistAddsBuilder form as the start-up form, modify the
Program.cs
statement.Application.Run(new PersistAdds());
表现为:
Application.Run(new PersistAddsBuilder());
构建项目,并通过按 Ctrl+F5 运行它。您应该在图 15-14 中看到结果。
图 15-14。使用命令生成器添加一行
它是如何工作的
最值得注意的不是你添加的行(是的,只是一个加一个注释)和你替换的行一样多。
`// create command builder
SqlCommandBuilder cb = new SqlCommandBuilder(da);
// Add a row
DataRow newRow = dt.NewRow();
newRow["Name"] = "Language Design";
newRow["GroupName"] = "Research and Development";
newRow["ModifiedDate"] = "2012-04-29";
dt.Rows.Add(newRow);`
显然,使用命令生成器比手工编码 SQL 更可取;但是,请记住,它们只对单个表有效,并且底层数据库表必须有一个主键或唯一键。此外,数据适配器SelectCommand
属性必须有一个包含键列的查询。
注意虽然所有五个数据提供者都在。NET Framework 类库有命令生成器类,但在定义它们的System.Data
命名空间中不存在任何类或接口。因此,如果您想了解更多关于命令构建器的知识,最好从您感兴趣的构建器的描述开始。System.Data.DataSet
类和System.Data.IDataAdapter
接口定义了命令构建器与之交互的底层组件,它们的文档提供了对命令构建器的约束的非正式规范。
并发
您已经看到,用数据集和数据适配器更新数据库相对简单。然而,我们把事情过于简单化了;您一直假设在处理断开连接的数据集时,没有对数据库进行其他更改。
假设两个不同的用户试图对数据集中的同一行进行冲突性的更改,然后试图将这些更改传播到数据库。会发生什么?数据库如何解决冲突?哪一行首先更新,第二个更新,还是根本不更新?答案不清楚。正如许多现实世界中的数据库问题一样,这完全取决于各种因素。然而,ADO.NET 提供了基本级别的并发控制,旨在防止更新异常。细节已经超出了本书的范围,但是下面是一个很好的概念性的开始。
基本上,数据集标记所有添加、修改和删除的行。如果某一行被传播到数据库,但在填充数据集后被其他人修改了,则该行的数据操作操作将被忽略。这种技术被称为开放式并发,本质上是数据适配器的工作。当调用Update
方法时,数据适配器试图协调所有的更改。这在用户很少争用相同数据的环境中工作得很好。
这种类型的并发不同于所谓的悲观并发,悲观并发在修改时锁定行(有时甚至在检索时)以避免冲突。大多数数据库管理器使用某种形式的锁定来保证数据完整性。
开放式并发的非连接处理对于成功的多层系统是必不可少的。鉴于数据库管理系统的悲观并发性,如何最有效地利用它是一个棘手的问题。现在不要担心这个问题,但是请记住,存在许多问题,您的应用越复杂,您就越有可能成为并发方面的专家。
使用数据集和 XML
XML 是. NET 中数据传输的基本媒介。事实上,XML 是 ADO.NET 的主要基础。数据集以 XML 格式在内部组织数据,并有多种方法来读写 XML。例如:
- You can use
ReadXmlSchema
andWriteXmlSchema
methods ofSystem.Data.DataSet
to import and export the data set structure as XML schema.- You can read the data (and optional mode) of the dataset from
ReadXml()
and useWriteXml().
to write it into an xml file, which is useful when exchanging data with another application or making a local copy of the dataset.- You can bind the dataset to an XML document (an instance of
System.Xml.XmlDataDocument
). Data set and data document are synchronized, so it can be modified by ADO.NET or XML operation.
让我们看看其中的一个例子:将数据集复制到 XML 文件中。
注意如果您不熟悉 XML,不要担心。ADO.NET 不需要任何详细的知识。当然,你知道的越多,你就越能清楚地理解正在发生的事情。
试试看:将数据集提取到 XML 文件中
您可以使用数据集的WriteXml
方法将数据集的内容和模式保存在一个 XML 文件中,或者使用WriteXml()
和WriteXmlSchema()
将它们保存在不同的文件中。WriteXml()
被重载,在这个例子中,我们将展示一个提取数据和模式的版本。
-
选择 DataSetandDataAdapter 项目,右键单击,然后选择添加 Windows 窗体。从打开的对话框中,确保选择了 Windows 窗体,并将
Form1.cs
重命名为WriteXML.cs
。单击“确定”将该表单添加到 DataSetandDataAdapter 项目中。 -
通过单击窗体的标题栏选择 WriteXML 窗体,并将 Size 属性的宽度设置为 289,高度设置为 124。
-
将 Button 控件拖到窗体上,并将其放在窗体的中央。选择此按钮控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 btnXML。
- 对于“位置”属性,将 X 设置为 74,Y 设置为 30。
- 对于“大小”属性,将“宽度”设置为 128,将“高度”设置为 23。
- 设置 Text 属性以生成 XML。
-
Now your WriteXML form in the Design view should look like Figure 15-15.
图 15-15。WriteXML 表单的设计视图
-
Double-click the empty surface of the
WriteXML.cs
form, and it will open the code editor window, showing theWriteXML _Load
event. Modify theWriteXML _Load
event to look like Listing 15-7.清单 15-7。T4
WriteXML.cs
`Using System.Data.SqlClient;
private void btnXML_Click(object sender, EventArgs e)
{
// Connection string
string connString = @" server=.\sql2012; database=AdventureWorks;
Integrated Security=true";// Query
string qry = @"select Name ,ProductNumber
from Production.Product";// Create connection
SqlConnection conn = new SqlConnection(connString);try
{
// Create Data Adapter
SqlDataAdapter da = new SqlDataAdapter();
da.SelectCommand = new SqlCommand(qry, conn);// Open connection
conn.Open();// Create and Fill Dataset
DataSet ds = new DataSet();
da.Fill(ds, "Production.Product");// Extract data set to XML file
ds.WriteXml(@"c:\productstable.xml");
MessageBox.Show("The XML file is Created");
}catch (Exception ex)
{
}finally
{
// Connection close
conn.Close();
}
}` -
To set the WriteXML form as the start-up form, modify the
Program.cs
statement.Application.Run(new PersistAddsBuilder());
表现为
Application.Run(new WriteXML());
构建项目,并通过按 Ctrl+F5 运行它。你的结果应该看起来像图 15-16 。
图 15-16。将数据表提取为 XML
-
Not much seems to have happened, but that’s because you wrote to a file rather than to the screen. Open
productstable.xml
(the path we saved this table to was c:; if you have changed the path, please refer to that) to see the XML. (One way in Visual Studio is to use File Open File.) Figure 15-17 shows the XML extracted for the first five product rows.图 15-17。提取为 XML 的数据表
提示默认情况下,提取的 XML 文档是纯文本文件。您可以在任何编辑器中打开productstable.xml
文件,甚至可以使用type
或more
命令从命令行查看它。
它是如何工作的
首先,我们需要创建一个 Select 查询来从数据库中提取数据。
// Query string qry = @"select Name ,ProductNumber from Production.Product";
接下来,我们需要创建数据集和数据适配器
// Create Data Adapter SqlDataAdapter da = new SqlDataAdapter(); da.SelectCommand = new SqlCommand(qry, conn);
创建数据集和数据适配器后,将数据集存储到 xml 文件中。
`// Create and Fill Dataset
DataSet ds = new DataSet();
da.Fill(ds, "Production.Product");
// Extract dataset to XML file
ds.WriteXml(@"c:\productstable.xml");`
注意,XML 只是将数据集映射为一个层次结构。第一个 XML 元素<NewDataSet>
是数据集名称(默认为NewDataSet
,因为您没有指定名称)。下一个元素<Production.Product>
使用数据表名称(因为只使用一个查询来填充数据集,所以只有一个数据表),它嵌套在数据集元素中。数据列元素<Name>
和<ProductNumber>
嵌套在该元素中。
每列的数据(以纯文本形式)出现在每个列元素的开始标记(例如,<Name>
)和结束标记(例如,</Name>
)之间。注意,<Production.Product>
元素代表单个行,而不是整个表格。因此,列元素包含在每行的开始标记<Production.Product>
和结束标记</Production.Product>
中。
如果您滚动到 XML 文件的底部,您会发现数据集的结束标记</NewDataSet>
。
了解类型化和非类型化数据集
数据集可以是有类型的或无类型的。到目前为止,您使用的数据集都是无类型的。他们是System.Data.DataSet
的实例。非类型化数据集没有内置架构。该模式只是隐式的。当您向数据集中添加表和列时,它会增长,但是这些对象是作为集合而不是 XML 模式元素公开的。但是,正如我们在上一节中提到的,您可以使用WriteXmlSchema
(或WriteXml
)显式导出非类型化数据集的模式。
类型化数据集是从System.Data.DataSet
派生的数据集,在声明数据集类时使用 XML 模式(通常在.xsd
文件中)。模式中的信息(表、列等)被提取出来,生成 C# 代码,并进行编译,因此新的数据集类是一个实际的。NET 类型与适当的对象和属性。
类型化或非类型化数据集都同样有效,但是类型化数据集更有效,并且可以使代码更简单。例如,使用非类型化数据集,您需要这样写:
ds.Tables[o].Rows[o]["CompanyName"];
获取 Customers 表的 CompanyName 列的值,假设该数据表是数据集中的第一个。使用类型化数据集,您可以将它的数据表和数据列作为类成员来访问。您可以用下面的代码替换前面的代码:
ds.Customers[o].CompanyName;
使代码更加直观。此外,Visual Studio 代码编辑器具有对类型化数据集的智能感知支持。
类型化数据集比非类型化数据集更有效,因为类型化数据集有一个已定义的模式,当它们被数据填充时,运行时类型识别和转换是不必要的,因为这已经在编译时处理了。每次加载结果集时,非类型化数据集都有更多的工作要做。
然而,类型化数据集并不总是最佳选择。如果您正在处理定义不明确、定义动态变化或者只是暂时感兴趣的数据,非类型化数据集的灵活性可能会超过类型化数据集的优势。
这一章已经够长了。因为我们不关心小样本程序的效率,所以我们不会使用类型化数据集,我们也不需要在这里介绍如何创建它们。
本书的重点是通过向你展示如何编写基本操作来解释 C# 如何与 ADO.NET 一起工作。如果你能自己编写代码,你就能理解 C# 在为你生成东西时做了什么,就像下一章关于使用 Windows 窗体一样。这对理解如何配置生成的组件和调试使用它们的应用是非常宝贵的。
虽然您可以自己编写一个.xsd
文件(或者用System.Data.DataSet.WriteXmlSchema()
为非类型化数据集导出一个 XSL 模式并修改它),然后使用xsd.exe
实用程序为类型化数据集创建一个类,但是这需要做大量的工作,容易出错,而且您很少(如果有的话)想要或需要这样做。
总结
在本章中,我们介绍了数据集和数据适配器的基础知识。数据集是具有数据表集合的数据的关系表示,每个数据表都具有数据行和数据列的集合。数据适配器是一个对象,它控制如何将数据加载到数据集(或数据表)中,以及如何将数据集数据的更改传播回数据源。
我们介绍了填充和访问数据集的基本技术,演示了如何过滤和排序数据表,并指出尽管数据集是独立于数据库的对象,但断开连接的操作并不是默认模式。
我们讨论了如何使用参数化 SQL 和数据适配器的UpdateCommand
、InsertCommand
和DeleteCommand
属性将数据修改传播回数据库,以及命令构建器如何简化单表更新。
我们简要地提到了并发的重要问题,然后介绍了 XML,ADO.NET 背后的基础技术。
最后,我们讨论了类型化和非类型化数据集。
既然你已经看到、理解并实践了 Windows 应用的 ADO.NET,在下一章中,我们将探索在 ASP.NET 应用中使用数据控制。*
十六、在 ASP.NET 应用中使用数据控件
本章重点介绍 web 应用开发背后的概念和 web 环境的关键组件,并向您展示如何在开发 web 应用时使用 ASP.NET 网站项目。(详细讨论 ASP.NET 框架超出了本书的范围。)
通常,一个功能完整的 web 项目需要在计算机上安装和配置 Internet 信息服务(IIS)。但是在本章中,为了简单起见,并帮助您理解使用数据控件的 ASP.NET 网站项目的基本原理,IIS 不是必需的。但是,如果您有它,您不需要卸载它。
在本章中,我将介绍以下内容:
- Understanding web functions
- Learn about ASP.NET and Web pages
- Understand Visual Studio 2012 website types
- Understand the layout of ASP.NET website.
- 了解 ASP.NET 网络应用的软件工程师
- Use the Repeater control
了解网络功能
当你在。NET 框架,您使用 ASP.NET Web 窗体来构建 Web 应用。Web 窗体技术在 ASP.NET 环境中工作,并接受来自任何。NET 兼容的语言,如 C#。
在深入研究 web 表单和学习如何开发 Web 应用之前,您需要了解是什么组件驱动了这项技术,以及这些组件如何服务于运行在 Web 上的各种应用。
基本上,有三个关键因素使所有的 web 应用发挥作用:web 服务器、web 浏览器和超文本传输协议(HTTP)。我们来看看他们的沟通过程:
- web 浏览器向 web 服务器发起对资源的请求。
- HTTP 向 web 服务器发送 GET 请求,web 服务器处理该请求。web 服务器发起响应;HTTP 将响应发送到 web 浏览器。
- 网络浏览器处理响应并在网页上显示结果。
- 用户输入数据或执行某些操作,迫使数据再次发送到 web 服务器。
- HTTP 将数据发送回 web 服务器,web 服务器处理这些数据。
- HTTP 将响应发送到 web 浏览器。
- 网络浏览器处理响应并在网页上显示结果。
现在,您已经对沟通过程有了大致的了解,让我们更仔细地看一下每个关键组件。
网络服务器
web 服务器负责通过 HTTP 接收和处理来自浏览器的所有请求。收到请求后,web 服务器将处理该请求并将响应返回给浏览器。在这之后,通常 web 服务器会关闭它与数据库的连接,并释放所有资源、打开的文件、网络连接等等,这些都成为 web 服务器上要处理的请求的一部分。
web 服务器完成所有这些数据、资源等的清理,以达到无状态。术语状态指的是在发送给服务器的请求和发送给浏览器的响应之间存储的数据。
当今的大多数网站都是作为应用运行的,并且由许多网页组成,一个网页上的数据通常负责将在下一个网页上显示的输出。在这种情况下,无状态就违背了这类网站的全部目的;因此,维护状态变得很重要。
为了有状态,web 服务器将通过预期来自 web 浏览器的额外请求,在一段时间内保持连接和资源活动。ASP.NET 提供了各种技术来应对无状态行为,比如 cookie、视图状态、查询字符串、会话和应用状态等等。
网络浏览器和 HTTP
web 浏览器是显示网页的客户端应用。web 浏览器使用 HTTP 向 web 服务器发送请求,然后 web 服务器使用用户想要查看或使用的数据来响应 web 浏览器或 web 客户端的请求。
HTTP 是一种通信协议,用于从 web 服务器请求网页,然后将响应发送到 web 浏览器。
了解 ASP.NET 和网页
所有人都可以去 ASP.NET。NET 开发者,因为它是微软的。NET 框架。ASP.NET 提供了一个 web 开发模型,通过使用任何。NET 兼容的语言,如 C#。ASP.NET 代码是编译的而不是解释的,它支持。NET 框架,如强类型、性能优化等等。在编译完代码后。NET CLR 将进一步将 ASP.NET 代码编译成本机代码,从而提高性能。
网页为您的 Web 应用提供用户界面。ASP.NET 为网页增加了可编程性。ASP.NET 使用代码实现应用逻辑,这些代码将被发送到服务器端执行。ASP.NET 的网页有以下特点:
- Based on Microsoft ASP.NET technology, the code running on the server dynamically generates web pages and outputs them to browsers or client devices.
- They are compatible with any supported language. NET common language runtime, including Microsoft Visual Basic and Microsoft Visual C#.
- They are based on Microsoft. NET framework. This provides all the benefits of the framework, including managed environment, type safety and inheritance.
网页由服务于用户请求的应用代码组成;为此,ASP.NET 将代码编译到程序集中。程序集是包含应用元数据的文件,文件扩展名为.dll
。代码编译后被翻译成一种独立于语言、独立于 CPU 的格式,称为微软中间语言 (MSIL),也称为中间语言 (IL)。运行网站时,MSIL 在。并被翻译成 CPU 特定的指令,供运行 web 应用的 PC 上的处理器使用。
了解 Visual Studio 2012 网站类型
Visual Studio 2012 提供了多种创建 web 项目或网站的方法。尽管网站只适用于 Internet 或 intranets,但 Visual Studio 2012 根据位置有三种类型,可以作为 web 开发人员工作的任何网站的基础。拥有这些选项的目的是它们真正简化了开发人员机器上的系统需求。
如果你曾经使用过经典的 ASP 应用(不是 ASP。回想一下 Visual Studio 6.0 的时代,那时开发人员需要使用 Internet 信息服务(IIS)来处理和测试 ASP web 应用。随着 Visual Studio 的发展,这个问题已经得到解决;现在,您可以在计算机上不安装 IIS 的情况下开发网站。
注意 IIS 是一个灵活、安全、易于管理的网络服务器,Windows 可以在网上托管任何东西。IIS 为其内部托管的 web 应用提供了完整的 web 管理工具。
通过访问文件新建网站,可以在 Visual Studio 2012 IDE 中构建一个新的网站项目。
让我们来看看 Visual Studio 2012 提供的网站类型。
注在下面的部分中,我在可用的项目模板列表中选择了 ASP.NET 空网站。以下部分(关于文件系统、HTTP 和 FTP 网站)适用于列表中所有可用的网站模板,如图图 16-1 所示。
文件系统网站
基于文件系统的网站像任何其他文件夹结构一样存储在计算机上。这种类型的网站的主要特点是,它使用一个非常轻量级的 ASP.NET 开发服务器,该服务器是 Visual Studio 2012 的一部分,因此它不要求 IIS 在开发人员的本地计算机上可用。
为了查看或测试文件系统网站,ASP.NET 开发服务器充当 web 服务器的角色。ASP.NET 开发服务器是一个在您的 Windows 计算机上本地运行的服务器,为 ASP.NET 网页提供服务,这使它适合于测试您的基于文件系统的 web 应用。
图 16-1 显示新建网站对话框,网站位置设置为文件系统;还要注意存储该网站的文件夹的路径:磁盘上的本地路径。
图 16-1。指定文件系统网站
FTP 网站
基于文件传输协议(FTP)的网站可以帮助您在本地计算机和远程网站之间管理和传输文件。FTP 网站提供了一个类似 Windows 资源管理器的界面,并公开了文件夹结构,文件、文档等保存在该结构中以供共享。
您可以访问 FTP 站点,将文件从远程 FTP 站点共享、传输或下载到本地计算机,也可以将文件上载到远程 FTP 站点。
若要查看或测试 FTP 网站,服务器计算机必须有一个可浏览的位置,即指向与 FTP 站点相同的文件的 HTTP URL。
图 16-2 显示了新网站对话框,其中网站位置设置为 FTP。
图 16-2。指定一个 FTP 网站
注意建立 FTP 站点需要用户的凭证通过。通常没有匿名的 FTP 站点;您应该使用ftp://user:pwd@ftpaddress:port
语法指定 FTP 地址。
http 网站
基于超文本传输协议(HTTP)的网站更适合于构建基于 web 的商业和企业产品。HTTP 网站需要开发人员本地计算机上的 IIS,因为它被配置为 IIS 虚拟目录中的应用。
注意 IIS Express 是包含在 Visual Studio 2012 中的 IIS 的简化版。IIS Express 服务器为 IIS Express 中的 web 应用带来了大量管理功能。
图 16-3 显示了网站位置设置为 HTTP 的新建网站对话框。
图 16-3。指定 HTTP 网站
了解 ASP.NET 网站的布局
当您选择 Visual Studio 提供的模板而不是 ASP.NET 空网站时,Visual Studio 2012 提供了大多数大型 Web 应用所需的许多表单和组件。为了使事情简单并帮助您保持专注,让我们使用一个空网站,向它添加一个 web 表单,并探索它的布局。
打开 Visual Studio 2012 IDE,选择文件新建网站。在新建网站对话框中,选择 ASP.NET 空网站作为项目模板,然后选择文件系统作为位置,Visual C# 作为语言,如图图 16-1 所示。在“Web location”下拉框旁边的文本框中,将路径修改为目录路径,这表示您将在文件系统上创建一个名为 Chapter16 的网站。单击确定。创建项目后,它将会打开,如图 16-4 中的所示。
图 16-4。空文件系统网站的布局
这个空的 web 应用加载时只有一个组件,那就是web.config
文件,下面将会解释。
web.config 文件
web.config
文件是一个 web 项目非常重要的文件。该文件通过提供一个中心位置来帮助开发人员,在该位置可以设置各种操作(如数据库连接、调试模式等)所需的所有设置,并且这些设置将在整个项目中应用和访问。
注意如果选择文件系统作为存储位置,web.config
文件不会自动添加到 ASP.NET 网站项目中。
web.config
文件的另一个特性是它很容易读写,就像记事本文件一样,因为它是 XML 格式的。
web.config
文件有许多预定义的标签,帮助您组织 web 应用的配置设置。需要记住的最重要的事情是,所有的标签都需要嵌入到父标签<Configuration> </Configuration>
中。
了解 ASP.NET Web Apps 的 Web UI
ASP.NET 应用或网站的 UI 是一个带有扩展名.aspx
的 web 表单或网页,这意味着活动服务器页面。
每个 web 表单或网页都将包含 HTML 格式的 UI 设计或表示,以及相关联的代码隐藏文件中的代码功能,扩展名为.cs
。因此,如果你的表单是Default.aspx
,这代表演示文稿,而Default.aspx.cs
文件代表代码。您将在本章的后面使用这些文件。与被称为经典 ASP 的 ASP 的旧版本不同,这种方法有助于将表示与逻辑分开,并使开发人员易于使用。
此外,如前几章所示,ASP。基于. NET 的应用能够包含多个页面。您将一个表单作为加载应用的默认页面,然后在页面之间移动。与 Windows 窗体不同,ASP.NET 使用不同的机制在确认事件后切换到另一个窗体。该机制被称为重定向,这在 ASP.NET 的Response
对象下可用。在本章的后面,你将看到这些最广泛使用的函数和对象是如何一起工作的。
试试看:使用 Web 表单
在本练习中,您将向项目中添加一个带有基本控件的 web 窗体,然后向控件中添加所需的功能。
-
导航到 Solution Explorer,选择 Chapter16 项目,右键单击它,并选择 Add New Item。
-
在“添加新项”对话框中,修改窗体名称以显示为 Login,并确保“语言”下拉列表显示 Visual C#。单击 OK 将登录表单添加到您的项目中。
-
Right-click the
Login.aspx
web form, and select the View Designer option; this will open theLogin.aspx
page in the Design view, where you can drag and drop controls onto the web page, as shown in Figure 16-5.图 16-5。新增网页的设计视图
-
In the Toolbox, from the Standard bar, drag a Label control (named Label1) onto the form inside the area titled div, as shown in Figure 16-5. Select the Label control, and if the Properties window is not shown already, press F4. Go to the properties of the Label control, and set its Id property to lblUserName and its Text property to Enter User Name.
注意就像 Windows 窗体和 Windows 控件的 Name 属性一样,ASP.NET 有一个控件名称的 Id 属性。
-
将一个 TextBox 控件(Id 为 TextBox1)拖到窗体上,并将其放在 Label 控件旁边。选择 TextBox,并将其 Id 属性更改为 txtUserName。
-
Drag a Button control (named Button1) onto the form, and place it next to the TextBox control. Select the Button control, and set its Id property to btnLogin and its Text property to Login. All three controls should appear in one line, as shown in Figure 16-6.
图 16-6。添加控件后登录表单的设计视图
-
现在,让我们像在步骤 2 中那样向应用添加一个 web 表单,并将该表单命名为 WebDataForm。
-
Your web project will now have two forms, Login and WebDataForm, as shown in Figure 16-7.
图 16-7。展示两个 web 表单的项目
-
Now let’s add the functionality. Open
Login.aspx
in the Design view by selecting it, right-clicking, and choosing View Designer. It will open as shown in Figure 16-6. Double-click the Login button, which will open the blank template for thebtnLogin_click
event. Modify thebtnLogin_click
event to look like Listing 16-1.清单 16-1。T4
Login.aspx.cs
protected void btnLogin_Click(object sender, EventArgs e) { if (txtUserName.Text == "agarwal") { Response.Redirect("WebDataForm.aspx"); } else { Response.Write("Invalid User Name, please try again!"); } }
-
Build the project and run the application by pressing Ctrl+F5. The
Login.aspx
form will appear in the browser. Enter a name in the provided text box, and click the Login button. If you will enter any other string than Agarwal, you should receive an “Invalid User Name, please try again!”error similar to that shown in Figure 16-8.
***图 16-8。**运行并测试登录表单*
- 理想情况下,进入阿加瓦尔,会被带到
WebDataForm.aspx
。但是在我们尝试之前,让我们首先向 WebDataForm 表单添加功能。
它是如何工作的
首先,验证您是否有一个成功的日志。
if (txtUserName.Text == "agarwal")
一旦这个条件为真,您希望用户能够自动导航到 WebDataForm。为此,您使用了 ASP。NET 的Response
对象和它的Redirect
函数,它接受你想重定向到的页面名称。
Response.Redirect("WebDataForm.aspx");
如果登录不正确,我们还会向用户显示一条错误消息。为了显示这个错误,我们使用了Response
对象的Write
方法,该方法获取您想要在页面上显示的字符串。
Response.Write("Invalid User Name, please try again!");
使用中继器控制
Repeater 控件是一个 ASP。特定于网络的控制;换句话说,它不存在于 Windows 窗体中。Repeater 控件用作容器控件,它允许您创建自定义列表以在网页上显示数据。Repeater 控件的另一个特性是它没有自己的内置呈现,这意味着 web 开发人员必须借助模板为 Repeater 控件提供布局。Repeater 控件能够使用各种类型模板,如 ItemTemplate、HeaderTemplate、SeperatorTemplate 和 FooterTemplate。
当网页加载 Repeater 控件时,Repeater 控件遍历数据源中的数据行,并为找到的每条记录呈现数据。
与 ListBox、GridView、TextBox 等在前面章节中用于数据库的控件不同,Repeater 控件没有默认外观;因此,它非常灵活和强大,可用于任何类型的列表,包括表格布局、逗号分隔的列表或 XML 格式的列表。
现在你已经有一半的应用准备好并开始工作了,如图 16-8 所示。下一个任务是向 WebDataForm 添加功能;为此,您将使用 ASP。NET 特定的数据控件称为 Repeater 控件,如前所述。
试试看:使用中继器控制
在本练习中,您将向 web 窗体添加一个 Repeater 控件,然后编写所需的功能,用数据库中的数据填充该控件。
-
导航到解决方案资源管理器。右键单击
WebDataForm.aspx
web 表单,选择视图设计器选项;这将在设计视图中打开WebDataForm.aspx
页面,在这里您可以将控件拖放到 web 页面上。 -
导航到工具箱,展开 Data 选项卡,然后将 Repeater 控件拖到
WebDataForm.aspx
,将其定位在表单的左上角。 -
Now press F4 to open the Properties window. Select the Repeater control, navigate to the Properties window, and set the Id property to RepData. Your WebDataForm will now look like Figure 16-9.
图 16-9。带有 Repeater 控件的 WebDataForm 的设计视图
-
现在您需要将数据访问代码绑定到 WebDataForm。像在 Windows 窗体应用中一样,双击 WebDataForm 的空白图面。这将打开
Page_Load
事件。 -
Modify the
Page_Load
event ofWebDataForm.aspx.cs
to look like Listing 16-2.清单 16-2。T3
WebDataForm.aspx.cs
`using System.Data;
using System.Data.SqlClientprotected void Page_Load(object sender, EventArgs e)
{
// Connection string
string connString = @"server=.\sql2012;database=AdventureWorks;
Integrated Security=true";//Query
string query = @" SELECT Title, BirthDate
FROM HumanResources.Employee";DataTable dt = new DataTable();
try
{
SqlDataAdapter da = new SqlDataAdapter(query, connString);
da.Fill(dt);
}
{
Response.Write(ex.Message.ToString());
}//Populate Repeater control with data
RepData.DataSource = dt;
RepData.DataBind();
}` -
通过按 Ctrl+Shift+B 保存项目并构建代码。应该会成功构建。
-
因为添加了数据访问代码,所以需要启用 Repeater 控件来访问这些数据。为此,您需要调整 WebDataForm 的 HTML。
-
Close
WebDataForm.aspx.cs
if open. Go to Solution Explorer, right-click WebDataForm, and select View Markup; you should see the HTML ofWebDataForm.aspx
, as shown in Figure 16-10.图 16-10。web data form 的标记视图
-
Modify the
<body>
HTML code segment to look like Listing 16-3.清单 16-3。 HTML 代码为
WebDataForm.aspx
`
`
-
保存项目,并按 Ctrl+Shift+B 构建项目。成功构建后,按 Ctrl+F5 运行 web 应用。
项目将加载Login.aspx
,如图图 16-8 ,但这次你将输入正确的用户名 agarwal ,它被硬编码在登录按钮的if
条件中。输入用户名后,点击登录按钮;你将被重定向到WebDataForm.aspx
,如图图 16-11 所示。
图 16-11。运行和测试 WebDataForm
它是如何工作的
首先,您必须向WebDataForm.aspx.cs
添加数据访问代码,这将构建查询,然后加载数据表。
`//Query
string query = @" SELECT Title, BirthDate
FROM HumanResources.Employee";
DataTable dt = new DataTable();`
构建数据表后,使用这个DataTable
对象填充DataAdapter
对象。
try { SqlDataAdapter da = new SqlDataAdapter(query, connString); da.Fill(dt); }
接下来,用数据填充 Repeater 控件。
//Populate Repeater control with data RepData.DataSource = dt; RepData.DataBind();
总结
在这一章中,你学习了一些关于叫做 ASP.NET 的网络技术的基础知识。您还了解了可以在 Visual Studio 2012 中创建的各种类型的网站。您了解了如何处理 web 页面,然后使用数据访问代码和 Repeater 控件。在下一章,你将学习如何处理文本和二进制数据。
十七、使用文本和二进制数据
有些类型的数据有特殊的格式,非常大,或者大小变化很大。在这里,我将向您展示处理文本和二进制数据的技术。在本章中,我将介绍以下内容:
- Understanding SQL Server Text and Binary Data Types
- Storing images in a database
- Retrieve images from the database
- Handling text data
我还将展示用于在tempdb
数据库中创建表的 T-SQL,它旨在保存任何临时表。我将首先介绍哪些数据类型支持这些类型的数据。
了解 SQL Server 文本和二进制数据类型
SQL Server 提供了类型CHAR
、NCHAR
、VARCHAR
、NVARCHAR
、BINARY
和VARBINARY
,用于处理相当小的文本和二进制数据。您可以将这些用于最大 8,000 字节的文本(字符)数据(Unicode 数据为 4,000 字节,NCHAR
和NVARCHAR
,每个字符使用 2 个字节)。
对于较大的数据,SQL Server 2012 称之为大值数据类型,您应该使用VARCHAR(MAX)
、NVARCHAR(MAX)
和VARBINARY(MAX)
数据类型。VARCHAR(MAX)
用于非 Unicode 文本,NVARCHAR(MAX)
用于 Unicode 文本,VARBINARY(MAX)
用于图像和其他二进制数据。
警告在 SQL Server 2000 中,使用NTEXT
、TEXT
和IMAGE
数据类型存储大量数据。这些数据类型已被弃用,并且在 SQL Server 的较新版本中已被删除。如果您使用遗留应用,您应该考虑将NTEXT
、TEXT
和IMAGE
分别转换为NVARCHAR(MAX)
、VARCHAR(MAX)
和VARBINARY(MAX)
。然而,System.Data.SqlDbType
枚举还不包括这些数据类型的成员,所以我们对列数据类型使用VARCHAR(MAX)
和VARBINARY(MAX)
,但是在为命令参数指定数据类型时使用Text
和Image
。
使用这些数据类型的替代方法是不将数据本身存储在数据库中,而是定义一个包含指向数据实际存储位置的路径的列。这对于访问大量数据更有效,并且通过将需求转移到文件服务器可以节省数据库服务器上的资源。它确实需要更复杂的协调,并且有可能导致数据库和数据文件不同步。我不会在这一章使用这种技术。
提示如果您正在使用一个不能超过 4GB 的 SQL Server Express 数据库,或者如果您不希望您的数据库存储大量信息并超过一定的大小限制,那么对于非常大的文本和图像数据,使用 SQL Server 提供的文本和二进制数据类型可能是您唯一的选择。
在 C# 程序中,二进制数据类型映射到字节数组(byte[]
),字符数据类型映射到字符串或字符数组(char[]
)。
注意 DB2、MySQL、Oracle、SQL 标准调用此类数据类型大型对象(LOBs);具体来说,它们是二进制大对象(BLOBs)和字符大对象(CLOBs)。但是,与许多数据库术语一样,BLOB 最初是否是任何事物的首字母缩略词仍有争议。不用说,它一直隐含着一种可以处理大量(无定形)数据的数据类型,SQL Server 文档使用 BLOB 作为大数据和数据类型的通称。
将图像存储在数据库中
让我们首先创建一个用于存储图像的数据库表,然后将一些图像加载到其中。我们将使用小图像,但使用VARBINARY(MAX)
来存储它们。在示例中,我将演示如何使用代码目录路径C:\VidyaVrat\C#2012 and SQL 2012\Chapter17\Code
中的图像;您可以使用计算机上一些图像所在位置的路径。
试试看:从文件加载图像二进制数据
在本例中,您将编写一个程序来创建一个数据库表,然后在其中加载和存储图像。
-
创建一个名为 Chapter17 的新 Windows 窗体应用项目。当解决方案资源管理器打开时,保存解决方案。
-
将第十七章项目重命名为文本和二进制数据。将
Form1.cs
文件重命名为LoadImages.cs
。通过单击窗体的标题栏选择 LoadImages 窗体,并将 Size 属性的宽度设置为 439,高度设置为 178。 -
将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtLoadImages。
- 对于 Location 属性,将 X 设置为 12,将 Y 设置为 12。
- 将 Multiline 属性设置为 True。
- 对于“大小”属性,将“宽度”设置为 401,将“高度”设置为 117。
- 将文本属性留空。
-
Now your LoadImages form in the Design view should look like Figure 17-1.
图 17-1。LoadImages 表单的设计视图
-
Navigate to Solution Explorer, select the
LoadImages.cs
form, right-click, and select View Code; this will take you to the code editor window. Add the code toLoadImages.cs
shown in Listing 17-1.清单 17-1。T4
LoadImages.cs
`using System.Data;
using System.Data.SqlClient;
using System.IO;
// change this path to the location of image in your computer
string imageFileLocation = @"C:\VidyaVrat\C#2012 and SQL 2012\Chapter17\Code";string imageFilePrefix = "SpaceNeedle";
string imageFileType = ".jpg";int numberImageFiles = 1;
int maxImageSize = 10000;SqlConnection conn = null;
SqlCommand cmd = null;private void LoadImages_Load(object sender, EventArgs e)
{
try
{
// Create connection
conn = new SqlConnection(@"server = .\sql2012;integrated security = true;
database = SQL2012Db");
conn.Open();//Create command
cmd = new SqlCommand();
cmd.Connection = conn;// Create table
CreateImageTable();// Prepare insert
PrepareInsertImages();// Loop for Inserting images
for (int i = 1; i <= numberImageFiles; i++)
{
ExecuteInsertImages(i);
}
}catch (SqlException ex)
{
MessageBox.Show(ex.Message + ex.StackTrace);
}finally
{
// Close connection
conn.Close();
txtLoadImages.AppendText(Environment.NewLine);
txtLoadImages.AppendText("Connection Closed.");
}
}private void ExecuteCommand(string cmdText)
{
int cmdResult;
cmd.CommandText = cmdText;
//txtLoad.AppendText("Executing command:\n");
// txtLoad.AppendText(cmd.CommandText);
//txtLoad.AppendText(Environment.NewLine);
cmdResult = cmd.ExecuteNonQuery();
}private void CreateImageTable()
{
ExecuteCommand(@"if exists
(select * from
INFORMATION_SCHEMA.TABLES
where TABLE_NAME = 'ImageTable')create table ImageTable
(
ImageFile nvarchar(20),
ImageData varbinary(max)
)");
}private void PrepareInsertImages()
{
cmd.CommandText = @"insert into ImageTable
values (@ImageFile, @ImageData)";cmd.Parameters.Add("@imagefile", SqlDbType.NVarChar, 20);
cmd.Parameters.Add("@imagedata", SqlDbType.Image, 1000000);cmd.Prepare();
}private void ExecuteInsertImages(int imageFileNumber)
{
string imageFileName = null;
byte[] imageImageData = null;imageFileName = imageFilePrefix + imageFileNumber.ToString() + imageFileType;
imageImageData = LoadImageFile(imageFileName, imageFileLocation, maxImageSize);cmd.Parameters["@ImageFile"].Value = imageFileName;
cmd.Parameters["@ImageData"].Value = imageImageData;ExecuteCommand(cmd.CommandText);
}private byte[] LoadImageFile(string fileName,string fileLocation,int maxImageSize)
{
byte[] imagebytes = null;
string fullpath = fileLocation + fileName;
txtLoadImages.AppendText("Loading File:");
txtLoadImages.AppendText(Environment.NewLine);
txtLoadImages.AppendText(fullpath);
FileStream fs = new FileStream(fullpath, FileMode.Open, FileAccess.Read);
BinaryReader br = new BinaryReader(fs);
imagebytes = br.ReadBytes(maxImageSize);
txtLoadImages.AppendText(Environment.NewLine);txtLoadImages.AppendText("Imagebytes has length " +
imagebytes.GetLength(0).ToString() + "bytes.");return imagebytes;
}` -
Build the project, and run the program by pressing Ctrl+F5. You should see output similar to that in Figure 17-2. It shows the information for loading an image into the database that you have on your computer at the specified location, and it shows the size of each image.
图 17-2。加载图像数据
-
To see the image you have inserted into the database, open SQL Server Management Studio and run a
SELECT
query on the image table you have created in theSQL2012Db
database, which was created in Chapter 3 (see Figure 17-3).图 17-3。查看图像数据
它是如何工作的
在LoadImages.cs
中,除了创建和打开一个连接之外,你要做三件主要的事情。
你连接到SQL2012Db
,你在第三章中创建的数据库。
` // Create connection
conn = new SqlConnection(@"server = .\sql2012;integrated security = true;
database = SQL2012Db");
// Open connection
conn.Open();`
您调用一个私有的类级方法来创建一个保存图像的表。
// Create table CreatelmageTable();
您调用一个私有的类级方法来准备一个命令(是的,您最终准备了一个命令,因为您期望多次运行它)来插入图像。
// Prepare insert Preparelnsertlmages();
然后循环遍历图像文件,并将它们插入到表格中。
// Loop for Inserting images for (int i = 1; i <= loader.numberlmageFiles; i++) { ExecutelnsertImages(i); }
因为可能已经有一个表了,所以您必须先删除这个表(如果它存在的话),然后再创建它。该步骤在应用每次运行时重复。
当您创建一个包含图像文件名称和图像的简单表格时,您为imagedata
列使用了VARBINARY(MAX)
数据类型。
` private void CreateImageTable()
{
ExecuteCommand(@"if exists
(select * from
INFORMATION_SCHEMA.TABLES
where TABLE_NAME = 'ImageTable')
drop table ImageTable
create table ImageTable
(
ImageFile nvarchar(20),
ImageData varbinary(max)
)");
}`
但是当您配置INSERT
命令时,您使用了SqlDbType
枚举的Image
成员,因为没有VARBINARY(MAX)
数据类型的成员。您为两种可变长度数据类型都指定了长度,因为如果不这样做就无法准备命令。
private void PrepareInsertImages() { cmd.CommandText = @"insert into ImageTable values (@ImageFile, @ImageData)";
` cmd.Parameters.Add("@imagefile", SqlDbType.NVarChar, 20);
cmd.Parameters.Add("@imagedata", SqlDbType.Image, 1000000);
cmd.Prepare();
}`
Executelnsertlmages
方法接受一个整数作为图像文件名的后缀,调用LoadlmageFile
获得包含图像的字节数组,将文件名和图像分配给它们对应的命令参数,然后执行命令插入图像。
` private void ExecuteInsertImages(int imageFileNumber)
{
string imageFileName = null;
byte[] imageImageData = null;
imageFileName = imageFilePrefix + imageFileNumber.ToString() + imageFileType;
imageImageData = LoadImageFile(imageFileName, imageFileLocation, maxImageSize);
cmd.Parameters["@ImageFile"].Value = imageFileName;
cmd.Parameters["@ImageData"].Value = imageImageData;
ExecuteCommand(cmd.CommandText);
}`
LoadlmageFile
方法读取图像文件,显示文件名和文件中的字节数,并以字节数组的形式返回图像。
`
private byte[] LoadImageFile(string fileName,string fileLocation,int maxImageSize)
{
byte[] imagebytes = null;
string fullpath = fileLocation + fileName;
txtLoadImages.AppendText("Loading File:");
txtLoadImages.AppendText(Environment.NewLine);
txtLoadImages.AppendText(fullpath);
FileStream fs = new FileStream(fullpath, FileMode.Open, FileAccess.Read);
BinaryReader br = new BinaryReader(fs);
imagebytes = br.ReadBytes(maxImageSize);
txtLoadImages.AppendText(Environment.NewLine);
txtLoadImages.AppendText("Imagebytes has length " +
imagebytes.GetLength(0).ToString() + " bytes.");
return imagebytes;
}`
从数据库中检索图像
现在您已经存储了一些图像,您将看到如何使用 Windows 窗体应用检索和显示它们。
试试看:显示储存的图像
要显示您存储的图像,请按照以下步骤操作:
-
选择文本和二进制数据项目,右键单击,并选择添加窗口窗体。在打开的对话框中,确保选中 Windows Form,并将
Form1.cs
重命名为DisplayImages.cs
;单击“确定”将该表单添加到文本和二进制数据项目中。 -
选择 DisplayImages 窗体,并将 Size 属性的宽度设置为 330,高度设置为 332。
-
将 Label 控件拖到窗体上,并将其放在窗体的左上角。选择此标签控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 lblImageName。
- 对于 Location 属性,将 X 设置为 12,将 Y 设置为 22。
- 对于文本属性,设置为图像名称。
-
将 TextBox 控件拖到窗体上,并将其放置在 Label 控件旁边。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtImageName。
- 对于“位置”属性,将 X 设置为 85,Y 设置为 22。
- 将文本属性留空。
-
将 Button 控件拖到窗体上,并将其放置在 TextBox 控件的旁边。选择此按钮控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 btnShowImage。
- 对于 Location 属性,将 X 设置为 277,将 Y 设置为 22。
- 设置 Text 属性以显示图像。
-
将 PictureBox 控件拖到窗体上,并将其放在窗体的中央。选择此 PictureBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 ptbImage。
- 对于位置属性,将 X 设置为 44,将 Y 设置为 61。
- 对于 Size 属性,将 Height 设置为 220,Width 设置为 221。
-
Now your DisplayImages form in the Design view should look like Figure 17-4.
图 17-4。显示图像表单的设计视图
-
Add a new class named
Images
to this Windows Form project. To add this, select the Text and Binary Data project, right-click, select Class… in the Add New Item dialog, name the classImages.cs
, and click Add to have it listed under your project. Once it’s added, replace the code inImages.cs
with the code in Listing 17-2.清单 17-2。我
mages.cs
`using System.Data.SqlClient;
using System.IO;
using System.Drawing;namespace Text_and_Binary_Data
{
public class Images
{
string imageFilename = null;
byte[] imageBytes = null;SqlConnection imageConnection = null;
SqlCommand imageCommand = null;
SqlDataReader imageReader = null;// Constructor
public Images()
{
integrated security = true; initial catalog = SQL2012db;");imageCommand = new SqlCommand(@" select imagefile,imagedata
from ImageTable", imageConnection);// Open connection and create data reader
imageConnection.Open();
imageReader = imageCommand.ExecuteReader();
}public Bitmap GetImage()
{
MemoryStream ms = new MemoryStream(imageBytes);
Bitmap bmap = new Bitmap(ms);return bmap;
}public string GetFilename()
{
return imageFilename;
}public bool GetRow()
{
if (imageReader.Read())
{
imageFilename = (string)imageReader.GetValue(0);
imageBytes = (byte[])imageReader.GetValue(1);return true;
}
else
{
return false;
}
}public void EndImages()
{
// Close the reader and the connection.
imageReader.Close();
imageConnection.Close();
}
}
}` -
Next, insert the code in Listing 18-3 into
Displaylmages.cs
in the constructor. You can accessDisplaylmages.cs
by right-clickingDisplaylmages.cs
and selecting View Code, which will take you to the Code view.清单 17-3。初始化
DisplayImages
构造器中的图像显示` public DisplayImages()
{
InitializeComponent();if (images.GetRow())
{
this.txtImageName.Text = images.GetFilename();
this.ptbImage.Image = (Image)images.GetImage();
}
else
{
this.txtImageName.Text = "DONE";
this.ptbImage.Image = null;
}
}` -
Insert the code in Listing 18-3 into the btnShowImage button’s
Click
event handler. You can access thebtnShowImage_Click
event handler by navigating to the Design view of the DisplayImages form and double-clicking the btnShowImage Button control.
***列表 17-4。** `btnShowImage_Click`事件中的`DisplayImages.cs`*
` private void btnShowImage_Click(object sender, EventArgs e)
{
if (images.GetRow())
{
this.txtImageName.Text = images.GetFilename();
this.ptbImage.Image = (Image)images.GetImage();
}
else
{
this.txtImageName.Text = "DONE";
this.ptbImage.Image = null;
}
}`
- To set the TypedAccessor form as the start-up form, modify the
Program.cs
statement.Application.Run(new LoadImages());
表现为
`Application.Run(new DisplayImages());`
构建项目,按 Ctrl+F5 运行它。你应该在图 17-5 中看到结果。
图 17-5。显示图像
它是如何工作的
您声明一个类型Images
来访问数据库,并为表单组件提供方法来轻松获取和显示图像。在它的构造函数中,您连接到数据库并创建一个数据读取器来处理检索您之前存储的所有图像的查询结果集。
` // Constructor
public Images()
{
imageConnection = new SqlConnection(@"data source = .\sql2012;
integrated security = true; initial catalog = SQL2012db;");
imageCommand = new SqlCommand(@" select imagefile,imagedata
from ImageTable", imageConnection);
// Open connection and create data reader
imageConnection.Open();
imageReader = imageCommand.ExecuteReader();
}`
当表单初始化时,新代码创建一个Images
实例,用GetRow()
查找图像,如果找到一个,分别用GetFilename
和Getlmage
方法将文件名和图像分配给文本框和图片框。
images = new Images(); if (images.GetRow()) { this.textBoxl.Text = images.GetFilename(); this.pictureBoxl.Image = (Image)images.GetImage(); } else { this.textBoxl.Text = "DONE"; this.pictureBoxl.Image = null; }
您在 Next 按钮的click
事件处理程序中使用相同的if
语句来查找下一个图像。如果没有找到,您在文本框中显示单词 DONE 。
图像以字节数组的形式从数据库返回。PictureBox 控件的 Image 属性可以是Bitmap
、Icon
或Metafile
(所有Image
的派生类)。Bitmap
支持多种格式,包括 BMP、GIF、JPEG。这里显示的getImage
方法返回一个Bitmap
对象:
`public Bitmap GetImage()
{
MemoryStream ms = new MemoryStream(imageBytes);
Bitmap bmap = new Bitmap(ms);
return bmap;
}`
Bitmap
的构造函数不接受字节数组,但它会接受一个MemoryStream
(它实际上是一个文件的内存表示),而MemoryStream
有一个接受字节数组的构造函数。因此,从字节数组创建一个内存流,然后从内存流创建一个位图。
处理文本数据
除了用于数据库列的数据类型之外,处理文本类似于处理图像。
试试看:从文件中加载文本数据
要从文件加载文本数据,请按照下列步骤操作:
-
选择文本和二进制数据项目,右键单击,并选择添加窗口窗体。在打开的对话框中,确保选中 Windows Form,并将
Form1.cs
重命名为LoadText.cs
;单击“确定”将该表单添加到文本和二进制数据项目中。 -
选择 LoadText 表单,并将 Size 属性的宽度设置为 496,高度设置为 196。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtLoadText。
- 对于 Location 属性,将 X 设置为 12,将 Y 设置为 12。
- 对于“大小”属性,将“宽度”设置为 456,将“高度”设置为 135。
- 将文本属性留空。
-
Now your LoadText form in the Design view should look like Figure 17-6.
图 17-6。LoadText 表单的设计视图
-
Next, insert the code in Listing 17-5 into
LoadText.cs
. You can accessLoadText.cs
by right-clickingLoadText.cs
and selecting View Code, which will take you to the Code view.清单 17-5。T4
LoadText.cs
`using System.Data;
using System.Data.SqlClient;
using System.IOstatic string fileName =
@"C:\VidyaVrat\C#2012 and SQL 2012\Chapter17\Code\Text and Binary Data\LoadText.cs";SqlConnection conn = null;
SqlCommand cmd = null;public LoadText()
{
InitializeComponent();
}private void LoadText_Load(object sender, EventArgs e)
{
try
{
// Create connection
conn = new SqlConnection(@"data source = .\sql2012;
integrated security = true;initial catalog = SQL2012Db;");//Create command
cmd.Connection = conn;// Open connection
conn.Open();// Create table
CreateTextTable();// Prepare insert command
PrepareInsertTextFile();// Load text file
ExecuteInsertTextFile(fileName);txtLoadText.AppendText("Loaded "+fileName+" into TextTable.\n");
}
catch (SqlException ex)
{
MessageBox.Show(ex.ToString());
}
finally
{
// Close connection
conn.Close();
}
}private void CreateTextTable()
{
ExecuteCommand(@"if exists(select *
from INFORMATION_SCHEMA.TABLES
where TABLE_NAME = 'TextTable')drop table TextTable ");
ExecuteCommand(@"create table TextTable
(
TextFile varchar(255),
TextData varchar(max)
)");
}private void ExecuteCommand(string commandText)
{
cmd.CommandText = commandText;
cmd.ExecuteNonQuery();
txtLoadText.AppendText("\n");
}private void PrepareInsertTextFile()
cmd.CommandText = @"insert into TextTable
values (@textfile, @textdata)";cmd.Parameters.Add("@textfile", SqlDbType.NVarChar, 30);
cmd.Parameters.Add("@textdata", SqlDbType.Text, 1000000);
}private void ExecuteInsertTextFile(string textFile)
{
string textData = GetTextFile(textFile);
cmd.Parameters["@textfile"].Value = textFile;
cmd.Parameters["@textdata"].Value = textData;
ExecuteCommand(cmd.CommandText);
}private string GetTextFile(string textFile)
{
string textBytes = null;
txtLoadText.AppendText("Loading File: " + textFile);FileStream fs = new FileStream(textFile, FileMode.Open, FileAccess.Read);
StreamReader sr = new StreamReader(fs);
textBytes = sr.ReadToEnd();txtLoadText.AppendText("TextBytes has length" + textBytes.Length + " bytes.\n");
return textBytes;
}` -
To set the LoadText form as the start-up form, modify the
Program.cs
statement.Application.Run(new DisplayImages());
表现为
Application.Run(new LoadText());
构建项目,并通过按 Ctrl+F5 运行它。您应该在图 17-7 中看到结果。
图 17-7。将文本文件加载到表格中
它是如何工作的
您只需加载 LoadText 程序的源代码。
// change this path to the location of text in your computer static string fileName = @"C:\VidyaVrat\C#2012 and SQL 2012\Chapter17\Code\Text and Binary Data\LoadText.cs";
使用 insert starement 和 add 参数设置 CommandText:
` cmd.CommandText = @"insert into TextTable
values (@textfile, @textdata)";
cmd.Parameters.Add("@textfile", SqlDbType.NVarChar, 30);
cmd.Parameters.Add("@textdata", SqlDbType.Text, 1000000);`
执行命令删除现有表并创建一个新表:
`ExecuteCommand(@"if exists(select *
from INFORMATION_SCHEMA.TABLES
where TABLE_NAME = 'TextTable')
drop table TextTable ");
ExecuteCommand(@"create table TextTable
(
TextFile varchar(255),
TextData varchar(max))"
);`
请注意,您首先检查该表是否存在。如果是这样,您可以删除它,以便重新创建它。
注意information_schema.tables
视图(一个命名查询)与同名的 SQL 标准INFORMATION_SCHEMA
视图兼容。它将您可以看到的表限制为您可以访问的表。微软建议您使用新的目录视图来获取 SQL Server 2012 中的数据库元数据,SQL Server 本身在内部使用它们。这个查询的目录视图应该是sys.tables
,列名应该是 name。我们在这里使用了INFORMATION SCHEMA
视图,因为您可能仍然会经常看到它。
GetTextFile
使用的是StreamReader
(从System.IO
派生而来),而不是您用于图像的BinaryReader
。TextReader
)将文件的内容读入一个string
。
`private string GetTextFile(string textFile)
{
string textBytes = null;
txtLoadText.AppendText("Loading File: " + textFile);
FileStream fs = new FileStream(textFile, FileMode.Open, FileAccess.Read);
StreamReader sr = new StreamReader(fs);
textBytes = sr.ReadToEnd();
txtLoadText.AppendText("TextBytes has length" + textBytes.Length + " bytes.\n");
return textBytes;
}`
否则,处理逻辑基本上与您在整本书中多次看到的一样:打开一个连接,访问一个数据库,然后关闭连接。
现在让我们检索您刚刚存储的文本。
从文本列中检索数据
从文本列中检索数据就像从较小的字符数据类型中检索数据一样。现在,您将编写一个简单的控制台程序来看看这是如何工作的。
试试看:检索文本数据
要从文本列中检索数据,请按照下列步骤操作:
-
选择文本和二进制数据项目,右键单击,并选择添加窗口窗体。在打开的对话框中,确保选择了 Windows 窗体,并将
Form1.cs
重命名为RetrieveText.cs
;单击“确定”将该表单添加到文本和二进制数据项目中。 -
选择 RetrieveText 表单,并将 Size 属性的宽度设置为 438,高度设置为 334。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtRetrieveText。
- 对于 Location 属性,将 X 设置为 12,将 Y 设置为 12。
- 对于“大小”属性,将“宽度”设置为 401,将“高度”设置为 269。
- 将文本属性留空。
-
Now your LoadText form in the Design view should look like Figure 17-8.
图 17-8。检索文本表单的设计视图
-
Next, insert the code in Listing 17-6 into
RetrieveText.cs
. You can accessRetrieveText.cs
by right-clickingRetrieveText.cs
and selecting View Code, which will take you to the Code view.清单 17-6。 RetrieveText.cs
`using System.Data;
using System.Data.SqlClient;string textFile = null;
char[] textChars = null;
SqlConnection conn = null;
SqlCommand cmd = null;
SqlDataReader dr = null;public RetrieveText()
{
InitializeComponent();// Create connection
conn = new SqlConnection(@"data source = .\sql2012;integrated security = true;
initial catalog = SQL2012Db;");// Create command
cmd = new SqlCommand(@"select textfile, textdata
from TextTable", conn);
conn.Open();// Create data reader
dr = cmd.ExecuteReader();
}public void RetrieveText_Load(object sender, EventArgs e)
{
try
{
while (GetRow() == true)
{
txtRetrieveText.AppendText(textFile);
txtRetrieveText.AppendText("\n============================\n");
}
}
catch (SqlException ex)
{
Console.WriteLine(ex.ToString());
}finally
{
// Close the reader and the connection.
dr.Close();
conn.Close();
}
}public bool GetRow()
{
long textSize;
int bufferSize = 100;
long charsRead;
textChars = new Char[bufferSize];if (dr.Read())
{
// Get file name
textFile = dr.GetString(0);
txtRetrieveText.AppendText("------ start of file\n");
txtRetrieveText.AppendText(textFile);
txtRetrieveText.AppendText("\n");
textSize = dr.GetChars(1, 0, null, 0, 0);
txtRetrieveText.AppendText("--- size of text: " + textSize + " characters ---
");txtRetrieveText.AppendText("\n--- first 100 characters in text -----\n");
charsRead = dr.GetChars(1, 0, textChars, 0, 100);
txtRetrieveText.AppendText(new String(textChars));
txtRetrieveText.AppendText("\n");
txtRetrieveText.AppendText("\n--- last 100 characters in text -----\n");
charsRead = dr.GetChars(1, textSize - 100, textChars, 0, 100);
txtRetrieveText.AppendText(new String(textChars));return true;
}
else
{
return false;
}
}` -
To set the LoadText form as the start-up form, modify the
Program.cs
statement.Application.Run(new LoadText());
表现为
Application.Run(new RetrieveText());
构建项目,并通过按 Ctrl+F5 运行它。您应该在图 17-9 中看到结果。
图 17-9。从表格中检索文本
它是如何工作的
查询数据库后,像这样:
`// Create connection
conn = new SqlConnection(@"data source = .\sql2012;integrated security = true;
initial catalog = SQL2012Db;");
// Create command
cmd = new SqlCommand(@"select textfile, textdata
from TextTable", conn);
// Open connection
conn.Open();
// Create data reader
dr = cmd.ExecuteReader();`
您遍历结果集(但是这里只有一行),从带有GetString()
的表中获取文件名,并打印它以显示显示的是哪个文件。然后用一个空字符数组调用GetCharsQ
来获得VARCHAR(MAX)
列的大小。
` if (dr.Read())
{
// Get file name
textFile = dr.GetString(0);
txtRetrieveText.AppendText("------ start of file\n");
txtRetrieveText.AppendText(textFile);
txtRetrieveText.AppendText("\n");
textSize = dr.GetChars(1, 0, null, 0, 0);
txtRetrieveText.AppendText("--- size of text: " + textSize + " characters ---
");
txtRetrieveText.AppendText("\n--- first 100 characters in text -----\n");
charsRead = dr.GetChars(1, 0, textChars, 0, 100);
txtRetrieveText.AppendText(new String(textChars));
txtRetrieveText.AppendText("\n");
txtRetrieveText.AppendText("\n--- last 100 characters in text -----\n");
charsRead = dr.GetChars(1, textSize - 100, textChars, 0, 100);
txtRetrieveText.AppendText(new String(textChars));
return true;
}
else
{
return false;
}`
不是打印整个文件,而是显示前 100 个字节,使用GetChars()
提取一个子串。对最后 100 个字符做同样的事情。
除此之外,这个程序就像其他检索和显示数据库字符数据的程序一样。
总结
在本章中,您了解了 SQL Server 的文本和二进制数据类型。您还练习了使用 SQL Server 大型对象和 ADO.NET 的数据类型来存储和检索二进制和文本数据。
在下一章,你将学习另一种数据库查询技术,称为语言集成查询(LINQ)。
十八、使用 LINQ
编写软件意味着你需要有一个位于后端的数据库,并且你的大部分时间都花在编写查询来检索和操作数据上。每当有人谈到数据时,我们往往会想到包含在关系数据库或 XML 文档中的信息。
在发布之前的数据访问类型。NET 3.5 只适用于驻留在传统数据源中的数据,就像刚才提到的两个数据源。但是随着。NET 3.5 和更新的版本,如。NET 4.0 和。NET 4.5 中集成了语言集成查询(LINQ),现在可以处理驻留在传统信息存储之外的数据。例如,您可以查询一个包含几百个整数值的泛型List
类型,并编写一个 LINQ 表达式来检索满足您的标准的子集,比如偶数或奇数。
你可能已经知道,LINQ 是。NET 3.0 和。净 3.5。它是 Visual Studio 2012 中的一组功能,将强大的查询功能扩展到 C# 和 VB .NET 的语言语法中。
LINQ 引入了一种标准的、统一的、易于学习的方法来查询和修改数据,并且可以扩展以支持潜在的任何类型的数据存储。Visual Studio 2012 包括 LINQ 提供程序程序集,这些程序集支持对各种类型的数据源(包括关系数据、XML 和内存中数据结构)使用 LINQ 查询。
在本章中,我们将介绍以下内容:
- Introduction to LINQ
- LINQ architecture
- LINQ project structure
- Working with LINQ objects
- Use LINQ to SQL
- Use LINQ to XML
LINQ 简介
LINQ 是微软在发布 Visual Studio 2008 和。NET Framework 版承诺彻底改变开发人员处理数据的方式。微软通过最近发布的继续改进 LINQ。NET 4.0/4.5 和 Visual Studio 2012。如上所述,LINQ 允许您查询各种类型的数据源,包括关系数据库、XML 文档,甚至内存中的数据结构。借助 C# 2012 中一级语言构造的 LINQ 查询表达式,LINQ 支持所有这些类型的数据存储。LINQ 具有以下优势:
- linq provides a general syntax for querying any type of data source; For example, you can query XML documents like SQL database, ADO.NET dataset, memory collection, or any other remote or local data source that you choose to use LINQ to connect and access.
- LINQ has built a bridge between relational data and object-oriented world, and strengthened the connection. LINQ speeds up development by catching many errors at compile time and including intellisense and debugging support.
- LINQ query expression (different from traditional SQL statement) is strongly typed.
注意强类型表达式确保在编译时以正确的类型访问值,从而防止在编译代码时而不是在运行时捕获类型不匹配错误。
LINQ 程序集在一个保护伞下提供了访问各种类型的数据存储的所有功能。表 18-1 列出了核心 LINQ 组件。
注虽然叫语言集成查询,但是 LINQ 可以用来更新数据库数据。这里我们将只讨论简单的查询,让您对 LINQ 有个初步的了解,但是 LINQ 是一个访问数据的通用工具。
LINQ 的建筑
LINQ 由三个主要部分组成:
- [LINQ] to the object
- To ADO.NET [linq], including
- linq!linq 你好 SQL(唉哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟哟德列奈)
- LINQ to data set (formerly known as LINQ over data set)
- LINQ to entity
- 查询表达式到 XML(原名 XLinq)
图 18-1 描述了 LINQ 架构,它清楚地显示了 LINQ 的各种组件及其相关的数据存储。
图 18-1。 LINQ 建筑
对象的 LINQ 处理内存中的数据。任何实现了IEnumerable<T>
接口(在System.Collections.Generic
名称空间中)的类都可以用标准查询操作符(sqo)来查询。
注sqo 是形成 LINQ 模式的方法集合。SQO 方法对序列进行操作,其中一个序列表示一个对象,其类型实现接口IEnumerable<T>
或接口IOueryable<T>
。SQO 提供的查询功能包括过滤、投影、聚合、排序等。
ADO.NET LINQ(也称为 LINQ 支持的 ADO。NET)处理来自外部来源的数据,基本上是 ADO.NET 可以连接到的任何东西。任何实现了IEnumerable<T>
或IOueryable<T>
(在System.Linq
命名空间中)的类都可以用 SQOs 查询。通过使用System.Data.Linq
名称空间可以实现从 LINQ 到 ADO.NET 的功能。
LINQ 到 XML 是一个全面的内存 XML 编程 API。像 LINQ 的其他部分一样,它包括 SQOs,也可以与 ADO.NET 的 LINQ 协同使用,但它的主要目的是统一和简化不同的 XML 工具(如 XQuery、XPath 和 XSLT)通常用来做的事情。LINQ 到 XML 的功能可以通过使用System.Xml.Linq
名称空间来实现。
注 LINQ。NET Compact Framework 包括桌面 LINQ 功能的一个子集。LINQ 和。NET 框架和 LINQ。NET Compact Framework 是在。NET Compact Framework,只支持 sqo。支持 LINQ 到数据集和 LINQ 到数据表,也支持 LINQ 到 XML,但 XPath 扩展除外。
在本章中,我们将使用 LINQ 到对象、LINQ 到 SQL 和 LINQ 到数据集的技术,因为它们与我们在本书中讨论的 C# 2012 数据库编程最密切相关。
LINQ 项目结构
Visual Studio 2012 允许您使用 LINQ 查询。要创建 LINQ 项目,请按照下列步骤操作:
-
打开 Visual Studio 2012,选择文件新建项目。
-
默认情况下,在出现的“新建项目”对话框中。在可用列表中选择了. NET Framework 4.5。Visual Studio 2012 支持的. NET Framework 版本。选择希望 LINQ 功能包含在其中的项目类型。对于这个例子,我们将使用一个 Windows 窗体应用项目。
-
为选择的项目指定名称 Chapter18 ,并单击 OK。一个名为 Chapter18 的新的 Windows 窗体应用将被创建。选择解决方案下名为 Chapter18 的项目,并将其重命名为 Linq。保存所有更改。
-
Open the
References
folder in Solution Explorer. You should see Linq-related assembly references added by default, as shown in Figure 18-2.图 18-2。 LINQ 推荐人
现在,您已经准备好使用 LINQ 项目,您需要做的就是将代码功能和所需的名称空间添加到项目中,并测试应用。让我们开始使用 LINQ。
用 LINQ 来物件
术语对象 LINQ 指的是使用 LINQ 查询来访问内存中的数据结构。可以查询任何支持IEnumerable<T>
的类型。这意味着 LINQ 查询不仅可以用于用户定义的列表、数组、字典等,还可以与。返回集合的 NET Framework APIs。例如,您可以使用System.Reflection
类返回存储在指定程序集中的类型信息,然后使用 LINQ 过滤这些结果。或者,您可以将文本文件导入到可枚举的数据结构中,并将内容与其他文件进行比较,提取行或部分行,将几个文件中的匹配行组合到一个新集合中,等等。与传统的foreach
循环相比,LINQ 查询有三个主要优势:
- They are more concise and readable, especially when filtering multiple conditions.
- They provide powerful filtering, sorting and grouping functions with minimal application code.
- They can be transplanted to other data sources with little modification.
一般来说,您想要对数据执行的操作越复杂,与传统的迭代技术相比,使用 LINQ 的好处就越大。
试试看:编写一个简单的 LINQ 对象查询
在本练习中,您将创建一个包含一个文本框的 Windows 窗体应用。应用将使用 Linq to Objects 从 TextBox 控件中的字符串数组中检索并显示一些名称。
-
右键单击 Chapter18 解决方案中的
Form1.cs
,选择重命名选项,将表单重命名为 LinqToObjects。 -
通过单击窗体的标题栏选择 LinqToObjects 窗体,并将 Size 属性的宽度设置为 308,高度设置为 311。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此文本框,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtDisplay。
- 对于 Size 属性,将 Width 设置为 244,Height 设置为 216。
- 将 Multiline 属性设置为 True。
-
Now your LinqToObjects form in the Design view should look like Figure 18-3.
图 18-3。LinqToObjects 表单的设计视图
-
Double-click the empty surface of the
LinqToObjects.cs
form, and it will open the code editior window, showing theLinqToObject_Load
event. Place the code in Listing 18-1 in theLinqToObjects_Load
event.清单 18-1。
LinqToObjects.cs
`//Define string array
string[] names = { "Life is Beautiful",
"Arshika Agarwal",
"Seven Pounds",
"Rupali Agarwal",
"Pearl Solutions",
"Vamika Agarwal",
"Vidya Vrat Agarwal",
"Lionbridge Technologies"
};//Linq query
IEnumerablenamesOfPeople = from name in names
where name.Length <= 16
select name;foreach (var name in namesOfPeople)
{
txtDisplay.AppendText(name+"\n");
}` -
Run the program by pressing Ctrl+F5, and you should see the results shown in Figure 18-4.
图 18-4。使用对象的 LINQ 从字符串数组中检索名字
它是如何工作的
您声明了一个名为names
的字符串数组。
string[] names = {"Life is Beautiful", "Arshika Agarwal", "Seven Pounds", "Rupali Agarwal", "Pearl Solutions", "Vamika Agarwal", "Vidya Vrat Agarwal", "Lionbridge Technologies" };
要从字符串数组中检索名称,可以使用IEnumerable<string>
查询字符串数组,并在foreach
的帮助下使用 LINQ 到对象查询语法遍历names
数组。
IEnumerable<string> namesOfPeople = from name in names where name.Length <= 16 select name; foreach (var name in namesOfPeople)
使用 LINQ 到 SQL
LINQ 到 SQL 是一种将关系数据作为对象进行管理和访问的工具。在某些方面,它在逻辑上类似于 ADO.NET,但它从更抽象的角度来看待数据,简化了许多操作。它连接到数据库,将 LINQ 结构转换为 SQL,提交 SQL,将结果转换为对象,甚至跟踪更改并自动请求数据库更新。
一个简单的 LINQ 查询需要三样东西:
- Entity class
- A data context
- A LINQ query
试试看:编写一个简单的 LINQ 到 SQL 查询
在本练习中,您将使用 LINQ 到 SQL 检索来自AdventureWorks
个人的所有联系信息。联系表。
-
导航到解决方案资源管理器,右击 Linq 项目,并选择添加 Windows 窗体。在打开的添加新项对话框中,确保选中 Windows Form,然后将
Form1.cs
重命名为 LinqToSql。单击添加。 -
通过单击窗体的标题栏选择 LinqToSql 窗体,并将 Size 属性的宽度设置为 355,高度设置为 386。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此文本框,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtLinqToSql。
- 将 Multiline 属性设置为 True。
- 将 ScrollBars 属性设置为垂直。
-
For the Size property, set Width to 315 and Height to 320. Now your LinqToSql form in the Design view should look like Figure 18-5.
图 18-5。LinqToSql 表单的设计视图
-
Before you begin coding the functionality, you must add the required assembly references. LinqToSql will require an assembly reference of
System.Data.Linq
to be added to the Linq project. To do so, select References, right-click, and choose Add Reference. From the opened Reference Manager dialog, scroll down to the assembly list, and select System.Data.Linq, as shown in Figure 18-6. Click OK.图 18-6。添加对 Linq 程序集的引用。
-
Open the newly added form
LinqToSql.cs
in the Code view. Add the code shown in Listing 18-2 inLinqToSql.cs
.清单 18-2。T4
LinqToSql.cs
`// Must add these two namespaces for LinqToSql
using System.Data.Linq;
using System.Data.Linq.Mapping;[Table(Name = "Person.Person")]
public class Contact
{
[Column]
public string Title;
[Column]
public string FirstName;
[Column]
public string LastName;
}private void LinqToSql_Load(object sender, EventArgs e)
{
// connection string
database = AdventureWorks";try
{
// Create data context
DataContext db = new DataContext(connString);// Create typed table
Tablecontacts = db.GetTable (); // Query database
var contactDetails =
from c in contacts
where c.Title == "Mr."
orderby c.FirstName
select c;// Display contact details
foreach (var c in contactDetails)
{
txtLinqtoSql.AppendText(c.Title);
txtLinqtoSql.AppendText("\t");
txtLinqtoSql.AppendText(c.FirstName);
txtLinqtoSql.AppendText("\t");
txtLinqtoSql.AppendText(c.LastName);
txtLinqtoSql.AppendText("\n");
}
}catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}` -
现在,要将 LinqToSql 表单设置为启动表单,请在代码编辑器中打开
Program.cs
,并将Application.Run(new LinqToObjects());
修改为Application.Run(new LinqToSql());
。 -
Build the solution, and then run the program by pressing Ctrl+F5; you should see the results shown in Figure 18-7.
图 18-7。将 LINQ 的详细联系信息检索到 SQL
它是如何工作的
你定义了一个实体类,Contact
。
[Table(Name = "Person.Person")] public class Contact { [Column] public string Title; [Column] public string FirstName; [Column] public string LastName; }
实体类提供了 LINQ 存储来自数据源的数据的对象。它们就像任何其他 C# 类一样,但是 LINQ 定义了告诉它如何使用这个类的属性。
属性将该类标记为一个实体类,并有一个可选的属性Name
,该属性可用于给出表名,默认为类名。这就是为什么你把这个类命名为Contact
而不是 Person.Contact
[Table(Name = "Person.Contact")] public class Contact
接下来,您必须将类型化表定义改为
Table<Contact> contacts = db.GetTable<Contact>();
为了保持一致,[Column]
属性将一个字段标记为保存表中数据的字段。可以在实体类中声明不映射到表列的字段,LINQ 会忽略它们,但是用[Column]
属性修饰的字段的类型必须与它们映射到的表列兼容。(请注意,由于 SQL Server 表名和列名不区分大小写,默认名称不必与数据库中使用的名称大小写相同。)
您创建了一个数据上下文。
// Create data context DataContext db = new DataContext(connString);
数据上下文做 ADO.NET 连接做的事情,但它也做数据提供者处理的事情。它不仅管理到数据源的连接,还将 LINQ 请求(用 SQO 表示)转换成 SQL,将 SQL 传递给数据库服务器,并从结果集中创建对象。
您创建了一个类型的表。
// Create typed table Table<Contact> contacts = db.GetTable<Contact>();
类型化表是一个集合(类型为System.Data.Linq.Table<T>
),它的元素具有特定的类型。DataContext
类的GetTable
方法告诉数据上下文访问结果,并指示将它们放在哪里。在这里,您从 Person 获得所有行(但只有三列)。Contact 表,数据上下文为 contacts 类型表中的每一行创建一个对象。
你声明了一个 C# 2012 隐式类型化的局部变量, contactDetails
,类型为var
。
// Query database var contactDetails =
隐式类型的局部变量顾名思义。当 C# 看到var
类型时,它会根据=
符号右边的initializer
中的表达式类型来推断局部变量的类型。
您用一个查询表达式初始化本地变量。
from c in contacts where c.Title == "Mr." orderby c.FirstName select c;
查询表达式由一个from
子句和一个查询体组成。您在这里使用查询体中的WHERE
条件。from
子句声明了一个迭代变量c
,用于迭代表达式contacts
的结果(即之前创建和加载的类型化表)。在每次迭代中,它将选择满足WHERE
子句的行(这里,标题必须是“Mr .”)。
最后,遍历custs
集合并显示每个客户。
// Display contact details foreach (var c in contactDetails) { txtLinqtoSql.AppendText(c.Title); txtLinqtoSql.AppendText("\t"); txtLinqtoSql.AppendText(c.FirstName); txtLinqtoSql.AppendText("\t"); txtLinqtoSql.AppendText(c.LastName); txtLinqtoSql.AppendText("\n");
}
尽管有了新的 C# 2008 特性和术语,但这种感觉还是很熟悉。一旦掌握了窍门,这是一种很有吸引力的查询编码替代方法。您基本上是编写一个查询表达式而不是 SQL 来填充一个集合,您可以用一个foreach
语句遍历这个集合。但是,您提供了一个连接字符串,但没有显式地打开或关闭连接。此外,不需要命令、数据读取器或索引器。您甚至不需要使用System.Data
或System.Data.SqlClient
名称空间来访问 SQL Server。
很酷,不是吗?
使用 LINQ 到 XML
LINQ 到 XML 提供了一个内存中的 XML 编程 API,该 API 将 XML 查询功能集成到 C# 2012 中,以利用 LINQ 框架并添加特定于 XML 的查询扩展。LINQ 到 XML 提供了集成到. NET 中的 XQuery 和 XPath 的查询和转换能力
从另一个角度来看,您也可以将 LINQ 到 XML 看作是一个全功能的 XML API,相当于现代化的、重新设计的 SystemXml API 加上 XPath 和 XSLT 的一些关键特性。LINQ 到 XML 提供了在内存中编辑 XML 文档和元素树的工具,以及流工具。图 18-8 显示了一个样本 XML 文档。
图 18-8。 XML 文档
试试看:编写一个简单的 LINQ 到 XML 的查询
在本练习中,您将使用 LINQ 到 XML 从 XML 文档中检索元素值。
-
导航到解决方案资源管理器,右击 Linq 项目,然后选择“Windows 窗体”。在打开的添加新项对话框中,确保选中 Windows Form,然后将
Form1.cs
重命名为 LinqToXml。单击添加。 -
通过单击窗体的标题栏选择 LinqToXml 窗体,并将 Size 属性的宽度设置为 377,高度设置为 356。
-
将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此文本框,导航到“属性”窗口,并设置以下属性:
- 将 Name 属性设置为 txtLinqToXml。
- 将 Multiline 属性设置为 True。
- 将 ScrollBars 属性设置为垂直。
- 对于“大小”属性,将“宽度”设置为 340,将“高度”设置为 298。
-
Now your LinqToXml form in the Design view should look like Figure 18-9.
图 18-9。LinqToXml 表单的设计视图
-
Open the newly added form
LinqToXml.cs
in code view. Add the code shown in Listing 18-3 inLinqToXml.cs
.清单 18-3。T4
LinqToXml.cs
` using System.Xml.Linq;
//Load the productstable.xml in memory
XElement doc = XElement.Load(@"C:\VidyaVrat\C#2012 and SQL
2012\Chapter18\Code\Linq\productstable.xml");//Query xml doc
var products = from prodname in doc.Descendants("products")
select prodname.Value;//Display details
foreach (var prodname in products)
{
txtLinqToXml.AppendText("Product's Detail= ");
txtLinqToXml.AppendText(prodname);
txtLinqToXml.AppendText("\n");
}`注意我们已经指定了
productstable.xml
文件,它位于我们机器上的特定位置;根据您的计算机和 XML 文件的可用性,您可以使用另一个 XML 文件路径。本章的源代码中也提供了productstable.xml
文件。 -
Now, to set the LinqToSql form as the start-up form, open
Program.cs
in the code editor, and modify theApplication.Run(new LinqToSql());
表现为:
Application.Run(new LinqToXml());
.生成解决方案,然后按 Ctrl+F5 运行程序。您应该会看到如图 18-10 所示的结果。
图 18-10。使用 LINQ 到 XML 检索产品详细信息
它是如何工作的
使用System.Linq.Xml
中的XElement
指定下面的语句,将 XML 文档加载到内存中。
XElement doc = XElement.Load(@"C:\VidyaVrat\C#2012 and SQL 2012\Chapter18\Code\Linq\productstable.xml ");
您还可以编写以下语句来查询 XML 文档,其中Descendents
方法将返回 XML 文档中指定元素的后代元素的值。
var products = from prodname in doc.Descendants("products") select prodname.Value;
总结
在本章中,我们讲述了使用 LINQ 进行简单查询的要点。我向您介绍了 LINQ 的三种风格,主要是对象的 LINQ、SQL 的 LINQ 和 XML 的 LINQ。在下一章,我将介绍 ADO.NET 实体框架。
十九、使用 ADO.NET 实体框架
许多数据库开发人员认为随着 ADO.NET 2.0 和 LINQ 的发布,数据库 API 已经足够成熟了,但是这些数据访问 API 还在继续发展。数据访问 API 使用起来相当简单,它们允许您模拟关系数据库中存在的相同类型的数据结构和关系。
但是,与数据集或数据表中的数据交互的方式不同于与数据库表中的数据交互的方式。数据的关系模型和编程的面向对象模型之间的差异是相当大的,ADO.NET 2.0 和 LINQ 在减少这两种模型之间的阻抗方面做得相对较少。
随着的发布。NET 框架 4.5 和 Visual Studio 2011,推出了新版本的 ADO.NET 实体框架 5.0。本章将向您介绍 ADO.NET 实体框架 5.0 数据模型,也称为实体数据模型(EDM)。
EDM 是微软实现对象关系映射(ORM)的方式。ORM 是一种处理关系数据并将关系数据映射到对象集合的方法,这些对象称为实体。在这一章中,你将会学到更多的知识,包括这种方法的优点。
在本章中,我将介绍以下内容:
- Understanding ADO.NET Entity Framework 5.0
- Understanding entity data model
- Use entity data model
了解 ADO.NET 实体框架 5.0
ADO.NET 实体框架(EF) 5.0 背后的愿景是扩展数据库编程的抽象级别,并完全消除程序员用来编写面向数据库的软件应用的数据模型和开发语言之间的阻抗不匹配。
ADO.NET EF 5.0 允许开发人员通过对象模型而不是通过传统的逻辑/关系数据模型来关注数据,帮助将逻辑数据模式抽象为概念模型、映射层和逻辑层,以允许通过名为EntityClient
的新数据提供者与该模型进行交互。
在本章中,我将回顾每一层的用途。
ADO.NET EF 5.0 允许开发人员编写更少的数据访问代码,减少维护,并将数据的结构抽象成一种更加业务友好的方式。它还可以帮助减少编译时错误的数量,因为它从概念模型生成强类型类。
如前所述,ADO.NET EF 5.0 生成了一个概念模型,开发者可以使用名为EntityClient
的新数据提供者来编写代码。EntityClient
遵循类似于熟悉的 ADO.NET 对象的模型,使用EntityConnection
和EntityCommand
对象返回一个EntityDataReader
。
了解实体数据模型
ADO.NET EF 3.5 的核心是它的实体数据模型。ADO.NET EF 3.5 支持逻辑存储模型,该模型表示来自数据库的关系模式。关系数据库通常以不同于应用可以使用的格式存储数据。这通常会迫使开发人员以与数据库中包含的数据相同的结构来检索数据。然后,开发人员通常将数据提供给更适合处理业务规则的业务实体。ADO.NET EF 5.0 使用地图图层在数据模型之间架起了一座桥梁。在 ADO.NET EF 5.0 的模型中有三个活跃的层次。
- Concept layer
- Mapping layer
- Logical layer
这三层允许数据从关系数据库映射到更加面向对象的业务模型。ADO.NET EF 3.5 使用 XML 文件定义了这些层。这些 XML 文件提供了一个抽象层次,因此开发人员可以根据 OO 概念模型而不是传统的关系数据模型进行编程。
使用概念模式定义语言(CSDL)在 XML 文件中定义概念模型。CSDL 定义了应用的业务层所知道的实体和关系。表示数据库模式的逻辑模型是使用存储模式定义语言(SSDL)在 XML 文件中定义的。使用映射模式语言(MSL)定义的映射层映射其他两层。这种映射允许开发人员根据概念模型进行编码,并将这些指令映射到逻辑模型中。
使用实体数据模型
今天运行的大多数应用都离不开后端数据库。应用和数据库高度相互依赖;也就是说,它们是紧密耦合的,因此显而易见,应用或数据库中的任何更改都会对另一端产生巨大影响;紧密耦合总是双向的,改变一侧需要与另一侧同步。如果更改没有正确地反映出来,应用将不会以期望的方式运行,系统将会崩溃。
让我们通过考虑下面的代码段来看看紧耦合,你在第十三章中使用它作为清单 13-3 的一部分:
`// Create connection
SqlConnection conn = new SqlConnection(@"
server = .\sql2012;
integrated security = true;
database = AdventureWorks");
// Create command
string sql = @"select Name,ProductNumber
from Production.Product";
SqlCommand cmd = new SqlCommand(sql, conn);
txtReader.AppendText("Command created and connected.\n\n");
try
{
// Open connection
conn.Open();
// Execute query via ExecuteReader
SqlDataReader rdr = cmd.ExecuteReader();
}`
假设您已经将上述代码与数据库一起部署到生产环境中,该数据库具有 select 查询中指定的列名。稍后,数据库管理员(DBA)决定更改所有表中的列名,以实现新的数据库策略:DBA 修改生产。Product 表,并将 Name 列更改为 ProductName,将 ProductNumber 列更改为 ProductSerialNumber。
完成这些数据库更改后,防止应用崩溃的唯一方法是修改源代码中引用 Name 和 ProductName 列的所有代码段,重新构建、重新测试并再次部署整个应用。因此,前面代码中修改后的代码段将如下所示:
// Create command string sql = @"select ProductName, ProductSerialNumber from Production.Product";
虽然从表面上看,进行这样的更改并不困难,但是如果考虑到可能有许多与数据库相关的代码段需要根据新的列命名方案修改列名,那么升级应用以使其能够与修改后的数据库一起工作可能会是一种繁琐而困难的方法。
有了 ADO.NET EF 5.0 的实体数据模型,微软使得实体关系建模变得可执行。微软通过结合 XML 模式文件和 ADO.NET EF 5.0 API 实现了这一点。架构文件用于定义概念层,以公开数据存储的架构(例如,SQL Server 2012 数据库的架构)并在两者之间创建映射。ADO.NET EF 5.0 允许你根据概念模式生成的类来编写程序。然后,当您从数据库中提取数据时,EDM 会通过允许您以面向对象的方式与关系数据库进行交互来处理所有的翻译。
EDM 使得客户端应用和数据库模式能够以松散耦合的方式独立发展,而不会相互影响或破坏。
ADO.NET 的 EDM 5.0 实体框架提供了应用所使用的数据库模式的概念视图。这个概念视图在应用中被描述为一个 XML 映射文件。XML 映射文件将实体属性和关联关系映射到数据库表。
这种映射是将应用从对关系数据库模式的更改中抽象出来的魔棒。因此,不必修改应用中所有面向数据库的代码段来适应数据库模式中的更改,只需修改 XML 映射文件,使其反映对数据库模式的所有更改。换句话说,ADO.NET 5.0 EDM 提供的解决方案是在不改变任何源代码的情况下修改 XML 映射文件来反映模式的变化。
试试看:创建实体数据模型
在本练习中,您将了解如何创建 EDM。
-
创建一个名为 Chapter19 的新 Windows 窗体应用项目。当解决方案资源管理器打开时,保存解决方案。
-
将 Chapter19 项目重命名为 EntityFramework。
-
Right-click the project and select Add New Item; from the provided Visual Studio templates, choose ADO.NET Entity Data Model, and name it
AWCurrencyModel.edmx
; your screen should look like Figure 19-1. Click Add.图 19-1。添加 ADO.NET 实体数据模型
-
The Entity Data Model Wizard will start, with the Choose Model Contents screen appearing first. Select the “Generate from database” option, as shown in Figure 19-2. Click Next.
图 19-2。实体数据模型向导—选择模型内容屏幕
-
The Choose Your Data Connection screen appears next, as shown in Figure 19-3. Click the New Connection button.
图 19-3。实体数据模型向导—选择您的数据连接屏幕
-
The Choose Data Source dialog box appears. Select Microsoft SQL Server from the “Data source” list, as shown in Figure 19-4. Click Continue.
图 19-4。实体数据模型向导—选择数据源对话框
-
Next, the Connection Properties dialog box appears. Enter .\SQL2012 in the “Server name” list box and ensure that the Use Windows Authentication radio button is selected. From the list box provided below the “Select or enter a database name” radio button, select Northwind. Your dialog box should look like Figure 19-5. Click the Test Connection button.
图 19-5。实体数据模型向导—连接属性对话框
-
消息框应该闪烁,显示消息“测试连接成功”单击确定。现在,在连接属性对话框中单击确定。
-
The Choose Your Data Connection window appears, again displaying all the settings you’ve made so far. Ensure the check box “Save entity connection settings in App.config as” is selected and has AWCurrencyEntities as a value entered in it, as shown in Figure 19-6. Click Next.
图 19-6。实体数据模型向导——选择显示设置的数据连接屏幕
-
The Choose Your Database Objects screen now appears. Expand the Tables node. If any of the table or tables are selected, remove all the check marks except for the ones beside the Sales.Currency table. Also, remove the check marks from the Views and Stored Procedures node. The screen will look like Figure 19-7. Click Finish.
***图 19-7。**实体数据模型向导—选择您的数据库对象屏幕*
- Navigate to Solution Explorer, and you will see that a new
AWCurrencyModel.edmx
object has been added to the project, as shown in Figure 19-8.
***图 19-8。**显示生成的实体数据模型的解决方案浏览器*
- Double-click
AWCurrencyModel.edmx
to view the generated Entity Data Model in the Design view. It should look like Figure 19-9.
***图 19-9。**设计视图中的实体数据模型*
- The generated Entity Data Model also has an XML mapping associated with it especially for its
EntityContainer
andEntitySet
s. To view the XML mapping, navigate to Solution Explorer, right-clickAWCurrencyModel.edmx
, and choose the Open With option. From the dialog box that appears, select XML (Text) Editor, and click OK. Notice the highlighted text in the mapping shown in Figure 19-10.
***图 19-10。**与实体数据模型相关联的 XML 映射*
- 切换到解决方案资源管理器,并将 Form1 重命名为
PublishCurrency.cs
。 - 将 TextBox 控件拖到窗体上,并将其放在窗体的中央。选择此 TextBox 控件,导航到“属性”窗口,并设置以下属性:
* 将 Name 属性设置为 txtCurrency。
* 对于 Location 属性,将 X 设置为 12,将 Y 设置为 12。
* 将 Multiline 属性设置为 True。
* 将 ScrollBars 属性设置为垂直。
* 对于“大小”属性,将“宽度”设置为 518,将“高度”设置为 247。
* 将文本属性留空。 - Now your PublishCurrency form in the Design view should like Figure 19-11.
***图 19-11。**发行货币表单的设计视图*
- Double-click the empty surface of the form, and it will open the code editor window, showing the
PublishCurrency_Load
event. Place the code listed in Listing 19-1 into the load event code template.
***清单 19-1。**使用实体数据模型*
` AWCurrencyEntities currContext = new AWCurrencyEntities();
foreach (var cr in currContext.Currencies)
{
txtCurrency .AppendText(cr.ModifiedDate.ToString());
txtCurrency.AppendText("\t\t");
txtCurrency.AppendText(cr.CurrencyCode.ToString());
txtCurrency.AppendText("\t\t");
txtCurrency.AppendText(cr.Name.ToString());
txtCurrency.AppendText("\t");
txtCurrency.AppendText("\n");
}`
- Build the solution, and run the project. When the PublishCurrency form appears, you will see all the currencies listed in the TextBox. The screen shown in Figure 19-12 should display.
***图 19-12。**显示发行货币表单*
它是如何工作的
因为您正在使用一个实体数据模型,所以您不需要处理SqlConnection
、SqlCommand
等等。在这里,您创建一个引用名为AWCurrencyEntities
的EntityContainer
的对象,该对象引用存储在App.config
文件中的整个连接字符串。
AWCurrencyEntities currContext = new AWCurrencyEntities();
将对象指定给EntityContainer
后,就该遍历由EntityContainer.EntitySet
组成的对象集了,这样就包含了 EntityContainer 对象的名称,它代表了后缀为EntitySet
的EntityContainer
。
注意EntityContainer
元素以数据库模式命名,所有逻辑上应该组合在一起的“实体集”都包含在一个EntityContainer
元素中。一个EntitySet
代表数据库中相应的表。您可以在.edmx
文件的ConceptualModel
元素下浏览您的EntityModel
对象的名称,如图图 19-10 所示。
`foreach (var cr in currContext.Currencies)
{
txtCurrency .AppendText(cr.ModifiedDate.ToString());
txtCurrency.AppendText("\t\t");
txtCurrency.AppendText(cr.CurrencyCode.ToString());
txtCurrency.AppendText("\t\t");
txtCurrency.AppendText(cr.Name.ToString());
txtCurrency.AppendText("\t");
txtCurrency.AppendText("\n");
}`
如您所见,EntityContainer
对象通过智能感知公开了列名。或者,如果你放一个.(点),你会看到销售的所有领域。货币表,这比您在前一章中试验的 DataReader 的rdr[0], rdr[1]
技术更简单。换句话说,实体框架已经“映射”了来自销售的每个记录。货币表转换为对象。这些属性与表中的列同名,但是使用对象符合面向对象的编码风格。
试试看:使用实体数据模型的模式抽象
在前面的练习中,您创建了一个名为 AWCurrencyModel 的实体数据模型(因为这是您的.edmx
文件的名称);在本练习中,您将看到该实体数据模型如何帮助开发人员实现模式抽象和修改数据库,而无需在整个项目中或数据访问层(DAL)中接触数据访问代码。也就是说,开发人员可以简单地从模型中移除表引用,然后再将它添加回来。这些列将被重新对齐,然后可以更新代码来引用相应的属性。
-
启动 SQL Server Management Studio,展开“数据库”节点,展开“AdventureWorks 数据库”节点,然后展开“表”节点。在表列表中,展开 Sales。货币节点,然后展开
Columns
文件夹。 -
选择“名称”列,右键单击,然后选择“重命名”选项。将 name 列重命名为 CurrencyName。
-
选择“修改日期”列,右键单击,然后选择“重命名”选项。将 ModifiedDate 列重命名为 ModifiedCurrencyDate。
-
所以,基本上,我们在这两列中添加了货币术语。现在,通过选择文件退出,退出 SQL Server Management Studio。
-
As you can imagine, our PublishCurrency form and the database have a column name mismatch, so we will now view the exception that the application will report because of this recent column name change. To do so, we will add a
TRY…CATCH
block to report the issue. Modify thePublishCurrency.cs
code to look like Listing 19-2.清单 19-2。将
TRY…CATCH
添加到PublishCurrency.cs
以显示异常详情`try
{ AWCurrencyEntities currContext = new AWCurrencyEntities();foreach (var cr in currContext.Currencies)
{
txtCurrency .AppendText(cr.ModifiedDate.ToString());txtCurrency.AppendText("\t\t");
txtCurrency.AppendText(cr.CurrencyCode.ToString());txtCurrency.AppendText("\t\t");
txtCurrency.AppendText("\t");
txtCurrency.AppendText("\n");
}
}catch(Exception ex)
{
MessageBox.Show(ex.Message + ex.StackTrace +
ex.InnerException);
}` -
现在,通过按 Ctrl+F5 构建并运行 PublishCurrency。PublishCurrency 详细信息表单应该加载并引发一个异常窗口,显示以下消息:“执行命令定义时出错。有关详细信息,请参见内部异常。
-
If you look at
InnerException
, you will see a message that indicates the cause of this exception; it’s because you have just renamed the Name and ModifiedDate columns of the Sales.Currency table. The exception details should look like Figure 19-13.图 19-13。反映最近重命名后无效列名的异常详细信息
-
单击确定关闭异常窗口,并关闭打开的表单,该表单将是空的,因为数据因发生异常而未加载。
-
Now you will see the advantage of entity data modeling. Assume the same issue occurred in the code you wrote in previous chapters; the only solution is to modify the column name in each and every SQL statement that maps to the table we modified. In a real-world scenario, this will not be possible, because database schema updates changes are invisible and so the Entity Data Model comes to rescue.
要修复这个应用,您必须修改由实体数据模型创建的 XML 映射文件,也就是您在本章前面创建的
AWCurrencyModel.edmx
文件。要查看 XML 映射,导航到 Solution Explorer,右键单击AWCurrencyModel.edmx
,并选择 Open With 选项。从提供的对话框中,选择 XML(文本)编辑器,然后单击确定。你会看到 XML 映射,如前面的图 19-10 所示。注意在打开的 XML 映射文件中,导航到
<!-- SSDL content -->
部分,将<Property Name="Name" Type="nvarchar" Nullable="false" MaxLength="50" />
XML 标签中的名称修改为CurrencyName
;修改后标签应该显示为<Property Name="CurrencyName" Type="nvarchar" Nullable="false" MaxLength="50" />
。注意表示数据库模式的逻辑模型是使用 SSDL 在 XML 文件中定义的。这就是为什么您需要修改列名来映射数据库模式。
-
Also, modify the
<Property Name="ModifiedDate" Type="datetime" Nullable="false" />
XML tag toModifiedCurrencyDate <Property Name="ModifiedCurrencyDate" Type="datetime" Nullable="false" />
XML tag to appear as<Property Name="ModifiedCurrencyDate" Type="datetime" Nullable="false" />
. The modified SSDL content section with theCurrencyName
andModifiedCurrencyDate
values will look like Figure 19-14.
***图 19-14。**修改 SSDL 内容部分*
- Now look for the
<!-- C-S mapping content -->
section and modify the<ScalarProperty Name="Name" ColumnName="Name" />
tag to be<ScalarProperty Name="Name" ColumnName="CurrencyName" />
.•
![Image](https://gitee.com/OpenDocCN/vkdoc-csharp-zh/raw/master/docs/begin-cs5-db/img/square.jpg) **注意**概念模型是使用 CSDL 在 XML 文件中定义的。CSDL 定义了应用的业务层所知道的实体和关系。这就是为什么您需要修改列名,使其可读并易于被实体找到。
- Next, modify the
<ScalarProperty Name="ModifiedDate" ColumnName="ModifiedDate" />
tag to appear as<ScalarProperty Name="ModifiedDate" ColumnName="ModifiedCurrencyDate" />
. The modifiedC-S
mapping content section with theCurrencyName
andModifiedCurrencyDate
values will look like Figure 19-15.
***图 19-15。**修改`C-S`映射内容段*
- 现在保存并构建 Chapter19 解决方案,并运行应用。当 PublishCurrency 表单打开时,应该用
ModifiedDate
、Name
和CurrencyCode
值填充文本框,如前面图 19-12 中的所示。 - 用
foreach
循环切换回PublishCurrency.cs
代码,如清单 19-2 所示。即使您已经修改了 AdventureWorks 数据库的 Sales 中的列名,您仍然应该看到文本框中显示的相同的列名,其中的EntityContainer
为cr.ModifiedDate
和cr.Name
。货币表。但是通过利用实体数据模型的模式抽象特性,您只需要在 XML 映射文件中的 SSDL 内容和C-S
映射内容部分下指定更新的列名。
总结
在本章中,您了解了 ADO.NET 5.0 实体数据模型的特性。
您还了解了模式抽象是如何工作的,以及它将如何帮助您实现数据库和数据访问代码或数据访问层之间的松散耦合。在下一章,你将学习如何使用 SQL CLR 对象。
二十、在 SQL Server 中使用 CLR
多年来,编写业务逻辑一直是特定于技术和软件的,尤其是在数据库方面。例如,如果您想创建一个需要复杂 SQL 代码的存储过程或其他数据库对象,唯一的方法就是在数据库中编写 T-SQL 逻辑,并用 C# 之类的编程语言编写调用代码,如前几章所示,我们在 SQL Server 中使用 T-SQL 创建了一个存储过程,然后用 C# 编写了调用代码。
这种方法仍然非常流行,但是有一种更简单的方法,它允许 C# 程序员控制和编码所有面向数据库的对象,例如。NET 语言,比如 C#,而不像以前那样使用 T-SQL。
在本章中,我将介绍以下内容:
- 引入 sql clr
- oot-SQL SQL clr
- Select enable SQL CLR integration between
- Create SQL CLR stored procedure
- Deploy SQL CLR stored procedures to SQL Server
- Execute SQL CLR stored procedures
SQL CLR 简介
SQL 公共语言运行库(CLR)是。集成到 SQL Server 2005 和更高版本中的. NET CLR。SQL CLR 为开发人员在处理与数据库相关的复杂业务逻辑时提供了一种选择,尤其是当 T-SQL 使这种处理变得不那么愉快时。
SQL CLR 是。NET CLR,它主要通过为已部署的内存管理和代码执行提供支持来实现运行时执行引擎的目的。NET SQL CLR 程序集。一个程序集是一个. NET 术语,指的是由元数据(关于数据的数据)和清单(关于程序集的数据)组成的 DLL 或 EXE 文件。
可以使用 SQL CLR 集成创建以下类型的对象:
- stored procedure
- Custom aggregation
- trigger
- Custom type
在 T-SQL 和 SQL CLR 之间选择
当您有两种选择来实现相同的功能时,根据您的场景和需求,一种可能比另一种更有优势。这里有几个要点可以帮助您决定在什么情况下选择 T-SQL 还是 SQL CLR:
- T-SQL is best used to perform declarative, set-based operations (select, insert, update and delete).
- T-SQL works within a database connection, and SQL CLR must get the connection.
- T-SQL also has a procedural ability. In other words, it can perform procedural operations such as
WHILE
, but T-SQL is not the best choice when it comes to rich or more complex logic. In this case, the SQL CLR with C# allows the programmer to better control the functions.- T-SQL is interpreted while SQL CLR is compiled. Therefore, interpreted code is slower than compiled procedure code.
- Before executing the SQL CLR code, it needs the CLR to be loaded by SQL Server, and T-SQL will not generate any such overhead.
- When any T-SQL code executes, it shares the same stack frame in memory, and each SQLCLR code needs its own stack frame, which will lead to larger memory allocation but better concurrency. T-SQL consists of a library full of data-centric functions, so it is more suitable for collection-based operations. SQL CLR is more suitable for operations of recursive, mathematical and string operation types.
启用 SQL CLR 集成
使用 C# 创建数据库对象后,必须在 SQL Server 2012 中启用 SQL CLR,以便使用或部署它。默认情况下,该功能是关闭的(config_value
设为 0);要启用它(config_value
设置为 1),请遵循以下步骤:
-
打开 SQL Server 2012 Management Studio 根据您的安装类型,使用 Windows 或 SQL 身份验证进行连接。
-
Once connected, click the New Query button, which will open a query window. Enter the following text in the query window, and notice the value of the config_value column, as shown in Figure 20-1.
图 20-1。显示 SQL CLR 的默认行为(禁用)
-
Next, you need to enable SQL CLR, to do so; modify the code to look like Figure 20-2, and it will enable the SQL CLR integration.
图 20-2。显示 SQL CLR 已启用
现在,您的 SQL Server 已经准备好执行使用 C# 编程语言构建的数据库对象,这与 T-SQL 不同。您将在本章的稍后部分执行此操作。
创建 SQL CLR 存储过程
Microsoft Visual Studio 2012 为各种 SQL Server 对象(如存储过程、触发器、函数等)提供了项目模板和类文件,您可以用 C# 将这些对象编码为动态链接库(DLL)等程序集。
试试看:使用 C# 创建 SQL CLR 存储过程
在本练习中,您将通过向 SQL Server 数据库项目中添加一个 SQL CLR C# 存储过程项模板来创建一个 SQL 存储过程。您将创建的 SQL CLR C# 存储过程将帮助您将货币数据插入 AdventureWorks。Sales.Currency 表,就像你在第十三章和清单 13-4 中所做的那样。但是这里您使用了不同的技术来完成相同的任务(货币插入)。
-
Create a new Windows Forms Application project named Chapter13. When Solution Explorer opens, save the solution, as shown in Figure 20-3.
图 20-3。展示 SQL Server 数据库项目模板
-
This will load an empty project, that is, one without any .cs class file in it, as shown in Figure 20-4.
图 20-4。解决方案资源管理器中列出的空项目
-
Right-click the Chapter20 project, choose Add New Item, and in the Add New Item dialog select SQL CLR Stored Procedure on the SQL CLR C# tab. Name it SQLCLRStoredProcedure.cs, as shown in Figure 20-5. Click Add.
图 20-5。将一个 SQL CLR C# 存储过程作为一个新项添加到项目中
-
Your Visual Studio environment will now look like Figure 20-6.
图 20-6。显示添加 SQL CLR C# 存储过程后的 Visual Studio
-
Replace the code in the StoredProcedure class with the code in Listing 20-1.
清单 20-1。T4
SQLCLRStoredProcedure.cs
`[Microsoft.SqlServer.Server.SqlProcedure()]
public static void InsertCurrency_CS(SqlString currencyCode, SqlString currencyName)
{
SqlConnection conn = null;try
{
conn = new SqlConnection(@"server = .\sql2012;integrated security = true;
database = AdventureWorks");SqlCommand cmdInsertCurrency = new SqlCommand();
cmdInsertCurrency.Connection = conn;SqlParameter parmCurrencyCode = new SqlParameter
("@CCode", SqlDbType.NVarChar, 3);
SqlParameter parmCurrencyName = new SqlParameter
("@Name", SqlDbType.NVarChar, 50);
parmCurrencyName.Value = currencyName;cmdInsertCurrency.Parameters.Add(parmCurrencyCode);
cmdInsertCurrency.Parameters.Add(parmCurrencyName);cmdInsertCurrency.CommandText =
"INSERT Sales.Currency (CurrencyCode, CurrencyName, ModifiedCurrencyDate)" +
" VALUES(@CCode, @Name, GetDate())";conn.Open();
cmdInsertCurrency.ExecuteNonQuery();
}catch (SqlException ex)
{
SqlContext.Pipe.Send("An error occured" + ex.Message + ex.StackTrace);
}finally
{
conn.Close();
}
}` -
保存项目,并生成解决方案。成功构建后,它将在项目的\bin\debug 文件夹下生成一个 Chapter20.dll 文件。
它是如何工作的
因为这是用 C# 编写的存储过程,所以它将把货币数据插入到 AdventureWorks 中。Sales.Currency 表,它有三列。其中,您将传递两个值作为输入参数。
[Microsoft.SqlServer.Server.SqlProcedure()] public static void InsertCurrency_CS(SqlString currencyCode, SqlString currencyName)
任何数据库应用最重要的部分是创建连接和命令。
SqlConnection conn = null; conn = new SqlConnection(@"server = .\sql2012;integrated security = true; database = AdventureWorks"); SqlCommand cmdInsertCurrency = new SqlCommand(); cmdInsertCurrency.Connection = conn;
一旦有了连接和命令对象,就需要设置这个存储过程将接受的参数。
`SqlParameter parmCurrencyCode = new SqlParameter("@CCode", SqlDbType.NVarChar, 3);
SqlParameter parmCurrencyName = new SqlParameter
("@Name", SqlDbType.NVarChar, 50);
parmCurrencyCode.Value = currencyCode;
parmCurrencyName.Value = currencyName;
cmdInsertCurrency.Parameters.Add(parmCurrencyCode);
cmdInsertCurrency.Parameters.Add(parmCurrencyName);`
设置完参数后,您将设置INSERT
语句,该语句将执行实际的任务,但是因为您只为这个销售选择了两个参数。货币表,对于第三列,即日期列,您将传递GetDate()
函数。
cmdInsertCurrency.CommandText ="INSERT Sales.Currency (CurrencyCode, CurrencyName, ModifiedCurrencyDate)" + " VALUES(@CCode, @Name, GetDate())";
接下来,打开连接并执行命令。
conn.Open(); cmdInsertCurrency.ExecuteNonQuery();
需要记住的最重要的一点是,这段代码实际上是从 SQL Server Management Studio 内部调用的,因此异常处理catch
块需要特别注意。
catch (SqlException ex) { SqlContext.Pipe.Send("An error occured" + ex.Message + ex.StackTrace); }
SqlContext
类允许您调用函数在 SQL Server 的错误窗口中显示错误。
将 SQL CLR 存储过程部署到 SQL Server 中
一旦为特定类型的数据库对象创建了 SQL CLR C# 类型的程序集,就需要将其部署到 SQL Server 中。部署后,SQL Server 可以像使用任何其他 T-SQL 数据库对象一样使用它。
尝试一下:在 SQL Server 中部署 SQL CLR C# 存储过程
在本练习中,您将把创建的程序集部署到SQL2012Db
数据库中,在执行时,这将把货币插入 AdventureWorks 中。销售。货币表。
-
打开 SQL Server 2012 Management Studio,并连接到 SQL Server。
-
选择 SQL2012 数据库(如果您没有这个数据库,您可以使用您选择的任何数据库),然后单击 New Query,这将打开一个新的空白查询窗口。
-
In the opened query window, insert the code in Listing 20-2.
清单 20-2。将程序集部署到 SQL Server 中
`Create Assembly SQLCLR_StoredProcedure
From
--change this path to reflect your database assebmly location
'C:\VidyaVrat\C#2012 and SQL 2012\Chapter20\Code\Chapter20\bin\Debug\Chapter20.dll'
WITH PERMISSION_SET = UNSAFE
GOCREATE PROCEDURE dbo.InsertCurrency_CS
(
@currCode nvarchar(3),
@currName nvarchar(50)
)
AS EXTERNAL NAME SQLCLR_StoredProcedure.StoredProcedures.InsertCurrency_CS;` -
Once code is added, click Execute or press F5. This should execute the command successfully. Then go to the Object Browser, select SQL2012DB, right-click, and choose Refresh. This will show the objects under Programmability and Assemblies in the Object Browser, as shown in Figure 20-7.
图 20-7。在 SQL Server 中部署程序集并在对象浏览器中显示对象
工作原理
此部署过程分为两步。首先,您必须在 SQL Server 中用自己的名字注册一个程序集(您用 C# 创建的)。
Create Assembly SQLCLR_StoredProcedure from 'C:\VidyaVrat\C#2012 and SQL 2012\Chapter20\Code\Chapter20\bin\Debug\Chapter20.dll' WITH PERMISSION_SET = UNSAFE GO
这个PERMISSION_SET
属性允许用户执行具有特定代码访问权限的程序集。UNSAFE
允许此程序集在 SQL Server 中拥有不受限制的访问权限。
其次,您必须创建存储过程,这将基本上调用您从 C# 程序集创建的存储过程。
CREATE PROCEDURE dbo.InsertCurrency_CS ( @currCode nvarchar(3), @currName nvarchar(50) ) AS EXTERNAL NAME SQLCLR_StoredProcedure.StoredProcedures.InsertCurrency_CS; GO
CREATE PROCEDURE
中使用的名字是你在 C# 类中赋予函数的名字(InsertCurrency_CS
)(参考清单 20-1 )。接下来你要设置传递给 C# 函数的输入参数(参见清单 20-1 )。
外部名称实际上在<SQL registered assembly>.<CS class name>.<CS function name>
的语法中,所以结果如下:
SQLCLR_StoredProcedure.StoredProcedures.InsertCurrency_CS
参考清单 20-1 和清单 20-2 中的类名、程序集名等等,这些都在这里使用。
执行 SQL CLR 存储过程
部署程序集并创建存储过程后,您就可以从 SQL 2012 执行此过程,并将货币插入 AdventureWorks。销售。货币表。
试试看:执行 SQL CLR 存储过程
在本练习中,您将执行InsertCurrency_CS
存储过程。
- 打开 SQL Server Management Studio(如果尚未打开),选择 SQL2012db 并单击“新建查询”按钮。
- 在查询窗口中,添加清单 20-3 中所示的代码来执行该过程并添加一种货币。
清单 20-3。执行存储过程插入货币
Exec dbo.InsertCurrency_CS 'ABC','United States of America'
注意你必须为货币代码指定一个唯一的值。例如,我使用 ABC,因为我知道美国没有这样的货币。但是如果您尝试输入 USD 或重复值,您将会收到系统错误。如果你给销售额增加了一个重复的值。对于 CurrencyCode 列,你会得到一个异常,如图图 20-8 所示。
图 20-8。出现重复条目时显示参数异常
工作原理
如清单 20-1 中的 C# 代码所示,insert 语句接受两个输入参数,每次执行时会自动传递用于ModifiedCurrencyDate
列的GetDate()
方法。
cmdInsertCurrency.CommandText ="INSERT Sales.Currency (CurrencyCode, CurrencyName, ModifiedCurrencyDate)" + " VALUES(@CCode, @Name, GetDate())";
因此,存储过程执行语句将看起来像清单 20-3 ,它为输入参数CurrencyCode
和CurrencyName
传递值。
Exec dbo.InsertCurrency_CS 'ABC','United States of America'
总结
在本章中,我介绍了 SQL CLR 集成的要点、它的优点以及开发人员可以创建的对象类型。您还了解了如何选择 SQL CLR 而不是 T-SQL,反之亦然。最后,您使用硬编码的 C# 逻辑和关键字(如 try catc h
)作为 C# 程序集创建了一个 SQL CLR C# 存储过程。然后部署并执行它来插入一种货币。相当酷!