ADO.NET is a technology that enables physical interaction with a database. Inter- nally, it leverages Component Object Model (COM) providers, but it exposes func- tionalities through .NET classes. Most of the complexity of communicating with the database is stripped away, and you only have to deal with ADO.NET classes.
Even if working with classes is somewhat hard, you can do lots of things to sim- plify the process of retrieving data and pouring it into objects. The reverse process
449 TECHNIQUE 102 Querying the database using ADO.NET
has the same issue; persisting entities data into a database is code expensive.
You have to create and open a connec- tion to the database, issue the com- mands, process the result, and close the connection. This flow is shown in figure B.1.
Another problem is that if you need to issue multiple commands that change data, you have to deal with the transaction, too. Let’s put all this stuff in practice.
Querying the database using ADO.NET
As we said before, querying the database involves many steps. In this section, we’re going to look at them so that you can understand how to issue a query to the database and get back objects that you can work with.
PROBLEM
Suppose you have to create a web form that shows orders. Because the Northwind database has hundreds of orders, you can’t show them all at once and you have to page them. This scenario is common in most web applications that need to show lists of data. The page doesn’t have to access the database directly but must rely on the business layer or the domain model (we talked about these two layers in chapter 2) to retrieve data. They must abstract persistence from UI.
SOLUTION
For this particular example, the UI problem isn’t what matters so let’s focus on the code that interacts with the database. What we have to do is create a method that opens a connection, sends the query to the database, iterates over the result, and, for each record, creates an object that fills its properties with database data. Finally, we have to close the connection and return the objects to the UI. Sounds easy, doesn’t it?
Connecting to the database is just a matter of instantiating the SqlConnection class located in the System.Data.SqlClient namespace, passing in the connection string and invoking the Open method.
NOTE The connection string contains information about the database loca- tion plus other additional information that can be different across different platforms. SqlConnection passes it to the COM infrastructure to physically connect to the database. The application configuration file contains a section where you can place any connection string, and the .NET Framework class library contains APIs to retrieve them. We’ll use such APIs in this appendix instead of always rewriting the connection string.
Because the connection implements the IDisposable interface, we can wrap it inside a using block, as in the following listing, so that it’s automatically disposed (and closed) at the end of the block.
TECHNIQUE 102
Open connection
Execute query
Process data
Close connection
Figure B.1 The query execution workflow. First, a connection is created and opened. Later, the query is executed and the result is processed by our code.
Finally, the connection is closed.
450 APPENDIX B Data access fundamentals
C#:
var connString = ConfigurationManager.
ConnectionStrings["conn"].ConnectionString;
using (var conn = new SqlConnection(connString)) {
conn.Open();
...
} VB:
Dim connString = ConfigurationManager.
ConnectionStrings("conn").ConnectionString Using conn = New SqlConnection(connString) conn.Open()
...
End Using
Okay, we’ve completed the first step. Now we need to create the Order class and put data inside it. For brevity’s sake, we won’t show the Order code here. It’s a simple class that has a property for each column in the Orders table with the addition of only the Customer and Order_Details properties (which reference the customer who placed the order and the details of the order).
After the class is created, we can issue a SELECT command to the server using the SqlCommand class, as shown in Listing B.2. This class is responsible for issuing any type of command to the database. Because we have to retrieve a set of records, we’ll use the ExecuteReader method, which returns an SqlDataReader instance. This instance is a read-only and forward-only kind of cursor.
C#:
string sql = "WITH cte AS " +
"(SELECT *, ROW_NUMBER() OVER(ORDER BY orderid) AS RowNumber " + "FROM orders) " +
"SELECT * FROM cte " +
"WHERE RowNumber >= @startIndex AND RowNumber <= @endIndex ";
using (var comm = new SqlCommand(sql, conn)) {
comm.Parameters.AddWithValue("startIndex", ((pageIndex-1) * pageCount));
comm.Parameters.AddWithValue("endIndex", (pageIndex * pageCount));
var result = new List<Order>();
conn.Open();
using (var reader = comm.ExecuteReader()) {
while (reader.Read()) {
...
} }
Listing B.1 Connecting to a database
Listing B.2 Issuing a command
451 TECHNIQUE 102 Querying the database using ADO.NET
VB:
Dim sql As String = "WITH cte AS " &
"(SELECT *, ROW_NUMBER() OVER(ORDER BY orderid) AS RowNumber " &
"FROM orders) " &
"SELECT * FROM cte " &
"WHERE RowNumber >= @startIndex AND RowNumber <= @endIndex "
Using comm = New SqlCommand(sql, conn)
comm.Parameters.AddWithValue("startIndex", ((pageIndex - 1) * pageCount)) comm.Parameters.AddWithValue("endIndex", (pageIndex * pageCount))
Dim result = New List(Of Order)() conn.Open()
Using reader = comm.ExecuteReader() While reader.Read()
...
End While End Using End Using
Now we have to create objects from the data reader. Once again, it’s simple. You just iterate over the records create an object for each one. Then, you pour data from record columns into object properties. This technique is shown in listing B.3. Keep in mind that the Get and GetNullable methods aren’t SqlDataReader methods but con- venient extension methods we’ve created to cut down on some lines of code. You’ll find these in the downloadable code for the book.
C#:
Order o = new Order() {
EmployeeID = reader.Get<int>("EmployeeID"), Freight = reader.GetNullable<decimal>("Freight"), OrderDate = reader.Get<DateTime>("OrderDate"), OrderID = reader.Get<int>("OrderID"),
RequiredDate = reader.GetNullable<DateTime>("RequiredDate"), ShipAddress = reader.Get<string>("ShipAddress"),
ShipCity = reader.Get<string>("ShipCity"), ShipCountry = reader.Get<string>("ShipCountry"), ShipName = reader.Get<string>("ShipName"),
ShippedDate = reader.GetNullable<DateTime>("ShippedDate"), ShipPostalCode = reader.Get<string>("ShipPostalCode"), ShipRegion = reader.Get<string>("ShipRegion"),
ShipVia = reader.GetNullable<int>("ShipVia") };
result.Add(o);
VB:
Dim o As New Order() With { _
.EmployeeID = reader.[Get](Of Integer)("EmployeeID"), _ .Freight = reader.GetNullable(Of Decimal)("Freight"), _ .OrderDate = reader.[Get](Of DateTime)("OrderDate"), _ .OrderID = reader.[Get](Of Integer)("OrderID"), _
.RequiredDate = reader.GetNullable(Of DateTime)("RequiredDate"), _ Listing B.3 Creating objects from a data reader
452 APPENDIX B Data access fundamentals
.ShipAddress = reader.[Get](Of String)("ShipAddress"), _ .ShipCity = reader.[Get](Of String)("ShipCity"), _ .ShipCountry = reader.[Get](Of String)("ShipCountry"), _ .ShipName = reader.[Get](Of String)("ShipName"), _
.ShippedDate = reader.GetNullable(Of DateTime)("ShippedDate"), _ .ShipPostalCode = reader.[Get](Of String)("ShipPostalCode"), _ .ShipRegion = reader.[Get](Of String)("ShipRegion"), _
.ShipVia = reader.GetNullable(Of Integer)("ShipVia") _ }
result.Add(o)
Congratulations! You’ve successfully connected to a database, issued a query, and cre- ated objects from it.
DISCUSSION
The code for this example wasn’t difficult to write, but embedding queries inside the code is something that’s not appealing for database administrators. They always prefer that you use stored procedures because these can be controlled.
Using stored procedures to query the database
Stored procedures offer a big advantage. They enable a high level of isolation between the code and the database. If you need to optimize or change a query, you can do it without recompiling the application.
PROBLEM
Suppose that you have to modify the problem in the previous section to use a stored procedure instead of the embedded SQL statement. This scenario is common when you have a DBA who wants full control over SQL statements issued to the database and you want to raise isolation between code and database.
SOLUTION
Invoking a stored procedure is extremely simple. The code differs only slightly from what we created previously. In fact, invoking a stored procedure is just a matter of using its name instead of the full SQL statement and setting the CommandType property of the SqlCommand class. The following listing shows the necessary code.
C#:
string sql = "GetOrders";
using (var comm = new SqlCommand(sql, conn)) {
comm.CommandType = CommandType.StoredProcedure;
...
} VB:
Dim sql As String = "GetOrders"
Using comm = New SqlCommand(sql, conn)
comm.CommandType = CommandType.StoredProcedure ...
End Using
Listing B.4 Invoking a stored procedure TECHNIQUE 103
453 TECHNIQUE 104 Persisting data into the database
Believe it or not, that’s all you need to do. With a tiny change you get lots of benefits.
DISCUSSION
Using a stored procedure is a must in many applications. Fortunately ADO.NET was designed to enable this feature, too. Thanks to this design, invoking stored proce- dures is easy.
So far, you’ve seen only how to query the database. We’re still missing the other side of the coin: saving data in an object into the database.
Persisting data into the database
When you need to save data into the database, the process is identical to what we did before. You open a connection, execute the command, and close the connection. The only optional variation is that if you have to send more than one command, you have to use a transaction to ensure an all-or-nothing update. If a command goes wrong, you can roll back the transaction and invalidate all previous commands; if everything works fine, you can commit the transaction so that all changes made by the commands become persistent. Figure B.2 shows this workflow.
Now let’s see how we can write code that represents the workflow shown in figure B.2.
PROBLEM
Suppose you have a form in which the user can update the order information. He can change the shipping address, as well as the shipping date or the shipment method. He can also add a new detail, modify an existing one (for instance, change the quantity or the discount), and remove one or more of them. What we have to do is create a data access code to handle all these modifications.
SOLUTION
To resolve this problem, you can create a method that accepts the order, and three parameters that represent the details that were added, modified, or removed. In that method you can then launch a command to update the order and launch other com- mands for each of the details.
TECHNIQUE 104
Open connection
Start transaction
Send commands
Close connection
Commit transaction Roll back transaction No Errors Yes
Figure B.2 The database update workflow. First, we open the connection and start the transaction. Next, we send commands to the database. If all the commands are executed correctly, we commit the transaction; otherwise, we roll it back.
454 APPENDIX B Data access fundamentals
Because we have to issue multiple commands, we have to wrap them inside a transac- tion and manually commit or roll it back, depending on errors. If the user was able to update only the order, the transaction doesn’t need to be completed.
Sending a command to update the database requires you to use another method of the SqlCommand class: ExecuteNonQuery. It doesn’t accept any parameter, but it returns an Int32 representing the number of rows that were affected by the command.
To start a transaction, you have to call the BeginTransaction method of the Sql- Connection class. That method returns a SqlTransaction object that you later have to pass to the SqlCommand object, along with the connection. To commit or roll back a transaction, you have to call the Commit or Rollback methods respectively, as in the following listing.
C#:
using (var conn = new SqlConnection(connString)) {
using (var tr = conn.BeginTransaction()) {
try {
string sql = "UpdateOrder";
using (var comm = new SqlCommand(sql, conn, tr)) {
comm.CommandType = CommandType.StoredProcedure;
comm.Parameters.AddWithValue("ShipAddress", order.ShipAddress);
comm.Parameters.AddWithValue("ShipCity", order.ShipCity);
comm.Parameters.AddWithValue("ShipCountry", order.ShipCountry);
comm.Parameters.AddWithValue("ShipName", order.ShipName);
comm.Parameters.AddWithValue("ShipPC", order.ShipPostalCode);
comm.Parameters.AddWithValue("ShipRegion", order.ShipRegion);
comm.Parameters.AddWithValue("ShipVia", order.ShipVia);
comm.Parameters.AddWithValue("OrderId", order.ShipVia);
comm.ExecuteNonQuery();
}
foreach (var detail in addedDetails) {
...
}
foreach (var detail in modifiedDetails) {
...
}
foreach (var detail in deletedDetails) {
...
}
tr.Commit();
} catch {
tr.Rollback();
Listing B.5 Persisting data using a transaction
Open connection Start transaction
Execute command
Commit if no exception Rollback if
exception
455 TECHNIQUE 104 Persisting data into the database
} } } VB:
Using conn = New SqlConnection(connString) Using tr = conn.BeginTransaction() Try
Dim sql As String = "UpdateOrder"
Using comm = New SqlCommand(sql, conn, tr) comm.CommandType = CommandType.StoredProcedure
comm.Parameters.AddWithValue("ShipAddress", order.ShipAddress) comm.Parameters.AddWithValue("ShipCity", order.ShipCity) comm.Parameters.AddWithValue("ShipCountry", order.ShipCountry) comm.Parameters.AddWithValue("ShipName", order.ShipName) comm.Parameters.AddWithValue("ShipPC", order.ShipPostalCode) comm.Parameters.AddWithValue("ShipRegion", order.ShipRegion) comm.Parameters.AddWithValue("ShipVia", order.ShipVia) comm.Parameters.AddWithValue("OrderId", order.ShipVia) comm.ExecuteNonQuery() End Using
For Each detail As var In addedDetails ...
Next
For Each detail As var In modifiedDetails ...
Next
For Each detail As var In deletedDetails ...
Next
tr.Commit() Catch
tr.Rollback() End Try
End Using End Using
The code inside the loop has been omitted because it simply invokes the stored proce- dures that add, modify, and delete details. That’s not at all different from the code used to update the order.
What’s interesting in this code is the transaction management. All code is inside a try/catch block. The last statement of the try block is the Commit method, and the only statement of the catch block is the Rollback method, which invalidates all the commands executed in the try block.
DISCUSSION
In the end, modifying data is similar to reading it. In the first case, you read it and cre- ate a set of classes and in the other one, you read classes and pour their values inside the database.
All the code we’ve written so far involves only the Order class (and the Order_Details class, in the last example), but the complete scenario requires more.
Are you thinking about how much code you would have to write to query all the classes? In a real-world project, you would end up writing thousands of lines of code.
Open connection
Start transaction
Execute command
Commit if no exception Rollback if
exception
456 APPENDIX B Data access fundamentals
But there’s more. Suppose that in another page, you have to show orders and their related customers. That means that the code we’ve seen so far is somewhat limited because it treats only one table and one class. You have to write new code to handle both the Order and Customer classes in a single query. As complexity grows, so do the lines of code.
And what about this problem: the objects paradigm is completely different from the database paradigm. Databases don’t have the concept of inheritance, they keep relationships using foreign keys (objects use references to other objects), and they organize data in rows and columns (objects organize data in properties that can con- tain a scalar value or other objects). Handling such differences in code is trivial in some scenarios but painful in others.
Working with pure ADO.NET classes represents the most basic way of writing data access code. You can use third-party libraries, like the Microsoft Enterprise Library, but that’s just a way to eliminate lots of lines of code. Now you know why Entity Frame- work greatly simplifies development.