Lambda expression creates multiple queries - vb.net

I have a strange LINQ issue of system.function. If my Lambda expression contains the datacontext twice it will generate multiple SQL-queries instead of (see second one) one SQL-query.
First
Dim whereFunction As Func(Of tasks, Boolean) = Function(task) New With {.condition =
(From i In myDataContext.taskInfo Where i.taskId = task.id).Any()
}.condition
Dim tasksLambda = myDataContext.tasks.Where(whereFunction)
Dim taskList = tasksLambda.ToList
'generates for each task one select to taskinfo on SQL Server Profiler -> bad
Second
Dim tasksNoLambda = (From task In myDataContext.tasks Where (From i In myDataContext.taskInfo Where i.taskId = task.id).Any())
taskList = tasksNoLambda .ToList
'generates only one select to tasks with subselect to taskinfo on SQL Server Profiler -> good
Can you explain WHY it makes multiple queries instead of subqueries and/or what's the difference between these two Lambda-queries.
Thanks in advance,
Daniel

myDataContext.tasks refers to a collection provided by the data source
.Where(whereFunction) is an expression that must be evaluated in the client because whereFunction can't be generally converted to an SQL expression (it is constructing new anonymous objects and I believe there's no SQL conversion for that).
myDataContext.taskInfo within the function refers back to the data source again.
Therefore there needs to be a lot of back-and-forth communication in the evaluation of the highest level expression, retrieving data from the data source, processing it in a function on the client, which in turn must retrieve more data from the data source.
Could you try the same thing without the anonymous object? Could you try this?
Dim whereFunction As Func(Of tasks, Boolean) = _
Function(task) (From i In myDataContext.taskInfo Where i.taskId = task.id).Any())
Got it. Second attempt -- try this one:
Dim whereFunction As System.Linq.Expressions.Expression(Of Func(Of tasks, Boolean)) = _
Function(task) (From i In myDataContext.taskInfo Where i.taskId = task.id).Any())
By representing the variable as a LINQ "Expression" I think it improves LINQ's ability to convert it to other forms such as SQL.

Try doing this:
Dim tasksLambda = _
From task In myDataContext.tasks
Group Join i In myDataContext.taskInfo On task.id Equals i.taskId Into Group
Where Group.Any()
Select task
This would be the best you can do to make it one query.
If that doesn't work then you can pull the tasks and taskInfo records into memory and do the filtering there. As long as you don't have too many records this can often be faster anyway.

Related

List contains duplicate Persons

