SQLHelper.vb missing ExecuteDataTable - asp.net

Why is there no Shared function for ExecuteDataTable in SQLHelper.vb. There is an: ExecuteReader, ExecuteDataset and ExecuteScaler.
This is not a problem as I will write my own. I am just wandering why this is the case. I would normally use a DataReader but I am writing a data logic layer and the DataTable needs to outlive the connection (DataReaders cannot outlive a connection).

ExecuteDataset() will already do what you need. A dataset is, in one sense, just a collection of DataTables.
I would normally use a DataReader but I am writing a data logic layer and the DataTable needs to outlive the connection (DataReaders cannot outlive a connection).
In this case, may I suggest that instead of building an ExecuteDatatable() method, you build an ExecuteEnumerable() method that uses a DataReader in an Iterator block. The code would look something like this:
Public Shared Iterator Function ExecuteEnumerable(Of T)( ... ) As IEnumerable(Of T)
Using cn As New SqlConnection( ... ), _
cmd As New SqlCommand( ... )
'As needed
'cmd.Parameters.Add( ... ).Value = ...
Using rdr As SqlDataReader = cmd.ExecuteReader()
While rdr.Read()
Yield transform(rdr)
End While
End Using
End Using
End Function
You'll notice that I skipped over a few things. I'm not familiar with the existing SqlHelper.vb file, and as you would want to match existing style I left room in the code for you to adapt for that. However, there are two important pieces I want to call out:
Note the cmd.Parameters.Add() call. One common failing of utility sql help classes is that they fail to adequately provide for query parameters. All to often the result is horribly insecure code. If you don't have a way right now to pass parameter data for your existing methods, you need to make one. That is priority 1.
The transform(rdr) call there will use a Func(IDataRecord, T) delegate that must be supplied as an argument to the function. For the ExecuteEnumerable() iterator concept to work, you must take a copy of the current values in the SqlDataReader object on each iteration. You could set up some kind of generic data transfer object here, as is done with the DataRow type using in a DataTable. However, rather than spending cpu and memory time creating a copy into a generic data transport object of some type, I prefer to use a delegate to have the code copy it directly into a strongly-typed business object. The downside is needing to send instructions on how to do that for your specific object with every call to method. Most often, though, this is easy enough to do with a shared factory method on your business object.

We can create same as DataSet like
' Execute a SqlCommand (that returns a resultset) against the specified SqlConnection
' using the provided parameters.
' e.g.:
' Dim dt As DataTable = ExecuteDataTable(conn, CommandType.StoredProcedure, "GetOrders", new SqlParameter("#prodid", 24))
' Parameters:
' -connection - a valid SqlConnection
' -commandType - the CommandType (stored procedure, text, etc.)
' -commandText - the stored procedure name or T-SQL command
' -commandParameters - an array of SqlParamters used to execute the command
' Returns: A dataset containing the resultset generated by the command
Public Overloads Shared Function ExecuteDataTable(ByVal connection As SqlConnection, _
ByVal commandType As CommandType, _
ByVal commandText As String, _
ByVal ParamArray commandParameters() As SqlParameter) As DataTable
If (connection Is Nothing) Then Throw New ArgumentNullException("connection")
' Create a command and prepare it for execution
Dim cmd As New SqlCommand
Dim dt As New DataTable
Dim dataAdatpter As SqlDataAdapter
Dim mustCloseConnection As Boolean = False
PrepareCommand(cmd, connection, CType(Nothing, SqlTransaction), commandType, commandText, commandParameters, mustCloseConnection)
Try
' Create the DataAdapter & DataSet
dataAdatpter = New SqlDataAdapter(cmd)
' Fill the DataSet using default values for DataTable names, etc
dataAdatpter.Fill(dt)
' Detach the SqlParameters from the command object, so they can be used again
cmd.Parameters.Clear()
Finally
If (Not dataAdatpter Is Nothing) Then dataAdatpter.Dispose()
End Try
If (mustCloseConnection) Then connection.Close()
' Return the dataset
Return dt
End Function ' ExecuteDataTable

