Chapter 14. ADO.NET Fundamentals

At the beginning of this book, you learned that ASP.NET is just one component in Microsoft's ambitious .NET platform. As you know, .NET also includes modern languages and a toolkit of classes that allows you to do everything from handling errors to analyzing XML documents. In this chapter, you'll explore another one of the many features in the .NET Framework: the ADO.NET data access model.

Quite simply, ADO.NET is the technology that .NET applications use to interact with a database. In this chapter, you'll learn about ADO.NET and the family of objects that provides its functionality. You'll also learn how to put these objects to work by creating simple pages that retrieve and update database records. However, you won't learn about one of the most interesting ways to access a database—using a code generation and data modeling tool called LINQ to Entities. Although LINQ to Entities is a powerful and practical way to generate a data model for your database, it may be overkill for your application, it may be unnecessarily complex, or it may not give you all the control you need (for example, if you want to perform unusual data tasks or implement elaborate performance-optimizing techniques). For these reasons, every ASP.NET developer should start by learning the ADO.NET fundamentals that are covered in this chapter.

Note

The LINQ to Entities features is a higher-level model. That means it actually uses the ADO.NET classes you'll learn about in this chapter to do its dirty work. Once you've mastered the essentials of ADO.NET, you'll be ready to explore LINQ to Entities in Chapter 24.

Understanding Databases

Almost every piece of software ever written works with data. In fact, a typical web application is often just a thin user interface shell on top of sophisticated data-driven code that reads and writes information from a database. Often, website users aren't aware (or don't care) that the displayed information originates from a database. They just want to be able to search your product catalog, place an order, or check their payment records.

The most common way to manage data is to use a database. Database technology is particularly useful for business software, which typically requires sets of related information. For example, a typical database for a sales program consists of a list of customers, a list of products, and a list of sales that draws on information from the other two tables. This type of information is best described using a relational model, which is the philosophy that underlies all modern database products, including SQL Server, Oracle, and even Microsoft Access.

As you probably know, a relational model breaks information down to its smallest and most concise units. For example, a sales record doesn't store all the information about the products that were sold. Instead, it stores just a product ID that refers to a full record in a product table, as shown in Figure 14-1.

Basic table relationships

Figure 14.1. Basic table relationships

Although it's technically possible to organize data into tables and store it on the hard drive in one or more files (perhaps using a standard like XML), this approach wouldn't be very flexible. Instead, a web application needs a full relational database management system (RDBMS), such as SQL Server. The RDBMS handles the data infrastructure, ensuring optimum performance and reliability. For example, the RDBMS takes the responsibility of providing data to multiple users simultaneously, disallowing invalid data, and committing groups of actions at once using transactions.

In most ASP.NET applications, you'll need to use a database for some tasks. Here are some basic examples of data at work in a web application:

  • E-commerce sites (like Amazon) use detailed databases to store product catalogs. They also track orders, customers, shipment records, and inventory information in a huge arrangement of related tables.

  • Search engines (like Google) use databases to store indexes of page URLs, links, and keywords.

  • Knowledge bases (like Microsoft Support) use less structured databases that store vast quantities of information or links to various documents and resources.

  • Media sites (like The New York Times) store their articles in databases.

You probably won't have any trouble thinking about where you need to use database technology in an ASP.NET application. What web application couldn't benefit from a guest book that records user comments or a simple e-mail address submission form that uses a back-end database to store a list of potential customers or contacts? This is where ADO.NET comes into the picture. ADO.NET is a technology designed to let an ASP.NET program (or any other .NET program, for that matter) access data.

Tip

If you're a complete database novice, you can get up to speed on essential database concepts using the video tutorials at http://www.asp.net/sql-server/videos. There, you'll find over nine hours of instruction that describes how to use the free SQL Server Express with Visual Studio. The tutorials move from absolute basics—covering topics such as database data types and table relationships—to more advanced subject matter such as full-text search, reporting services, and network security.

Configuring Your Database

Before you can run any data access code, you need a database server to take your command. Although there are dozens of good options, all of which work equally well with ADO.NET (and require essentially the same code), a significant majority of ASP.NET applications use Microsoft SQL Server.

This chapter includes code that works with SQL Server 7 or later, although you can easily adapt the code to work with other database products. If you don't have a full version of SQL Server, there's no need to worry—you can use the free SQL Server Express (as described in the next section). It includes all the database features you need to develop and test a web application.

Note

This chapter (and the following two chapters) use examples drawn from the pubs and Northwind databases, which are sample databases included with some versions of Microsoft SQL Server. These databases aren't preinstalled in modern versions of SQL Server. However, you can easily install them using the scripts provided with the online samples. See the readme.txt file for full instructions, or refer to the section named "The sqlcmd Command-Line Tool" later in this chapter.

SQL Server Express

If you don't have a test database server handy, you may want to use SQL Server 2005 Express Edition, the free data engine included with some versions of Visual Studio and downloadable separately.

SQL Server Express is a scaled-down version of SQL Server that's free to distribute. SQL Server Express has certain limitations—for example, it can use only one CPU and a maximum of 1GB of RAM, and databases can't be larger than 10GB. However, it's still remarkably powerful and suitable for many midscale websites. Even better, you can easily upgrade from SQL Server Express to a paid version of SQL Server if you need more features later.

For a comparison between SQL Server Express and other editions of SQL Server, refer to http://www.microsoft.com/sqlserver/2008/en/us/editions.aspx. SQL Server Express is included with the Visual Studio installation, but if you need to download it separately or you want to download the free graphical management tools that work with it, go to http://www.microsoft.com/express/database.

Browsing and Modifying Databases in Visual Studio

As an ASP.NET developer, you may have the responsibility of creating the database required for a web application. Alternatively, it may already exist, or it may be the responsibility of a dedicated database administrator. If you're using a full version of SQL Server, you'll probably use a graphical tool such as SQL Server Management Studio to create and manage your databases.

Tip

SQL Server Express doesn't include SQL Server Management Studio in the download that you use to install it. However, you can download it separately from http://www.microsoft.com/express/Database/InstallOptions.aspx.

If you don't have a suitable tool for managing your database, or you don't want to leave the comfort of Visual Studio, you can perform many of the same tasks using Visual Studio's Server Explorer window. (Confusingly enough, the Server Explorer window is called the Database Explorer window in Visual Studio Web Developer Express.)

You may see a tab for the Server Explorer on the right side of the Visual Studio window, grouped with the Toolbox and collapsed. If you do, click the tab to expand it. If not, choose View

Browsing and Modifying Databases in Visual Studio

