Write a Query based on a Query - Not Working With Concatenation - vb.net

I need to create a datasource for a combobox using a Linq to SQL query.
i.e. cboFindPerson.DataSource = LQPersonList
(where LQPersonList is a query)
But this time, I need to first concatenate the LastName and FirstName fields, and then order by the FullName, like this.
'-- Create the first Query with concatenation
Dim LQ = From b In DCAppMain.tblPeopleMain
Where b.PeopleID = lngPeopleID And b.CurrentEmployee = True
Select FullName = b.LastName & ", " & b.FirstName, b.PeopleID
'-- Create the 2nd Query based on first so I can order by FullName
Dim LQPersonList = From c In LQ
Order By c.
But when I get to c., intellisense says no fields are available.
I've written queries based on queries before w/o issue. I've also concatenated fields w/o issue. But apparently putting the two together is an issue.
I've been searching on this for several hours now, but can't find an answer that is on target.

Instead of using:
Dim LQ = From b In DCAppMain.tblPeopleMain
Where b.PeopleID = lngPeopleID And b.CurrentEmployee = True
Select FullName = b.LastName & ", " & b.FirstName, b.PeopleID
you need to use:
Dim LQ = From b In DCAppMain.tblPeopleMain Where b.PeopleID = lngPeopleID And
b.CurrentEmployee = True Select New With{.FullName = b.LastName & ", " &
b.FirstName, .PeopleID = b.PeopleID}
this is because when you pull individual properties out of a LINQ statement you end up with an IEnumerable(Of AnonymousType) which is fine but if you want intellisense to pick up the values that you are pulling out of your collection of objects then you need to assign them names.
So using the second LINQ statement you are basically creating a constructor for the anonymous type that also defines the properties FullName and PeopleID on the anonymous objects that make up the collection returned by the LINQ statement.

Related

Most efficient way of deleting records from a view with multiple base tables?

