Chapter 16. ADO.NET and Relational Databases

If you are working with a relational database, you have the option of accessing your data with LINQ, with LINQ and ADO.NET, or directly with ADO.NET. ADO.NET was designed to provide a disconnected data architecture (as database connections are typically considered “precious resources”), though it does have a connected alternative.

In a disconnected architecture, data is retrieved from a database and cached on your local machine. You manipulate the data on your local computer and connect to the database only when you wish to alter records or acquire new data.

There are significant advantages to disconnecting your data architecture from your database. The biggest advantage is that your application, whether running on the Web or on a local machine, will create a reduced burden on the database server, which may help your application to scale well. Database connections are resource-intensive, and it is difficult to have thousands (or hundreds of thousands) of simultaneous continuous connections. A disconnected architecture is resource-frugal, though there are times that all you want to do is connect to the database, suck out a stream of data, and disconnect; and ADO.NET has a Reader class that allows for that as well.

ADO.NET typically connects to the database to retrieve data, and connects again to update data when you’ve made changes. Most applications spend most of their time simply reading through data and displaying it; ADO.NET provides a disconnected subset of the data for your use while reading and displaying, but it is up to you as the developer to keep in mind that the data in the database may change while you are disconnected, and to plan accordingly. I cover this in some detail later in this chapter.

Relational Databases and SQL

Although one can certainly write an entire book on relational databases, and another on SQL, the essentials of these technologies aren’t hard to understand. A database is a repository of data. A relational database organizes your data into tables. Consider the Northwind database.

Tip