Please see the code below:
Public Function ExecuteDynamicQuery(ByVal strSQL As String, ByVal list As List(Of clsType), ByVal tyType As clsType) As List(Of clsType) Implements IGenie.ExecuteDynamicQuery
Dim objParameterValues As New clsParameterValues
Dim iConnectionBLL As iConnectionBLL
Dim objCon As DbConnection
Dim objDR As DbDataReader
Dim paramValues() As DbParameter
objParameterValues = New clsParameterValues
iConnectionBLL = New clsConnectionBLL()
objCon = iConnectionBLL.getDatabaseTypeByDescription("Genie2")
Using objCon
paramValues = objParameterValues.getParameterValues
objDR = clsDatabaseHelper.ExecuteReader(objCon, CommandType.Text, strSQL, paramValues)
Do While objDR.Read
Dim tyType2 As clsType = tyType
tyType.PopulateDataReader(objDR)
list.Add(tyType2)
Loop
objDR.Close()
Return list
End Using
End Function
An SQL statement is passed to the function along with clsType (the base type). A list of types is returned e.g. a list of Persons. For example, in this case strSQL = "SELECT * FROM Persons". A list of 500 persons is returned but they are all the same person (the last person added to the list). I realise this is because the list is referncing the same object for each entry. How do I change this?
This is a situation where making the method generic would be useful. For instance:
Public Function MyGenericMethod(Of T As New)() As List(Of T)
Dim results As New List(Of T)()
For i As Integer = 0 To 9
Dim item As New T()
' Populate item ...
results.Add(item)
Next
Return results
End Function
For what it's worth, though, I see people trying do this kind of thing often, and it never sits well with me. I'm always the first one in line to suggest that common code should be encapsulated and not duplicated all over the place, but, I've never been convinced that creating some sort of data access layer that encapsulates the calls to ADO, but doesn't also encapsulate the SQL, is a good idea.
Consider for a moment that ADO, is in-and-of-itself an encapsulation of that part of the data-access layer. Sure, it can take a few more lines of code than you might like to execute a simple SQL command, but that extra complexity is there for a reason. It's necessary in order to support all of the features of the data source. If you try to simplify it, inevitably, you will one day need to use some other feature of the data source, but it won't be supported by your simplified interface. In my opinion, each data access method should use all of the necessary ADO objects directly rather than trying to some how create some common methods to do that. Yes, that does mean that many of your data access methods will be very similar in structure, but I think you'll be happier in the long run.
I've reduced your original code. The following sample is functionally equivalent to what you posted. Without knowing more about your types, it will hard to give you anything more than this, but maybe the reduction will make the code clear enough for you to spot a solution:
Public Function ExecuteDynamicQuery(ByVal sql As String, ByVal list As List(Of clsType), ByVal type As clsType) As List(Of clsType) Implements IGenie.ExecuteDynamicQuery
Dim paramValues() As DbParameter = New clsParameterValues().getParameterValues()
Using conn As DbConnection = iConnectionBLL.getDatabaseTypeByDescription("Genie2"), _
rdr As DbDataReader = clsDatabaseHelper.ExecuteReader(conn, CommandType.Text, sql, paramValues)
While rdr.Read()
type.PopulateDataReader(rdr)
list.Add(type)
End While
Return list
End Using
End Function
There are a few additional bits of advice I can give you:
You must have some way to accept parameter information for your query that is separate from the query itself. The ExecuteReader() method that you call supports this, but you only ever pass it an empty array. Fix this, or you will get hacked.
A implementation that uses Generics (as posted in another answer) would be much simpler and cleaner. The Genie interface you're relying doesn't seem to be adding much value. You'll likely do better starting over with a system that understands generics.
The problem of re-using the same object over and over can be fixed by creating a new object inside the loop. As written, the only way to do that is with a New clsType (and it seems you may have Option Strict Off, such that this could blow up on you at run time), through some messy reflection code, a switch to using generics as suggested in #2, or a by accepting a Func(Of clsType) delegate that can build the new object for you.

UPDATE statement in Oracle

