Using ADO.NET to access data

Một phần của tài liệu ASP.NET 4.0 in Practice phần 10 potx (Trang 24 - 32)

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.

Một phần của tài liệu ASP.NET 4.0 in Practice phần 10 potx (Trang 24 - 32)

Tải bản đầy đủ (PDF)

(55 trang)