Programmatically creating linked tables in access - sql-server

We need to find a way to programatically ****link all the tables in a SQL Server database to an access db. We will be invoking this access database from a program that uses .net/SQL Server 2008.
While invoking the application we would like to add the linked tables so that the users can just run the reports/modules from access without having to worry about linking the tables. Is there a way we can do this?

Here are some notes.
Dim sLocalName As String
Dim tdf As TableDef
Dim rs As dao.Recordset
''This is a basic connection string, you may need to consider password and so forth
cn = "ODBC;DSN=TheDSNName;Trusted_Connection=Yes;APP=Microsoft Office 2010;DATABASE=TheDatabaseName;"
''All fields from tables
strSQL = "SELECT TABLE_CATALOG, " _
& "TABLE_SCHEMA, " _
& "TABLE_NAME, " _
& "TABLE_TYPE " _
& "FROM [" & cn & "].INFORMATION_SCHEMA.tables " _
& "WHERE TABLE_TYPE = 'BASE TABLE'"
Set rs = CurrentDb.OpenRecordset(strSQL)
Do While Not rs.EOF
sLocalName = rs!TABLE_SCHEMA & "_" & rs!TABLE_NAME
With CurrentDb
If DLookup("Name", "MSysObjects", "Name='" & sLocalName & "'") <> vbNullString Then
If .TableDefs(sLocalName).Connect <> cn Then
.TableDefs(sLocalName).Connect = cn
.TableDefs(sLocalName).RefreshLink
End If
Else
''If the table does not have a unique index, you will neded to create one
''if you wish to update.
Set tdf = .CreateTableDef(sLocalName)
tdf.Connect = cn
tdf.SourceTableName = rs!TABLE_NAME
.TableDefs.Append tdf
.TableDefs.Refresh
''This will produce a message box if the table does not have a unique index
''DoCmd.TransferDatabase acLink, "ODBC Database", cn, acTable, rs!TABLE_NAME, sLocalName
End If
End With
rs.MoveNext
Loop

You'll need an ODBC connection to the SQL database. Once this connection ready, you can use it for all tables that you want to link:
DoCmd.TransferDatabase acLink, _
"ODBC Database", _
myODBCconnection, _
myDatabaseName, _
acTable, _
myTableName
I guess you can declare your ODBC connector "on the fly", as proposed here for example.
To enumerate your tables, you have the following options:
Enumerate them in the code: one transferDatabase line per table
Save the table names in a local table, and browse the table
Save the table names in a file (text, xml) anywhere on the network and browse the file
Access the system table on the server that holds the table list, and browse the table
Use the ADOX object to browse all tables in your database server: be carefull not to include system tables. This solution might be also quite confusing because you'll have to first open an ADODB connection to your database, and you'll then use an ODBC connection to open the tables
In all cases, this procedure shall be launched with the autoexec macro, meaning that links will be created\updated each time the user opens the mdb client.

You would use ADOX to do the actual linking.
As far as enumerating the tables in a database you are connected to, you could do something as simple as running this query against your SQL Server, but there are a lot of ways to skin that cat:
SELECT * FROM INFORMATION_SCHEMA.TABLES

You can achieve the equivalent by using a Linked Server in SQL Server that points to the Access db. This will give you access to all the tables in the Access db so that you can reference them like:
Select ..
From [LinkedServerName]...[AccessTableName]
Btw, a linked server may be overkill for what you want. Look into the OPENROWSET function which effectively let's you pass a connection string.
EDIT: I originally read the question to literally mean "link tables in SQL Server to access" which I translated to mean from SQL to Access. So, given that, my solution would apply. However, if the desire is to go from Access to SQL, then that is different and other solutions presented would be more appropriate.

Related

Searching for Field in ODBC Machine Data Sources - MS Access

