A Generic Data Access Layer in VB - Net 2
A Generic Data Access Layer in VB - Net 2
dal.aspx, dal.aspx.vb, customer.vb, consumers.vb, dalrequest.vb, AbstractProvider.vb, dalfactory.vb, sqlprovider.vb, xmlsettings.vb, dal.xml
(extract 10 files into new web project named dal or whatever, set dal.aspx as startup, press F5)
After reading "Professional Design Patterns in VB .NET, Building Adaptable Applications", Johnny Papa's articles, Steven Smith's seminar on DAL, and then using
LLBLGen Data Access Layer, plus other lesser inputs, I felt I needed 5 things from a DAL.
1) The automatic generation of data manipulation classes and stored procedures for each table in your database like LLBLGen
2) The ability to easily add another Data Provider even within the same application
3) Shortcut interfaces for ado.net commands that can be used from the middle tier without being specific to one Data Provider
4) The ability to receive back various types of classes like DataReader, DataSet, DataTable, DataRow, and custom classes.
5) and various miscellaneous features such as stored procedures vs text commands, built in error checking, security levels, etc.
Since I couldn't find this thing laying around, I began finding time to work on it and ran into many interesting OOP concepts that I hoped that I already understood,
but came to find out no I didn't until struggling with the code for a while. For example, even though I've read many books entries on Factory Design Pattern I couldn't
grasp what it was that was really gained from it.
In following diagram note that middle classes have Data Provider specific code in them (eg Sql Server). This is to show you the difference between my DAL and
using LLBLGen in the strict way it was made to be used. Classes on left will not have any Data Provider specific code in them. On the right we have Stored
Procedures. I want to compare what I am doing with using the LLBLGen in the standard way. I still generate with LLBLGen the classes and SPs for my database,
but then I modify the generated classes to be Data Provider non-specific Middle-Tier classes. I then often modify some of the SPs to be more specifically practical
for the current Application.
http://www.computer-consulting.com/dal.htm 05/13/2010
Data Access Layer Page 2 of 9
I will have a detailed code example that I am currently using for SQL Server 2000 that you may want to use or easily modify for Oracle or any provider like OleDb,
available, but I will be focusing mostly on the different ways of going about keeping this process simple and maintainable and easy to use from different types of
classes and why I made the choices I have. The main goal is making a DAL that anyone can maintain.
From the requirements list, the DataProvider variation will be handled by different subclasses of one AbstractDataProvider class. You copy my SqlProvider class in
sqlprovider.vb to a file called oracleprovider.vb and edit away for an hour at most and you have your new concrete data provider subclass. To handle
CommandType variation we will use a DALRequest class with default settings to our favorite. I will be calling the output variation consumers to contrast with the
usage of provider. To handle the variation of consumers, I will vary the input signature of overloaded method called Execute of the SqlProvider subclass. To handle
the variation of Commands I will overload Execute and vary the code from overload to overload very slightly so it is easy to understand and change.
http://www.computer-consulting.com/dal.htm 05/13/2010
Data Access Layer Page 3 of 9
Below is diagram of Class Design showing all the main files in this download, with client-tier on bottom, business-tier as customer, and rest is DAL.
http://www.computer-consulting.com/dal.htm 05/13/2010
Data Access Layer Page 4 of 9
Now if I could just get a generator to compile so that the classes are ready to use this DAL, that would be a great savings of coding time, (maybe a next article?).
Let's look at each class individually to see what we can learn about the whole simple process starting with the front end. I am not delivering any Stored Procedures
with this because you can test all of this with just a few tweaks to Northwind's SPs or place some text commands very easily with this once you understand the
structure. It might look complex, but once you see the design things fall easily into place. First in dal.aspx there is a datagrid and a textbox and 3 buttons: getorders,
delete one record, and insert a customer.
Looking at the code for these button in the codebehind: dal.aspx.vb,,, we see that we are merely instantiating a class from the middle tier and calling one of its
methods. This is all I allow in this tier's responses to events. Never any data provider specific code. Some may say that a datareader is specific, or dataset, but they
may be made as abstract as you want with wrapper classes as we will see was necessary with the datareader class. You will see that datareader is not the
sqldatareader, but a custom wrapper class. I feel that a dataset class is not specific to sql or oracle or oledb, so I did not choose to make a wrapper as I could have
for the dataset or datatable. The main thing to notice here is that if I want to receive back a datareader for datagrid binding, then I simply input to the middle-tier
method, an new and empty datareader object. I could here, input only a type of datareader and and an instantiation of nothing and get my DAL to work fine and
have been lighter weight in what I passed, but inputing the non null instantiation that is empty saves me code in my sqlProvider class as you will see so I decided on
this course. If I want to receive a dataset back, I simply input a new dataset object, etc This works for any custom class that you want to define also which is very
easy to do, but this only works for consumers that you have handled in the DAL. So, it is a 1 or 2 word change of code to get any type of consumer object back for
binding or whatever use.
First notice the declaration of dataProvider object. AbstractProvider, which we will be looking at soon is an abstract class that is non-specific as regards data
providing, so that if your middle-tier is composed of 100 classes like customer, you won't have to go to 100 places to change to Oracle. You go to the default
constructor of the DALRequest class and change the default from sql to oracle as the default. Also if a client has all data on Oracle, but their customer table on Sql
Server, you can just change one property of the DALRequest class each time you need to switch to sql for a request. The DALFactory class uses the DALRequest
Provider property/field to get the proper concrete provider class instantiated. Also note that to execute a selected stored procedure, I need set the command
property/field, clearallparameters, code one line for each input and output parameter, and then code one line for execution. One line must be added to switch to
using text commands versus stored procs the default. Also note that adding a customer requires the return of the primary key identity that I use so I must input an
integer to tell the concrete data provider which overload to use to return that integer information. In the customer dispose method I close the dataprovider
connection later than for the other consumers in the case of a datareader. (calling close an extra time is safe)
Public Class customer
Dim dataProvider As AbstractProvider = DALFactory.GetProvider(DalRequest.Provider)
Public Function getOrders(ByVal id As String, ByVal consumer As DataReader) As DataReader
DalRequest.Command = "pr_orders_GetOrders"
dataProvider.ClearAllParameters()
dataProvider.AddParameter("@customerID", ssenumSqlDataTypes.ssSDT_String, 8, id, dataProvider.Direction.input)
Return dataProvider.Execute(consumer)
End Function
Public Function DeleteTopOrder(ByVal id As String)
DalRequest.Command = "pr_orders_DeleteTopOrder"
dataProvider.ClearAllParameters()
dataProvider.AddParameter("@customerID", ssenumSqlDataTypes.ssSDT_String, 8, id, 1)
dataProvider.Execute()
End Function
Public Function AddCustomer(ByVal customerID As String, ByVal consumer As Integer) As Integer
dataProvider.ClearAllParameters()
dataProvider.AddParameter("@customerid", ssenumSqlDataTypes.ssSDT_String, 5, customerID, dataProvider.Direction.Input)
dataProvider.AddParameter("@id" , ssenumSqlDataTypes.ssSDT_Integer, 4, 0, dataProvider.Direction.Output)
DalRequest.Command = "pr_customers_Insert"
Return dataProvider.Execute(1)
End Function
Sub dispose() ' needed only if consumer is datareader
dataProvider.Connection.ConSql.Close()
End Sub
End Class
Notice that in the consumer.vb file there are only 3 classes and 1 enumerator defined. That is because dataset doesn't necessarily need one since it is a class that
is already abstracted from being a Sql class or an Oracle class etc. There is no abstracted datareader class except the one we've defined here. There is only
sqldatareader or oledbdatareader, etc. So DataReader is a simple wrapper that will be used to carry the specific thing. Its use is to isolate the specific data provider
code from the codebehind or middle-tier. Order is a custom class that could have as many properties as we need. A very interesting thing to consider is why the
connection and direction constructions are necessary to isolate. Without these constructions I could not find anyway to isolate even though there may surely be
some other way. These are interesting kinds of questions when you are designing a data access layer. If I use the built in ParameterDirection enumerator isolation
http://www.computer-consulting.com/dal.htm 05/13/2010
Data Access Layer Page 5 of 9
is corrupted and when I want to change the code for the next client I find I have to go to 100 places and change code instead of one place. One interesting thing I
did not expect is that in customer when I declare DataProvider as type AbstractProvider, and then set it equal to a specific output of the DalFactory if the abstract
factory did not have exactly the same direction property as in the specific concrete provider, what showed up was the abstract direction, not the specific. This forces
the last two classes here. This class library could grow over time. The connection class was necessitated because I needed to close a connection from the middle
tier in the case of the datareader consumer. The direction enumerator copies the ParameterDirection enumerator of the system.data namespace. I'd rather not have
a system.data structure in my abstract provider class, so I created my own. One of these latter is required in order to instantiate the capability of creating both input
and output parameters for stored procedures.
Imports System.Data.SqlClient
Public Class DataReader
Public ReturnedDataReader As IDataReader
End Class
Public Class Order
Public OrderDate As String
End Class
Public Class Connection
Public ConSql As SqlConnection
' add another property here for Oracle etc
End Class
Public Enum Direction
input = 1
output = 2
both = 3
returnitem = 6
End Enum
This class is very useful for controlling defaults and making it easy to refer to any current properties requested as it is shared. SqlDataProvider relies on this. As you
can see I like fields tied to enumerators versus method properties unless the method properties will be used for greater control. Sub New provides the default values
that an application will use mainly. Only one property has to be set each time, Command either as text command or stored procedure name as string. UserType is
linked to dal.xml read/write config file that is accessed via xmlsetting shared class. Each UserType has a different userid and password in dal.xml. These could be in
your employee or customer table as well and make programming more safe as certain kinds of users would not be able to execute certain commands on your data.
Public Class DalRequest
Public Shared Provider As ProviderType
Public Shared RoleObject As UserType
Public Shared Role As String
Public Shared CommandType As CommandType
Public Shared Command As String
Public Shared Transaction As Boolean
Public Shared Locking As Boolean
Public Shared ParamCache As Boolean
Public Shared TableName As String
Shared Sub New()
Provider = ProviderType.Sql
RoleObject = New UserType()
Role = RoleObject.Admin
CommandType = CommandType.StoredProcedure
Command = ""
Transaction = False
Locking = False
ParamCache = False
TableName = "data"
End Sub
End Class
Public Enum ProviderType
Sql
Oracle
Oledb
End Enum
Public Class UserType
Public Shared External As String
Public Shared Internal As String
Public Shared SuperUser As String
Public Shared Admin As String
Sub New()
External = "external"
Internal = "internal"
SuperUser = "superuser"
Admin = "admin"
End Sub
End Class
This abstract class enforces a minimum interface on all its subclasses. You can add more functionality to a concrete provider that has such available, but you must
add the enfiorced interface. Instead of using an abstract class I could have used an Interface instead, which is how I actually first attempted this. I found that the
complexity of defining the connection and direction properties and implementing them was more difficult than using the abstract class and that books with significant
coverage on Interfaces was not available to me even though I have many books supposedly covering this area. Coverage was very spotty. It would be nice if
someone would do an entire book on Interfaces. This is one value of the factory pattern which is comprised of 2 classes here: AbstractProvider class and the
DalFactory class.
http://www.computer-consulting.com/dal.htm 05/13/2010
Data Access Layer Page 6 of 9
Public Connection As Connection
Public Direction As Direction
Public MustOverride Sub Execute()
Public MustOverride Function Execute(ByVal consumer As Integer) As Integer
Public MustOverride Function Execute(ByVal consumer As DataReader) As DataReader
Public MustOverride Function Execute(ByVal consumer As DataSet) As DataSet
Public MustOverride Function Execute(ByVal consumer As DataTable) As DataTable
Public MustOverride Function Execute(ByVal consumer As DataRow) As DataRow
Public MustOverride Function Execute(ByVal consumer As Order) As Order
Public MustOverride Sub ClearAllParameters()
Public MustOverride Sub AddParameter(ByVal parameterName As String, ByVal dataType As ssenumSqlDataTypes, _
ByVal size As Integer, ByVal value As String, ByVal direction As Integer)
End Class
The other value of the Factory Design Pattern is that the DalFactory class will choose the proper concrete class for us in a way that is not specific to the
DataProvider so that we can do this instantiation in hundreds of places and never have to go and change this declaration and instantiation code ever again, just add
to the code in the DalFactory, and the default or current value of the DalRequest.provider property.
Public Class DALFactory
Public Shared Function GetProvider(ByVal provider As Integer) As AbstractProvider
Select Case provider
Case ProviderType.Sql
Return New SQLProvider()
Case ProviderType.Oracle
'Return New OracleProvider()
Case ProviderType.Oledb
'Return New OledbProvider()
End Select
End Function
End Class
At the highest level of overview, there are imports, 1 enumeration of sqlDataTypes, a Parameter class for purposes of wrapping the specific parameters of specific
data providers like the sqlParameters class, and then the main SqlProvider which is inheriting the AbstractProvider class. System.IO, and System.Text are needed
for logging exceptions. Microsoft.VisualBasic.ControlChars is for using crlf, and then System.Data and System.Data.Client are necessary to manipulate the Sql
Server database in the most efficient manner currently existing. We will need to be able to convert our abstract parameters into sqlparameters and thus the
SqlDataTypes enumerator is needed.
Imports System.IO
Imports System.Text
Imports System.Data
Imports System.Data.SqlClient
Imports Microsoft.VisualBasic.ControlChars
Public Enum ssenumSqlDataTypes
<Serializable()> Public Class Parameter
<Serializable()> Public Class SQLProvider : Inherits AbstractProvider
With the parameter class and the methods of the SqlDataProvider below we can easily handle stored procedure parameters and make stored procedures as easy
as Sql text commands. The reason we need an abstract parameter class and then convert to sql parameters inside the concrete data provider class is that we must
be abstract in the middle tier and specific inside the data provider. I know I am repeating myself, but if you don't get this stuff it takes some thought time and
repetition helps. I have collected these somewhat disparate pieces here to help you get that this parameter stuff is not that complex. I had a little trouble with it at
first. When you decide to create your OracleDataProvider, you will have to consider how to change the convertParametertoOracleParameter method. You will also
have to make a few changes to the datatype enumerator also. Now on to more interesting things.
<Serializable()> Public Class Parameter
Public DataType As SqlDbType '//--- The datatype of the parameter
Public Direction As ParameterDirection '//--- The direction of the parameter
Public ParameterName As String '//--- The Name of the parameter
Public Size As Integer '//--- The size in bytes of the parameter
Public Value As String '//--- The value of the parameter
Sub New(ByVal sParameterName As String, ByVal lDataType As SqlDbType, ByVal iSize As Integer, _
ByVal sValue As String, ByVal iDirection As Integer )
ParameterName = sParameterName
DataType = lDataType
Size = iSize
Value = sValue
Direction = iDirection
End Sub
End Class
Private m_oParmList As ArrayList = New ArrayList() ' holds parameters for a stored procedure
Public Overloads Overrides Sub ClearAllParameters()
m_oParmList.Clear ()
End Sub
Public Overloads Overrides Sub AddParameter(ByVal sParameterName As String, _
ByVal lSqlType As ssenumSqlDataTypes, ByVal iSize As Integer, ByVal sValue As String, ByVal iDirection As Integer)
Dim eDataType As SqlDbType
Dim oParam As Parameter = Nothing
Select Case lSqlType
Case ssenumSqlDataTypes.ssSDT_String
eDataType = SqlDbType.VarChar
http://www.computer-consulting.com/dal.htm 05/13/2010
Data Access Layer Page 7 of 9
Case ssenumSqlDataTypes.ssSDT_Integer
eDataType = SqlDbType.Int
Case ssenumSqlDataTypes.ssSDT_DateTime
eDataType = SqlDbType.DateTime
Case ssenumSqlDataTypes.ssSDT_Bit
eDataType = SqlDbType.Bit
Case ssenumSqlDataTypes.ssSDT_Decimal
eDataType = SqlDbType.Decimal
Case ssenumSqlDataTypes.ssSDT_Money
eDataType = SqlDbType.Money
End Select
oParam = New Parameter(sParameterName, eDataType, iSize, sValue, iDirection)
m_oParmList.Add(oParam)
End Sub
Private Function ConvertParameterToSqlParameter(ByVal oP As Parameter) As SqlParameter
Dim oSqlParameter As SqlParameter = New SqlParameter(oP.ParameterName, oP.DataType, oP.Size)
With oSqlParameter
.Value = oP.Value
.Direction = oP.Direction
End With
Return oSqlParameter
End Function
Below you can see the overview that shows the detail of the sqlDataTypes, the shadowing of the connection and direction properties over the abstract versions in
AbstractProvider, the sub new, the location of the parameter methods, and finally the 7 overloads of the Execute method that does the main work. The 7 overloads
are for the 7 consumers I was interested in. 1-updates/deletes which I don't want to return anything. I could have unified updates/deletes/inserts by having them all
return integers/how many rows affected, but what if your primary key is not integer like mine? 2-inserts, 3-datareader, 4-dataset, 5-datatable, 6-datarow, 7-custom
class order. Actually as you will see, the code difference between the overloads is very small number of lines and so therefore you could easily unify all overloads
into one Execute method. I chose not to because I like the method of overloading using one input object that makes it so easy to remember which overload does
what for me! Also, this overloading makes it very easy to add features in the future since the length of each Execute method is so small. It is very easy to add a new
consumer.
Imports System.IO
Imports System.Text
Imports System.Data
Imports System.Data.SqlClient
Imports Microsoft.VisualBasic.ControlChars
Public Enum ssenumSqlDataTypes ssSDT_Bit
ssSDT_DateTime
ssSDT_Decimal
ssSDT_Integer
ssSDT_Money
ssSDT_String
End Enum <Serializable()> Public Class Parameter
<Serializable()> Public Class SQLProvider : Inherits AbstractProvider
Public Shadows Direction As New Direction()
Public Shadows Connection As New Connection()
Private connectionString As String
Private m_oParmList As ArrayList = New ArrayList() ' holds parameters for a stored procedure
Sub New()
connectionString = XmlSetting.Read("appsettings", DalRequest.Role)
Me.Connection.ConSql = New SqlConnection(connectionString)
End Sub
Public Overloads Overrides Sub Execute() ' handles update and delete commands which return nothing here
Public Overloads Overrides Function Execute(ByVal consumer As Integer) As Integer
Public Overloads Overrides Function Execute(ByVal consumer As DataReader) As DataReader
Public Overloads Overrides Function Execute(ByVal consumer As DataSet) As DataSet
Public Overloads Overrides Function Execute(ByVal consumer As DataTable) As DataTable
Public Overloads Overrides Function Execute(ByVal consumer As DataRow) As DataRow
Public Overloads Overrides Function Execute(ByVal consumer As Order) As Order
Public Sub LogError(ByVal e As SqlException, ByVal Command As String)
Public Overloads Overrides Sub ClearAllParameters()
Public Overloads Overrides Sub AddParameter(ByVal sParameterName As String, ByVal lSqlType As ssenumSqlDataTypes, _
ByVal iSize As Integer, ByVal sValue As String, ByVal iDirection As Integer)
Private Function ConvertParameterToSqlParameter(ByVal oP As Parameter) As SqlParameter
End Class
Let's look at the simplicity of a few of the overloads. Note that you must declare overloads and overrides when you are subclassing. Note that because of the choice
of my method signature differentiation, I am able to use the input to save lines of code. I return what I input. Ooh I like it when things like this happen. For my error
http://www.computer-consulting.com/dal.htm 05/13/2010
Data Access Layer Page 8 of 9
handling I chose to throw and log the problems. Note how stored procedure parameter conversion is handled by a simple logic with a 2 line loop to loop through all
parameters you created. The difference between SPs and Text commands boils down to not much.
Not much difference here. Remember that order is a custom class defined in consumer.vb library. It could be defined anywhere. To use this for another custom
class, you would have to create another overload of Execute since each class has different properties that need setting. I wouldn't want one hundred overloads of
Execute in sqlProvider. The way I handle this, since every time that only one datarow is returned I want it to set the properties of the associated class and work with
the class not the datarow, is to use LLBLGen to generate data layer classes for each table, and then I convert them to middle-tier classes by taking out all data
provider specific code. Then I return a datarow from my DAL to the class and set the classes properties with it. Then I work with the class in the front end.
In the past I have found reasons to have a read/write config file like the one below.
<apsettings>
<htmpath>c:\inetpub\wwwroot\dal\</htmpath>
<errpath>c:\inetpub\wwwroot\dal\</errpath>
http://www.computer-consulting.com/dal.htm 05/13/2010
Data Access Layer Page 9 of 9
</apsettings>
The xmlsetting.vb file class which has shared methods, allows me to make simple calls like the ones below to read and write to this xml file. This simple class allows
me same element names since I use one level of context to grab a value. I specify a child in the context of a parent node. Note that xmlsetting.vb requires that the
name of the xml file must match the project name. In this case, DAL.
dim connectionString as string = xmlsetting.read("appsettings", "internal")
xmlsetting.write ("appsetting", "Internal", "safe")
I am not writing this article because I consider myself a data architecture expert at all, but I feel a great need for development in this area and for more explanation
with examples like the direction and connection cases which I could not find anywhere. Please contact me with ideas or criticisms so that I can continue to evolve
this important area as I know many must be working on similar things.
Go to the article about modifying the LLBL class generator to work with DAL:
Send mail to Computer Consulting with questions or comments about this web site.
Last modified: January 26, 2006
http://www.computer-consulting.com/dal.htm 05/13/2010