Since I posted [Data Access Layer for Classic ASP], I've been trying to find time to post the ASP.Net version too..
This is a complete example of creating Data Access Layer(DAL) using the traditional yet powerful "Object to Relational mapping model" or "Custom Entity Classes" that support Paging too..
Many people talk about using the Datasets method for creating a DAL as in [Tutorial 1: Creating a Data Access Layer] and how it is fast to generate.
but for me the Traditional 'Object to Relational mapping' model seems more simpler to work with and to extend, and about the slow creation time issue: as a developer you know that creating these classes for the 2nd time things gets more faster, most of its code is cut+paste-friendly and there are software that would generate it for you too..
They would also say that datasets method is data-source independent (no DBMS), not just database independent, well my proposed DAL is surely database independent and what are the chances that your application will move away from a DBMS? well,ZERO. and still those classes can encapsulate anything you want, even if data is saved in csv files.
The 'Object to Relational mapping' model consists of :
Entity Class - represents a single record or row from a database resultset.
Entity Collection Class - contains a collection of entity objects or a tabular list of data from a resultset.
Add to that, DAL uses a very helpful generic Command class i posted before to [Write Less & Generic Data Access Code in ADO.NET 2.0].
As an example of DAL, we'll use a simple Table of 'Products', here is the SQL Create script:CREATE TABLE [Products] (
[ID] [int] PRIMARY KEY IDENTITY(1, 1),
[Name] [varchar] (50) NOT NULL DEFAULT (''),
[Description] [varchar] (500) NOT NULL DEFAULT (''),
[Status] [tinyint] NOT NULL DEFAULT (0),
[Price] [decimal](18, 0) NOT NULL DEFAULT (0)
)
The Entity ClassImports System.Data
Imports System.Data.Common
Imports Microsoft.VisualBasic
Public Class Product
'' Table Fields
Public ID As Integer
Public Name As String
Public Description As String
Public Price As Decimal
Public Status As ProductStatus
Public Enum ProductStatus
None = 0
Active = 1
Inactive = 2
End Enum
Public Sub New(Optional ByVal ProductID As Integer = 0)
SelectItem(ProductID)
End Sub
''' <summary>Initialize Product Fields</summary>
Public Sub Initialize()
ID = 0
Name = ""
Description = ""
Price = 0
Status = ProductStatus.Active
End Sub
Friend Sub Populate(ByRef dr As DbDataReader)
Populate(CType(dr, Object))
End Sub
Friend Sub Populate(ByRef dr As DataRow)
Populate(CType(dr, Object))
End Sub
Private Sub Populate(ByRef dr As Object)
ID = CInt(dr("ID"))
Name = dr("Name")
Description = dr("Description")
Price = CDec(dr("Price"))
Status = CByte(dr("Status"))
End Sub
''' <summary>Select Product by ID</summary>
Public Sub SelectItem(ByVal ProductID As Integer)
Call Initialize()
If ProductID = 0 Then Exit Sub
Dim cmd As New GenericCommand("SQLConn")
Dim rdr As DbDataReader
cmd.CommandText = "Select Top 1 * from Products Where ID=@ID"
cmd.AddParam("@ID", ProductID)
rdr = cmd.ExecuteReader()
If rdr.Read() Then Populate(rdr)
rdr.Close()
rdr = Nothing
cmd = Nothing
End Sub
''' <summary>Insert new Product and get new Identity</summary>
Public Sub InsertItem()
If ID <> 0 Then Exit Sub
Dim cmd As New GenericCommand("SQLConn")
cmd.CommandText = "Insert Into Products (Name,Description,Price,Status) Values (@Name,@Description,@Price,@Status)"
cmd.AddParam("@Name", Name)
cmd.AddParam("@Description", Description)
cmd.AddParam("@Price", Price)
cmd.AddParam("@Status", Status)
ID = CInt(cmd.ExecuteIdentity())
cmd = Nothing
End Sub
''' <summary>Update Product</summary>
Public Sub UpdateItem()
If ID = 0 Then Exit Sub
Dim cmd As New GenericCommand("SQLConn")
cmd.CommandText = "Update Products set Name=@Name,Description=@Description,Price=@Price,Status=@Status where ID=@ID"
cmd.AddParam("@Name", Name)
cmd.AddParam("@Description", Description)
cmd.AddParam("@Price", Price)
cmd.AddParam("@Status", Status)
cmd.AddParam("@ID", ID)
cmd.ExecuteNonQuery()
cmd = Nothing
End Sub
''' <summary>Delete This product</summary>
Sub DeleteItem()
If ID = 0 Then Exit Sub
Dim cmd As New GenericCommand("SQLConn")
cmd.CommandText = "DELETE FROM Products WHERE ID=@ID"
cmd.AddParam("@ID", ID)
cmd.ExecuteNonQuery()
cmd = Nothing
Call Initialize()
End Sub
End Class
and The Entity Collection ClassPublic Class Products
Inherits CollectionBase
''' <summary>PageSize=0 means no paging</summary>
Public PageSize As Integer
''' <summary>PageIndex=0 means no paging</summary>
Public PageIndex As Integer
Private mPageCount As Integer
''' <summary>To Get Top records if larger than 0</summary>
Public TopRecords As Integer
Public Sub New()
Call Initialize()
PageSize = 0
PageIndex = 0
TopRecords = 0
End Sub
''' <summary>clear collection</summary>
Public Sub Initialize()
mPageCount = 0
List.Clear()
End Sub
''' <summary>Gets or sets the element at the specified zero-based index</summary>
Default Public Property Item(ByVal Index As Integer) As Product
Get
Return List.Item(Index)
End Get
Set(ByVal value As Product)
List.Item(Index) = value
End Set
End Property
''' <summary>Adds an object to the end of the Collection</summary>
Public Function Add(ByVal Obj As Product) As Integer
Return List.Add(Obj)
End Function
''' <summary>Select Products by Status, More Search Params can be added..</summary>
Public Sub SelectItems(Optional ByVal Status As Product.ProductStatus = Product.ProductStatus.None)
Call Initialize()
Dim Tbl As DataTable
Dim Top As String = ""
If TopRecords > 0 Then Top = "TOP " & TopRecords
If PageSize > 0 And PageIndex > 0 Then Top = "TOP " & (PageIndex * PageSize)
Dim Cmd As New GenericCommand("SQLConn")
Cmd.PageSize = PageSize
Cmd.PageIndex = PageIndex
Cmd.CountCommandText = "SELECT COUNT(*) FROM Products"
Cmd.CommandText = "SELECT " & Top & " * FROM Products"
If Status > 0 Then
Cmd.CountCommandText += " where Status=@Status"
Cmd.CommandText += " where Status=@Status"
Cmd.AddParam("@Status", Status)
End If
Tbl = Cmd.ExecuteDataTable("Products")
mPageCount = Cmd.PageCount
Dim p As Product
For Each Row As DataRow In Tbl.Rows
p = New Product
p.Populate(Row)
Add(p)
p = Nothing
Next
Tbl = Nothing
Cmd = Nothing
End Sub
Public ReadOnly Property PageCount() As Integer
Get
Return mPageCount
End Get
End Property
Protected Overrides Sub Finalize()
MyBase.Finalize()
End Sub
End Class
A Sample of usage to insert a product:Dim p As New Product
p.Name = "Product1"
p.Status = Product.ProductStatus.Active
p.InsertItem()
Response.Write(p.ID & ":" & p.Name)
p = Nothing
And a Sample of usage to select list of products with pagingDim ps As New Products
ps.PageSize = 10
ps.PageIndex = 2
ps.SelectItems(Product.ProductStatus.Active)
For i As Integer = 0 To ps.Count - 1
Dim p As Product = ps(i)
Response.Write(p.ID & ":" & p.Name & "<br>")
p = Nothing
Next
Response.Write("PageCount: " & ps.PageCount)
ps = Nothing
Also note that the paging method used in the collection class is DataSet paging method, to read more about paging methods comparison and tweaking: please check [Paging in ASP and ASP.Net].
After I posted a previous article comparing between many methods of Paging in ASP and ASP.Net, I wanted to focus on how to tweak paging using DataSet as it is unpopular tip and it saves lots of time to return records in closer pages which usually gets more hits than further pages...
The method DbDataAdapter.Fill(dataSet As DataSet, startRecord As Integer, maxRecords As Integer, srcTable As String) specifies the start record and number of records to fill the DataSet with, and of course you need to run a count query first to calculate PageCount'Count Query
Cmd.CommandText = "Select count(ID) from Users where.."
Count = CInt(Cmd.ExecuteScalar())
PageCount = Math.Ceiling(Count / PageSize)
'Select Query
cmd.CommandText = "Select Top " & (PageIndex * PageSize) & " * from Users where.. Order By.."
Dim DS As New DataSet
Dim DA As DbDataAdapter = PFactory.CreateDataAdapter()
DA.SelectCommand = Cmd
DA.Fill(DS, (PageIndex - 1) * PageSize, PageSize, SrcTable)
Dim DT As New DataTable= DS.Tables(SrcTable)
DA = Nothing
DS = nothing
For Each Row As DataRow In DT.Rows
'Do Something
Next
DT = Nothing
MSDN @ [ADO.Net & Paging Through a Query Result ] Says:
This might not be the best choice for paging through large query results because, although the DataAdapter fills the target DataTable or DataSet with only the requested records, the resources to return the entire query are still used ..That is true, At first testing the time results for this method was bad even for closer pages..
Remember that the database server returns the entire query results even though only one page of records is added to the DataSet.
To tweak that: I added a top clause in the select query to select Top(PageSize * PageIndex), so if we want the 2nd page where page size is 10: the database returns only 20 records, and the DataSet is filled with the 2nd 10 records.. instead of letting the database to return the entire table. and this tweak makes DataSet timing chart similar to 'Top Clause' SQL paging method [ more on that @ Paging in ASP and ASP.Net]
Although I posted a long article comparing between many methods of Paging in ASP and ASP.Net, I wanted to highlight more on this method of paging using DataReader cause you won't see out there very often.. although it is simpler than SQL Paging methods and it is slightly faster than usual DataSet paging method..
It was inspired by the article [How do I page through a Recordset?] which Shows & compares between paging methods for classic ASP.
The function do paging by fetching the Top PageSize * PageIndex Records and then it loops to move the start record of current Page index..' Paging Params (1-based)
Dim PageSize As Integer = 10
Dim PageIndex As Integer = 3
' Create+Open Conn on SQL Server 2005 Express
Dim Conn As New SqlConnection("Data Source=127.0.0.1\sqlexpress,1433;Initial Catalog=DB;Persist Security Info=False;User ID=user;Password=pass;Network Library=dbmssocn")
Conn.Open()
'Create SQL Command
Dim Cmd As New SqlCommand()
Cmd.Connection = Conn
'Execute Count Query To calculate PageCount
Cmd.CommandText = "SELECT COUNT(*) FROM users where status=1"
Dim Count As Integer = Cmd.ExecuteScalar()
Dim PageCount As Integer = Math.Ceiling(Count / PageSize)
'Query Records
'If you forget to add 'Top PageIndex * PageSize', Performance degrades badly in large tables
Cmd.CommandText = "SELECT Top " & (PageIndex * PageSize) & " * FROM users where status=1"
Dim rdr As SqlDataReader = Cmd.ExecuteReader()
'Move Reader to start record
For I As Integer = 0 To ((PageIndex - 1) * PageSize) - 1
If Not rdr.Read Then Exit For
Next
'Loop the desired Records
'if you didn't Add Top clause you will notice that all records in table are fetched!!
While rdr.Read()
'Do Something
End While
rdr.Close()
rdr = Nothing
Conn.Close()
Conn = Nothing
Cmd = Nothing
*If you forget to add Top (PageIndex * PageSize), Performance degrades badly in large tables
Optimizing web pages for Google and other search engines is not enough. A successful concept for online marketing your blog or site has to be regarded in a more comprehensive way and must include the community [The Wisdom of Crowds!].
Web 2.0 is part of the community and vice versa. Social bookmarking is part of it, too. so you should make it as easy as possible for your visitors to bookmark your site. and gathering information on the best social bookmarking sites, their icons, submission URLs, and ranking is quite a pain.
This is a simple JavaScript that you can paste to any web page or blog to add the top 20 ranked social bookmarking sites as advised in [List of social bookmarking sites] and It should look like Icons on the right side -->>
And as you might know: loading many small images can slow down your page loading, as browser opens a new connection for each image to download, a well-known better approach is to put all this related images in one image, and use CSS to set background-position..
Here is the JavaScript to paste
<script type='text/javascript'>
function SocialBookmarks(){
var holder = document.getElementById("SocialButtonsPlace");
if (!holder) return;
var title = encodeURIComponent(document.title);
var url = encodeURIComponent(location.href);
/* these are the social bookmaking sites' icons ,ordered by rank, you can remove a line
but don't change the 'pos' so icon would show up correctly */
var socials = [
{'name':'Yahoo','pos':-16,'url':'http://myweb2.search.yahoo.com/myresults/bookmarklet?t='+title+'&u='+url},
{'name':'Google','pos':-32,'url':'http://www.google.com/bookmarks/mark?op=edit&bkmk='+url+'&title='+title},
{'name':'Live','pos':-48,'url':'https://favorites.live.com/quickadd.aspx?url='+url+'&title='+title},
{'name':'Facebook','pos':-64,'url':'http://www.facebook.com/sharer.php?u='+url+'&t='+title},
{'name':'Digg','pos':-80,'url':'http://digg.com/submit?phase=2&url='+url+'&title='+title},
{'name':'Ask','pos':-96,'url':'http://myjeeves.ask.com/mysearch/BookmarkIt?v=1.2&t=webpages&url='+url+'&title='+title},
{'name':'Technorati','pos':-112,'url':'http://technorati.com/faves?sub=addfavbtn&add='+url+'&title='+title},
{'name':'Delicious','pos':-128,'url':'http://del.icio.us/post?url='+url+'&title='+title},
{'name':'StumbleUpon','pos':-144,'url':'http://www.stumbleupon.com/submit?url='+url+'&title='+title},
{'name':'Squidoo','pos':-160,'url':'http://www.squidoo.com/lensmaster/bookmark?'+url},
{'name':'Propeller','pos':-176,'url':'http://www.propeller.com/submit/?U='+url+'&T='+title+'&C='+title},
{'name':'Slashdot','pos':-192,'url':'http://slashdot.org/bookmark.pl?url='+url+'&title='+title},
{'name':'Reddit','pos':-208,'url':'http://reddit.com/submit?url='+url+'&title='+title},
{'name':'Fark','pos':-224,'url':'http://cgi.fark.com/cgi/fark/submit.pl?new_url='+url+'&new_comment='+title+'&linktype='},
{'name':'Newsvine','pos':-240,'url':'http://www.newsvine.com/_wine/save?u='+url+'&h='+title},
{'name':'Furl','pos':-256,'url':'http://www.furl.net/storeIt.jsp?t='+title+'&u='+url},
{'name':'Blinklist','pos':-272,'url':'http://www.blinklist.com/index.php?Action=Blink/addblink.php&Url='+url+'&Title='+title},
{'name':'dzone','pos':-288,'url':'http://www.dzone.com/links/add.html?url='+url+'&title='+title},
{'name':'Magnolia','pos':-304,'url':'http://ma.gnolia.com/bookmarklet/add?url='+url+'&title='+title},
{'name':'SWiK','pos':-320,'url':'http://stories.swik.net/?submitUrl&url='+url}
];
for(var i=0; i<socials.length; i++)
holder.innerHTML = holder.innerHTML + '<li><A class=\'SocialButton\' style=\'background-position:0 '+socials[i].pos+'px\' href=\''+socials[i].url+'\' title=\''+socials[i].name+'\' target=\'_blank\'> </A></li>';
}
/* This call can be placed in body onload event */
SocialBookmarks();
</script>
And place this CSS code in the HTML Head or in an included CSS file
#SocialButtonsPlace {
list-style:none;
/* must do - Browsers WORLD WAR!*/
margin:0; padding: 0;
}
#SocialButtonsPlace Li{
float:left;
}
.SocialButton {
display:block;
height:16px;
width:16px;
line-height:16px;
margin:2px;
text-decoration:none;
background-image:url(https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikWuhPxjyWXhmT9ax0HQ-a-A1tkGL_hUK5WnSPdXu2uN9C1YsuEJMb5YyVD3rXtuMX4W3bRQDMMT5pWzXxBe5ezHun2B8uVxJzud3WwYjhWrScnKfoB8e3hT4kt7vqlxJzyzhFqLxcQf5_/s400/bar.gif);
}
And create a place holder UL where the icons will show up
<UL id="SocialButtonsPlace"></UL>
Don't forget to save the icons [Bar Image] and upload it some where on your site and change its location in CSS.
You may also use an Image-Map to display the bar image horizontally or vertically and define the clickable regions..
that's it.. enjoy!
If you are interested in SEO or web search or marketing your blog, you should check that great article
[Can Social Bookmarking Improve Web Search] by Paul Heymann , One look on the author photo and you will want to read it..
Or watch the video
Can Social Bookmarks Improve Web Search?
Presentation (February 12th, 2008)
Conference on Web Search and Data Mining (WSDM2008)
A very important method of increasing data access efficiency is to cache frequently accessed data. like country list or top articles.. in situations where the data changes less frequently, such as once a day or every few hours, it is inefficient to hit the database for every page request.
As many would suggest to cache the HTML Output of a portion of a page(Fragment Caching) as a country select box or caching the entire page(Output Caching), there are cases where Data Caching will be more powerful. for example you may need to display that list of countries in more than one page/Portion with different look and/or use that list programmatically.
In a previous post [Data Access Layer for Classic ASP]: I proposed DAL Classes to encapsulate Data Access code for a certain Table/Object, and now I'll extend the DAL to handle Data caching too, please refer to my previous post before continuing..
I've modified the "Users" class to handle saving and reading from cache, which is saved to ADO XML format or Advanced Data Tablegram (ADTG) Binary format using "Save" method of ADO Recordset, Which has many advantages :
- You don't need to change much of your Data Access code to read from cache
- Opening the recordset from XML will act the same as Opening Database when you use Paging
- You can use The "Filter" method of ADO Recordset to filter records as you would do in a SQL Query
Here is the New ClassClass Users
'private Arr to hold users Objects
Private arr
'0 : Binary ADTG , 1: XML
Public CacheType 'as Byte
' Cache File Path
Public CachePath 'as String
''''' Paging Variables
Private mPageCount 'as Long
Public PageIndex 'as Long
Public PageSize 'as Long
'for getting top records if larget than 0
Public TopRecords 'as Long
'destroy arr
Private Sub Class_Terminate()
Erase arr
end Sub
Private Sub Class_Initialize()
Initialize
TopRecords = 0
PageIndex = 0
PageSize = 0
CacheType = 0
CachePath = ""
end Sub
'Initialize collection
Public Sub Initialize()
redim Arr(-1)
mPageCount = 0
End Sub
'return Upper Bound of objects array
Public Property get UpperBound()
UpperBound = UBound(arr)
End Property
'return object at index i
Public Property Get ElementAt(i)
set ElementAt = arr(i)
End Property
'return Upper Bound of objects array
Public Property Get PageCount()
PageCount = mPageCount
End Property
'Select users by Status ( u can add more search parameters or create another selectUsers method)
Public Sub SelectUsers(Status)
Initialize
Dim rs : Set rs = server.CreateObject("ADODB.Recordset")
'Do paging
If PageSize>0 And PageIndex>0 Then
RS.CursorLocation = adUseClient
RS.CacheSize = PageSize
RS.PageSize = PageSize
End If
'if open from XML
If CachePath>"" Then
If Status>0 Then rs.Filter = "Status="& Status
Rs.Open CachePath,,,,adCmdFile
'if Open from DB
Else
If TopRecords>0 Then
rs.MaxRecords = TopRecords
Top = " top "& TopRecords &" "
End If
Dim SQL : SQL= "SELECT "& Top &" * From users"
If Status>0 Then SQL = SQL & " where Status="& Status
rs.Open SQL , ConnStr, adOpenForwardOnly,adLockReadOnly,adCmdText
End If
' if paging : get page count
If Not rs.EOF And PageSize>0 And PageIndex>0 Then
RS.AbsolutePage = PageIndex
mPageCount = RS.PageCount
End If
Dim i : i=0
'(TopRecords=0 Or i<TopRecords) condition is needed to get correct Top N records when opening from Cache
' ,MaxRecords doesn't seem to work in that case
While Not rs.EOF And (TopRecords=0 Or i<TopRecords) And (PageSize=0 Or i<PageSize)
'loop until EOF or Paging Size reached
'create Object and set Values then add to collection
Dim u : Set u = New User
u.ID = CLng(Rs("ID"))
u.Name = Rs("Name")
u.Email = Rs("Email")
u.Password = Rs("Password")
u.LastLogin = cdate(rs("LastLogin"))
u.Status = cbyte(Rs("Status"))
ReDim Preserve arr(i)
Set arr(i) = u
set u = Nothing
rs.MoveNext
i=i+1
Wend
rs.Close
Set rs = Nothing
End Sub
' Open Recordset and Save it
Public Sub CacheUsers(Status)
If CachePath="" Then Exit Sub
Dim rs : Set rs = server.CreateObject("ADODB.Recordset")
Dim Top
If TopRecords>0 Then
rs.MaxRecords = TopRecords
Top = " top "& TopRecords &" "
End if
Dim SQL : SQL= "SELECT "& Top &" * From users"
If Status>0 Then SQL = SQL & " where Status="& Status
rs.Open SQL , ConnStr, adOpenForwardOnly,adLockReadOnly,adCmdText
Call SaveRS(rs)
rs.Close
Set rs = Nothing
End Sub
'Handle saving Recordset to Stream
Private Sub SaveRS(rs)
Const adTypeText = 2
Const adTypeBinary = 1
Const adSaveCreateOverWrite = 2
Const adPersistXML = 1
Const adPersistADTG = 0
Dim Stream : Set Stream = Server.CreateObject("ADODB.Stream")
If CacheType=1 Or CacheType=2 Then
Stream.Type = adTypeText
Stream.Open
rs.Save Stream, adPersistXML
Else
Stream.Type = adTypeBinary
Stream.Open
rs.Save Stream, adPersistADTG
end If
Application.Lock
Stream.SaveToFile CachePath,adSaveCreateOverWrite
Stream.Close
Application.UnLock
Set Stream = Nothing
End Sub
End Class
Sample Usage
1- Save Cache when Target Data is updatedDim aUsers : Set aUsers = New Users
aUsers.CacheType = 1 'XML cache
aUsers.CachePath = Server.MapPath("users.xml")
aUsers.TopRecords = Top
Call aUsers.CacheUsers(0)
Set aUsers = Nothing
2- Read CacheDim aUsers : Set aUsers = New Users
aUsers.CacheType = 1 'XML cache
aUsers.CachePath = Server.MapPath("users.xml")
aUsers.TopRecords = Top
Call aUsers.SelectUsers(0)
Dim i,aUser
For i=0 To aUsers.UpperBound
Set aUser = aUsers.ElementAt(i)
'do something
Set aUser = Nothing
Next
Set aUsers = Nothing
Time Comparison
I did a timing test to compare between reading from a SQL Server and from XML/Binary file, but Reading from a local SQL Server (on the same machine as IIS) will be always faster, so to get real world results i simulated a LAN/WAN Connection on SQL Server [How to? check: Simulating WAN Connection for SQL Server Performance Testing].
And to simulate concurrent connections on IIS and calculate timing i used the very neat [Microsoft Web Stress Tool] on a PC of P3.2GHZ, 1GB Ram, Windows XP, SQL Server 2005 Express.
| method / Records | 100 | 500 | 1000 | 3000 |
|---|---|---|---|---|
| Open WAN SQL Server | 559.25 | 614.80 | 980.85 | 2489.85 |
| Open LAN SQL Server | 276.70 | 436.00 | 777.60 | 2368.90 |
| Open XML Cache | 72.25 | 369.95 | 821.10 | 2469.31 |
| Open Binary Cache | 60.45 | 311.25 | 666.45 | 2062.20 |
As you can see reading less than 1000 records from XML Cache is slightly faster than reading from LAN SQL Server and time saving gets bigger when compared to WAN SQL server(reading >1000 records gets slower in XML due to loading&parsing overhead).
You can also notice that reading from Binary(ADTG) is faster than reading from XML and LAN/WAN SQL Server in all cases, and files created in binary are Approx. 50% smaller than the equivalent XML files.
Web developer, jQuery plugin author, social media fan and Technology Blogger.