My talk at a Fawcette conference XXXXX VSLIVE TEMPLATE XXXX
Download
Report
Transcript My talk at a Fawcette conference XXXXX VSLIVE TEMPLATE XXXX
ADO.NET Tips and Tricks
How to get the most out of your
data access…
Who I Am
Shawn Wildermuth ([email protected])
Independent Consultant (http://adoguy.com)
C# MVP
MCSD.NET
INETA Speaker
Book Author
– “Pragmatic ADO.NET”
Editor of http://ONDotnet.com
This Presentation can be found at:
– http://adoguy.com/presentations
Overview
Can you do ADO.NET Wrong?
Unmanaged Resources
Connections
DataSets
Typed DataSets
CommandBuilders
DataAdapters
DataSets & ASP.NET
Distributed DataSets
Transactions
Vendor Neutral Code
Are You Doing Anything Wrong?
Can you write bad ADO.NET Code?
Yes…but:
– It is usually intuitive about what is right
– Disconnected nature makes it harder to break
Areas of confusion about ADO.NET
– Easier to scale, but not fool-proof
– Disconnected does not mean scalable
– Concurrency is harder than older systems
(e.g. ADO)
– “Why Can’t I Lock Rows”
Unmanaged Resources
Most ADO.NET objects are IDisposable
– Make sure and Dispose()
– Inheritance from Component means Dispose()
– Not disposing objects will cause leaky code
Dim conn As New SqlConnection("...")
Dim cmd As SqlCommand = conn.CreateCommand()
cmd.CommandText = "..."
conn.Open()
Dim rdr As SqlDataReader = cmd.ExecuteReader()
While rdr.Read()
Console.WriteLine(rdr.GetString(0))
End While
conn.Close()
' potential leaky code even though you closed the connection!
Unmanaged Resources (3)
Use Try…Finally
– No “using” in VB.NET, so must do it all in finally
– Ensures always disposed, even if exception
Dim conn as new SqlConnection("...")
Dim cmd as SqlCommand = conn.CreateConnection
Try
cmd.CommandText = "..."
conn.Open()
Dim rdr as SqlDataReader = cmd.ExecuteReader()
While rdr.Read() = True
Console.WriteLine(rdr.GetString(0))
End While
Finally ' Clean Up
conn.Close()
rdr.Dispose()
cmd.Dispose()
conn.Dispose()
End Try
Connections
Connections are precious
– In DataReaders, let it close it when it is done:
Try
conn.Open()
Dim rdr As SqlDataReader =
cmd.ExecuteReader(CommandBehavior.CloseConnection)
While rdr.Read()
' Closes Here on last read
Console.WriteLine(rdr.GetString(0))
End While
Finally
' Make sure we didn’t throw an exception and leak a connection
conn.Close()
End Try
Connections (2)
Connections are still precious
– With DataSets, never open them:
Dim ds As New DataSet
Dim conn As New SqlConnection("...")
Dim adapter As New SqlDataAdapter("...")
' The adapter will always preserve the state
' of the connection during the fill
' No Need to Open Or Close the connection because the adapter will
' close it as soon as possible since it is already closed
adapter.Fill(ds)
' If it is open, it will leave it open
conn.Open()
adapter.Fill(ds)
conn.Close()
Connections (3)
Don’t Hardcode Connection Strings
– VS.NET Designer Does it Wrong
– Store Them in .Config Files
<configuration>
<appSettings>
<add key="SqlConnString"
value="Server=localhost;Database=Northwind;..." />
</appSettings>
</configuration>
<configuration>
<appSettings>
<!-- Better yet, encrypt them in the .config file -->
<add key="SqlConnString"
value="E234998F98A98F..." />
</appSettings>
</configuration>
Connections (4)
Use Connection Factories
– Isolates Connection String Storage
– Increases Security by Controlling Access
Public Class ConnectionStrings
Public Shared Function GetConnection() _
As OleDbConnection
return new OleDbConnection( _
ConfigurationSettings.AppSettings("ConnString"))
End Function
End Class
Connections (5)
Secure Connection Strings with Encryption
– You could write your own Encryption Library
– Better would be if MS supplied one
In Windows 2000 and later, use DPAPI
DPAPI Eliminates the Key Management Problem
Encrypts on a Per Machine or Per User Basis
Caveat: May Require Encrypting on Every Machine
Described in Detail in a Microsoft PAP:
– http://msdn.microsoft.com/library/en-us/dnnetsec/html/SecNetHT07.asp
DataSets
DataSets have full database schema
– Tables
– Columns
– Primary and Foreign Keys
– Constraints
– Even Triggers
Using Full Schema
– Allows validation without a network roundtrip
– Relationships allows hierarchical view of data
DataSets (2)
Use DataSets for data caching
– Most data is not volatile
– Saves roundtrips to the database
– Less code than writing custom classes
Public Class _default
Inherits Page
Dim _dataSet As DataSet
Const MYDATASET As String = "MYDATASET"
Sub OnLoad(...)
If Session(MYDATASET) Is Nothing Then
_dataSet = New DataSet
Else
_dataSet = DirectCast(Session(MYDATASET), DataSet)
End If
'...
Session(MYDATASET) = _dataSet
End Sub
End Class
DataSets (3)
DataSets can incrementally grow
– DataAdapter.Fill adds to DataSet, not replace
– Allows cache to be built up as needed
' Fill DataSet with Product #1
adapter.SelectCommand.Parameters("@product").Value = 1
adapter.Fill(_dataSet)
'...
' Fill DataSet with Product #2
adapter.SelectCommand.Parameters("@product").Value = 2
adapter.Fill(_dataSet)
' Now DataSet has both products in, not just one or the other
DataSets (4)
Use DataSets for intermittent connectivity
– Store locally between times of connectivity
– Stored DataSets preserve state of changes
– Great for mobile apps (not just CF)
Public Class MainForm
Inherits Form
Dim _dataSet as DataSet
Sub OnClose(...)
_dataSet.WriteXml("foo.xml", XmlWriteMode.DiffGram)
End Sub
Sub OnLoad(...)
_dataSet.ReadXml("foo.xml", XmlReadMode.DiffGram)
End Sub
End Class
Typed DataSets
Use Typed DataSets for Stable Schemas
– Data Validate at Compile Time
– Improves Developer Productivity with
Intellisense
– Easier to maintain in light of schema changes
than code
– Loads XSD Schema in with Code (faster)
– Demo
CommandBuilders
Good for Simple Optimistic Concurrency
– Concurrency based on original values
– Robust, but Inefficient
– Update and Deletes are huge
– No support for Stored Procedures
DELETE FROM CUSTOMER WHERE ( (CustomerID = @p3) AND
((FirstName IS NULL AND @p4 IS NULL) OR (FirstName = @p5)) AND
((LastName IS NULL AND @p6 IS NULL) OR (LastName = @p7)) AND
((MiddleName IS NULL AND @p8 IS NULL) OR (MiddleName = @p9)) AND
((Address IS NULL AND @p10 IS NULL) OR (Address = @p11)) AND
((Apartment IS NULL AND @p12 IS NULL) OR (Apartment = @p13)) AND
((City IS NULL AND @p14 IS NULL) OR (City = @p15)) AND
((State IS NULL AND @p16 IS NULL) OR (State = @p17)) AND
((Zip IS NULL AND @p18 IS NULL) OR (Zip = @p19)) AND
((HomePhone IS NULL AND @p20 IS NULL) OR (HomePhone = @p21)) AND
((BusinessPhone IS NULL AND @p22 IS NULL) OR (BusinessPhone = @p23)) AND
((DOB IS NULL AND @p24 IS NULL) OR (DOB = @p25)) AND
((Discount IS NULL AND @p26 IS NULL) OR (Discount = @p27)) AND
((CheckedOut IS NULL AND @p28 IS NULL) OR (CheckedOut = @p29)) )
CommandBuilders (2)
Conclusion
– Great for Prototyping
– Bad for Production Code
– Designer can achieve same at compile time
DataAdapters
Use different adapters to load and update
– Batch Adapters to load multiple tables
– Single table adapters to update tables
Use Designer Support
–
–
–
–
Add a Component to project to hold Adapters
Use designer to create Mappings
Get CommandBuilder Behavior at Compile-time
Or Map to Stored Procedures (preferred)
Demo
DataSets & ASP.NET
Cache DataSets for non-volatile data
– Keep DataSets around for longer than a Page
– Store them in session/global/cache
– Expiring Caches are great for DataSets
– Growing the Cache as needed with DataSets
Public Class _default
Inherits Page
Dim _dataSet As DataSet
Const MYDATASET As String = "MYDATASET"
Sub OnLoad(...)
If Session(MYDATASET) Is Nothing Then
_dataSet = New DataSet
Else
_dataSet = DirectCast(Session(MYDATASET), DataSet)
End If
'...
Session(MYDATASET) = _dataSet
End Sub
End Class
DataSets and ASP.NET (2)
“What caching should I use?”
– Session for user specific data
E.g. User preferences, shopping cart, etc.
– Application for app-level data
E.g. e-commerce catalog, historical data, etc.
– Cache objects for volatile data
E.g. stock prices, weather data, etc.
Distributed DataSets
Treat DataSets as messages
– Same in Remoting DataSets as Web Services
– Remote the factory that delivers DataSets
– When updating, only return changes
– Minimize the data across the wire with Merge
Class DataSetFactory 'Remoted Class
' Remotely returns the DataSet
Public Function GetData() as DataSet
'...
End Function
Public Function SaveChanges(ByVal ds as DataSet) as DataSet
'...
End Function
End Class
Dim newDS as DataSet = factory.SaveChanges(ds.GetChanges())
ds.Merge(newDs)
Distributed DataSets (2)
Is fine to use DataSets in Web Services
– Default serialization is incorrect
DiffGram Format is Platform Specific
– You must make it an XML Document manually
Can use GetXml() or XmlDataDocument
<WebMethod> _
Public Function GetData() as XmlDocument
Dim ds as new DataSet()
' Fill the DataSet
return new XmlDataDocument(ds)
End Function
Distributed DataSets (3)
Typed DataSets are great in Web Services
– Can refer to .xsd in hand-coded WSDL
– While allowed, some toolkits don’t support it
– No way to make ?wsdl do it (at least not yet)
<definitions
...
xmlns:tds="http://yoursvr/typedds.xsd">
<types>
<xsd:complexType ref="tds:yourTypedDataSet" />
</types>
</definitions>
Distributed DataSets (4)
Remoting may not work as expected
– DataSets Remoted by Value
DataSet derive from MarshalByValueComponent
So always remoted by value!
– Remoting DataTables/Rows does not help
References to part of DataSet data transmit entire
DataSet
– Remoting DataSets can work
Must transmit XML or subset of XML (of the
DataSet)
Transactions
Server Transactions are usually preferable
– Generally shorter, therefore better
– Server Tx allows results to be returned
– Server Tx are not server cursors
Transactions (2)
Client Transactions Have their Place
– Allows a client lock of rows (connected model)
– Connection must be maintained for length of Tx
– Be careful when using them for locking
Scalability and performance will suffer
Must have recovery mechanism
Transactions (3)
Dim tx As SqlTransaction
Dim cmd As SqlCommand
Dim rdr As SqlDataReader
Try
conn.Open()
tx = conn.BeginTransaction()
' Get some Data
cmd = conn.CreateCommand()
cmd.CommandText = "SELECT * FROM Authors WHERE au_id = '172-32-1176'"
cmd.Transaction = tx ' Don’t forget to set the transaction into the cmd
rdr = cmd.ExecuteReader()
' Get the zip and close the reader
rdr.Read()
Dim zip As String = rdr("zip").ToString()
rdr.Close()
' Make a change
cmd.CommandText = "UPDATE authors SET zip = ‘12345’"
cmd.ExecuteNonQuery()
' Commit the Tx
tx.Commit()
Catch
tx.Rollback()
Finally
cmd.Dispose()
rdr.Dispose()
conn.Close()
conn.Dispose()
End Try
Should You Write Vendor Neutral Code?
The Promise of Vendor Neutral SQL is wrong
– Tuning databases too important
– Standard SQL isn’t complete
– Only do it if you have a compelling need
You can get half the promise
– Write your ADO.NET in a vendor neutral way
– You may need to port the back-end code
– Saves you from rewriting it all
Using Vendor Neutral Code
Interfaces are key
– IDbConnection is a great place to start
– Connection Factory to return IDbConnection
– Commands: IDbConnection.CreateCommand()
– Readers: IDbCommand.ExecuteReader()
Sub GetReader(SQL as String) as IDataReader
' Connection String Factory
Dim conn as IDbConnection =
ConnectionStrings.GetConnection()
' Follow the Rest of the model
Dim cmd as IDbCommand = conn.CreateCommand()
cmd.CommandText = SQL
Dim reader as IDataReader = cmd.ExecuteReader()
return reader
End Sub
Using Vendor Neutral Code (2)
Works with DataAdapters too (mostly)
– IDbConnection is a still a great place to start
– No Factory for Data Adapters unfortunately
– Can use the connection to create the command
Sub GetAdapter(SQL as String) as IDbDataAdapter
' Connection String Factory
Dim conn as IDbConnection =
ConnectionStrings.GetConnection()
' Create the DataAdapter
Dim adapter as new SqlDataAdapter()
' Follow the Rest of the model
adapter.SelectCommand = conn.CreateCommand()
adapter.SelectCommand.CommandText = SQL
return adapter
}
Using Vendor Neutral Code (3)
Vendor Specific Features
– Can cast to get specific features of an engine
– The more you do, the less vendor neutral it is
– This should be the exception, not the rule
Dim cmd as IDbCommand = GetCommand("SELECT * FROM AUTHORS FOR XML;")
If cmd is SqlCommand Then
Dim sqlCmd as SqlCommand = DirectCast(cmd, SqlCommand)
Dim xmlReader as XmlReader = sqlCmd.ExecuteXmlReader()
End If
Conclusion
You can write ADO.NET badly
Bad ADO.NET not nearly as toxic as bad
ADO
DataSets are your friend…really
Relationship of DataSets and XSD is good
Distributed DataSets are not that hard
CommandBuilders are vaguely evil
Questions?