Handling Database Writes in Data Access Layer Management
My first article on this topic demonstrated database reads-to-entities. Reads are relatively easy. Just read one or more result sets and build objects; you don't need transactions. This article demonstrates database writes. The write part of managing a data access layer (DAL) is where things can go awry. Writing requires managing changes, validation, transactions, and a variety of changes, including new, changed, and deleted objects.
This article demonstrates how to add and modify objects in a collection and then, using the basic DAL introduced in my previous article, how to manage writing those changes back to the persistence store. Although the code sample implicitly demonstrates insert and update behaviors, the text provides hints for handling validation and deletion too.
Modifying the Custom Objects
This example uses a simple console application to modify objects. (Listing 1 shows the code for the demo.) The beauty of a custom objects approach is that literally any kind of client can be used to modify the objects; they are just classes. Custom business objects also are lightweight and easy to use, and they behave in a persistence-layer-agnostic way.
Listing 1: Custom Business Objects Demo
Imports MYVB.BusinessObjects
Imports MYVB.DataAccess
Module Module1
Sub Main()
Dim customers As List(Of Customer) = _
CustomerAccess.GetCustomers()
For Each c As Customer In customers
Console.WriteLine(c.ContactName)
Next
' Add a customer, modify a second customer, and save
Dim newCustomer As Customer = New Customer( _
"PAUKI", "Fatman Soda Company", "P. Kimmel", _
"Boss", "", "", "", "", "", "", "")
customers.Add(newCustomer)
customers(0).ContactName = "Paul Kimmel"
' Trying to write all is terribly inefficient
' You could A) track changes or B) insert only those you
' know are new or have changed
CustomerAccess.PutCustomers(customers)
Console.ReadLine()
End Sub
End Module
As you can see, you can modify the collection and objects in any way desired. All of the writing is done at the collection level. However, you could easily write one object at a time, manage deletes, or incorporate change management logic to promote writing only objects that have changed.
Implementing the Write Behavior
For the CustomerAccess class to work with the basic DAL, you simply need to orchestrate calling the basic DataAccess class's Write method (see Listing 2). The DataAccess.Write method needs to know the stored procedure to call (although you could include a literal SQL overloaded method), what kind of object it is writing, a write event handler, whether anything comes back from the write, and the list of parameters and values to send to the stored procedure.
Listing 2: Both DataAccess.Write Methods
Public Shared Sub Write(Of T)(ByVal o As T, _
ByVal procedureName As String, _
ByVal handler As WriteEventHandler(Of T), _
ByVal ParamArray parameters() As IDbDataParameter)
Using connection As IDbConnection = _
factory.CreateConnection(factory.ConnectionString)
connection.Open()
Write(o, procedureName, handler, connection, Nothing, parameters)
End Using
End Sub
Public Shared Sub Write(Of T)(ByVal o As T, _
ByVal procedureName As String, _
ByVal handler As WriteEventHandler(Of T), _
ByVal connection As IDbConnection, _
ByVal transaction As IDbTransaction, _
ByVal ParamArray parameters() As IDbDataParameter)
Dim command As IDbCommand = factory.CreateCommand(procedureName)
command.CommandType = CommandType.StoredProcedure
command.Connection = connection
command.Transaction = transaction
If (parameters Is Nothing = False) Then
Dim p As IDbDataParameter
For Each p In parameters
command.Parameters.Add(p)
Next
End If
command.ExecuteNonQuery()
If (handler <> Nothing) Then
handler(o, command)
End If
End Sub
The first version of Write creates a connection for you, and the second version permits passing in both a connection and a transaction.
Because the basic DataAccess class manages just the general behavior, you do need to extend the specific entity data-access classes to indicate the objects to act on and how to act on them. For example, writing a Customer means you have to tell the DataAccess class about Customer stored procedures and Customer fields. The complete listing for the CustomerAccess class is shown in Listing 3, and the remaining sub-sections describe each of the new elements.
Listing 3: The Complete Implementation of the CustomerAccess Class
Imports System
Imports System.Data
Imports System.Diagnostics
Imports System.Configuration
Imports System.Transactions
Imports System.Collections.Generic
Imports System.Web
Imports MYVB.BusinessObjects
Public Class CustomerAccess
Public Shared Function GetCustomers() As List(Of Customer)
Return DataAccess.Read(Of List(Of Customer))("GetCustomers", _
AddressOf OnReadCustomers)
End Function
Public Shared Sub PutCustomers(ByVal customers As List(Of Customer))
' Reference and import System.Transactions
Using scope As TransactionScope = New _
TransactionScope(TransactionScopeOption.Required)
For Each c As Customer In customers
PutCustomer(c)
Next
scope.Complete()
End Using
End Sub
' what if unique key is auto. Use write handler to set after insert
' change stored proedure to insert on null key
' key becomes output parameters
Public Shared Sub PutCustomer(ByVal cust As Customer)
Debug.Assert(cust Is Nothing = False)
Dim Factory As DbFactory = DbFactory.CreateFactory()
Using connection As IDbConnection = Factory.CreateConnection( _
Factory.ConnectionString)
connection.Open()
' build parameters and write
Dim customerID As IDbDataParameter = _
Factory.CreateParameter("@CustomerID", cust.CustomerID)
customerID.DbType = DbType.String
customerID.Size = 5
customerID.Direction = ParameterDirection.Input
Dim companyName As IDbDataParameter = _
Factory.CreateParameter("@CompanyName", cust.CompanyName)
companyName.DbType = DbType.String
companyName.Size = 40
companyName.Direction = ParameterDirection.Input
Dim contactName As IDbDataParameter = _
Factory.CreateParameter("@ContactName", cust.ContactName)
contactName.DbType = DbType.String
contactName.Size = 30
contactName.Direction = ParameterDirection.Input
Dim contactTitle As IDbDataParameter = _
Factory.CreateParameter("@ContactTitle", cust.ContactTitle)
contactTitle.DbType = DbType.String
contactTitle.Size = 30
contactTitle.Direction = ParameterDirection.Input
Dim address As IDbDataParameter = _
Factory.CreateParameter("@Address", cust.Address)
address.DbType = DbType.String
address.Size = 60
address.Direction = ParameterDirection.Input
Dim city As IDbDataParameter = _
Factory.CreateParameter("@City", cust.City)
city.DbType = DbType.String
city.Size = 15
city.Direction = ParameterDirection.Input
Dim region As IDbDataParameter = _
Factory.CreateParameter("@Region", cust.Region)
region.DbType = DbType.String
region.Size = 15
region.Direction = ParameterDirection.Input
Dim postalCode As IDbDataParameter = _
Factory.CreateParameter("@PostalCode", cust.PostalCode)
postalCode.DbType = DbType.String
postalCode.Size = 10
postalCode.Direction = ParameterDirection.Input
Dim country As IDbDataParameter = _p>
Factory.CreateParameter("@Country", cust.Country)
country.DbType = DbType.String
country.Size = 15
country.Direction = ParameterDirection.Input
Dim As IDbDataParameter = _
Factory.CreateParameter("@Phone", cust.Phone)
phone.DbType = DbType.String
phone.Size = 24
phone.Direction = ParameterDirection.Input
Dim fax As IDbDataParameter = _
Factory.CreateParameter("@Fax", cust.Fax)
fax.DbType = DbType.String
fax.Size = 24
fax.Direction = ParameterDirection.Input
DataAccess.Write(Of Customer)(cust, _
"PutCustomer", Nothing, connection, Nothing, _
customerID, _
companyName, _
contactName, _
contactTitle, _
address, _
city, _
region, _
postalCode, _
country, _
phone, _
fax)
End Using
End Sub
'ALTER PROCEDURE dbo.GetCustomersAndOrders
'AS
'SELECT * FROM CUSTOMERS
* FROM ORDERS
Public Shared Function GetCustomersWithOrders() As List(Of Customer)
Return DataAccess.Read(Of List(Of Customer)) _
("GetCustomersAndOrders", _
AddressOf OnReadCustomersWithOrders)
End Function
Public Shared Function OnReadCustomersWithOrders( _
ByVal reader As IDataReader) As List(Of Customer)
Debug.Assert(reader Is Nothing = False)
Dim customers As List(Of Customer) = New List(Of Customer)()
If (reader Is Nothing) Then Return customers
customers = OnReadCustomers(reader)
If (reader.NextResult()) Then
Dim allOrders As List(Of Orders) = _
DataAccess.OnReadAnyList(Of Orders)(reader)
Dim customer As Customer
Dim _order As Orders
For Each customer In customers
For Each _order In allOrders
If (_order.CustomerID = customer.CustomerID) Then
customer.CustomerOrders.Add(_order)
End If
Next
Next
End If
Return customers
End Function
Public Shared Function OnReadCustomers( _
ByVal reader As IDataReader) As List(Of Customer)
If (reader Is Nothing) Then Return New List(Of Customer)()
Dim customers As List(Of Customer) = New List(Of Customer)()
While (reader.Read())
customers.Add(OnReadCustomer(reader))
End While
Return customers
End Function
Private Shared Function OnReadCustomer( _
ByVal reader As IDataReader) As Customer
Debug.Assert(reader Is Nothing = False)
Dim customerID As String = ""
Dim companyName As String = ""
Dim contactName As String = ""
Dim contactTitle As String = ""
Dim address As String = ""
Dim city As String = ""
Dim region As String = ""
Dim postalCode As String = ""
Dim country As String = ""
Dim phone As String = ""
Dim fax As String = ""
customerID = DataAccess.SafeRead(Of String)(customerID, _
reader, "CustomerID")
companyName = DataAccess.SafeRead(Of String)(companyName, reader, _
"CompanyName")
contactName = DataAccess.SafeRead(Of String)(contactName, reader, _
"ContactName")
contactTitle = DataAccess.SafeRead(Of String)(contactTitle, reader, _
"ContactTitle")
address = DataAccess.SafeRead(Of String)(address, reader, "Address")
city = DataAccess.SafeRead(Of String)(city, reader, "City")
region = DataAccess.SafeRead(Of String)(region, reader, "Region")
postalCode = DataAccess.SafeRead(Of String)(postalCode, reader, _
"PostalCode")
country = DataAccess.SafeRead(Of String)(country, reader, "Country")
phone = DataAccess.SafeRead(Of String)(phone, reader, "Phone")
fax = DataAccess.SafeRead(Of String)(fax, reader, "Fax")
Return New Customer(customerID, companyName, contactName, _
contactTitle, address, city, region, postalCode, country, _
phone, fax)
End Function
End Class
Although this code is straightforward and monolithic, its consistency across entities in any domain promotes code generation. At a minimum, a tool like CodeRush (when it finally offers a VB version) will make writing this code a breeze.