Related

SqlDataAdaptor update and close connection - how can I close connection via data Adaptor

I have the following code:
Dim da As SqlDataAdapter
Dim rst As DataRow = MyrstEdit("SELECT * FROM tblHotels WHERE ID = 19",, da).Rows(0)
rst("HotelName") = "My Cool"
rst("FirstName") = "Albert"
rst("City") = "Edmonton"
da.Update(rst.Table)
So, the above is nice and short. And it works rather nice.
And of course this being asp.net, then centralizing things like connection code (that I don't have to create over and over) is a also rather nice. And why bother with a connection during coding, so above reduces coding workload.
How then can I ensure the connection object is disposed and closed correctly?
From reading, since I do not open the conneciton, then
The Fill() does open, then close.
And I have to assume that the da.Update() ALSO must then by logic open, then close the conneciton.
However, I still should clean up the connection object after above is done.
Question:
Is disposing of the sql data adaptor object sufficient here to also dispose the connection object that the data adaptor is using?
eg:
da.Dispose()
The MyRstEdit routine is this:
Public Function MyrstEdit(strSQL As String,
Optional strCon As String = "",
Optional ByRef oReader As SqlDataAdapter = Nothing) As DataTable
' can pass custom connection string - if not passed, then default
If strCon = "" Then
strCon = GetConstr() ' global func to get application con string
End If
Dim mycon As New SqlConnection(strCon)
oReader = New SqlDataAdapter(strSQL, mycon)
Dim rstData As New DataTable
Dim cmdBuilder = New SqlCommandBuilder(oReader)
Try
oReader.Fill(rstData)
oReader.AcceptChangesDuringUpdate = True
Catch
End Try
Return rstData
End Function
So, the return sqlDataAdaptor object must be holding the connection, since the scope in above routine is limited.
So do I have to dispose the connection object?
Can I do this with the sqlAdaptor?
I can't dispose/close the connection object in above sub, since my da.Update() in the calling code still needs that connection until I do the update.
How then can I ensure the connection object is disposed and closed correctly?
Don't worry about it; it's not your job. DataAdapter makes it, DataAdapter will clean it up
However, I still should clean up the connection object after above is done
No, for the aforementioned reason
Is disposing of the sql data adaptor object sufficient here to also dispose the connection object that the data adaptor is using?
Yes, unless you have good reason to believe that Microsoft's code has a critical flaw and their classes will benefit from your code micromanaging the resources they create..
You can also read the reference source if you want to know what they do internally
The MyRstEdit routine is this:
It's hard to understand why it exists in that form; you'd be better off just passing a datatable around and creating dataadapters as and when you need them. MyRstEdit isn't well named; it doesn't seem to edit anything, it always overwrites the passed in adapter with stuff that a any passed in adapter might already know (the connstr and sql) and then doesn't really do anything that couldn't just be put to
Using da As New SqlDataAdapter("SELECT * FROM tblHotels WHERE ID = 19", GetConStr())
Dim dt as New DataTable
da.Fill(dt)
Dim rst = dt(0)
rst("HotelName") = "My Cool"
rst("FirstName") = "Albert"
rst("City") = "Edmonton"
New SqlCommandBuilder(da)
da.Update(rst.Table)
End Using
About the most useful thing it does is apply a command builder, but that's only a single line and only needed for an update..
Perhaps you could create an extension method that applies to a DataAdapter, that gets the first row, so you could say:
Using da As SqlDataAdapter("SELECT * FROM tblHotels WHERE ID = 19", GetConStr())
Dim rst = da.FirstRow()
rst("HotelName") = "My Cool"
rst("FirstName") = "Albert"
rst("City") = "Edmonton"
New SqlCommandBuilder(da)
da.Update(rst.Table)
End Using
But it isn't saving much over "just using it how MS intended", and there is still that block of "data columns accessed by string and hence intellisense can't help" in the middle.
If you're looking to find ways of making your SqlDataAdapter life easier it might be an opportunity to skip it and have a look at Dapper instead; a set of extension methods on a SqlConnection that map classes to and from:
Using c as New SqlConnection(GetConStr())
Dim h = Await c.SingleAsync(Of Hotel)("SELECT * FROM tblHotels WHERE ID = #id", New With { .ID = 19 } )
h.HotelName = "My Cool"
h.FirstName = "Albert"
h.City = "Edmonton"
Await c.ExecuteAsync("UPDATE tblHotels SET HotelName = #HotelName, FirstName = #FirstName, City = #City WHERE ID = #ID", h)
End Using
You still have to write the queries, but it's a one time op, or you could look at something like Dapper Contrib.. The main use here is that instead of being some DataRow object you access by "string column names" you have a first class VB.NET object - Hotel, with named typed proeprties, and dapper can create them from SQL queries and put their values directly into SQL parameters (another thing that is absent from your existing code)

.Net: code for BtnLock_Click

I have been trying to apply a solution to some functionality that a user requires on their system.
A user requires me to implement a locking system on their system. They have multiple users which may require to access the site, but the user would like the ability for them to independently lock a records in the web site site, for them to add notes to and to then unlock this so other users are able to do the same.
I have a button on my web page simply named btnLock and i have added an additional column in my database called LockedBy and have the following stored procedure...
ALTER PROCEDURE LockWeeklyTest
(
#AgendaID BIGINT,
#LockingUser VARCHAR(20)
)
AS
BEGIN
SET NOCOUNT ON
UPDATE
WeeklyAgenda
SET
LockedBy = #LockingUser
WHERE
AgendaID = #AgendaID
AND
LockedBy IS NULL
END
I have a class named Weekly Class and have the following code...
Public Shared Sub LockWeeklyAgenda(ByVal WeeklyClass As WeeklyClass)
Using dbConnection As New SqlConnection(ConfigurationManager.AppSettings("dbConnection"))
dbConnection.Open()
Dim dbTrans As SqlTransaction
dbTrans = dbConnection.BeginTransaction()
Using dbCommand As SqlCommand = dbConnection.CreateCommand
With dbCommand
.Transaction = dbTrans
.CommandType = CommandType.StoredProcedure
.CommandText = "LockWeeklyTest"
'Add Parameters for Update
.Parameters.AddWithValue("#AgendaID", WeeklyClass.AgendaID)
.Parameters.AddWithValue("#LockingUser", WeeklyClass.LockedBy)
dbCommand.ExecuteNonQuery()
End With
End Using 'dbCommand
dbTrans.Commit()
End Using
End Sub
I was thinking that the below code for the butlock would populate my Loggedby field with the username but this isnt the case.
Protected Sub btnLock_Click(sender As Object, e As System.EventArgs) Handles btnLock.Click
Dim lock As New WeeklyClass
If lock.LockedBy = "Null" Then
lock.LockedBy = System.Environment.UserName
'lock.AgendaID = AgendaID
End If
' save to the database using the Class DAL
WeeklyClassDAL.LockWeeklyAgenda(lock)
End Sub
I know that the Stored Procedure works as i have tested with the following statement as an example...
EXEC LockWeeklyTest 11, 'Betty'
Im sure that its something to do with the btnlock_click, but im not 100% sure what this is.
Any help is much appriechiated.
Your problem is this line:
If lock.LockedBy = "Null" Then
"Null" is actually a string containing the word Null. What you're after is:
If String.IsNullOrEmpty(lock.LockedBy) Then
That way, if it is actually null or empty, your LockedBy will be set. Currently, it's only setting the LockedBy if LockedBy already equals the string value "Null", which it won't directly after being declared. Is this logic really necessary considering LockedBy will always be null directly after you've declared the WeeklyClass?
Something doesn't look quite right with the AgendaID:
During the button click event the value has been commented out but is still passed through to the stored procedure inside the data layer's 'LockWeeklyAgenda' method.
It's also not defined as a nullable parameter inside the stored procedure itself, so the value that's being sent would depend on the WeeklyClass class' constructor..
can you please also show how the WeeklyClass code looks like?

Iterating over SearchResultCollection is Very Slow

I'm running an LDAP query that returns multiple entries and stores them inside a SearchResultCollection. I'm iterating over the SearchResultCollection like so:
// results is my SearchResultCollection object
foreach (SearchResult sr in results)
{
... do things to each SearchResult in here ...
}
This seems like the most logical way to do this, but loop is incredibly slow. When I step through the loop with the debugger, I find that it's the very first step of initializing the foreach loop that takes the time - the actual iterations are instantaneous.
In addition, when I view the contents of the SearchResultCollection while debugging, the watch takes just as long to load the contents of the variable.
I have a theory that the SearchResultCollection doesn't actually contain complete SearchResult objects, but rather references to entries in the Active Directory server that are then individually fetched when I iterate over the SearchResultCollection object. Can anyone confirm this theory? And is there a better (faster) way to fetch a set of LDAP entries?
I suffered from the same problem and the methodology in the post below was much quicker then my own:
http://msdn.microsoft.com/en-us/library/ms180881(v=vs.80).aspx
... to answer you question yes it seems as if you try to process the entries directly while still in the loop by using something like:
If Not UserAccount.GetDirectoryEntry().Properties("sAMAccountName").Value Is Nothing Then sAMAccountName = UserAccount.Properties("sAMAccountName")(0).ToString()
... this has a drastic impact on performance, you can get around this by adding the collection to a Dictionary within the loop and then processing the dictionary:
Dim searchResult As SearchResult
Dim dictLatestLogonDatesTemp As New Dictionary(Of String, Date)
SearchResults1 = mySearcher.FindAll()
For Each searchResult In SearchResults1
Dim propertyKey,sAMAccountName As String
Dim dteLastLogonDate As Date = Nothing
For Each propertyKey In searchResult.Properties.PropertyNames
Dim valueCollection As ResultPropertyValueCollection = searchResult.Properties(propertyKey)
For Each propertyValue As Object In valueCollection
If LCase(propertyKey) = LCase("sAMAccountName") Then sAMAccountName = propertyValue
If LCase(propertyKey) = LCase("lastLogon") Then dteLastLogonDate = Date.FromFileTime(propertyValue)
Next propertyValue
Next propertyKey
If sAMAccountName <> Nothing Then dictLatestLogonDatesTemp.Add(sAMAccountName, dteLastLogonDate)
Next searchResult
It is a bit restrictive bacause a dictionary ony has two entries but you can comma seperate other values or use a dictionary of values in the dictionary:
Dim tempDictionary As Dictionary(Of String, Dictionary(Of String, String))
Hope this helps someone!
I would like to submit that adding them to a datatable also works very well and allows for more properties than a dictionary. I went from iterating 2000 users in the searchresultscollection taking almost 2 minutes to less that 1-2 seconds. Hope this helps others.
Private Sub getAllUsers()
Dim r As SearchResultCollection
Dim de As DirectoryEntry = New DirectoryEntry(GetCurrentDomainPath)
Dim ds As New DirectorySearcher(de)
ds.SearchScope = SearchScope.Subtree
ds.PropertiesToLoad.Add("name")
ds.PropertiesToLoad.Add("distinguishedName")
ds.PropertiesToLoad.Add("objectSID")
ds.Filter = "(&(objectCategory=person)(objectClass=user))" '(!userAccountControl:1.2.840.113556.1.4.803:=2) not disabled users
PleaseWait.Status("Loading Users...")
Application.DoEvents()
r = ds.FindAll()
Dim dt As New DataTable
dt.Columns.Add("Name")
dt.Columns.Add("SID")
For Each sr As SearchResult In r
Dim SID As New SecurityIdentifier(CType(sr.Properties("objectSID")(0), Byte()), 0)
dt.Rows.Add(sr.Properties("name")(0).ToString(), SID.ToString)
Next
With lstResults
lstResults.DataSource = dt
.DisplayMember = "name"
.ValueMember = "SID"
.Items.Sort()
End With
End Sub
There may be some ways to decrease the response time:
restrict the scope of the search
use a more restrictive search filter
use a base object closer to the object(s) being retrieved.
see also
LDAP: Mastering Search Filters
LDAP: Programming practices
LDAP: Search Best Practices

How to check if sqlParameter is empty?

I have a class, which has many "New()" functions to initiate it with various parameters. I would like to create a New() function that will init the class with no arguments. The problem is there is a "fillData" function which fills the data for the class, and takes in the stored procedure to do the work, but has a stored procedure parameter. I don't want to create a new fillData function, but would like to use the fillData function with no additional need for a stored procedure. My way of going about this was to pass an empty SqlParameter var to the fillData function, but no mater what I do, when I check the params parameter for being empty, it always seems to think there's stuff in there.
I would like to check if this "params" parameter is empty:
Public Sub New()
'just created this so i could pass something
'and continue using fillData as is.
Dim p(1) As SqlParameter
Dim db As sqldb.command
db = New sqldb.command(ConfigurationManager.ConnectionStrings("sqlConnectionString").ConnectionString)
fillData("r2m_getAllListingsSorted", p)
End Sub
...
Private Sub fillData(ByVal sp As String, ByVal params As SqlParameter())
Dim db As sqldb.command
Dim r As SqlDataReader
db = New sqldb.command(ConfigurationManager.ConnectionStrings("sqlConnectionString").ConnectionString)
'right here, im having the trouble, seems no matter how I try to "params",
'the code always defaults to db.executeReader(sp, p)
'I've tried params.Length = 0 and params.ToString = "" also.
If params.Value Then
r = db.executeReader(sp)
Else
r = db.executeReader(sp, params)
End If
How would I modify this so i could continue to use the fillData function, without passing a SqlParameter parameter to it?
Thank you!
You can make your SqlParameter Optional and give it a default value of Nothing:
Private Sub fillData(ByVal sp As String,
ByVal Optional params As SqlParameter() = Nothing)
Then call the method without the params argument:
fillData("StoredProcName")
You can then check the parameter inside the fillData method to see if it's Nothing before using it.
If params is Nothing Then
// Do something if there's no params
Else
// Do something if there is params
End If

DbNull.Value Stored Procedure Parameter?

I have a web service which has a generic function that returns a dataset from results of stored procedures... Some of the stored procedures have optional parameters where the value can be null but not all the time.
Anyhow I am trying to pass in a parameter which has a value of DBNull.Value
and I get this There was an error generating the XML document. back from the web service when doing so
If I leave this parameter out it works fine... but really would like to know why DBNull.Value causes this problem.
I beleive that's becuase a System.DBNull value is a null in database table but a null field in a procedure effectively equates to the null/nothing keyword. Not a database null value. I'm not sure of the technical differences under the hood.
But in your stored proc you can just default it to null and not send the value as you've already done or i believe if you sent null/nothing it would also work.
You can pass a NULL value in the SqlParemeter, but you must do some type conversion to make sure the right null value gets passed.
In this example, there is a parameter called "Count" which is an integer, which gets passed as null:
Using dtResult as New DataTable
Using cn as SqlConnection = New SqlConnection ("ConnectionString")
Using sqlcmd as SqlCommand - New SqlCommand("StoredProceName", cn) with {.CommandType=CommandType.StoredProcedure}
Dim sp As SqlParameter
sp = sqlcmd.Parameters.Add("#Count", SqlDbType.Int)
sp.value = CType(Nothing, SqlTypes.SqlInt32)
Using myDR as SqlDataReader = sqlcmd.ExecuteReader
dtResult.Load(myDR)
end using
return dtResult
End Using ' For sqlcmd
cn.Close() ' Close the connection
end using ' For cn
end using ' For dtResult

Resources