Using the Data Connections node in the Server Explorer, you can connect to existing databases or create new ones. Assuming you've installed the pubs database (see the readme.txt file for instructions), you can create a connection to it by following these steps:

  1. Right-click the Data Connections node, and choose Add Connection. If the Choose Data Source window appears, select Microsoft SQL Server and then click Continue.

  2. If you're using a full version of SQL Server, enter localhost as your server name. This indicates the database server is the default instance on the local computer. (Replace this with the name of a remote computer if needed.) If you're using SQL Server Express, you'll need to use the server name localhostSQLEXPRESS instead, as shown in Figure 14-2. The SQLEXPRESS part indicates that you're connecting to a named instance of SQL Server. By default, this is the way that SQL Server Express configures itself when you first install it.

    Creating a connection in Visual Studio

    Figure 14.2. Creating a connection in Visual Studio

  3. Click Test Connection to verify that this is the location of your database. If you haven't installed a database product yet, this step will fail. Otherwise, you'll know that your database server is installed and running.

  4. In the Select or Enter a Database Name list, choose the pubs database. (In order for this to work, the pubs database must already be installed. You can install it using the database script that's included with the sample code, as explained in the following section.) If you want to see more than one database in Visual Studio, you'll need to add more than one data connection.

    Tip

    Alternatively, you can choose to create a new database by right-clicking the Data Connections node and choosing Create New SQL Server Database.

  5. Click OK. The database connection will appear in the Server Explorer window. You can now explore its groups to see and edit tables, stored procedures, and more. For example, if you right-click a table and choose Show Table Data, you'll see a grid of records that you can browse and edit, as shown in Figure 14-3.

Editing table data in Visual Studio

Figure 14.3. Editing table data in Visual Studio

Tip

The Server Explorer window is particularly handy if you're using SQL Server Express, which gives you the ability to place databases directly in the App_Data folder of your web application (instead of placing all your databases in a separate, dedicated location). If Visual Studio finds a database in the App_Data folder, it automatically adds a connection for it to the Data Connections group. To learn more about this feature, check out the "User Instance Connections" section later in this chapter.

The sqlcmd Command-Line Tool

SQL Server includes a handy command-line tool named sqlcmd.exe that you can use to perform database tasks from a Windows command prompt. Compared to a management tool like SQL Server Management Studio, sqlcmd doesn't offer many frills. It's just a quick-and-dirty way to perform a database task. Often, sqlcmd is used in a batch file—for example, to create database tables as part of an automated setup process.

The sqlcmd tool is found in a directory like c:Program FilesMicrosoft SQL Server100ToolsBinn. The easiest way to run sqlcmd is to launch the Visual Studio command prompt (open the Start menu and choose All Programs

The sqlcmd Command-Line Tool

When running sqlcmd, it's up to you to supply the right parameters. To see all the possible parameters, type this command:

sqlcmd -?

Two commonly used sqlcmd parameters are –S (which specifies the location of your database server) and –i (which supplies a script file with SQL commands that you want to run). For example, the downloadable code samples include a file named InstPubs.sql that contains the commands you need to create the pubs database and fill it with sample data. If you're using SQL Server Express, you can run the InstPubs.sql script using this command:

sqlcmd -S localhostSQLEXPRESS -i InstPubs.sql

If you're using a full version of SQL Server on the local computer, you don't need to supply the server name at all:

sqlcmd -i InstPubs.sql

And if your database is on another computer, you need to supply that computer's name with the –S parameter (or just run sqlcmd on that computer).

Note

The parameters you use with sqlcmd are case sensitive. For example, if you use –s instead of –S, you'll receive an obscure error message informing you that sqlcmd couldn't log in.

Figure 14-4 shows the feedback you'll get when you run InstPubs.sql with sqlcmd.

Running a SQL script with sqlcmd.exe

Figure 14.4. Running a SQL script with sqlcmd.exe

In this book, you'll occasionally see instructions about using sqlcmd to perform some sort of database configuration. However, you can usually achieve the same result (with a bit more clicking) using the graphical interface in a tool like SQL Server Management Studio. For example, to install a database by running a SQL script, you simply need to start SQL Server Management Studio, open the SQL file (using the File

Running a SQL script with sqlcmd.exe

SQL Basics

When you interact with a data source through ADO.NET, you use SQL to retrieve, modify, and update information. In some cases, ADO.NET will hide some of the details for you or even generate required SQL statements automatically. However, to design an efficient database application with a minimal amount of frustration, you need to understand the basic concepts of SQL.

SQL (Structured Query Language) is a standard data access language used to interact with relational databases. Different databases differ in their support of SQL or add other features, but the core commands used to select, add, and modify data are common. In a database product such as SQL Server, it's possible to use SQL to create fairly sophisticated SQL scripts for stored procedures and triggers (although they have little of the power of a full object-oriented programming language). When working with ADO.NET, however, you'll probably use only the following standard types of SQL statements:

  • A Select statement retrieves records.

  • An Update statement modifies existing records.

  • An Insert statement adds a new record.

  • A Delete statement deletes existing records.

If you already have a good understanding of SQL, you can skip the next few sections. Otherwise, read on for a quick tour of SQL fundamentals.

Tip

To learn more about SQL, use one of the SQL tutorials available on the Internet, such as the one at http://www.w3schools.com/sql. If you're working with SQL Server, you can use its thorough Books Online help to become a database guru.

Running Queries in Visual Studio

If you've never used SQL before, you may want to play around with it and create some sample queries before you start using it in an ASP.NET site. Most database products provide some sort of tool for testing queries. If you're using a full version of SQL Server, you can try SQL Server Management Studio. If you don't want to use an extra tool, you can run your queries using the Server Explorer window described earlier. Just follow these steps in Visual Studio:

  1. Right-click your connection, and choose New Query.

  2. Choose the table (or tables) you want to use in your query from the Add Table dialog box (as shown in Figure 14-5), click Add, and then click Close.

    Adding tables to a query

    Figure 14.5. Adding tables to a query

  3. You'll now see a handy query-building window. You can create your query by adding check marks next to the fields you want, or you can edit the SQL by hand in the lower portion of the window. Best of all, if you edit the SQL directly, you can type in anything—you don't need to stick to the tables you selected in step 2, and you don't need to restrict yourself to Select statements.

  4. When you're ready to run the query, select Query Designer

    Adding tables to a query
Executing a query

Figure 14.6. Executing a query

Tip

When programming with ADO.NET, it always helps to know your database. If you have information on hand about the data types it uses, the stored procedures it provides, and the user account you need to use, you'll be able to work more quickly and with less chance of error.

The Select Statement

To retrieve one or more rows of data, you use a Select statement. A basic Select statement has the following structure:

SELECT [columns]
  FROM [tables]
  WHERE [search_condition]
  ORDER BY [order_expression ASC | DESC]

This format really just scratches the surface of SQL. If you want, you can create more sophisticated queries that use subgrouping, averaging and totaling, and other options (such as setting a maximum number of returned rows). By performing this work in a query (instead of in your application), you can often create far more efficient applications.

The next few sections present sample Select statements. After each example, a series of bulleted points breaks the SQL down to explain how each part of it works.

A Sample Select Statement

The following is a typical (and rather inefficient) Select statement for the pubs database. It works with the Authors table, which contains a list of authors:

SELECT * FROM Authors
  • The asterisk (*) retrieves all the columns in the table. This isn't the best approach for a large table if you don't need all the information. It increases the amount of data that has to be transferred and can slow down your server.

  • The From clause identifies that the Authors table is being used for this statement.

  • The statement doesn't have a Where clause. This means all the records will be retrieved from the database, regardless of whether it has 10 or 10 million records. This is a poor design practice, because it often leads to applications that appear to work fine when they're first deployed but gradually slow down as the database grows. In general, you should always include a Where clause to limit the possible number of rows (unless you absolutely need them all). Often, queries are limited by a date field (for example, including all orders that were placed in the last three months).

  • The statement doesn't have an Order By clause. This is a perfectly acceptable approach, especially if order doesn't matter or you plan to sort the data on your own using the tools provided in ADO.NET.

Improving the Select Statement

Here's another example that retrieves a list of author names:

SELECT au_lname, au_fname FROM Authors WHERE State='CA' ORDER BY au_lname ASC
  • Only two columns are retrieved (au_lname and au_fname). They correspond to the first and last names of the author.

  • A Where clause restricts results to those authors who live in the specified state (California). Note that the Where clause requires apostrophes around the value you want to match, because it's a text value.

  • An Order By clause sorts the information alphabetically by the author's last name. The ASC part (for ascending) is optional, because that's the default sort order.

An Alternative Select Statement

Here's one last example:

SELECT TOP 100 * FROM Sales ORDER BY ord_date DESC

This example uses the Top clause instead of a Where statement. The database rows will be sorted in descending order by order date, and the first 100 matching results will be retrieved. In this case, it's the 100 most recent orders. You could also use this type of statement to find the most expensive items you sell or the best-performing employees.

The Where Clause

In many respects, the Where clause is the most important part of the Select statement. You can find records that match several conditions using the And keyword, and you can find records that match any one of a series of conditions using the Or keyword. You can also specify greater-than and less-than comparisons by using the greater-than (>) and less-than (<) operators.

The following is an example with a different table and a more sophisticated Where statement:

SELECT * FROM Sales WHERE ord_date < '2000/01/01' AND ord_date > '1987/01/01'

This example uses the international date format to compare date values. Although SQL Server supports many date formats, yyyy/mm/dd is recommended to prevent ambiguity.

If you were using Microsoft Access, you would need to use the U.S. date format, mm/dd/yyyy, and replace the apostrophes around the date with the number (#) symbol.

String Matching with the Like Operator

The Like operator allows you to perform partial string matching to filter records where a particular field starts with, ends with, or contains a certain set of characters. For example, if you want to see all store names that start with B, you could use the following statement:

SELECT * FROM Stores WHERE stor_name LIKE 'B%'

To see a list of all stores ending with S, you would put the percent sign before the S, like this:

SELECT * FROM Stores WHERE stor_name LIKE '%S'

The third way to use the Like operator is to return any records that contain a certain character or sequence of characters. For example, suppose you want to see all stores that have the word book somewhere in the name. In this case, you could use a SQL statement like this:

SELECT * FROM Stores WHERE stor_name LIKE '%book%'

By default, SQL is not case sensitive, so this syntax finds instances of BOOK, book, or any variation of mixed case.

Finally, you can indicate one of a set of characters, rather than just any character, by listing the allowed characters within square brackets. Here's an example:

SELECT * FROM Stores WHERE stor_name LIKE '[abcd]%'

This SQL statement will return stores with names starting with A, B, C, or D.

Aggregate Queries

The SQL language also defines special aggregate functions. Aggregate functions work with a set of values but return only a single value. For example, you can use an aggregate function to count the number of records in a table or to calculate the average price of a product. Table 14-1 lists the most commonly used aggregate functions.

Table 14.1. SQL Aggregate Functions

Function

Description

Avg(fieldname)

Calculates the average of all values in a given numeric field

Sum(fieldname)

Calculates the sum of all values in a given numeric field

Min(fieldname) and Max(fieldname)

Finds the minimum or maximum value in a number field

Count(*)

Returns the number of rows in the result set

Count(DISTINCT fieldname)

Returns the number of unique (and non-null) rows in the result set for the specified field

For example, here's a query that returns a single value—the number of records in the Authors table:

SELECT COUNT(*) FROM Authors

And here's how you could calculate the total quantity of all sales by adding together the qty field in each record:

SELECT SUM(qty) FROM Sales

The SQL Update Statement

The SQL Update statement selects all the records that match a specified search expression and then modifies them all according to an update expression. At its simplest, the Update statement has the following format:

UPDATE [table] SET [update_expression] WHERE [search_condition]

Typically, you'll use an Update statement to modify a single record. The following example adjusts the phone column in a single author record. It uses the unique author ID to find the correct row.

UPDATE Authors SET phone='408 496-2222' WHERE au_id='172-32-1176'

This statement returns the number of affected rows. (See Figure 14-7 for an example in Visual Studio.) However, it won't display the change. To do that, you need to request the row by performing another Select statement:

Executing an update query in Visual Studio

Figure 14.7. Executing an update query in Visual Studio

SELECT phone FROM Authors WHERE au_id='172-32-1176'

As with a Select statement, you can use an Update statement with several criteria:

UPDATE Authors SET au_lname='Whiteson', au_fname='John'
    WHERE au_lname='White' AND au_fname='Johnson'

You can even use the Update statement to update an entire range of matching records. The following example increases the price of every book in the Titles table that was published in 1991 by one dollar:

UPDATE Titles SET price=price+1
    WHERE pubdate >= '1991/01/01' AND pubdate < '1992/01/01'

The SQL Insert Statement

The SQL Insert statement adds a new record to a table with the information you specify. It takes the following form:

INSERT INTO [table] ([column_list]) VALUES ([value_list])

You can provide the information in any order you want, as long as you make sure the list of column names and the list of values correspond exactly:

INSERT INTO Authors (au_id, au_lname, au_fname, zip, contract)
       VALUES ('998-72-3566', 'Khan', 'John', 84152, 0)

This example leaves out some information, such as the city and address, in order to provide a simple example. However, it provides the minimum information that's required to create a new record in the Authors table.

Remember, database tables often have requirements that can prevent you from adding a record unless you fill in all the fields with valid information. Alternatively, some fields may be configured to use a default value if left blank. In the Authors table, some fields are required, and a special format is defined for the ZIP code and author ID.

One feature the Authors table doesn't use is an automatically incrementing identity field. This feature, which is supported in most relational database products, assigns a unique value to a specified field when you perform an insert operation. When you insert a record into a table that has a unique incrementing ID, you shouldn't specify a value for the ID. Instead, allow the database to choose one automatically.

The SQL Delete Statement

The Delete statement is even easier to use. It specifies criteria for one or more rows that you want to remove. Be careful: once you delete a row, it's gone for good!

DELETE FROM [table] WHERE [search_condition]

The following example removes a single matching row from the Authors table:

DELETE FROM Authors WHERE au_id='172-32-1176'

Note

If you attempt to run this specific Delete statement, you'll run into a database error. The problem is that this author record is linked to one or more records in the TitleAuthor table. The author record can't be removed unless the linked records are deleted first. (After all, it wouldn't make sense to have a book linked to an author that doesn't exist.)

The Delete and Update commands return a single piece of information: the number of affected records. You can examine this value and use it to determine whether the operation is successful or executed as expected.

The rest of this chapter shows how you can combine SQL with the ADO.NET objects to retrieve and manipulate data in your web applications.

The Data Provider Model

ADO.NET relies on the functionality in a small set of core classes. You can divide these classes into two groups: those that are used to contain and manage data (such as DataSet, DataTable, DataRow, and DataRelation) and those that are used to connect to a specific data source (such as Connection, Command, and DataReader).

The data container classes are completely generic. No matter what data source you use, once you extract the data, it's stored using the same data container: the specialized DataSet class. Think of the DataSet as playing the same role as a collection or an array—it's a package for data. The difference is that the DataSet is customized for relational data, which means it understands concepts such as rows, columns, and table relationships natively.

The second group of classes exists in several different flavors. Each set of data interaction classes is called an ADO.NET data provider. Data providers are customized so that each one uses the best-performing way of interacting with its data source. For example, the SQL Server data provider is designed to work with SQL Server. Internally, it uses SQL Server's TDS (tabular data stream) protocol for communicating, thus guaranteeing the best possible performance. If you're using Oracle, you can use an Oracle data provider (which is available at http://www.oracle.com/technology/tech/windows/odpnet) instead.

Each provider designates its own prefix for naming classes. Thus, the SQL Server provider includes SqlConnection and SqlCommand classes, and the Oracle provider includes OracleConnection and OracleCommand classes. Internally, these classes work quite differently, because they need to connect to different databases using different low-level protocols. Externally, however, these classes look quite similar and provide an identical set of basic methods because they implement the same common interfaces. This means your application is shielded from the complexity of different standards and can use the SQL Server provider in the same way the Oracle provider uses it. In fact, you can often translate a block of code for interacting with a SQL Server database into a block of Oracle-specific code just by editing the class names in your code.

In this chapter, you'll use the SQL Server data provider. However, the classes you'll use fall into three key namespaces, as outlined in Table 14-2.

Table 14.2. ADO.NET Namespaces for SQL Server Data Access

Namespace

Purpose

System.Data.SqlClient

Contains the classes you use to connect to a Microsoft SQL Server database and execute commands (like SqlConnection and SqlCommand).

System.Data.SqlTypes

Contains structures for SQL Server–specific data types such as SqlMoney and SqlDateTime. You can use these types to work with SQL Server data types without needing to convert them into the standard .NET equivalents (such as System.Decimal and System.DateTime). These types aren't required, but they do allow you to avoid any potential rounding or conversion problems that could adversely affect data.

System.Data

Contains fundamental classes with the core ADO.NET functionality. This includes DataSet and DataRelation, which allow you to manipulate structured relational data. These classes are totally independent of any specific type of database or the way you connect to it.

In the rest of this chapter, you'll consider how to write web page code that uses the classes in these namespaces. First, you'll consider the most straightforward approach—direct data access. Then, you'll consider disconnected data access, which allows you to retrieve data in the DataSet and cache it for longer periods of time. Both approaches complement each other, and in many web applications you'll use a combination of the two.

Direct Data Access

The most straightforward way to interact with a database is to use direct data access. When you use direct data access, you're in charge of building a SQL command (like the ones you considered earlier in this chapter) and executing it. You use commands to query, insert, update, and delete information.

When you query data with direct data access, you don't keep a copy of the information in memory. Instead, you work with it for a brief period of time while the database connection is open, and then close the connection as soon as possible. This is different than disconnected data access, where you keep a copy of the data in the DataSet object so you can work with it after the database connection has been closed.

The direct data model is well suited to ASP.NET web pages, which don't need to keep a copy of their data in memory for long periods of time. Remember, an ASP.NET web page is loaded when the page is requested and shut down as soon as the response is returned to the user. That means a page typically has a lifetime of only a few seconds (if that).

Note

Although ASP.NET web pages don't need to store data in memory for ordinary data management tasks, they just might use this technique to optimize performance. For example, you could get the product catalog from a database once, and keep that data in memory on the web server so you can reuse it when someone else requests the same page. This technique is called caching, and you'll learn to use it in Chapter 23.

To query information with simple data access, follow these steps:

  1. Create Connection, Command, and DataReader objects.

  2. Use the DataReader to retrieve information from the database, and display it in a control on a web form.

  3. Close your connection.

  4. Send the page to the user. At this point, the information your user sees and the information in the database no longer have any connection, and all the ADO.NET objects have been destroyed.

To add or update information, follow these steps:

  1. Create new Connection and Command objects.

  2. Execute the Command (with the appropriate SQL statement).

This chapter demonstrates both of these approaches. Figure 14-8 shows a high-level look at how the ADO.NET objects interact to make direct data access work.

Direct data access with ADO.NET

Figure 14.8. Direct data access with ADO.NET

Before continuing, make sure you import the ADO.NET namespaces. In this chapter, we assume you're using the SQL Server provider, in which case you need these two namespace imports:

Imports System.Data
Imports System.Data.SqlClient

Creating a Connection

Before you can retrieve or update data, you need to make a connection to the data source. Generally, connections are limited to some fixed number, and if you exceed that number (either because you run out of licenses or because your database server can't accommodate the user load), attempts to create new connections will fail. For that reason, you should try to hold a connection open for as short a time as possible. You should also write your database code inside a Try/Catch error handling structure so you can respond if an error does occur, and make sure you close the connection even if you can't perform all your work.

When creating a Connection object, you need to specify a value for its ConnectionString property. This ConnectionString defines all the information the computer needs to find the data source, log in, and choose an initial database. Out of all the details in the examples in this chapter, the ConnectionString is the one value you might have to tweak before it works for the database you want to use. Luckily, it's quite straightforward. Here's an example that uses a connection string to connect to SQL Server:

Dim myConnection As New SqlConnection()
myConnection.ConnectionString = "Data Source=localhost;" & _
 "Initial Catalog=Pubs;Integrated Security=SSPI"

If you're using SQL Server Express, your connection string will include an instance name, as shown here:

Dim myConnection As New SqlConnection()
myConnection.ConnectionString = "Data Source=localhostSQLEXPRESS;" & _
 "Initial Catalog=Pubs;Integrated Security=SSPI"

The Connection String

The connection string is actually a series of distinct pieces of information separated by semicolons (;). Each separate piece of information is known as a connection string property.

The following list describes some of the most commonly used connection string properties, including the three properties used in the preceding example:

Data source:

This indicates the name of the server where the data source is located. If the server is on the same computer that hosts the ASP.NET site, localhost is sufficient. The only exception is if you're using a named instance of SQL Server. For example, if you've installed SQL Server Express, you'll need to use the data source localhostSQLEXPRESS, because the instance name is SQLEXPRESS. You'll also see this written with a period, as .SQLEXPRESS, which is equivalent.

Initial catalog:

This is the name of the database that this connection will be accessing. It's only the "initial" database because you can change it later by using the Connection.ChangeDatabase() method.

Integrated security:

This indicates you want to connect to SQL Server using the Windows user account that's running the web page code, provided you supply a value of SSPI (which stands for Security Support Provider Interface). Alternatively, you can supply a user ID and password that's defined in the database for SQL Server authentication, although this method is less secure and generally discouraged.

ConnectionTimeout:

This determines how long your code will wait, in seconds, before generating an error if it cannot establish a database connection. Our example connection string doesn't set the ConnectionTimeout, so the default of 15 seconds is used. You can use 0 to specify no limit, but this is a bad idea. This means that, theoretically, the code could be held up indefinitely while it attempts to contact the server.

You can set some other, lesser-used options for a connection string. For more information, refer to the Visual Studio Help. Look under the appropriate Connection class (such as SqlConnection or OleDbConnection) because there are subtle differences in connection string properties for each type of Connection class.

Windows Authentication

The previous example uses integrated Windows authentication, which is the default security standard for new SQL Server installations. You can also use SQL Server authentication. In this case, you will explicitly place the user ID and password information in the connection string. However, SQL Server authentication is disabled by default in SQL Server 2000 and later versions, because it's not considered to be as secure.

Here's the lowdown on both types of authentication:

  • With SQL Server authentication, SQL Server maintains its own user account information in the database. It uses this information to determine whether you are allowed to access specific parts of a database.

  • With integrated Windows authentication, SQL Server automatically uses the Windows account information for the currently logged-in process. In the database, it stores information about what database privileges each user should have.

Tip

You can set what type of authentication your SQL Server uses using a tool such as SQL Server Management Studio. Just right-click your server in the tree, and select Properties. Choose the Security tab to change the type of authentication. You can choose either Windows Only (for the tightest security) or SQL Server and Windows, which allows both Windows authentication and SQL Server authentication. This option is also known as mixed-mode authentication.

For Windows authentication to work, the currently logged-on Windows user must have the required authorization to access the SQL database. This isn't a problem while you test your websites, because Visual Studio launches your web applications using your user account. However, when you deploy your application to a web server running IIS, you might run into trouble. In this situation, all ASP.NET code is run by a more limited user account that might not have the rights to access the database. Although the exact user depends on your version of IIS (see the discussion in Chapter 26), the best approach is usually to grant access to the IIS_IUSRS group.

User Instance Connections

Every database server stores a master list of all the databases that you've installed on it. This list includes the name of each database and the location of the files that hold the data. When you create a database (for example, by running a script or using a management tool), the information about that database is added to the master list. When you connect to the database, you specify the database name using the Initial Catalog value in the connection string.

Note

If you haven't made any changes to your database configuration, SQL Server will quietly tuck the files for newly created databases into a directory like c:Program FilesMicrosoft SQL ServerMSSQL.10.MSSQLSERVERMSSQLData (although the exact path depends on the version of SQL Server you're using). Each database has at least two files—an .mdf file with the actual data and an .ldf file that stores the database log. Of course, database professionals have a variety of techniques and tricks for managing database storage, and can easily store databases in different locations, create multiple data files, and so on. The important detail to realize is that ordinarily your database files are stored by your database server, and they aren't a part of your web application directory.

Interestingly, SQL Server Express has a feature that lets you bypass the master list and connect directly to any database file, even if it's not in the master list of databases. This feature is called user instances. Oddly enough, this feature isn't available in the full edition of SQL Server.

To use this feature, you need to set the User Instances value to True (in the connection string) and supply the file name of the database you want to connect to with the AttachDBFilename value. You don't supply an Initial Catalog value.

Here's an example connection string that uses this approach:

myConnection.ConnectionString = "Data Source=localhostSQLEXPRESS;" & _
  "User Instance=True;AttachDBFilename=|DataDirectory|Northwind.mdf;" & _
  "Integrated Security=True"

There's another trick here. The file name starts with |DataDirectory|. This automatically points to the App_Data folder inside your web application directory. This way, you don't need to supply a full file path, which might not remain valid when you move the web application to a web server. Instead, ADO.NET will always look in the App_Data directory for a file named Northwind.mdf.

User instances is a handy feature if you have a web server that hosts many different web applications that use databases and these databases are frequently being added and removed. However, because the database isn't in the master list, you won't see it in any administrative tools (although most administrative tools will still let you connect to it manually, by pointing out the right file location). But remember, this quirky but interesting feature is available in SQL Server Express only—you won't find it in the full version of SQL Server.

Storing the Connection String

Typically, all the database code in your application will use the same connection string. For that reason, it usually makes the most sense to store a connection string in a class member variable or, even better, a configuration file.

You can also create a Connection object and supply the connection string in one step by using a dedicated constructor:

Dim myConnection As New SqlConnection(connectionString)
' myConnection.ConnectionString is now set to connectionString.

You don't need to hard-code a connection string. The <connectionStrings> section of the web.config file is a handy place to store your connection strings. Here's an example:

<configuration>
  <connectionStrings>
    <add name="Pubs" connectionString=
"Data Source=localhost;Initial Catalog=Pubs;Integrated Security=SSPI"/>
  </connectionStrings>
  ...
</configuration>

You can then retrieve your connection string by name. First, import the System.Web.Configuration namespace. Then, you can use code like this:

Dim connectionString As String = _
  WebConfigurationManager.ConnectionStrings("Pubs").ConnectionString

This approach helps to ensure all your web pages are using the same connection string. It also makes it easy for you to change the connection string for an application, without needing to edit the code in multiple pages. The examples in this chapter all store their connection strings in the web.config file in this way.

Making the Connection

Once you've created your connection (as described in the previous section), you're ready to use it.

Before you can perform any database operations, you need to explicitly open your connection:

myConnection.Open()

To verify that you have successfully connected to the database, you can try displaying some basic connection information. The following example writes some basic information to a Label control named lblInfo (see Figure 14-9).

Testing your connection

Figure 14.9. Testing your connection

Here's the code with basic error handling:

' Define the ADO.NET Connection object.
Dim connectionString As String = _
  WebConfigurationManager.ConnectionStrings("Pubs").ConnectionString
Dim myConnection As New SqlConnection(connectionString)

Try
    ' Try to open the connection.
    myConnection.Open()
    lblInfo.Text = "<b>Server Version:</b> " & myConnection.ServerVersion
    lblInfo.Text &= "<br /><b>Connection Is:</b> " & _
      myConnection.State.ToString()
Catch err As Exception
    ' Handle an error by displaying the information.
    lblInfo.Text = "Error reading the database."
    lblInfo.Text &= err.Message
Finally
    ' Either way, make sure the connection is properly closed.
    ' (Even if the connection wasn't opened successfully,
    '  calling Close() won't cause an error.)
    myConnection.Close()
    lblInfo.Text &= "<br /><b>Now Connection Is:</b> "
    lblInfo.Text &=  myConnection.State.ToString()
End Try

Once you use the Open() method, you have a live connection to your database. One of the most fundamental principles of data access code is that you should reduce the amount of time you hold a connection open as much as possible. Imagine that as soon as you open the connection, you have a live, ticking time bomb. You need to get in, retrieve your data, and throw the connection away as quickly as possible in order to ensure your site runs efficiently.

Closing a connection is just as easy, as shown here:

myConnection.Close()

Another approach is to use the Using statement. The Using statement declares that you are using a disposable object for a short period of time. As soon as you finish using that object and the Using block ends, the common language runtime will release it immediately by calling the Dispose() method. Here's the basic structure of the Using block:

Using object
    ...
End Using

It just so happens that calling the Dispose() method of a connection object is equivalent

to calling Close() and then discarding the connection object from memory. That means you can shorten your database code with the help of a Using block. The best part is that you don't need to write a Finally block—the Using statement releases the object you're using even if you exit the block as the result of an unhandled exception.

Here's how you could rewrite the earlier example with a Using block:

' Define the ADO.NET Connection object.
Dim connectionString As String = _
  WebConfigurationManager.ConnectionStrings("Pubs").ConnectionString
Dim myConnection As New SqlConnection(connectionString)

Try
    Using myConnection
        ' Try to open the connection.
        myConnection.Open()
        lblInfo.Text = "<b>Server Version:</b> " & myConnection.ServerVersion
        lblInfo.Text &= "<br /><b>Connection Is:</b> " & _
          myConnection.State.ToString()
    End Using
Catch err As Exception
    ' Handle an error by displaying the information.
lblInfo.Text = "Error reading the database."
    lblInfo.Text &= err.Message
End Try

lblInfo.Text &= "<br /><b>Now Connection Is:</b> "
lblInfo.Text &= myConnection.State.ToString()

There's one difference in the way this code is implemented as compared to the previous example. The error handling code wraps the Using block. As a result, if an error occurs the database connection is closed first, and then the exception handling code is triggered. In the first example, the error handling code responded first, and then the Finally finally block closed the connection afterward. Obviously, this rewrite is a bit better, as it's always good to close database connections as soon as possible.

The Select Command

The Connection object provides a few basic properties that supply information about the connection, but that's about all. To actually retrieve data, you need a few more ingredients:

  • A SQL statement that selects the information you want

  • A Command object that executes the SQL statement

  • A DataReader or DataSet object to access the retrieved records

Command objects represent SQL statements. To use a Command, you define it, specify the SQL statement you want to use, specify an available connection, and execute the command. To ensure good database performance, you should open your connection just before you execute your command and close it as soon as the command is finished.

You can use one of the earlier SQL statements, as shown here:

Dim myCommand As New SqlCommand()
myCommand.Connection = myConnection
myCommand.CommandText = "SELECT * FROM Authors ORDER BY au_lname"

Or you can use the constructor as a shortcut:

Dim myCommand As New SqlCommand(_
  "SELECT * FROM Authors ORDER BY au_lname ", myConnection)

Note

It's also a good idea to dispose of the Command object when you're finished, although it isn't as critical as closing the Connection object.

The DataReader

Once you've defined your command, you need to decide how you want to use it. The simplest approach is to use a DataReader, which allows you to quickly retrieve all your results. The DataReader uses a live connection and should be used quickly and then closed. The DataReader is also extremely simple. It supports fast-forward-only read-only access to your results, which is generally all you need when retrieving information. Because of the DataReader's optimized nature, it provides better performance than the DataSet. It should always be your first choice for direct data access.

Before you can use a DataReader, make sure you've opened the connection:

myConnection.Open()

To create a DataReader, you use the ExecuteReader() method of the command object, as shown here:

' You don't need the new keyword, as the Command will create the DataReader.
Dim myReader As SqlDataReader
myReader = myCommand.ExecuteReader()

These two lines of code define a variable for a DataReader and then create it by executing the command. Once you have the reader, you retrieve a single row at a time using the Read() method:

myReader.Read()    ' The first row in the result set is now available.

You can then access the values in the current row using the corresponding field names. The following example adds an item to a list box with the first name and last name for the current row:

lstNames.Items.Add(myReader("au_lname") & ", " & myReader("au_fname"))

To move to the next row, use the Read() method again. If this method returns True, a row of information has been successfully retrieved. If it returns False, you've attempted to read past the end of your result set. There is no way to move backward to a previous row.

As soon as you've finished reading all the results you need, close the DataReader and Connection:

myReader.Close()
myConnection.Close()

Putting It All Together

The next example demonstrates how you can use all the ADO.NET ingredients together to create a simple application that retrieves information from the Authors table. You can select an author record by last name using a drop-down list box, as shown in Figure 14-10.

Selecting an author

Figure 14.10. Selecting an author

The full record is then retrieved and displayed in a simple label, as shown in Figure 14-11.

Author information

Figure 14.11. Author information

Filling the List Box

To start, the connection string is defined as a private variable for the page class and retrieved from the connection string:

Private connectionString As String = _
  WebConfigurationManager.ConnectionStrings("Pubs").ConnectionString

The list box is filled when the Page.Load event occurs. Because the list box is set to persist its view state information, this information needs to be retrieved only once—the first time the page is displayed. It will be ignored on all postbacks.

Here's the code that fills the list from the database:

Protected Sub Page_Load(ByVal sender As Object, _
  ByVal e As EventArgs) Handles Me.Load

    If Not Page.IsPostBack Then
        FillAuthorList()
    End If
End Sub

Private Sub FillAuthorList()
    lstAuthor.Items.Clear()

    ' Define the Select statement.
    ' Three pieces of information are needed: the unique id
    ' and the first and last name.
    Dim selectSQL As String = "SELECT au_lname, au_fname, au_id FROM Authors"
' Define the ADO.NET objects.
    Dim con As New SqlConnection(connectionString)
    Dim cmd As New SqlCommand(selectSQL, con)
    Dim reader As SqlDataReader

    ' Try to open database and read information.
    Try
        con.Open()
        reader = cmd.ExecuteReader()

        ' For each item, add the author name to the displayed
        ' list box text, and store the unique ID in the Value property.
        Do While reader.Read()
            Dim newItem As New ListItem()
            newItem.Text = reader("au_lname") & ", " & reader("au_fname")
            newItem.Value = reader("au_id").ToString()
            lstAuthor.Items.Add(newItem)
        Loop
        reader.Close()

    Catch err As Exception
        lblResults.Text = "Error reading list of names."
        lblResults.Text &= err.Message
    Finally
        con.Close()
    End Try
End Sub

This example looks more sophisticated than the previous bite-sized snippets in this chapter, but it really doesn't introduce anything new. It uses the standard Connection, Command, and DataReader objects. The Connection is opened inside an error handling block so your page can handle any unexpected errors and provide information. A Finally block makes sure the connection is properly closed, even if an error occurs.

The actual code for reading the data uses a loop. With each pass, the Read() method is called to get another row of information. When the reader has read all the available information, this method will return False, the loop condition will evaluate to False, and the loop will end gracefully.

The unique ID (the value in the au_id field) is stored in the Value property of the list box for reference later. This is a crucial ingredient that is needed to allow the corresponding record to be queried again. If you tried to build a query using the author's name, you would need to worry about authors with the same name. You would also have the additional headache of invalid characters (such as the apostrophe in O'Leary) that would invalidate your SQL statement.

Retrieving the Record

The record is retrieved as soon as the user changes the selection in the list box. To make this possible, the AutoPostBack property of the list box is set to True so that its change events are detected automatically.

Protected Sub lstAuthor_SelectedIndexChanged(ByVal sender As Object, _
  ByVal e As EventArgs) Handles lstAuthor.SelectedIndexChanged

    ' Create a Select statement that searches for a record
    ' matching the specific author ID from the Value property.
    Dim selectSQL As String
    selectSQL = "SELECT * FROM Authors "
    selectSQL &= "WHERE au_id='" & lstAuthor.SelectedItem.Value & "'"

    ' Define the ADO.NET objects.
    Dim con As New SqlConnection(connectionString)
    Dim cmd As New SqlCommand(selectSQL, con)
    Dim reader As SqlDataReader

    ' Try to open database and read information.
    Try
        con.Open()
        reader = cmd.ExecuteReader()
        reader.Read()

        ' Build a string with the record information,
        ' and display that in a label.
        Dim sb As New StringBuilder()
        sb.Append("<b>")
        sb.Append(reader("au_lname"))
        sb.Append(", ")
        sb.Append(reader("au_fname"))
        sb.Append("</b><br />")
        sb.Append("Phone: ")
        sb.Append(reader("phone"))
        sb.Append("<br />")
        sb.Append("Address: ")
        sb.Append(reader("address"))
        sb.Append("<br />")
        sb.Append("City: ")
        sb.Append(reader("city"))
        sb.Append("<br />")
        sb.Append("State: ")
        sb.Append(reader("state"))
        sb.Append("<br />")
        lblResults.Text = sb.ToString()

        reader.Close()

    Catch err As Exception
        lblResults.Text = "Error getting author. "
        lblResults.Text &= err.Message
    Finally
        con.Close()
    End Try
End Sub

The process is similar to the procedure used to retrieve the last names. There are only a couple of differences:

  • The code dynamically creates a SQL statement based on the selected item in the drop-down list box. It uses the Value property of the selected item, which stores the unique identifier. This is a common (and useful) technique.

  • Only one record is read. The code assumes that only one author has the matching au_id, which is reasonable since this field is unique.

Note

This example shows how ADO.NET works to retrieve a simple result set. Of course, ADO.NET also provides handy controls that go beyond this generic level and let you provide full-featured grids with sorting and editing. These controls are described in Chapter 15 and Chapter 16. For now, you should concentrate on understanding the fundamentals about ADO.NET and how it works with data.

Updating Data

Now that you understand how to retrieve data, it isn't much more complicated to perform simple insert, update, and delete operations. Once again, you use the Command object, but this time you don't need a DataReader because no results will be retrieved. You also don't use a SQL Select command. Instead, you use one of three new SQL commands: Update, Insert, or Delete.

To execute an Update, Insert, or Delete statement, you need to create a Command object. You can then execute the command with the ExecuteNonQuery() method. This method returns the number of rows that were affected, which allows you to check your assumptions. For example, if you attempt to update or delete a record and are informed that no records were affected, you probably have an error in your Where clause that is preventing any records from being selected. (If, on the other hand, your SQL command has a syntax error or attempts to retrieve information from a nonexistent table, an exception will occur.)

Displaying Values in Text Boxes

Before you can update and insert records, you need to make a change to the previous example. Instead of displaying the field values in a single, fixed label, you need to show each detail in a separate text box. Figure 14-12 shows the revamped page. It includes two new buttons that allow you to update the record (Update) or delete it (Delete), and two more that allow you to begin creating a new record (Create New) and then insert it (Insert New).

A more advanced author manager

Figure 14.12. A more advanced author manager

The record selection code is identical from an ADO.NET perspective, but it now uses the individual text boxes:

Protected Sub lstAuthor_SelectedIndexChanged(ByVal sender As Object, _
  ByVal e As EventArgs) Handles lstAuthor.SelectedIndexChanged

    ' Create a Select statement that searches for a record
    ' matching the specific author ID from the Value property.
    Dim selectSQL As String
    selectSQL = "SELECT * FROM Authors "
    selectSQL &= "WHERE au_id='" & lstAuthor.SelectedItem.Value & "'"

    ' Define the ADO.NET objects.
    Dim con As New SqlConnection(connectionString)
    Dim cmd As New SqlCommand(selectSQL, con)
    Dim reader As SqlDataReader
' Try to open database and read information.
    Try
        con.Open()
        reader = cmd.ExecuteReader()
        reader.Read()

        ' Fill the controls.
        txtID.Text = reader("au_id").ToString()
        txtFirstName.Text = reader("au_fname").ToString()
        txtLastName.Text = reader("au_lname").ToString()
        txtPhone.Text = reader("phone").ToString()
        txtAddress.Text = reader("address").ToString()
        txtCity.Text = reader("city").ToString()
        txtState.Text = reader("state").ToString()
        txtZip.Text = reader("zip").ToString()
        chkContract.Checked = CType(reader("contract"), Boolean)
        reader.Close()
        lblResults.Text = ""

    Catch err As Exception
        lblResults.Text = "Error getting author. "
        lblResults.Text &= err.Message
    Finally
        con.Close()
    End Try
End Sub

To see the full code, refer to the online samples for this chapter. If you play with the example at length, you'll notice that it lacks a few niceties that would be needed in a professional website. For example, when creating a new record, the name of the last selected user is still visible, and the Update and Delete buttons are still active, which can lead to confusion or errors. A more sophisticated user interface could prevent these problems by disabling inapplicable controls (perhaps by grouping them in a Panel control) or by using separate pages. In this case, however, the page is useful as a quick way to test some basic data access code.

Adding a Record

To start adding a new record, click Create New to clear all the text boxes. Technically this step isn't required, but it simplifies the user's life:

Protected Sub cmdNew_Click(ByVal sender As Object, _
  ByVal e As EventArgs) Handles cmdNew.Click

    txtID.Text = ""
    txtFirstName.Text = ""
    txtLastName.Text = ""
    txtPhone.Text = ""
    txtAddress.Text = ""
    txtCity.Text = ""
    txtState.Text = ""
    txtZip.Text = ""
    chkContract.Checked = False
lblResults.Text = "Click Insert New to add the completed record."
End Sub

The Insert New button triggers the ADO.NET code that inserts the finished record using a dynamically generated Insert statement:

Protected Sub cmdInsert_Click(ByVal sender As Object, _
  ByVal e As EventArgs) Handles cmdInsert.Click

    ' Perform user-defined checks.
    ' Alternatively, you could use RequiredFieldValidator controls.
    If txtID.Text = "" Or txtFirstName.Text = "" Or txtLastName.Text = "" Then
        lblResults.Text = "Records require an ID, first name, and last name."
        Return
    End If

    ' Define ADO.NET objects.
    Dim insertSQL As String
    insertSQL = "INSERT INTO Authors ("
    insertSQL &= "au_id, au_fname, au_lname, "
    insertSQL &= "phone, address, city, state, zip, contract) "
    insertSQL &= "VALUES ('"
    insertSQL &= txtID.Text & "', '"
    insertSQL &= txtFirstName.Text & "', '"
    insertSQL &= txtLastName.Text & "', '"
    insertSQL &= txtPhone.Text & "', '"
    insertSQL &= txtAddress.Text & "', '"
    insertSQL &= txtCity.Text & "', '"
    insertSQL &= txtState.Text & "', '"
    insertSQL &= txtZip.Text & "', '"
    insertSQL &= Val(chkContract.Checked) & "')"

    Dim con As New SqlConnection(connectionString)
    Dim cmd As New SqlCommand(insertSQL, con)

    ' Try to open the database and execute the update.
    Dim added As Integer = 0
    Try
        con.Open()
        added = cmd.ExecuteNonQuery()
        lblResults.Text = added.ToString() & " records inserted."
    Catch err As Exception
        lblResults.Text = "Error inserting record. "
        lblResults.Text &= err.Message
    Finally
        con.Close()
    End Try

    ' If the insert succeeded, refresh the author list.
    If added > 0 Then
        FillAuthorList()
    End If
End Sub

If the insert fails, the problem will be reported to the user in a rather unfriendly way (see Figure 14-13). This is typically the result of not specifying valid values. If the insert operation is successful, the page is updated with the new author list.

A failed insertion

Figure 14.13. A failed insertion

Note

In a more polished application, you would use validators (as shown in Chapter 9) and provide more useful error messages. You should never display the detailed database error information shown in Figure 14-13, because it could give valuable information to malicious users.

Creating More Robust Commands

The previous example performed its database work using a dynamically pasted-together SQL string. This off-the-cuff approach is great for quickly coding database logic, and it's easy to understand. However, it has two potentially serious drawbacks:

  • Users may accidentally enter characters that will affect your SQL statement. For example, if a value contains an apostrophe ('), the pasted-together SQL string will no longer be valid.

  • Users might deliberately enter characters that will affect your SQL statement. Examples include using the single apostrophe to close a value prematurely and then following the value with additional SQL code.

The second of these is known as SQL injection attack, and it facilitates an amazingly wide range of exploits. Crafty users can use SQL injection attacks to do anything from returning additional results (such as the orders placed by other customers) or even executing additional SQL statements (such as deleting every record in another table in the same database). In fact, SQL Server includes a special system stored procedure that allows users to execute arbitrary programs on the computer, so this vulnerability can be extremely serious.

You could address these problems by carefully validating the supplied input and checking for dangerous characters such as apostrophes. One approach is to sanitize your input by doubling all apostrophes in the user input (in other words, replace ' with "). Here's an example:

Dim authorID As String = txtID.Text.Replace("'", "''")

A much more robust and convenient approach is to use a parameterized command. A parameterized command is one that replaces hard-coded values with placeholders. The placeholders are then added separately and automatically encoded.

For example, this SQL statement:

SELECT * FROM Customers WHERE CustomerID = 'ALFKI'

would become this:

SELECT * FROM Customers WHERE CustomerID = @CustomerID

You then need to add a Parameter object for each parameter in the Command.Parameters collection.

The following example rewrites the insert code of the author manager example with a parameterized command:

Protected Sub cmdInsert_Click(ByVal sender As Object, _
  ByVal e As EventArgs) Handles cmdInsert.Click

    ' Perform user-defined checks.
    If txtID.Text = "" Or txtFirstName.Text = "" Or txtLastName.Text = "" Then
        lblResults.Text = "Records require an ID, first name, and last name."
        Return
    End If

    ' Define ADO.NET objects.
    Dim insertSQL As String
    insertSQL = "INSERT INTO Authors ("
    insertSQL &= "au_id, au_fname, au_lname, "
insertSQL &= "phone, address, city, state, zip, contract) "
    insertSQL &= "VALUES ("
    insertSQL &= "@au_id, @au_fname, @au_lname, "
    insertSQL &= "@phone, @address, @city, @state, @zip, @contract)"

    Dim con As New SqlConnection(connectionString)
    Dim cmd As New SqlCommand(insertSQL, con)

    ' Add the parameters.
    cmd.Parameters.AddWithValue("@au_id", txtID.Text)
    cmd.Parameters.AddWithValue("@au_fname", txtFirstName.Text)
    cmd.Parameters.AddWithValue("@au_lname", txtLastName.Text)
    cmd.Parameters.AddWithValue("@phone", txtPhone.Text)
    cmd.Parameters.AddWithValue("@address", txtAddress.Text)
    cmd.Parameters.AddWithValue("@city", txtCity.Text)
    cmd.Parameters.AddWithValue("@state", txtState.Text)
    cmd.Parameters.AddWithValue("@zip", txtZip.Text)
    cmd.Parameters.AddWithValue("@contract", chkContract.Checked)

    ' Try to open the database and execute the update.
    Dim added As Integer = 0
    Try
        con.Open()
        added = cmd.ExecuteNonQuery()
        lblResults.Text = added.ToString() & " record inserted."
    Catch err As Exception
        lblResults.Text = "Error inserting record. "
        lblResults.Text &= err.Message
    Finally
        con.Close()
    End Try

    ' If the insert succeeded, refresh the author list.
    If added > 0 Then
        FillAuthorList()
    End If
End Sub

Now that the values have been moved out of the SQL command and to the Parameters collection, there's no way that a misplaced apostrophe or scrap of SQL can cause a problem.

Note

For basic security, always use parameterized commands. Many of the most infamous attacks on e-commerce websites weren't fueled by hard-core hacker knowledge but were made using simple SQL injection by modifying values in web pages or query strings.

Updating a Record

When the user clicks the Update button, the information in the text boxes is applied to the database as follows:

Protected Sub cmdUpdate_Click(ByVal sender As Object, _
  ByVal e As EventArgs) Handles cmdUpdate.Click

    ' Define ADO.NET objects.
    Dim updateSQL As String
    updateSQL = "UPDATE Authors SET "
    updateSQL &= "au_fname=@au_fname, au_lname=@au_lname, "
    updateSQL &= "phone=@phone, address=@address, city=@city, state=@state, "
    updateSQL &= "zip=@zip, contract=@contract "
    updateSQL &= "WHERE au_id=@au_id_original"

    Dim con As New SqlConnection(connectionString)
    Dim cmd As New SqlCommand(updateSQL, con)

    ' Add the parameters.
    cmd.Parameters.AddWithValue("@au_fname", txtFirstName.Text)
    cmd.Parameters.AddWithValue("@au_lname", txtLastName.Text)
    cmd.Parameters.AddWithValue("@phone", txtPhone.Text)
    cmd.Parameters.AddWithValue("@address", txtAddress.Text)
    cmd.Parameters.AddWithValue("@city", txtCity.Text)
    cmd.Parameters.AddWithValue("@state", txtState.Text)
    cmd.Parameters.AddWithValue("@zip", txtZip.Text)
    cmd.Parameters.AddWithValue("@contract", chkContract.Checked)
    cmd.Parameters.AddWithValue("@au_id_original", lstAuthor.SelectedItem.Value)

    ' Try to open database and execute the update.
    Dim updated As Integer = 0
    Try
        con.Open()
        updated = cmd.ExecuteNonQuery()
        lblResults.Text = updated.ToString() & " record updated."
    Catch err As Exception
        lblResults.Text = "Error updating author. "
        lblResults.Text &= err.Message
    Finally
        con.Close()
    End Try

    ' If the update succeeded, refresh the author list.
    If updated > 0 Then
        FillAuthorList()
    End If
End Sub

Deleting a Record

When the user clicks the Delete button, the author information is removed from the database. The number of affected records is examined, and if the delete operation was successful, the FillAuthorList() function is called to refresh the page.

Protected Sub cmdDelete_Click(ByVal sender As Object, _
  ByVal e As EventArgs) Handles cmdDelete.Click

    ' Define ADO.NET objects.
    Dim deleteSQL As String
    deleteSQL = "DELETE FROM Authors "
    deleteSQL &= "WHERE au_id=@au_id"

    Dim con As New SqlConnection(connectionString)
    Dim cmd As New SqlCommand(deleteSQL, con)
    cmd.Parameters.AddWithValue("@au_id ", lstAuthor.SelectedItem.Value)

    ' Try to open the database and delete the record.
    Dim deleted As Integer = 0
    Try
        con.Open()
        deleted = cmd.ExecuteNonQuery()
        lblResults.Text &= "Record deleted."
    Catch err As Exception
        lblResults.Text = "Error deleting author. "
        lblResults.Text &= err.Message
    Finally
        con.Close()
    End Try

    ' If the delete succeeded, refresh the author list.
    If deleted > 0 Then
        FillAuthorList()
    End If
End Sub

Interestingly, delete operations rarely succeed with the records in the pubs database, because they have corresponding child records linked in another table of the pubs database. Specifically, each author can have one or more related book titles. Unless the author's records are removed from the TitleAuthor table first, the author cannot be deleted. Because of the careful error handling used in the previous example, this problem is faithfully reported in your application (see Figure 14-14) and doesn't cause any real problems.

A failed delete attempt

Figure 14.14. A failed delete attempt

To get around this limitation, you can use the Create New and Insert New buttons to add a new record and then delete this record. Because this new record won't be linked to any other records, its deletion will be allowed.

Note

If you have a real-world application that needs to delete records and these records might have linked records in a child table, there are several possible solutions. One slightly dangerous option is to configure the database to use cascading deletes to automatically wipe out linked records. Another option is to do the deleting yourself, with additional ADO.NET code. But the best choice is usually not to delete the record at all (after all, you may need it for tracking and reporting later). Instead, use a bit column to keep track of records that shouldn't be displayed, like a Discontinued column in a Products table or a Removed column in the Authors table. You can then add a condition to your Select query so that it doesn't retrieve these records (as in SELECT * FROM Products WHERE Discontinued=0).

Disconnected Data Access

When you use disconnected data access, you keep a copy of your data in memory using the DataSet. You connect to the database just long enough to fetch your data and dump it into the DataSet, and then you disconnect immediately.

There are a variety of good reasons to use the DataSet to hold onto data in memory. Here are a few:

  • You need to do something time-consuming with the data. By dumping it into a DataSet first, you ensure that the database connection is kept open for as little time as possible.

  • You want to use ASP.NET data binding to fill a web control (like a GridView) with your data. Although you can use the DataReader, it won't work in all scenarios. The DataSet approach is more straightforward.

  • You want to navigate backward and forward through your data while you're processing it. This isn't possible with the DataReader, which goes in one direction only—forward.

  • You want to navigate from one table to another. Using the DataSet, you can store several tables of information. You can even define relationships that allow you to browse through them more efficiently.

  • You want to save the data to a file for later use. The DataSet includes two methods—WriteXml() and ReadXml()—that allow you to dump the content to a file and convert it back to a live database object later.

  • You need a convenient package to send data from one component to another. For example, in Chapter 22 you'll learn to build a database component that provides its data to a web page using the DataSet. A DataReader wouldn't work in this scenario, because the database component would need to leave the database connection open, which is a dangerous design.

  • You want to store some data so it can be used for future requests. Chapter 23 demonstrates how you can use caching with the DataSet to achieve this result.

Selecting Disconnected Data

With disconnected data access, a copy of the data is retained in memory while your code is running. Figure 14-15 shows a model of the DataSet.

The DataSet family of objects

Figure 14.15. The DataSet family of objects

You fill the DataSet in much the same way that you connect a DataReader. However, although the DataReader holds a live connection, information in the DataSet is always disconnected.

The following example shows how you could rewrite the FillAuthorList() method from the earlier example to use a DataSet instead of a DataReader. The changes are highlighted in bold.

Private Sub FillAuthorList()

    lstAuthor.Items.Clear()

    ' Define ADO.NET objects.
    Dim selectSQL As String
    selectSQL = "SELECT au_lname, au_fname, au_id FROM Authors"
    Dim con As New SqlConnection(connectionString)
    Dim cmd As New SqlCommand(selectSQL, con)
    Dim adapter As New SqlDataAdapter(cmd)
    Dim dsPubs As New DataSet()

    ' Try to open database and read information.
Try
        con.Open()

        ' All the information in transferred with one command.
        ' This command creates a new DataTable (named Authors)
        ' inside the DataSet.
        adapter.Fill(dsPubs, "Authors")

    Catch err As Exception
        lblResults.Text = "Error reading list of names. "
        lblResults.Text &= err.Message
    Finally
        con.Close()
    End Try

    For Each row As DataRow In dsPubs.Tables("Authors").Rows
        Dim newItem As New ListItem()
        newItem.Text = row("au_lname") & ", " & _
          row("au_fname")
        newItem.Value = row("au_id").ToString()
        lstAuthor.Items.Add(newItem)
    Next
End Sub

The DataAdapter.Fill() method takes a DataSet and inserts one table of information. In this case, the table is named Authors, but any name could be used. That name is used later to access the appropriate table in the DataSet.

To access the individual DataRows, you can loop through the Rows collection of the appropriate table. Each piece of information is accessed using the field name, as it was with the DataReader.

Selecting Multiple Tables

A DataSet can contain as many tables as you need, and you can even add relationships between the tables to better emulate the underlying relational data source. Unfortunately, you have no way to connect tables together automatically based on relationships in the underlying data source. However, you can add relations with a few extra lines of code, as shown in the next example.

In the pubs database, authors are linked to titles using three tables. This arrangement (called a many-to-many relationship, shown in Figure 14-16) allows several authors to be related to one title and several titles to be related to one author. Without the intermediate TitleAuthor table, the database would be restricted to a one-to-many relationship, which would allow only a single author for each title.

A many-to-many relationship

Figure 14.16. A many-to-many relationship

In an application, you would rarely need to access these tables individually. Instead, you would need to combine information from them in some way (for example, to find out what author wrote a given book). On its own, the Titles table indicates only the author ID. It doesn't provide additional information such as the author's name and address. To link this information together, you can use a special SQL Select statement called a Join query. Alternatively, you can use the features built into ADO.NET, as demonstrated in this section.

The next example provides a simple page that lists authors and the titles they have written. The interesting thing about this page is that it's generated using ADO.NET table linking.

To start, the standard ADO.NET data access objects are created, including a DataSet. All these steps are performed in a custom CreateList() method, which is called from the Page.Load event handler so that the output is created when the page is first generated:

' Define the ADO.NET objects.
Dim connectionString As String = _
  WebConfigurationManager.ConnectionStrings("Pubs").ConnectionString
Dim con As New SqlConnection(connectionString)

Dim selectSQL As String = "SELECT au_lname, au_fname, au_id FROM Authors"
Dim cmd As New SqlCommand(selectSQL, con)
Dim adapter As New SqlDataAdapter(cmd)
Dim dsPubs As New DataSet()

Next, the information for all three tables is pulled from the database and placed in the DataSet. This task could be accomplished with three separate Command objects, but to make the code a little leaner, this example uses only one and modifies the CommandText property as needed.

Try
    con.Open()
    adapter.Fill(dsPubs, "Authors")

    ' This command is still linked to the data adapter.
    cmd.CommandText = "SELECT au_id, title_id FROM TitleAuthor"
    adapter.Fill(dsPubs, "TitleAuthor")

    ' This command is still linked to the data adapter.
    cmd.CommandText = "SELECT title_id, title FROM Titles"
adapter.Fill(dsPubs, "Titles")

Catch err As Exception
    lblList.Text = "Error reading list of names. "
    lblList.Text &= err.Message
Finally
    con.Close()
End Try

Defining Relationships

Now that all the information is in the DataSet, you can create two DataRelation objects to make it easier to navigate through the linked information. In this case, these DataRelation objects match the foreign key restrictions that are defined in the database.

Note

A foreign key is a constraint that you can set up in your database to link one table to another. For example, the TitleAuthor table is linked to the Titles and the Authors tables by two foreign keys. The title_id field in the TitleAuthor table has a foreign key that binds it to the title_id field in the Titles table. Similarly, the au_id field in the TitleAuthor table has a foreign key that binds it to the au_id field in the Authors table. Once these links are established, certain rules come into play. For example, you can't create a TitleAuthor record that specifies author or title records that don't exist.

To create a DataRelation, you need to specify the linked fields from two different tables, and you need to give your DataRelation a unique name. The order of the linked fields is important. The first field is the parent, and the second field is the child. (The idea here is that one parent can have many children, but each child can have only one parent. In other words, the parent-to-child relationship is another way of saying a one-to-many relationship.) In this example, each book title can have more than one entry in the TitleAuthor table. Each author can also have more than one entry in the TitleAuthor table:

Dim Titles_TitleAuthor As New DataRelation("Titles_TitleAuthor", _
  dsPubs.Tables("Titles").Columns("title_id"), _
  dsPubs.Tables("TitleAuthor").Columns("title_id"))

Dim Authors_TitleAuthor As New DataRelation("Authors_TitleAuthor", _
  dsPubs.Tables("Authors").Columns("au_id"), _
  dsPubs.Tables("TitleAuthor").Columns("au_id"))

Once you've create these DataRelation objects, you must add them to the DataSet:

dsPubs.Relations.Add(Titles_TitleAuthor)
dsPubs.Relations.Add(Authors_TitleAuthor)

The remaining code loops through the DataSet. However, unlike the previous example, which moved through one table, this example uses the DataRelation objects to branch to the other linked tables. It works like this:

  1. Select the first record from the Author table.

  2. Using the Authors_TitleAuthor relationship, find the child records that correspond to this author. To do so, you call the DataRow.GetChildRows() method, and pass in the DataRelationship object that models the relationship between the Authors and TitleAuthor table.

  3. For each matching record in TitleAuthor, look up the corresponding Title record to get the full text title. To do so, you call the DataRow.GetParentRows() method and pass in the DataRelationship object that connects the TitleAuthor and Titles table.

  4. Move to the next Author record, and repeat the process.

The code is lean and economical:

For Each rowAuthor As DataRow In dsPubs.Tables("Authors").Rows

    lblList.Text &= "<br /><b>" & rowAuthor("au_fname")
    lblList.Text &= " " & rowAuthor("au_lname") & "</b><br />"

    For Each rowTitleAuthor As DataRow In _
     rowAuthor.GetChildRows(Authors_TitleAuthor)

        Dim rowTitle As DataRow
        rowTitle = rowTitleAuthor.GetParentRows(Titles_TitleAuthor)(0)

        lblList.Text &= "&nbsp;&nbsp;"
        lblList.Text &= rowTitle("title") & "<br />"
    Next
Next

Figure 14-17 shows the final result.

Hierarchical information from two tables

Figure 14.17. Hierarchical information from two tables

If authors and titles have a simple one-to-many relationship, you could use simpler code, as follows:

For Each rowAuthor As DataRow In dsPubs.Tables("Authors").Rows
    ' Display author.

    For Each rowTitle As DataRow In rowAuthor.GetChildRows(Authors_Titles)
        ' Display title.
    Next
Next

But having seen the more complicated example, you're ready to create and manage multiple DataRelation objects on your own.

The Last Word

This chapter gave you a solid introduction to ADO.NET. You know now how to connect to a database in your web pages, retrieve the information you need, and execute commands to update, insert, and delete data.

Although you've seen all the core concepts behind ADO.NET, there's still much more to learn. In the following chapters, you'll learn about ASP.NET's data binding system and rich data controls, and you'll see how you can use them to write more practical data-driven web pages. And much later, you'll learn about how you can take your skills to the next level by building ADO.NET-powered components (Chapter 22) and using the higher-level LINQ to Entities framework (Chapter 24).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.237.29