Yesterday I had to run a query in MS Access 2010. One field I needed was not in the tables I usually use (already linked through the ODBC Database) and I didn't know what table it was a part (there are several hundred tables in the Machine Data Sources). Aside from manually importing all the tables and looking in each one for this field is there a way I can search for a field without knowing the table either 1. without importing any tables from the ODBC Databases, or if not 2. importing a handful of possible tables and searching once those tables have been linked into my active MS Access 2010 session?
Install Access Dependency Checker, link all tables and search for column name (enable checkbox for search in linked tables)
You could do this in a Function using ADO schema's.
Try this function in a standard module:
Function ListTablesContainingField(SelectFieldName) As String
Dim cn As New ADODB.Connection
Dim rs As ADODB.Recordset
Dim strTempList As String
Set cn = CurrentProject.Connection
'Get names of all tables that have a column called <SelectFieldName>
Set rs = cn.OpenSchema(adSchemaColumns, _
Array(Empty, Empty, Empty, SelectFieldName))
'List the tables that have been selected
While Not rs.EOF
'Exclude MS system tables
If Left(rs!Table_Name, 4) <> "MSys" Then
strTempList = strTempList & "," & rs!Table_Name
End If
rs.MoveNext
Wend
ListTablesContainingField = Mid(strTempList, 2)
rs.Close
Set cn = Nothing
End Function

My userform turns blank because I can't edit linked table connection

I am currently developing an access application, which dynamically builds userforms through a VBA module. In order to create the forms, I need data from an MS SQL DB. I collect this data through linked table connections, which are established upon initiation of the entire application by using the AttachDSNlessTable method:
https://support.microsoft.com/en-us/kb/892490
The userinputs are collected in the userforms, and subsequently inserted into some other linked tables (:ResultTables) on the same sql-server.
MY PROBLEM IS that VB-based linked table connections does not let me insert my userform inputs to the resulttables. When I view the form in FormView, it is instead completely blank. All the controls are nonetheless visible in DesignView.
The problem doesn't exist when I manually create the linked tables, apparently because I can choose an index which then allows me to fill new rows in the table.
I am completely sure that the user has the right access to the sql-server to perform update/insert/delete procedures. I have tried to index the table on the server, but the index is not inherented in Access. Once the table is linked, I can't edit the connection. I have also tried the approaches suggested for blank userforms:
https://support.microsoft.com/en-us/kb/93261
Option Compare Database
'//Name : AttachDSNLessTable
'//Purpose : Create a linked table to SQL Server without using a DSN
'//Parameters
'// stLocalTableName: Name of the table that you are creating in the current database
'// stRemoteTableName: Name of the table that you are linking to on the SQL Server database
'// stServer: Name of the SQL Server that you are linking to
'// stDatabase: Name of the SQL Server database that you are linking to
'// stUsername: Name of the SQL Server user who can connect to SQL Server, leave blank to use a Trusted Connection
'// stPassword: SQL Server user password
Function AttachDSNLessTable(stLocalTableName As String, stRemoteTableName As String, stServer As String, stDatabase As String, Optional stUsername As String, Optional stPassword As String)
On Error GoTo AttachDSNLessTable_Err
Dim td As TableDef
Dim stConnect As String
For Each td In CurrentDb.TableDefs
If td.Name = stLocalTableName Then
CurrentDb.TableDefs.Delete stLocalTableName
End If
Next
If Len(stUsername) = 0 Then
'//Use trusted authentication if stUsername is not supplied.
stConnect = "ODBC;DRIVER=SQL Server;SERVER=" & stServer & ";DATABASE=" & stDatabase & ";Trusted_Connection=Yes"
Else
'//WARNING: This will save the username and the password with the linked table information.
stConnect = "ODBC;DRIVER=SQL Server;SERVER=" & stServer & ";DATABASE=" & stDatabase & ";UID=" & stUsername & ";PWD=" & stPassword
End If
Set td = CurrentDb.CreateTableDef(stLocalTableName, dbAttachSavePWD, stRemoteTableName, stConnect)
CurrentDb.TableDefs.Append td
CurrentDb.TableDefs.Refresh
'td.CreateIndex (ID2)
AttachDSNLessTable = True
Exit Function
AttachDSNLessTable_Err:
AttachDSNLessTable = False
MsgBox "AttachDSNLessTable encountered an unexpected error: " & Err.Description
End Function
Linking an SQL Server table should detect the Primary Key automatically. It works for me using DoCmd.TransferDatabase TransferType:=acLink.
Apparently it doesn't work for the CurrentDb.CreateTableDef method.
So I suggest using this command to link the tables:
DoCmd.TransferDatabase TransferType:=acLink, _
DatabaseType:="ODBC", _
DatabaseName:=stConnect, _
ObjectType:=acTable, _
Source:=stRemoteTableName, _
Destination:=stLocalTableName, _
StructureOnly:=False, _
StoreLogin:=True
I have written more about creating DSN-less linked tables here, but most of it deals with linking SQL Server Views, where the PK isn't automatically detected.
thanks for your reply! In the end I created a create-sql-statement for the primary key. It works smoothly:
SQL_PrimaryKey = "CREATE INDEX PrimaryKey ON " & QMT_QMTmonitoring_TransactionFact & _
" (ID) WITH PRIMARY"
Database_Link.Execute SQL_PrimaryKey