Microsoft provides the Northwind database as a free download on Microsoft.com (just search on “Northwind database”; the first link should take you to the download page), or you can download the file (SQL200SampleDb.msi) from my site (http://www.JesseLiberty.com) by clicking on Books, then again on Books, and then scrolling down to this book and clicking on Northwind Database. Unzip the file, and double-click on the .msi file to install it.

Tables, Records, and Columns

The Northwind database describes a fictional company buying and selling food products. The data for Northwind is divided into 13 tables, including Customers, Employees, Orders, Order Details, Products, and so forth.

Every table in a relational database is organized into rows, where each row represents a single record. The rows are organized into columns. All the rows in a table have the same column structure. For example, the Orders table has these columns: OrderID, CustomerID, EmployeeID, OrderDate, and so on.

For any given order, you need to know the customer’s name, address, contact name, and so forth. You could store that information with each order, but that would be very inefficient. Instead, you use a second table called Customers, in which each row represents a single customer. In the Customers table is a column for the CustomerID. Each customer has a unique ID, and that field is marked as the primary key for that table. A primary key is the column or combination of columns that uniquely identifies a record in a given table.

The Orders table uses the CustomerID as a foreign key. A foreign key is a column (or combination of columns) that is a primary (or otherwise unique) key from a different table. The Orders table uses the CustomerID (the primary key used in the Customers table) to identify which customer has placed the order. To determine the address for the order, you can use the CustomerID to look up the customer record in the Customers table.

This use of foreign keys is particularly helpful in representing one-to-many or many-to-one relationships between tables. By separating information into tables that are linked by foreign keys, you avoid having to repeat information in records. A single customer, for example, can have multiple orders, but it is inefficient to place the same customer information (name, phone number, credit limit, etc.) in every order record. The process of removing redundant information from your records and shifting it to separate tables is called normalization.

Normalization

Normalization not only makes your use of the database more efficient, but it also reduces the likelihood of data corruption. If you kept the customer’s name in both the Customers table and the Orders table, you would run the risk that a change in one table might not be reflected in the other. Thus, if you changed the customer’s address in the Customers table, that change might not be reflected in every row in the Orders table (and a lot of work would be necessary to make sure that it was reflected). By keeping only the CustomerID in Orders, you are free to change the address in Customers, and the change is automatically reflected for each order.

Just as C# programmers want the compiler to catch bugs at compile time rather than at runtime, database programmers want the database to help them avoid data corruption. The compiler helps avoid bugs in C# by enforcing the rules of the language (e.g., you can’t use a variable you’ve not defined). SQL Server and other modern relational databases avoid bugs by enforcing constraints that you request. For example, the Customers database marks the CustomerID as a primary key. This creates a primary key constraint in the database, which ensures that each CustomerID is unique. If you were to enter a customer named Liberty Associates, Inc., with the CustomerID of LIBE, and then tried to add Liberty Mutual Funds with a CustomerID of LIBE, the database would reject the second record because of the primary key constraint.

Declarative Referential Integrity

Relational databases use declarative referential integrity (DRI) to establish constraints on the relationships among the various tables. For example, you might declare a constraint on the Orders table that dictates that no order can have a CustomerID unless that CustomerID represents a valid record in Customers. This helps avoid two types of mistakes. First, you can’t enter a record with an invalid CustomerID. Second, you can’t delete a customer record if that CustomerID is used in any order. The integrity of your data and its relationships is thus protected.

SQL

The most popular language for querying and manipulating databases is Structured Query Language (SQL), usually pronounced “sequel.” SQL is a declarative language, as opposed to a procedural language, and it can take awhile to get used to working with a declarative language when you are used to languages such as C#.

The heart of SQL is the query. A query is a statement that returns a set of records from the database. The queries in Transact-SQL (the version used by SQL Server) are very similar to the queries used in LINQ, though the actual syntax is slightly different.

For example, you might like to see all the CompanyNames and CustomerIDs of every record in the Customers table in which the customer’s address is in London. To do so, write:

Select CustomerID, CompanyName from Customers where city = 'London'

This returns the following six records as output:

CustomerID CompanyName
---------- ----------------------------------------
AROUT      Around the Horn
BSBEV      B's Beverages
CONSH      Consolidated Holdings
EASTC      Eastern Connection
NORTS      North/South
SEVES      Seven Seas Imports

SQL is capable of much more powerful queries. For example, suppose the Northwind manager would like to know what products were purchased in July 1996 by the customer “Vins et alcools Chevalier.” This turns out to be somewhat complicated. The Order Details table knows the ProductID for all the products in any given order. The Orders table knows which CustomerIDs are associated with an order. The Customers table knows the CustomerID for a customer, and the Products table knows the product name for the ProductID. How do you tie all this together? Here’s the query:

select o.OrderID, productName
from [Order Details] od
join orders o on o.OrderID = od.OrderID
join products p on p.ProductID = od.ProductID
join customers c on o.CustomerID = c.CustomerID
where c.CompanyName = 'Vins et alcools Chevalier'
and orderDate >= '7/1/1996' and orderDate <= '7/31/1996'

This asks the database to get the OrderID and the product name from the relevant tables. First, look at Order Details (which we’ve called od for short), and then join that with the Orders table for every record in which the OrderID in the Order Details table is the same as the OrderID in the Orders table.

When you join two tables, you can say, “Get every record that exists in either table” (this is called an outer join), or, as we’ve done here, “Get only those records that exist in both tables” (called an inner join). That is, an inner join states to get only the records in Orders that match the records in Order Details by having the same value in the OrderID field (on o.Orderid = od.Orderid).

Tip

SQL joins are inner joins by default. Writing join orders is the same as writing inner join orders.

The SQL statement goes on to ask the database to create an inner join with Products, getting every row in which the ProductID in the Products table is the same as the ProductID in the Order Details table.

Then, create an inner join with customers for those rows where the CustomerID is the same in both the Orders table and the Customers table.

Finally, tell the database to constrain the results to only those rows in which the CompanyName is the one you want, and the dates are in July.

The collection of constraints finds only three records that match:

OrderID ProductName
----------- ----------------------------------------
10248 Queso Cabrales
10248 Singaporean Hokkien Fried Mee
10248 Mozzarella di Giovanni

This output shows that there was only one order (10248) in which the customer had the right ID and in which the date of the order was July 1996. That order produced three records in the Order Details table, and using the product IDs in these three records, you got the product names from the Products table.

You can use SQL not only for searching for and retrieving data, but also for creating, updating, and deleting tables, and generally managing and manipulating both the content and the structure of the database.

The ADO.NET Object Model

The ADO.NET object model is rich, but at its heart it is a fairly straightforward set of classes. The most important of these is the DataSet. The DataSet represents a subset of the entire database, cached on your machine without a continuous connection to the database.

Periodically, you’ll reconnect the DataSet to its parent database, update the database with changes you’ve made to the DataSet, and update the DataSet with changes in the database made by other processes.

This is highly efficient, but to be effective, the DataSet must be a robust subset of the database, capturing not just a few rows from a single table, but also a set of tables with all the metadata necessary to represent the relationships and constraints of the original database. This is, not surprisingly, what ADO.NET provides.

The DataSet is composed of DataTable objects as well as DataRelation objects. These are accessed as properties of the DataSet object. The Tables property returns a DataTableCollection, which in turn contains all the DataTable objects.

DataTables and DataColumns

The DataTable can be created programmatically or as a result of a query against the database. The DataTable has a number of public properties, including the Columns collection, which returns the DataColumnCollection object, which in turn consists of DataColumn objects. Each DataColumn object represents a column in a table.

DataRelations

In addition to the Tables collection, the DataSet has a Relations property, which returns a DataRelationCollection consisting of DataRelation objects. Each DataRelation represents a relationship between two tables through DataColumn objects. For example, in the Northwind database, the Customers table is in a relationship with the Orders table through the CustomerID column.

The nature of this relationship is one-to-many, or parent-to-child. For any given order, there will be exactly one customer, but any given customer might be represented in any number of orders.

Rows

DataTable’s Rows collection returns a set of rows for that table. Use this collection to examine the results of queries against the database, iterating through the rows to examine each record in turn. Programmers experienced with ADO are often confused by the absence of the RecordSet with its moveNext and movePrevious commands. With ADO.NET, you don’t iterate through the DataSet; instead, you access the table you need, and then you can iterate through the Rows collection, typically with a foreach loop. You’ll see this in the example in this chapter.

Data Adapter

The DataSet is an abstraction of a relational database. ADO.NET uses a DataAdapter as a bridge between the DataSet and the data source, which is the underlying database. DataAdapter provides the Fill( ) method to retrieve data from the database and populate the DataSet.

Instead of tying the DataSet object too closely to your database architecture, ADO.NET uses a DataAdapter object to mediate between the DataSet object and the database. This decouples the DataSet from the database and allows a single DataSet to represent more than one database or other data source.

DBCommand and DBConnection

The DBConnection object represents a connection to a data source. This connection can be shared among different command objects. The DBCommand object allows you to send a command (typically, a SQL statement or a stored procedure) to the database. Often, these objects are implicitly created when you create a DataAdapter, but you can explicitly access these objects; for example, you can declare a connection as follows:

string connectionString = "server=.\sqlexpress;" +
"Trusted_Connection=yes; database=Northwind";

You can then use this connection string to create a connection object or to create a DataAdapter object.

DataReader

An alternative to creating a DataSet (and DataAdapter) is to create a DataReader. The DataReader provides connected, forward-only, read-only access to a collection of tables by executing either a SQL statement or stored procedures. DataReaders are lightweight objects that are ideally suited for filling controls with data and then breaking the connection to the backend database.

Getting Started with ADO.NET

Enough theory! Let’s write some code and see how this works. Working with ADO.NET can be complex, but for many queries, the model is surprisingly simple.

In this example, we’ll create a console application, and we’ll list out bits of information from the Customers table in the Northwind database.

Begin by creating a (SQL Server-specific) DataAdapter object:

SqlDataAdapter DataAdapter =
new SqlDataAdapter(
commandString, connectionString);

The two parameters are commandString and connectionString. The commandString is the SQL statement that will generate the data you want in your DataSet:

string commandString =
 "Select CompanyName, ContactName from Customers";

The connectionString is whatever string is needed to connect to the database. Typically, this will be SQL Server Express, which is installed with Visual Studio.

With the DataAdapter in hand, you’re ready to create the DataSet and fill it with the data that you obtain from the SQL select statement:

DataSet DataSet = new DataSet( );
DataAdapter.Fill(DataSet,"Customers");

That’s it. You now have a DataSet, and you can query, manipulate, and otherwise manage the data. The DataSet has a collection of tables; you care only about the first one because you’ve retrieved only a single table:

DataTable dataTable = DataSet.Tables[0];

You can extract the rows you’ve retrieved with the SQL statement and add the data to the listbox:

foreach (DataRow dataRow in dataTable.Rows)
{
    lbCustomers.Items.Add(
    dataRow["CompanyName"] +
    " (" + dataRow["ContactName"] + ")" );
}

The listbox is filled with the company name and contact name from the table in the database, according to the SQL statement we passed in. Example 16-1 contains the complete source code for this example.

Example 16-1. Working with ADO.NET
using System;
using System.Data;
using System.Data.SqlClient;

namespace Working_With_ADO.NET
{
    class Program
    {
        static void Main(string[] args)
        {
            string connectionString = "server=.\sqlexpress;" +
            "Trusted_Connection=yes; database=Northwind";

            // get records from the Customers table
            string commandString =
            "Select CompanyName, ContactName from Customers";

            // create the data set command object
            // and the DataSetSqlDataAdapter DataAdapter =
            new SqlDataAdapter(
            commandString, connectionString);

            DataSet DataSet = new DataSet( );
            // fill the DataSet object
            DataAdapter.Fill(DataSet, "Customers");

            // Get the one table from the DataSet
            DataTable dataTable = DataSet.Tables[0];

            // for each row in the table, display the info
            foreach (DataRow dataRow in dataTable.Rows)
            {
                Console.WriteLine("CompanyName: {0}. Contact: {1}", dataRow["
CompanyName"],
                    dataRow["ContactName"]);
            }
        }
    }
}

Output (partial)
CompanyName: Centro comercial Moctezuma. Contact: Francisco Chang
CompanyName: Chop-suey Chinese. Contact: Yang Wang
CompanyName: Comércio Mineiro. Contact: Pedro Afonso
CompanyName: Consolidated Holdings. Contact: Elizabeth Brown
CompanyName: Drachenblut Delikatessen. Contact: Sven Ottlieb
CompanyName: Du monde entier. Contact: Janine Labrune
CompanyName: Eastern Connection. Contact: Ann Devon
CompanyName: Ernst Handel. Contact: Roland Mendel
CompanyName: Familia Arquibaldo. Contact: Aria Cruz
CompanyName: FISSA Fabrica Inter. Salchichas S.A.. Contact: Diego Roel
CompanyName: Folies gourmandes. Contact: Martine Rancé
CompanyName: Folk och fä HB. Contact: Maria Larsson
CompanyName: Frankenversand. Contact: Peter Franken
CompanyName: France restauration. Contact: Carine Schmitt
CompanyName: Franchi S.p.A.. Contact: Paolo Accorti
CompanyName: Furia Bacalhau e Frutos do Mar. Contact: Lino Rodriguez
CompanyName: Galería del gastrónomo. Contact: Eduardo Saavedra
CompanyName: Godos Cocina Típica. Contact: José Pedro Freyre
CompanyName: Gourmet Lanchonetes. Contact: André Fonseca
CompanyName: Great Lakes Food Market. Contact: Howard Snyder
CompanyName: GROSELLA-Restaurante. Contact: Manuel Pereira
CompanyName: Hanari Carnes. Contact: Mario Pontes
CompanyName: HILARION-Abastos. Contact: Carlos Hernández
CompanyName: Hungry Coyote Import Store. Contact: Yoshi Latimer
CompanyName: Hungry Owl All-Night Grocers. Contact: Patricia McKenna
CompanyName: Island Trading. Contact: Helen Bennett
CompanyName: Königlich Essen. Contact: Philip Cramer
CompanyName: La corne d'abondance. Contact: Daniel Tonini
CompanyName: La maison d'Asie. Contact: Annette Roulet
CompanyName: Laughing Bacchus Wine Cellars. Contact: Yoshi Tannamuri
CompanyName: Lazy K Kountry Store. Contact: John Steel

With just a few lines of code, you have extracted a set of data from the database and displayed it. This code will:

  • Create the string for the connection:

    string connectionString = "server=.\sqlexpress;" +
    "Trusted_Connection=yes; database=northwind";
  • Create the string for the select statement:

    string commandString =
    "Select CompanyName, ContactName from Customers";
  • Create the DataAdapter and pass in the select and connection strings:

    SqlDataAdapter DataAdapter =
    new SqlDataAdapter(
    commandString, connectionString);
  • Create a new DataSet object:

    DataSet DataSet = new DataSet( );
  • Fill the DataSet from the Customers table using the DataAdapter:

    DataAdapter.Fill(DataSet,"Customers");
  • Extract the DataTable from the DataSet:

    DataTable dataTable = DataSet.Tables[0];
  • Use the DataTable to fill the listbox:

    foreach (DataRow dataRow in dataTable.Rows)
    {
     lbCustomers.Items.Add(
     dataRow["CompanyName"] +
     " (" + dataRow["ContactName"] + ")" );
    }
..................Content has been hidden....................

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