Jonathan,
Servers have reached (and passed) the point where holding data in memory is
viable and preferable to being connected to the underlying data source all
the time. Given that distributed architectures are so prevalent these days,
the load on the database becomes more of an issue of short-lived objects in
RAM.
This is what ADO .NET is all about (disconnected access to your data). Test
after test and benchmark after benchmark have shown it to be a more
efficient way of modeling the entire application than the older connected
ways of ADO. When dealing with large amounts of data (1,000's of records),
you don't grab and store it all at once, you make several trips to the
database, fetching the data you happen to need at that time (perhaps
hundreds of records on each call). This just isn't the issue that you think
it is.
A DataAdapter is really a form of composite class. It uses information from
your connection in conjunction with 4 child command objects to perform your
CRUD actions against the database. In the case of a read operation, it
turns out that a DataAdapter uses a DataReader internally to iterate through
the data and copy it to a DataTable so that the connection can then be
closed and the data can be used in memory only. The DataSet keeps track of
any edits that are done to its underlying DataTable data and when the time
comes to propagate the changes back to the original data source the
DataAdapter simply says to the DataSet "give me your changed data only and
I'll push those changes back to the source". It does this by using the
internal command object it has that is set up for doing Creates, Updates or
Deletes (based on what has changed to the data, the DataAdapter invokes the
appropriate command to perform the correct action). The other nice thing
that the DataAdapter does is that is automatically opens and closes the
connection for you, ensuring that the connection is only open for the
shortest possible time. So you get the benefit of an automated copy from
the database / make changes to the database, with the efficiency of
DataReaders for the select operations.
Now, DataReaders have their place. For situations where a read-only,
forward-only firehose-type cursor is needed (like searching a data source
for a matching record), you can't beat a DataReader. But, even in this
case, once you've found the data you are looking for, you should copy it to
a local container, close your DataReader and your Connection.
You asked for a "rock solid" example that wouldn't break down (without
using). Here's the VB way (sorry, I'm not efficient enough to show you the
C# translation):
Public Class DataLayer
Dim con As New
SQLClient.SQLConnection(ConfigurationManager.ConnectionStrings("ASPNETDB").ToString())
Public Function GetProductData() As DataSet
Dim cmd As New SQLClient.SQLCommand(con)
cmd.CommandType = StoredProcedure
cmd.CommandText = "storedProcName"
Dim da As new SQLCLient.SQLDataAdapter(cmd, con)
Dim ds As New DataSet()
Try
da.Fill(ds)
Catch e As SQLClient.SQLException
'DataBase thrown exception handling here
Catch e As Exception
'All other exceptions handled here
Finally
'A DataAdapter will close the connection for you, but if there
was an
'exception before it gets the chance this will cover us and it
won't cause
'any problems if the connection has already been closed.
con.close()
con.Dispose()
End Try
Return ds
End Function
End Class
Within this data layer class, you could add other methods for doing other
CRUD related operations, each method reusing the connection object. This is
a good example of abstracting data logic from business logic as well.
As to non-deterministic finalization, I don't see why you think this breaks
encapsulation. "When" the object is cleaned up does not have anything to do
with object's functionality being encapsulated within it. After all, we
aren't doing any database cleanup in the Finalize method, nor the Dispose
method. These methods simply prepare the object for destruction, but even
still their functionality is encapsulated within the object.
Jonathan Wood said:
Scott,
public static SqlDataReader ExecuteProcedure(string procName)
{
string connStr =
ConfigurationManager.ConnectionStrings["ASPNETDB"].ToString();
using (SqlConnection conn = new SqlConnection(connStr))
{
//You can get rid of the "using" statement with your command since a
command does not
//hold on to any unmanaged resources and does not need to be
disposed.
Okay, I'll look at that.
You would capture the data you need and store it (in a DataSet or
DataTable) and return that to your calling code, not the DataReader. You
are correct that the reader must stay open as long as you are iterating
through the data (much like a firehose cursor), so you want to get in and
get out as quick as you can. ADO .NET (unlike ADO) is all about
"diconnected" access to your data.
Yes, but isn't a DataReader more efficient in that it doesn't load the
entire results in memory? I'm only going to display some data based on the
results. So don't I lose that advantage by using an approach that loads
all the data into RAM?
Either use a DataAdapter to automatically execute your command and manage
the opening and closing of your connection (so you won't have to worry
about using "using" on the connection obejct or loop through your
DataReader data as soon as you get it and copy it over to a DataTable.
Then just close your reader/connection and return the container.
I'm still not clear on what a DataAdapter is. I'll save your message
though and do a bit more digging.
Using is a nice feature, but hardly required. If you simply call
.close() on your DataReader and your Connection (actually, if you set up
your DataReader properly, it will close your connection when you close
the reader) or use a DataAdapter, which opens and closes these objects
for you automatically, then "using" is irrelevant.
But doesn't that approach break down if there's an exception. I'd love to
see you reproduce the code I posted without using that was rock solid and
would clean up quickly even if exceptions were thrown.
You can look at deterministic finalization another way too: You can get
into trouble with it, if you simply forget to destroy an object
reference, so in either case, you have to be a good programmer.
Well, that's garbage collection and not deterministic finalization.
Unfortunately, it just happens to appear that we can only get one or the
other. I understand the benefits of automatically disposed objects, but I
find the lack of deterministic finalization a step away from
encapsulation, the main point of OOP. And I keep running into this as a
hard issue for me in C#.
Thanks.