How to programmatically create an ODBC Linked Table to a SQL Server View and have it be editable?

When I create a DSN connection to SQL Server using the wizard, I am able to link it to a view. In this case, Access recognizes it as an editable table.
But if I use a DSN-less connection to a view using vba code (Method 1 from https://support.microsoft.com/en-us/kb/892490), it is linked as a table that is NOT updatable.
I don't know why there is a difference, but how can I make a connection to a view in SQL Server (either as a table or a query in Access) and have it be updatable?
Edit: When I make a DSN-less connection using a table in SQL Server rather than a view, it is updatable in Access. I would have guessed my problem has to do with views not having a unique ID, but I'm confused why a DSN connection can be updatable while DSN-less cannot.
It's not because it's DSN-less, but because you created it via VBA. If you link the view via the Access GUI, it asks you for the primary key.
But via VBA, it doesn't know the primary key, so the linked view is not updateable. With a table, Access gets the primary key automatically via ODBC, so the table works.
Solution: set the primary key after linking the view via VBA:
S = "CREATE INDEX PrimaryKey ON MyViewName (MyPrimaryKeyField) WITH PRIMARY"
DB.Execute S
If you have many views, and re-link them regularly (e.g. going from dev to production database), it becomes impractical to hardcode their names and PKs. I wrote a function to retrieve all primary key indexes from linked views, and re-create them after linking.
If you want, I can dig it up.
Edit:
This is what I do:
' This function returns the full DSN-less connect string
Private Function ODBC_String() As String
' In the real world there are several constants and variable in there
ODBC_String = "ODBC;DRIVER={SQL Server};SERVER=aaa;DATABASE=bbb;UID=ccc;PWD=ccc;LANGUAGE=us_english;TRUSTED_CONNECTION=No"
End Function
To link a table or view the first time, I use this (strTable is the table/view name):
DoCmd.TransferDatabase acLink, "ODBC", ODBC_String(), acTable, strTable, strTable, False, True
For tables, the primary key (PK) is determined automatically. For a view, I get the Access dialog window to specify the PK, same as if I link the view manually.
The PK information is stored in the TableDef object for the linked view, so I never have to hardcode it anywhere.
To store the PK information for all linked views, I have this table (it's a local table in the Access frontend for simplicity):
t_LinkedViewPK
ViewName Text(100)
IndexFields Text(255)
and this function. All Views (and only Views) are called "v_*", so I can list them by name.
I'm actually not sure if you can determine from a TableDef object whether it points to a table or view.
Private Sub StoreViewPKs()
Dim TD As TableDef
Dim idx As index
Dim FD As Field
Dim RS As Recordset
Dim S As String
' DB is a global Database object, set to CurrentDB
DB.Execute "Delete * From t_LinkedViewPK"
Set RS = DB.OpenRecordset("t_LinkedViewPK")
For Each TD In DB.TableDefs
If TD.Name Like "v_*" Then
' Views must have exactly one index. If not: panic!
If TD.Indexes.Count <> 1 Then
MsgBox "View " & TD.Name & " has " & TD.Indexes.Count & " Indizes.", vbCritical
Stop
End If
Set idx = TD.Indexes(0)
' Build field list (the index may contain multiple fields)
S = ""
For Each FD In idx.Fields
If S <> "" Then S = S & ", "
S = S & FD.Name
Next FD
RS.AddNew
RS!ViewName = TD.Name
RS!IndexFields = S
RS.Update
End If
Next TD
RS.Close
End Sub
When I make changes to table or view structures, or change the source database (this is done by changing the output of ODBC_String()), I call this function:
Public Function Sql_RefreshTables()
Dim TD As TableDef
Dim S As String
Dim IdxFlds As String
DB.TableDefs.Refresh
' save current Indizes for Views (recreated after .RefreshLink)
Call StoreViewPKs
For Each TD In DB.TableDefs
If Len(TD.Connect) > 0 Then
If Left(TD.Connect, 5) = "ODBC;" Then
Debug.Print "Updating " & TD.Name
TD.Connect = ODBC_String()
TD.RefreshLink
' View?
If TD.Name Like "v_*" Then
IdxFlds = Nz(DLookup("IndexFields", "t_LinkedViewPK", "ViewName = '" & TD.Name & "'"))
If IdxFlds = "" Then Stop
' Create PK
S = "CREATE INDEX PrimaryKey ON " & TD.Name & " (" & IdxFlds & ") WITH PRIMARY"
DB.Execute S
End If
End If
End If
Next TD
DB.TableDefs.Refresh
End Function
Note:
Instead of the table t_LinkedViewPK, a dictionary object could be used. But while developing this, it was very useful to have it as an actual table.
Andre's answer is correct. I am using a little bit more complex code to create the index - it is just cosmetic change:
Public Function RefreshIndexes()
On Error Resume Next
CurrentDb.Execute "CREATE UNIQUE INDEX [__uniqueindex] ON MyViewName (MyPrimaryKeyField) WITH PRIMARY;", dbFailOnError
CurrentDb.Execute "CREATE UNIQUE INDEX [__uniqueindex] ON MyViewName2 (MyPrimaryKeyField2) WITH PRIMARY;", dbFailOnError
End Function
I call this command when user opens Access and only for views that do not have index.

Changing DROP TABLE code to APPEND/INSERT

This code currently is using a DROP TABLE to transfer data from Access to SQL Server, that is when it was intended for one person to use. Now it is going to be used by multiple people so I need to alter the code to APPEND. I am trying to figure out how to get it to check the destination table to see if the record already exists, if it does it will skip over it, if not it will write that employees data to the table. The SQL table is prebuilt and it has a ROW ID column and TIME STAMP column that the access table does not have in order to keep track of the records being entered.
The code currently looks like:
Public Function Update()
Dim cdb As DAO.Database, qdf As DAO.QueryDef
Dim err As DAO.Error
Const DestinationTableName = "AC_CDData"
Const ConnectionString = _
"ODBC;" & _
"Driver={SQL Server Native Client 10.0};" & _
"Server=SERVER;" & _
"Database=DB;" & _
"UID=ID;" & _
"PWD=PW;"
Set cdb = CurrentDb
Set qdf = cdb.CreateQueryDef("")
qdf.Connect = ConnectionString
qdf.SQL = _
"IF EXISTS " & _
"(" & _
"SELECT * FROM INFORMATION_SCHEMA.TABLES " & _
"WHERE TABLE_NAME='" & DestinationTableName & " '" & _
") " & _
"DROP TABLE [" & DestinationTableName & "]"
qdf.ReturnsRecords = False
On Error GoTo Update_qdfError
qdf.Execute dbFailOnError
On Error GoTo 0
Set qdf = Nothing
Set cdb = Nothing
DoCmd.TransferDatabase _
acExport, _
"ODBC Database", _
ConnectionString, _
acTable, _
"CDData", _
DestinationTableName, _
False
Exit Function
Update_qdfError:
For Each err In DAO.Errors
MsgBox err.Description, vbCritical, "Error " & err.Number
Next
End Function
The DoCmd.TransferDatabase does a complete transfer of the data from the Access table, to the SQL Server database. This function can not do partial inserts based on existing records.
What you can do, however, is export the data into a temporary new table (without dropping the existing table), followed by executing an SQL MERGE statement, to incorporate the new records of the temporary table, with the existing table.
You'll need to know how to perform SQL queries from VBA against your SQL database, and you'll need to know how to use the SQL MERGE statement. A google search will quickly give you the answer to both.
Just setup a simple standared linked table to SQL server in Access.
Since the column of the existing record is a primary key (or simply an index set = unique), then you only need ONE LINE of code to append your data.
This will work:
CurrentDb.Execute "INSERT INTO AC_CDData SELECT * FROM CDData;"
Any row that already exists will be ignored due to key (or index) violation. Thus only new non existing records will be appended with the above one line of code.
Edit:
As for the target table being a SQL linked table? Your application on startup does not care if you have some linked tables. Your application does not use nor care about such linked tables unless you use them.
The assuming here is you have a local data table. The linked table ONLY comes into play when you going to do the export. As noted you cannot export unless you are connected to SQL server. The “code” or testing if you are connected is not dealt with in this question but even if it was part of the question in both cases you still have to test for a connection and once again when you do finally determine you have a connect and you decide to export then again the linked table as per above will work.
So there is really no reason I can think of as to why having a linked table that is ONLY used during the append to the SQL table will be any kind of issue or problem.
As stated, then the one line of VBA code should suffice here.
In case of concurrent usage the process need to be changed:
DestinationTableName must be unique per session (thread) and need to be changed (from constant to variable)
Tabel with DestinationTableName neame need and need to be dropped before the export data (as it was in current solution).
DoCmd.TransferDatabase will not change - the data will be exported into the unique temp table.
For simplifying the Merge process the stored procedure can be written on the SQL Server side (with all needed to APSERT logic) with the source table name as parameter. This SP need to be called after the DoCmd.TransferDatabase completion
In this case the flow will be the following:
DestinationTableName unique name generation
Dropping this table on the SQL Server side (qdf.Execute ...)
Export data to the temp table (DoCmd.TransferDatabase ...)
Merge data in the SQL Server side (call to created SP with needed logic of update / insert)

What is the fastest method for importing from the active sheet in Excel to SQL Server?

What is the fastest way to import data into a SQL Server database directly from the active worksheet in Excel?
I have had great success simply looping through the rows and columns, generating a SQL string, opening an ADODB.Connection and executing the SQL. That solution, however, is too slow for large datasets.
So I am testing the Jet Provider with ADO. It's definitely faster, but it requires that the workbook be saved, and I cannot require the user to save the workbook before uploading from it.
The following code works if the workbook has been saved, but if ActiveWorkbook has never been saved ActiveWorkbook.Path returns a null string and the code fails.
Sub UploadViaJet()
Dim objADO As New ADODB.Connection
Dim strSQL As String
Dim lngRecsAff As Long
Set objADO = New ADODB.Connection
objADO.Open "Provider=Microsoft.Jet.OLEDB.4.0;" & _
"Data Source=" & ActiveWorkbook.Path & _
"\" & ActiveWorkbook.Name & ";" & _
"Extended Properties=Excel 8.0"
strSQL = "SELECT * INTO [odbc;Driver={SQL Server};" & _
"Server=<server>;Database=<database>;" & _
"UID=<UID>;PWD=<PWD>].test_table " & _
"FROM [" & ActiveSheet.Name & "$]"
objADO.Execute strSQL, lngRecsAff, adExecuteNoRecords
End Sub
Is there a simpler and faster way to do this?
You can create a datasource (using a linked server or OPENROWSET) and just do a SELECT on SQL Server side:
SELECT *
INTO mytable
FROM OPENROWSET('Microsoft.Jet.OLEDB.4.0', 'Excel 8.0;Database=C:\myfile.xls', 'SELECT * FROM [Sheet$]')
The path with the Excel workbook should be accessible by the SQL Server.
Here is a KB article on how to do this.
Solution using Import Excel File
Since you have a restriction on not forcing the user to save the file, and most proposed solutions require that the file be saved, I would use VBA to
create a workbook object in memory
copy paste from active worksheet to workbook
SAVEAS workbook to a temp file name (TempExcelFile.xls) and path
use Quassnoi's method above to import into SQL Server
overwrite TempExcelFile.xls with zeroes
delete TempExcelFile.xls
This way the user will not be forced to save the worksheet, and they will be prompted to save it just like they would during the natural course of their work.
To facilitate multiple active worksheets being simultaneously imported, I would use either a timestamp or a GUID to create the file names.
Solution using ADO Queries
Have your users create their workbooks from a template that has VBA macros built in Make sure you protect the workbook to prevent unauthorized access to the macros/code behind.
Execute a macro that will loop through the rows and columns, read the cell values, build a SQL query and execute it - use stored procedures with parameters to prevent injection attacks.
Generally I import large Excel files using SSIS.
Have you tried just putting an ActiveWorkBook.SaveAs("some temp filename") at the top, and importing from that? I do something similar with a macro to FTP the contents of the ActiveSheet to a remote server and it works.

Resources