We are building a client program where parameters for storage in a web server with Oracle backend are set in the .Net client program and uploaded as a dataset via webservice.
In the webservice code, data is read from the dataset and added to UPDATE statements on the web server (Oracle backend).
Because the server will run on the customer's LAN behind a firewall and because of the dynamic nature of the parameters involved, no sprocs are being used - SQL strings are built in the logic.
Here is an example string:
UPDATE WorkOrders
SET TravelTimeHours = :TravelTimeHours,
TravelTimeMinutes = :TravelTimeMinutes,
WorkTimeHours = :WorkTimeHours,
WorkTimeMinutes = :WorkTimeMinutes,
CompletedPersonID = :CompletedPersonID,
CompletedPersonName = :CompletedPersonName,
CompleteDate = :CompleteDate
WHERE WorkOrderNumber = :WorkOrderNumber
When debugging code in VS 2010 and stepping into the server code, we receive the following error:
ORA-01036: illegal variable name/number
when executing the SQL command on destination oracle machine, we were prompted to enter the bind
variables for the above statement, and as long as we used the correct date format, the UPDATE statement
worked correctly.
QUESTIONS:
1) is it possible that oracle threw the ORA-01036 error when the month format was wrong?
2) why don't we have to convert the date format from the ASP.net website running on the Oracle machine?
does Oracle have a default conversion routine that excludes the bind variable entry screen?
3) if the date format was not the problem, what precisely does ORA-1036 mean and how do I discover
WHICH variable had an illegal name/number?
This is a snippet of a function that takes the type of the dataset (WOName) and returns the appropriate SQL string.
Many Cases exist but have been removed for readability.
Private Function GetMainSQLString(ByVal WOName As String) As String
Dim Result As String = ""
Select Case WOName
Case "Monthly Site Inspection"
Dim sb As New StringBuilder
sb.Append("UPDATE WorkOrders SET ")
sb.Append("CompletedPersonID = :CompletedPersonID, CompletedPersonName = :CompletedPersonName, CompleteDate = :CompleteDate, ")
sb.Append("SupervisorID = :SupervisorID, SupervisorName = :SupervisorName ")
sb.Append("WHERE WorkOrderNumber = :WorkOrderNumber")
Result = sb.ToString
End Select
Return Result
End Function
This is a snippet of a function that takes the Oracle command object byRef and adds the required parameters to it,
depending upon which of the possible 15 types of dataset(WOName) is received from the client program.
Many Cases exist but have been removed for readability.
The updated Cmd object is then returned to the main program logic, where ExecuteNonQuery() is called.
The test values of params below are as follows:
dr.Item("CompletedPersonID") 21
dr.Item("CompletedPersonName") Pers Name
dr.Item("CompleteDate") #8/16/2010#
dr.Item("SupervisorID") 24
dr.Item("SupervisorName") Sup Name
dr.Item("WorkOrderNumber") 100816101830
Private Function addMainCmdParams(ByVal WOName As String, ByRef cmd As OracleCommand, ByVal dr As DataRow) As OracleCommand
Select Case WOName
Case "Monthly Site Inspection"
cmd.Parameters.Add(":CompletedPersonID", Oracle.DataAccess.Client.OracleDbType.Int32).Value = dr.Item("CompletedPersonID")
cmd.Parameters.Add(":CompletedPersonName", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("CompletedPersonName")
cmd.Parameters.Add(":CompleteDate", Oracle.DataAccess.Client.OracleDbType.Date).Value = dr.Item("CompleteDate")
cmd.Parameters.Add(":SupervisorID", Oracle.DataAccess.Client.OracleDbType.Int32).Value = dr.Item("SupervisorID")
cmd.Parameters.Add(":SupervisorName", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("SupervisorName")
cmd.Parameters.Add(":WorkOrderNumber", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("WorkOrderNumber")
End Select
Return cmd
End Function
While running this today, this precise code WAS successful; but another similar case was not. I still distrust any implicit typecasting performed by Oracle (if any) - and I'm especially suspicious of how Oracle handles any of these parameters that are passed with a dbNull.value - and I know it's going to happen. so if that's the problem I'll have to work around it. There are too many optional parameters and columns that don't always get values passed in for this system to break on nulls.
One Oracle "gotcha" that can cause this error is the fact that, by default, Oracle maps parameters to parameter symbols in the query by sequence, not by name. If the number/type of parameters does not match, you get an error like this one.
The solution is to tell Oracle to bind by name:
cmd.BindByName = true
Without diving into the details of your code, this may or may not be the answer to your specific problem, but this setting should be the default, and should be part of any command setup that uses parameters. It's rather amazing to watch this one statement fix some obscure problems.
EDIT: This assumes that you're using Oracle's data access provider. In .NET, you should be using this, not Microsoft's Oracle provider.
The error has nothing to do with date formats, it means that a variable in the statement was not bound.
Could be as simple as a spelling mistake (would be nice if Oracle included the variable name in the error message).
Can you update your question with the surrounding code that creates, binds, and executes the statement?
This is a snippet of a function that takes the type of the dataset (WOName) and returns the appropriate SQL string.
Many Cases exist but have been removed for readability.
Private Function GetMainSQLString(ByVal WOName As String) As String
Dim Result As String = ""
Select Case WOName
Case "Monthly Site Inspection"
Dim sb As New StringBuilder
sb.Append("UPDATE WorkOrders SET ")
sb.Append("CompletedPersonID = :CompletedPersonID, CompletedPersonName = :CompletedPersonName, CompleteDate = :CompleteDate, ")
sb.Append("SupervisorID = :SupervisorID, SupervisorName = :SupervisorName ")
sb.Append("WHERE WorkOrderNumber = :WorkOrderNumber")
Result = sb.ToString
End Select
Return Result
End Function
This is a snippet of a function that takes the Oracle command object byRef and adds the required parameters to it,
depending upon which of the possible 15 types of dataset(WOName) is received from the client program.
Many Cases exist but have been removed for readability.
The updated Cmd object is then returned to the main program logic, where ExecuteNonQuery() is called.
The test values of params below are as follows:
dr.Item("CompletedPersonID") 21
dr.Item("CompletedPersonName") Pers Name
dr.Item("CompleteDate") #8/16/2010#
dr.Item("SupervisorID") 24
dr.Item("SupervisorName") Sup Name
dr.Item("WorkOrderNumber") 100816101830
Private Function addMainCmdParams(ByVal WOName As String, ByRef cmd As OracleCommand, ByVal dr As DataRow) As OracleCommand
Select Case WOName
Case "Monthly Site Inspection"
cmd.Parameters.Add(":CompletedPersonID", Oracle.DataAccess.Client.OracleDbType.Int32).Value = dr.Item("CompletedPersonID")
cmd.Parameters.Add(":CompletedPersonName", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("CompletedPersonName")
cmd.Parameters.Add(":CompleteDate", Oracle.DataAccess.Client.OracleDbType.Date).Value = dr.Item("CompleteDate")
cmd.Parameters.Add(":SupervisorID", Oracle.DataAccess.Client.OracleDbType.Int32).Value = dr.Item("SupervisorID")
cmd.Parameters.Add(":SupervisorName", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("SupervisorName")
cmd.Parameters.Add(":WorkOrderNumber", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("WorkOrderNumber")
End Select
Return cmd
End Function
While running this today, this precise code WAS successful; but another similar case was not. I still distrust any implicit typecasting performed by Oracle (if any) - and I'm especially suspicious of how Oracle handles any of these parameters that are passed with a dbNull.value - and I know it's going to happen. so if that's the problem I'll have to work around it. There are too many optional parameters and columns that don't always get values passed in for this system to break on nulls.

Linq to Datarow, Select multiple columns as distinct?

basically i'm trying to reproduce the following mssql query as LINQ
SELECT DISTINCT [TABLENAME], [COLUMNNAME] FROM [DATATABLE]
the closest i've got is
Dim query = (From row As DataRow In ds.Tables("DATATABLE").Rows _
Select row("COLUMNNAME") ,row("TABLENAME").Distinct
when i do the above i get the error
Range variable name can be inferred
only from a simple or qualified name
with no arguments.
i was sort of expecting it to return a collection that i could then iterate through and perform actions for each entry.
maybe a datarow collection?
As a complete LINQ newb, i'm not sure what i'm missing.
i've tried variations on
Select new with { row("COLUMNNAME") ,row("TABLENAME")}
and get:
Anonymous type member name can be
inferred only from a simple or
qualified name with no arguments.
to get around this i've tried
Dim query = From r In ds.Tables("DATATABLE").AsEnumerable _
Select New String(1) {r("TABLENAME"), r("COLUMNNAME")} Distinct
however it doesn't seem to be doing the distinct thing properly.
Also, does anyone know of any good books/resources to get fluent?
You start using LINQ on your datatable objects, you run the query against dt.AsEnumberable, which returns an IEnumerable collection of DataRow objects.
Dim query = From row As DataRow In ds.Tables("DATATABLE").AsEnumerable _
Select row("COLUMNNAME") ,row("TABLENAME")
You might want to say row("COLUMNNAME").ToString(), etc. Query will end up being an IEnumerable of an anonymous type with 2 string properties; is that what you're after? You might need to specify the names of the properties; I don't think the compiler will infer them.
Dim query = From row As DataRow In ds.Tables("DATATABLE").AsEnumerable _
Select .ColumnName = row("COLUMNNAME"), .TableName = row("TABLENAME")
This assumes that in your original sql query, for which you used ADO to get this dataset, you made sure your results were distinct.
Common cause of confusion:
One key is that Linq-to-SQL and (the Linq-to-object activity commonly called) LINQ-to-Dataset are two very different things. In both you'll see LINQ being used, so it often causes confusion.
LINQ-to-Dataset
is:
1 getting your datatable the same old way you always have, with data adapters and connections etc., ending up with the traditional datatable object. And then instead of iterating through the rows as you did before, you're:
2 running linq queries against dt.AsEnumerable, which is an IEnumerable of datarow objects.
Linq-to-dataset is choosing to (A) NOT use Linq-to-SQL but instead use traditional ADO.NET, but then (B) once you have your datatable, using LINQ(-to-object) to retrieve/arrange/filter the data in your datatables, rather than how we've been doing it for 6 years. I do this a lot. I love my regular ado sql (with the tools I've developed), but LINQ is great
LINQ-to-SQL
is a different beast, with vastly different things happening under the hood. In LINQ-To-SQL, you:
1 define a schema that matches your db, using the tools in in Visual Studio, which gives you simple entity objects matching your schema.
2 You write linq queries using the db Context, and get these entities returned as results.
Under the hood, at runtime .NET translates these LINQ queries to SQL and sends them to the DB, and then translates the data return to your entity objects that you defined in your schema.
Other resources:
Well, that's quite a truncated summary. To further understand these two very separate things, check out:
LINQ-to-SQL
LINQ-to-Dataset
A fantastic book on LINQ is LINQ in Action, my Fabrice Marguerie, Steve Eichert and Jim Wooley (Manning). Go get it! Just what you're after. Very good. LINQ is not a flash in the pan, and worth getting a book about. In .NET there's way to much to learn, but time spent mastering LINQ is time well spent.
I think i've figured it out.
Thanks for your help.
Maybe there's an easier way though?
What i've done is
Dim comp As StringArrayComparer = New StringArrayComparer
Dim query = (From r In ds.Tables("DATATABLE").AsEnumerable _
Select New String(1) {r("TABLENAME"), r("COLUMNNAME")}).Distinct(comp)
this returns a new string array (2 elements) running a custom comparer
Public Class StringArrayComparer
Implements IEqualityComparer(Of String())
Public Shadows Function Equals(ByVal x() As String, ByVal y() As String) As Boolean Implements System.Collections.Generic.IEqualityComparer(Of String()).Equals
Dim retVal As Boolean = True
For i As Integer = 0 To x.Length - 1
If x(i) = y(i) And retVal Then
retVal = True
Else
retVal = False
End If
Next
Return retVal
End Function
Public Shadows Function GetHashCode(ByVal obj() As String) As Integer Implements System.Collections.Generic.IEqualityComparer(Of String()).GetHashCode
End Function
End Class
Check out the linq to sql samples:
http://msdn.microsoft.com/en-us/vbasic/bb688085.aspx
Pretty useful to learn SQL. And if you want to practice then use LinqPad
HTH
I had the same question and from various bits I'm learning about LINQ and IEnumerables, the following worked for me:
Dim query = (From row As DataRow In ds.Tables("DATATABLE").Rows _
Select row!COLUMNNAME, row!TABLENAME).Distinct
Strangely using the old VB bang (!) syntax got rid of the "Range variable name..." error BUT the key difference is using the .Distinct method on the query result (IEnumerable) object rather than trying to use the Distinct keyword within the query.
This LINQ query then returns an IEnumerable collection of anonymous type with properties matching the selected columns from the DataRow, so the following code is then accessible:
For Each result In query
Msgbox(result.TABLENAME & "." & result.COLUMNNAME)
Next
Hoping this helps somebody else stumbling across this question...

What does the "New ... With" syntax do in VB Linq?

What (if any) is the difference between the results of the following two versions of this VB Linq query?
' assume we have an XElement containing employee details defined somewhere else
Dim ee = From e In someXML.<Employee> _
Select New With {.Surname = e.<Surname>, .Forename = e.<Forename>}
and
Dim ee = From e In someXML.<Employee> _
Select Surname = .Surname = e.<Surname>, .Forename = e.<Forename>
ie what is the point of the New ... With syntax?
I suspect that this has a simple answer, but I can't find it - any links to suitable tutorials or Microsoft documentation would be appreciated.
The difference is that the 1st explicitly creates an anonymous type. The 2nd is a query expression, and may use an existing type rather than creating an anonymous type. From the documentation linked by Cameron MacFarland:
Query expressions do not always require the creation of anonymous types. When possible, they use an existing type to hold the column data. This occurs when the query returns either whole records from the data source, or only one field from each record.
My understanding is that there is no difference.
New With is aimed to out-of-query usage like
Dim X = New With { .Surname = "A", .Forename = "B" }
Specifically for Linq queries, you can skip New With, but it is still useful for other situations. I am not sure, however, since I do not know VB 9 :)
There is no functional difference between the two pieces of code you listed. Under the hood both pieces code will use an anonymous type to return the data from the query.
The first piece of code merely makes the use of an anonymous type explicit. The reason this syntax is allowed is that it's possible to return any type from a Select clause. But the type must be used explicitly.
Dim x = From it in SomeCollection Select New Student With { .Name = it.Name }
Joel is incorrect in his statement that the second query may use an existing type. Without an explicit type, a select clause which uses an explicit property name will always return an anonymous type.
They're called Anonymous Types.
The main reason for their use is to keep the data from a query in a single object, so the iterators can continue to iterate over a list of objects.
They tend to work as temporary types for storage in the middle of a large or multi-part LINQ query.
There is no difference. The compiler will infer the anonymous type.
You most likely want to return the Value of the elements as in e.<Surname>.Value, which returns a String instead of an XElement.
Your 2nd example could be simplified as
Dim ee = From e In someXML.<Employee> _
Select e.<Surname>.Value, e.<Forename>.Value
because the compiler will also infer the names of the members of the anonymous type.
However, if you have the following class
Class Employee
Private _surname As String
Public Property Surname() As String
Get
Return _surname
End Get
Set(ByVal value As String)
_surname = value
End Set
End Property
Private _forename As String
Public Property Forename() As String
Get
Return _forename
End Get
Set(ByVal value As String)
_forename = value
End Set
End Property
End Class
Then you could change the 1st query to produce an IQueryable(Of Employee) instead of the anonymous type by using New ... With like so:
Dim ee = From e In someXML.<Employee> _
Select New Employee With {.Surname = e.<Surname>.Value, _
.Forename = e.<Forename>.Value}
One difference is that Anonymous types aren't serializable.

Adding Nodes to Tree via LINQ creates "query operator not supported" during runtime

I am trying to get my head around a LINQ issue. The ultimate goal is to load tree view with data acquired from LINQ to SQL (view). The issue is when I try to access the data acquired it keeps throwing a "query operator not supported during runtime". The code involved is no more than:
Dim tasklistdata As New lqDataContext
Dim UserID As Integer
UserID = (From vwUser In tasklistdata.Operators _
Where vwUser.UserName Is Login.txtUsername.Text _
Select vwUser.OperatorID).Single
Dim lqHospitalList = From vwHospitalList In tasklistdata.SPM_Accounts _
Where vwHospitalList.OperatorID = UserID _
Order By vwHospitalList.CustomerName, vwHospitalList.Class _
Select vwHospitalList.CustomerName, vwHospitalList.Class, vwHospitalList.ClassCount, vwHospitalList.Charges
tvHospitalSelect.Nodes.Add(lqHospitalList(0).CustomerName)
if I were to change the code to lqHospitalList.First.CustomerName it works as expected, however changing it to array position is where the issue arises. Please direct me to where my flaw in logic is.
Linq querys are not lists, so they don't have an index. To iterate over all the items you can use a foreach like so:
For Each hospital In lqHospitalList
tvHospitalSelect.Nodes.Add(hospital.CustomerName)
Next
If you want to convert the query to a list:
lqHospitalList.ToList
or array:
lqHospitalList.ToArray