I need write a VB function that deletes all records from the base tables of a view that have been initialised with default values/default constraint key values.
My understanding is that the only way to do this is to delete the records from each base table individually, but I am not sure if there is an easier, more efficient way of doing this than what I am trying to attempt. I would like some guidance/advice if possible.
This is the only way I can think of of doing this:
Run a query that returns base table names from the view:
DECLARE #vn as nvarchar(max) = 'dbo.TABLE_NAME'
SELECT referenced_server_name, referenced_database_name, referenced_entity_name as SourceTable,referenced_minor_name as SourceColumn, referenced_minor_id as depnumber
FROM sys.dm_sql_referenced_entities (#vn, 'OBJECT')
where referenced_minor_name IS NOT NULL
ORDER BY referenced_entity_name, referenced_minor_id
run sp_helpconstraint N'<table_name>', which will give me a list of all default values/default constraint types, as well as the column names I will need to compare default values to the values in each table and determine whether or not a record should be deleted from them.
Questions
Is there an easier/more efficient way of trying to delete a record from a view?
NOTE: the full function I ended up writing has been added to the answers below for anyone interested in the answer
I am not sure if there is an easier, more efficient way of doing this than what I am trying to attempt. I would like some guidance/advice if possible
TLDR; there isn't
You need to understand that a view is NOT data in the database; it's a stored SQL query that is run every time you select from the view.
It might even be the case that SQL Server takes your query and mixes it with the query that provides the view and optimizes them and runs them so it's not even necessarily the case that it runs the view query, gets all million records that the view represents, and then rifles through them looking for the one guy called Constantinople Ernhardt - SQL Server might consider it better to silently and transparently rewrite the query you gave so it's planned and run completely differently to what you might think - it does this for every query, in a process called optimization.
Your view is:
CREATE VIEW MyView AS
SELECT * FROM Person p JOIN Address a on p.AddressId = a.Id
You write:
SELECT * FROM MyView WHERE Name = 'Abc' and HouseName = 'def'
You might think it does this (and conceptually, you're right):
SELECT * FROM
(
SELECT * FROM Person p JOIN Address a on p.AddressId = a.Id
) x WHERE Name = 'Abc' and HouseName = 'def'
But it probably gets rewritten to this:
SELECT * FROM Person p JOIN Address a on p.AddressId = a.Id WHERE Name = 'Abc' and HouseName = 'def'
So now that's out the way and you can see that a view is just a query on top of a table, that gets run every time you select from it - how do you delete data from a query?
You can't, because queries don't have data; they retrieve data from tables
The only way to "delete data from a view" is to delete data from the table the view selects the data from
You can only do this by DELETE statements on the table(s) concerned
There is a facility where you can write an INSTEAD OF trigger on a view, and then delete from the view, and SQL Server will run the trigger (which deletes from the underlying tables). It might look like you're deleteing data from the view, but really, you're just causing a mechanism to remove data from the underlying tables in the same way that the view is a mechanism that drags data out of those tables.
You could write a stored procedure that deletes data, but again, that's just a mechanism to delete data from the underlying table
Choose any method you like, as suits your business goals and desire for encapsulating your software in a certain way. For example, in the past I've had a software I couldn't change (lost the sourcecode or whatever) and it has been hard wired to SELECT FROM users or DELETE FROM users - we wanted the software to carry on working even though the users table was being renamed to members. We renamed the table then created a view called users that just did SELECT * FROM members - that allows the app to carry on working, reading data. Then we created INSTEAD OF triggers to update and delete data in the members table when the app attempted to do that operation to the users view (which the app still thought was a table)
So why is it so hard? Well the data that comes out of your view might not even relate to a table row any more. Here's a simple version:
CREATE VIEW MyView AS
SELECT MAX(SUBSTR(Name, 4, 999)) MaxFirstName FROM Person GROUP BY Gender
Suppose there were two people called Mr Lee Smith and Ms Lee Smith, you've taken the max of the function output and got Lee Smith and now you want to delete Lee Smith out of the person table by analyzing the view and deleting... what out of the Person table? Which record? Grouping mushed all the records up together. The MAX name of one an the MIN birthdate from another..
Here's another example, bit more ridiculous but imaginable:
CREATE VIEW MyView AS
SELECT name as x FROM person
UNION
SELECT street FROM address
This could easily produce the distinct value "Penny Lane" - but is it a person, or a road? Which should be deleted if we delete from this view where x = 'Penny Lane'
There isn't a magic bullet where you can run it and it will says "this table uses these 3 tables" so you can delete from them. It wouldn't even be a good premise. Your view might select from one data table and one lookup table, and deleting Gender type 1 from the lookup table just because you're deleting Donald Trump from the users table, would be a bad call
If you want to provide delete facilities on a view, you need to code something up; there isn't an automagical solution that can work out what data from what tables should be deleted and what should remain. Just imagine how difficult it would be to analyze a view that joins 9 tables, with a mix of join styles, plus another 8 lists of VALUES, 3 calls to a cross applied table valued function that parses some json, sling a couple of row generating recursive CTEs in there and a pivot too...
Absolutely no chance that anyone would have written the magic button to pick all that apart into "work out the list of base tables and what data should be deleted from which to satisfy DELETE FROM MyView WHERE output_of_parsing_function = 'Hello'
This is the final working function I ended up using. (feel free to suggest a better way)
If DI.CommonDataFunctions.IsThisAView(BaseTableName, Globals.dif) Then
Dim viewColumnNames As New List(Of String)
Dim tableName As String
Dim viewTables As New List(Of String)
Dim isDefault
Dim primaryKeyName As String
//return table names from view
Dim qp As New List(Of SqlParameter)
qp.Add(New SqlParameter("#vn", $"dbo.{BaseTableName}"))
Dim sql As String = "
SELECT
referenced_entity_name as SourceTable,referenced_minor_name as SourceColumn
FROM
sys.dm_sql_referenced_entities (#vn, 'OBJECT')
WHERE
referenced_minor_name IS NOT NULL
ORDER BY
referenced_entity_name, referenced_minor_id"
Using dr As New DataReader(Globals.dif.GetDBDetails)
Dim constraintKeys As New Dictionary(Of String, String)()
Dim primaryKeyList As New List(Of Int32)
Dim table As String
dr.ExecuteReader(sql, qp)
Do While dr.Read
tableName = dr.Item("SourceTable").ToString.ToUpper.Trim
viewColumnNames.Add(dr.Item("SourceColumn").ToString.ToUpper.Trim)
If Not viewTables.Contains(tableName) Then
viewTables.Add(tableName)
End If
Loop
For Each table In viewTables
Dim columnName As String
Dim defaultConstraintValue
isDefault = True
table = table
dr.ExecuteReader("
SELECT Col.Column_Name from
INFORMATION_SCHEMA.TABLE_CONSTRAINTS Tab,
INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE Col
WHERE
Col.Constraint_Name = Tab.Constraint_Name
AND Col.Table_Name = Tab.Table_Name
AND Constraint_Type = 'PRIMARY KEY'
AND Col.Table_Name = '" + table + "'")
While dr.Read
primaryKeyName = dr.Item(0)
End While
//return default constraints
dr.ExecuteReader("
SELECT
ColumnName = c.name,
TableName = t.name,
df.definition
FROM
sys.default_constraints df
INNER JOIN
sys.tables t ON df.parent_object_id = t.object_id
INNER JOIN
sys.columns c ON c.object_id = df.parent_object_id AND df.parent_column_id = c.column_id
WHERE
t.Name = N'" + table + "'")
While dr.Read
defaultConstraintValue = dr.Item("definition").ToString
//delete "(( ))" Or "( )" from default constraint
If defaultConstraintValue.StartsWith("((") AndAlso defaultConstraintValue.EndsWith("))") Then
defaultConstraintValue = defaultConstraintValue.Substring(0, defaultConstraintValue.Length - 2)
defaultConstraintValue = defaultConstraintValue.Substring(2)
ElseIf defaultConstraintValue.StartsWith("(") AndAlso defaultConstraintValue.EndsWith(")") Then
defaultConstraintValue = defaultConstraintValue.Substring(0, defaultConstraintValue.Length - 1)
defaultConstraintValue = defaultConstraintValue.Substring(1)
End If
If defaultConstraintValue.StartsWith("'") AndAlso defaultConstraintValue.EndsWith("'") Then
defaultConstraintValue = defaultConstraintValue.Substring(0, defaultConstraintValue.Length - 1)
defaultConstraintValue = defaultConstraintValue.Substring(1)
If Not IsNumeric(defaultConstraintValue) Then
defaultConstraintValue = "'" + defaultConstraintValue + "'"
End If
End If
columnName = dr.Item("ColumnName").ToString.ToUpper.Trim
constraintKeys.Add(columnName, defaultConstraintValue)
End While
Next
Dim sql2 = "SELECT " + primaryKeyName + " FROM " + BaseTableName
If constraintKeys IsNot Nothing Then
Dim isFirstFilter = True
sql2 &= " WHERE "
For Each constraintKey In constraintKeys
If viewColumnNames.Contains(constraintKey.Key) AndAlso constraintKey.Key <> "FAMILY_UID" Then
If isFirstFilter = False Then
sql2 &= " And "
End If
If IsNumeric(constraintKey.Value) Then
Dim intConverted = CInt(constraintKey.Value)
sql2 &= constraintKey.Key + " = " + intConverted.ToString + " "
If isFirstFilter = True Then
isFirstFilter = False
End If
Else
sql2 &= constraintKey.Key + " = " + constraintKey.Value + " "
If isFirstFilter = True Then
isFirstFilter = False
End If
End If
End If
Next
End If
dr.ExecuteReader(sql2)
While dr.Read
primaryKeyList.Add(dr.Item(primaryKeyName))
End While
If primaryKeyList.Count > 0 Then
For Each table In viewTables
Dim isFirstFilter = True
Dim sql3 = "DELETE FROM " + table + " WHERE " + primaryKeyName + " IN ("
For Each primaryKey In primaryKeyList
sql3 &= primaryKey.ToString
If Not primaryKey = primaryKeyList(primaryKeyList.Count - 1) Then
sql3 &= ", "
End If
Next
sql3 &= ")"
Using CEx As New CommandExecutor(Globals.dif)
CEx.ExecuteNonQuery(sql3)
End Using
Next
End If
End Using
End If

How do I access multiple records from the same table using SQLDataAdapter?

This almost works. I get an error at the last line that looks like it's complaining about the C1 reference. Is there a simple way around this? There is nothing wrong with the query or connection.
Dim CmdString As String
Dim con As New SqlConnection
Try
con.ConnectionString = PubConn
CmdString = "select * from " & PubDB & ".dbo.Suppliers as S " & _
" join " & PubDB & ".dbo.Address as A" & _
" on S.Supplier_Address_Code = A.Address_IDX" & _
" join " & PubDB & ".dbo.Contacts as C1" & _
" on S.Supplier_Contact1 = C1.Contact_IDX" &
" join " & PubDB & ".dbo.Contacts as C2" & _
" on S.Supplier_Contact2 = C2.Contact_IDX" &
" WHERE S.Supplier_IDX = " & LookupIDX
Dim cmd As New SqlCommand(CmdString)
cmd.Connection = con
con.Open()
Dim DAdapt As New SqlClient.SqlDataAdapter(cmd)
Dim Dset As New DataSet
DAdapt.Fill(Dset)
con.Close()
With Dset.Tables(0).Rows(0)
txtAddress1.Text = .Item("Address1").ToString
txtAddress2.Text = .Item("Address2").ToString
txtSupplierName.Text = .Item("Address_Title").ToString
txtAttn.Text = .Item("Attn").ToString
txtBusinessPhone1.Text = .Item("C1.Contact_Business_Phone").ToString
You would not include the "C1" table alias as part of your column name. It will be returned from your query as Contact_Business_Phone.
For accessing multiple rows you could use the indexer as you are in the example above "Rows(0)" by placing your With block into a For loop and accessing the "Rows(i)" with your loop variable. However, this would not help much as your are assigning this to individual text boxes, so you'd only see the last value on your page/screen.
The alias C1 is used by SQL Server and is not persisted to the result set. Have you taken this query into SQL Management Studio to see the results?
Since you requested all columns (*) and joined to the Contacts table twice, you'll end up with duplicate column names in the result. For example, if the Contacts table has a LastName field, you'll end up with TWO LastName columns in your result.
I haven't tried to duplicate this in my local environment, but I can't imagine the data adapter is going to like having duplicate column names.
I recommend specifically including the columns you want to return instead of using the *. That's where you'll use the alias of C1, then you can rename the duplicate columns using the AS keyword:
SELECT C1.LastName AS [Supplier1_LastName],
C2.LastName AS [Supplier2_LastName],
...
This should solve your problem.
Good Luck!
You should only be pulling back the columns that you're in fact interested in, as opposed to *. It's sort of hard to tell exactly what data exists in which tables since you're pulling the full set, but at a quick guess, you'll want in your select statement to pull back A.Address1, A.Address2, A.AddressTitle, ?.Attn (not sure which table this actually derives from) and C1.Contact_Business_Phone. Unless you actually NEED the other fields, you're much better off specifying the individual fields in your query, besides having the possible duplicate field issue that you're running into here, it can also be a significant performance hit pulling everything in. After you clean up the query and only pull in the results you want, you can safely just reference them the way you are for the other fields, without needing a table alias (which as others have pointed out, isn't persisted to the result set anyways).

Datagrid Duplication due to Select Statement VB

I am using an Access database, I believe the problem lies in my SQL statement. I have a relational database, with two tables -
StaffDetails [columns(
StaffID,
FirstName,
LastName)]
and
StaffTraining [columns(
StaffID,
Month)].
I have a combobox (cbMonth) and dependent on what month is chosen if the user selects 'January' then I would like the datagrid (DGTraining) to show the First Name and Last Name of the members of staff whose ID are within the chosen month. Sorry if this is not the clearest explanation, hopefully my code below makes my issue clearer:
Dim SqlQuery As String = "SELECT [StaffDetails.StaffID], [StaffDetails.FirstName], [StaffDetails.LastName], [StaffTraining.StaffID] FROM [StaffDetails], [StaffTraining] WHERE StaffTraining.TrainingMonth='" & cbMonth.Text & "'"
Dim da As OleDbDataAdapter = New OleDbDataAdapter(SqlQuery, conn)
Dim ds As DataSet = New DataSet
da.Fill(ds, "Training")
Dim dt As DataTable = ds.Tables("Training")
With DGTraining
.AutoGenerateColumns = True
.DataSource = ds
.DataMember = "Training"
End With
You are missing your join and are getting a cross join. 2 ways of addressing:
FROM [StaffDetails] inner join [StaffTraining] on [StaffDetails].staffID = [StaffTraining].staffID
That is the join logic thats more common and easier to read. You could add to your where clause (old method, harder to read and not as commonly accepted:
...where [StaffDetails].staffID = [StaffTraining].staffID and ...
Your last comment needs amending like this...
Dim SqlQuery As String = "SELECT [StaffDetails.StaffID], [StaffDetails.FirstName],
[StaffDetails.LastName], FROM [StaffDetails]
INNER JOIN [StaffTraining] ON [StaffDetails].StaffID = [StaffTraining].StaffID
WHERE [StaffTraining].TrainingMonth='" & cbMonth.Text & "'"
Also... dependant on how you have set up cbMonth you may want cbMonth.SelectedValue or cbMonth.SelectedText

SQL statement for combobox row source

I'm trying to define a SQL statement to use as the Row Source for a ComboBox on an MSAccess form. The SQL should select records from a table tblI where a particular table field matches a variant parameter varS set by the user; however, if varS is Null or not present in another table tblS, the SQl should select all records in tblI.
I can code the first parts of this (varS matches or is null):
SELECT tblI.ID, tblI.S FROM tblI WHERE ((tblI.S = varS) OR (varS Is Null)) ORDER BY tblI.ID;
Where I'm struggling is incorporating the final element (varS not present in tblS). I can code a test for the absence of varS in tblS:
Is Null(DLookup("[tbls.ID]","tblS","[tblS.ID]= " & varS))
but I can't work out how to incorporate this in the SQL statement. Should this work?
SELECT tblI.ID, tblI.S FROM tblI WHERE tblI.S = varS OR varS Is Null OR DLookup("[tbls.ID]","tblS","[tblS.ID]= " & varS) Is Null ORDER BY tblI.ID;
When run as a query it returns every record in tblS no matter the value of varS.
Table structure:
tblI contains 2 fields, Autonumber ID and Long S
tblS contains 1 field, Autonumber ID
My own approach to this problem would be something like this:
Private Sub SetComboSource(vID as Variant)
Dim sSQL as String
sSQL = "SELECT tblI.ID, tblI.S " & _
"FROM tblI "
If IsNull(vID) = False Then
If IsNumeric(vID) = True Then
If DCount("ID", "tblS", "ID = " Clng(vID)) > 0 Then
sSQL = sSQL & "WHERE tblI.S = " & CLng(vID)
End If
End If
End If
sSQL = sSQL & " ORDER BY tblI.ID"
Me.cboComboBox.RowSource = sSQL
End Sub
BTW, I recommend you give your tables and fields more descriptive names and then use aliasing in your SQL, especially for table names. I also think it's best to avoid using Variant variables. I usually use Longs for something like this and I take a value less than 1 to mean that the user didn't select anything, or selected ALL, or whatever meaning you want to derive from it. In other words, my ID's are always a number greater than zero and an ID of less than 1 in a variable means that the ID is empty. Which I use as a signal to create a new record, or to return all records, or whatever meaning you want to derive from it in the given context.
The following should work;
SELECT tblI.ID, tblI.S
FROM tblI
WHERE tbl.ID=varS
OR varS NOT IN(SELECT ID from tblS)

VB.NET 2010 & MS Access 2010 - Conversion from string "" to type 'Double' is not valid

I am new to VB.Net 2010. Here is my problem: I have a query that uses a combo box to fetch many items in tblKBA. All IDs in the MS Access database are integers. The combo box display member and value member is set to the asset and ID of tblProducts.
myQuery = "SELECT id, desc, solution FROM tblKBA WHERE tblKBA.product_id = '" + cmbProducts.SelectedValue + "'"
In addition to getting items from the KBA table, I want to fetch the department details from the department table, possibly done in the same query. I am trying to do it in two separate queries.
myQuery = "select telephone, desc, website from tblDepartments where tblDepartments.product_id = tblProducts.id and tblProducts.id = '" + cmbProducts.SelectedValue + "' "
All help will be appreciated!
Change the '+' to a '&' then the compiler would be happy.
try adding .toString to cmbproducts.selectedvalue or do "tblKBA.product_id.equals(" & cmbProducts.selectedValue.toString & ")"
1.) Don't use string concatenation to build your query. Use parameters.
2.) I am guessing that tblKBA.product_id is a double and not a string, so don't put quotes around it.
myQuery = "SELECT id, desc FROM tblKBA WHERE tblKBA.product_id = ?"
3 things. Test your value before building the select statement. Second, Use .SelectedItem.Value instead of .SelectedValue. Third, protect yourself from sql injection attack. Use parameters, or at the very least check for ' values.
If IsNumeric(cmbProducts.SelectedItem.Value) = False Then
'No valid value
Return
End If
myQuery = String.Format("SELECT id, desc FROM tblKBA WHERE tblKBA.product_id = {0}", cmbProducts.SelectedItem.Value.Replace("'", "''"))