MS Access 2016 Woes - Lockups in MultiUser (Inserting Dummy Entries to Add New Item) - vba

I found an interesting conundrum with a database I administer. To create a new Stock Item, it adds a dummy entry and then opens this new entry in the usual Form for editing. See the code below.
The code works perfectly fine until you have the database open on more than one PC.
If the user on the 1st PC adds a new item, the 2nd PC freaks out over the dummy entry. This causes 10-20 second delay on everything they do on the 2nd PC.
I'm trying to think of a simple / elegant way to achieve this without using a dummy entry (because it doesn't actually have a StockCode until the user enters one, I think on the 2nd PC the program chokes on the dummy entry with no StockCode)
I really have no idea at this point.
Case vbKeyF1 ' F1 Key to Add New Record
stokmastSQL = "INSERT INTO tblSTOKMAST (STOCKCODE, PER, SELL1, SELL2, SELL3, GST) VALUES ('', '', 0.00, 0.00, 0.00, 'N');"
DoCmd.SetWarnings False ' Turn off SQL warnings for Action Queries
DoCmd.RunSQL stokmastSQL
DoCmd.SetWarnings True ' Re-enable SQL warnings for Action Queries
[Forms]![frmSTOKMASTLIST]![lst_STOKMASTLIST].Requery ' Requery ListBox after change
SQL = "SELECT STOCKCODE, DESCR, PER, SELL1 FROM tblSTOKMAST ORDER BY STOCKCODE" ' Re-initialize Record Source of ListBox
[Forms]![frmSTOKMASTLIST]![lst_STOKMASTLIST].RowSource = SQL
[Forms]![frmSTOKMASTLIST]!lst_STOKMASTLIST.SetFocus ' Set Focus to ListBox and select first record
[Forms]![frmSTOKMASTLIST]!lst_STOKMASTLIST.Selected(0) = True
DoCmd.OpenForm "frmSTOKMASTEDIT", , , "[STOCKCODE] = '" & [Forms]![frmSTOKMASTLIST]![lst_STOKMASTLIST].Column(0) & "'" ' Open new record in frmSTOKMASTEDIT
KeyCode = 0
End Select

Well, there is a MASSIVE HUGE LARGE difference here between a long delay, and that of something not working.
What you described so far is something that is SLOW, not that it does not work.
About once a week on SO, this issue comes up, and has come up for about the last 20 years in a row!!!
You don't want to try and write a boatload of code to fix this issue, since virtually EVERYTHING you do will have to be fixed, and in 99% of cases, writing code will NOT fix this issue!!!
In other words, this is NOT due to the dummy entry, but something wrong.
You don't mention/note if the database is split - but lets leave that issue for a bit.
The first test/thing to try?
Do this:
launch the applcation. Now, open ANY table (and you not noted if you using linked tables here - a MASSIVE issue we need to know here).
Now, from the nav pane, open ANY table. Now minimize the table (assuming the application is in windowed mode. But, regardless, JUST OPEN any table.
At this point, now launch the form that creates the new record. is it slow?
The above is what we call a persistent connection. By opening a table (any table), then this forces access to keep the database open. And in multi-user mode, this "opining" process can be VERY slow (like 20 seconds).
So, first and foremost, try the above. So, you can open ANY table (and if using linked tables, then ANY linked table). Now, just leave that table open, and now again try your form. The delay (even with 2 users) should not be gone.
if you don't address this delay, then that dealy will start to appear everywhere, and will appear even without writing code.
And conversly, then writing code will not fix this issue.
If you determine the above fixes this issue? Then on application startup, you can in VBA code create what we call a persistent connection. Another way is to open a form (any form) bound to a existing table.
As noted, if the above does not fix this issue, then we have to look at how that new record is added, and perhaps it uses something like dmax() or some such to get the "max ID value". In that case? Then adding a index to that column will/can fix this issue.
So, try the above first (since you can do this without any code). Just open any table, and THEN launch/use the form(s) in question, and see if the long delay goes away.

Related

Update access backend schema with vba code

I have created an application in ms access with vba code. The app is split in front end and back end. Back end holds the user data. Now i want to add some new features but this needs some excessive back end changes in the schema structure.
My question is, what is the best way or practice to deliver to the end user the changes i want every time I upgrade my application? Of course it has to be something that keeps the current data intact.
I have seen some suggestions about a free program called CompareEm that compares the old and new database, and produces the appropriate vba code to do the job.
I was also considering if it would be more convenient to copy an empty database that has the wanted schema alongside the uograded frontend and having a module that compares the old database of the user with the empty one and try to change the old schema according the new one. (First my removing all relations, converting the tables, and then reapplying the new relations).
In any case i would like something that could be done automatically or by some customer code, in order to avoid messing up with users data.
Any help is appreciated.
This is one weak spot of Access. We don't have a really great scripting language and system built that allows scripting out of database "schema".
So while the split design is a must have, and makes update of the code and application part very easy, a update of the back end (BE) is a challenge.
I have done this in the past. And the way you go about this?
Well, you have to make up a rule, and the rule is this:
Anytime you add a new field/column, a new table?
You MUST write the change(s) into a code module. In other words, you NEVER use the GUI anymore to add a new column (or say change the length). You code to that sub that will do this for you. In fact, in one application I had (many customers, all 100% Access runtime).
So, to add new features, changes, fixes? I often had to add a few new columns or tables. So what I would do is WRITE the code into that "change/update" routine. (and I also passed it a version number). On startup, that code would be called.
It started out with say about 5 lines of code. A few years later? I think it had well over 100 lines of code. And worked very well. If they opend a older data file (the BE), then if it really was a older verison - or say they had not paid for upgrades, then their current verison of software could and would be several upgrades behind. But, as noted, since on startup all of the "updates" to the database were ALWAYS written as code, then even if they were say 5 versions back, then all of the version numbered code would run, make the changes to the BE, and they would be up to date.
So, this means you have to code this out. It not all that hard, but you DO HAVE do adopt this way of working.
So, the code in that module looked like this:
There were two common cases. The first case was a WHOLE new table. I did not want to write out the code to create that table, so what I would do is INCLUDE the whole new table in the FE for this purpose (and I would append the letter "C" to the table).
So, the code on startup would check if the table exists, and if it does not, then I would execute a transfer command to copy the table. The code stub looked like this:
' check for new table tblRemindDefaults
On Error GoTo reminddefaultsadd
Set rst = CurrentDb.OpenRecordset("tblRemindDefaults")
So, in above, I set the error handler and try to open the table. If the table does not exist, then above calls the routine remindDefaultsAdd. Its job of course is to add this new table to the BE.
The above code stub looked like this:
remindadd:
Dim strFromDB As String
Dim strToDB As String
Dim strSql As String
Dim cp As Object
Dim shellpath As String
strFromDB = CurrentProject.FullName
strToDB = strBackEnd
DoCmd.TransferDatabase acExport, "Microsoft Access", strToDB, acTable, "tblGroupRemindC", "tblGroupRemind", True
Note how then I simply copy a table from the FE to the BE.
The next type of upgrade was adding a new column to a existing table.
And again, similar to above, the code would check for the column, and if not present, I would add the column. In this case we not creating a new table, so no copy of the table.
Typical code looked like this:
' add our new default user field
Dim nF As dao.field
Dim nT As dao.TableDef
Dim nR As dao.Relation
strFromDB = CurrentProject.FullName
strToDB = strBackEnd
Set db = OpenDatabase(strToDB)
Set nT = db.TableDefs("tblEmployee")
nT.Fields.Append nT.CreateField("DefaultUser", dbText, 25)
nT.Fields.Refresh
Set nT = Nothing
So in above we add a new field called DefaultUser to the table.
If you OPEN DIRECT the BE, and do NOT use linked tables, then you are free and able to modify the table in question with code. You can ALSO use SQL ddl's statements. So while I noted that scripting support in Access is not all that great, you can do this:
Set db = OpenDatabase(strToDB)
strSql = "ALTER TABLE tblEmployee ADD COLUMN DefaultUser varCHAR(25);"
CurrentDB.Execute strSQL, dbFailOnError.
So, you can write out code using table defs, create the field and add it. Or you can use SQL DDL's statements and execute those. Of course each upgrade part I wrote had a version number test.
So, the simple concept is that every time you need a new column, new table etc.?
You write out this code to make the change. So, you might say work for a month or 2 add a whole bunch of new features - and some of these features will require new columns. So you NEVER use the GUI to do this. You write little code routines in your upgrade module, and the run them. Then keep on developing. So, when you done, all your updates to the table(s) are now done, and you can deploy the new FE to those users. On startup, the update code will run based on version number. (I have a small one row table in the FE with version number, and also a small one row table in the BE that has version number). After a upgrade, the new columns, new tables are now in the BE, and then of course I update that version number.
So, you can use a combination of SQL ddl commands, or use code to create the field def as I did above.
The only really important issue is that table changes can NOT be made against linked tables. So you have to create + open a SEPERATE instance of the database object as I did per above. And on startup, you have to ensure that no main form that is bound to a linked table has or is running yet. (since that will open the BE, and you can't make table changes against a open table).
So, it not too hard to make the updates. But the HARD part is your self discipline . You have to ALWAYS go to your upgrade routines and add the new column in code. So that way after working for a few weeks, you have coded out the table changes, and thus are able to re-run that code against older existing BE's out in the field.
Welcome to Stack Overflow! When you send out a new version of your front-end db, you would need either to (1) include a script to update the backend db, or (2) a fresh copy of the back-end db and a script to transfer the data from the previous version. In either case, you need to consider customers who may have skipped an update, so you would need some kind of version stamp on the back-end db.
You can accomplish either strategy with Access DDL (see here: https://learn.microsoft.com/en-us/office/client-developer/access/desktop-database-reference/data-definition-language) and/or Access DAO (see here: https://learn.microsoft.com/en-us/office/client-developer/access/desktop-database-reference/microsoft-data-access-objects-reference).
Please let us know if you have any specific questions but keep in mind SO is not intended for offering general advice or product reviews.

Access 2010 vba Array vs. Query

I maintain an Access DB at work that we use to send out hourly updates on employee productivity. Currently I have the form arranged into columns: The first column contains a ComboBox that we can select an employee's name from. Once a name is selected, the next column fills in automatically with the agent's employee ID (TID), through this code:
AgentName = rs.Fields("AgentName")
sqlString2 = "SELECT * FROM " & "AllAgents WHERE FullName ='" & AgentName & "'"
rs2.Open sqlString2, CurrentProject.Connection, adOpenKeyset, adLockOptimistic
AgentTID = rs2.Fields("TID").Value
rs2.Close
Everything works fine when working in the office on the corporate network, but I've just discovered that working over the VPN causes horrendous slowdown when using this form. Slowness is an issue I've fought with over the VPN forever, but I had a thought in this case that may potentially aleve the slowness, I judt want to know if I'm correct before I go to the trouble of re-coding everything.
My idea is that I could create an array in VBA that would be populated with the agents' name & ID's when the form first loads, or even when the DB is opened on each inidividual laptop. Then the DB would only need to read from the 'AllAgents' table once, and could simply use the array as a source instead. What I doin't know is if this would have an effect or not. Basically, if an Access DB is saved onto a network drive and accessed over a VPN, would the array be stored in the RAM of the laptop? If it is, I would assume this would alleviate the speed issues and would be worthwhile taking the time to re-code.
Thanks,
The thing about form-level or global variables in Access is that you better have good error handling in the application. If an uncaptured error occurs it can result in those variables being for lack of better word discarded. The result would be the next time you try to access the data in the array you get another exception.
Here are a few things you could try before going the array route:
Your combo box probably doesn't need to be bound to your recordset rs directly. Set the source of the combo box at design time to the underlying query or table.
This makes it possible to simply refer to the combo box's bound field using something like this: AgentName = cboAgentName.Value
(If you can eliminate an unnecessary recordset object from the form the better off you will be in the long run)
Your lookup code shouldn't need to use SELECT *. This just isn't a good practice to use in production code. Instead, use SELECT TID. Basically, only return in your query the fields you actually need.
You don't need to use the adOpenKeySet option, which is unnecessary overhead. You should be able to use adOpenForwardOnly.
I would also suggest looking at the AllAgents table to make sure that there is an index on the field you are using for the lookup. If there isn't, think about adding one.
You still might need to go the array route, but these are relatively simple things that you can use to try to troubleshoot performance without introducing massive code changes to the application.

Access Query not updating fast enough

Im building a key inventory mgmt system. I've created a query that show's me keys currently not in use by identifying which keys have been returned, aren't lost or have never been rented. I copied this query into the look up field for key_id in my keyActivity table (used to record key sign outs). The issue is that the query does not update to provide available keys until the table keyActivity is closed and opened again
Example: I open keyActivity, indicate that key_id = 5 is lost. When I go to a new record and select the key to sign out, key_id = 5 is presented as being available. It is not until I close the table, open it again, that key = 5 is removed from the list.
Here you can see key 5 is indicated as lost in id 5 but in id 7 when selecting a key, 5 is available when it shouldn't be.
Is there anyway to fix this or set this up to work as intended. I plan on using forms to present all the information. Is there a form solution perhaps?
The suggestion you would be better off with a Form to change table data. It can be easily requery-ed to update the table according to the changes you make and to display the udpated data accordingly. Please also read on the given references for further info.
In terms of data updating and locks in a multi user environment this article could be helpful.
"Access is NOT a database server. It's a desktop database. It has been pushed to the limit to support mutli-user environments, but only in the sense that you can share the "back end" database across a network."
...
...
"Even the record locking is performed by the Front End. All of the front end
database applications share the "lock file" (a file with the same name as
the database file, but with the extension LDB); but that file is simply a
mechanism that the front ends use to determine which front end can make
changes to the database."
....
Here is a difference between requery and refresh:
Me.Requery forces the entire recordset (underlying data) for the form to reload. This means ALL of the records in your current form will reload. Your current position will be lost, so if you're sitting on record 10 of 100, you'll find yourself back on the first record. Me.Requery is essentially the same as closing and reopening the form. Any new records added by other concurrent users will be available. Likewise any records that have been deleted will disappear. Requery essentially "re-runs the query" that pulled the data into the form in the first place. You can also use requery to update the data in a list box or combo box.
Me.Refresh saves the current record that you're working on. It will also retrieve any changes (but not additions or deletions) to any records shown in the current form. Any calculations on the form (unbound fields) are recalculated. Refresh does NOT reload the recordset. You do not lose your position in the form (you stay on the current record). Any new records added by other users will not be shown.
Reference
MS Access - Write to Table Immediately After Changing Value in Form

Table updating - but report is empty. Reopening report shows correct data

Should be an easy one for the experienced?
I collect data from multiple other tables and do calculations in VBA with which I add rows with DoCmd.RunSQL to a printout table. (Before this, I delete previous printout rows.)
After this I do a db.Close + Set db=Nothing, then display a dialog whether to preview or print. Upon selecting Preview, the report is shown, but it's empty. (As in, all the labels are there, there are 0 rows of data, and all the other data fields are blank.)
The report associates printout rows with the current user, so it's valid to "just open" the report, and then it shows the correct data.
If I keep the table open while running, I can see the DELETE FROM taking effect ('##Deleted##' or something similar), but I can't see INSERT INTOs taking effect until I re-open the table.
I use no connection, I use Dim db as dao.Database + Set db=CurrentDB; data is written to a local (non-linked) table; there's nothing special in the query (sort of, SELECT field1,field2... FROM mytbl WHERE user=currentuser) the report is based on; there's no code module for the report.
What's going on here, and how do I fix it?
Boy, am I feeling stupid now. I copied the DoCmd.OpenReport line from an identical report that used another table as data. The problem is, when I changed it to have an OpenArg, I had too few commas and it unnoticably ended up being a "filter". Code hinting is there for a reason... oh well :)

How does Access 2007's moveNext/moveFirst/, etc., feature work?

I'm not an Access expert, but am an SQL expert. I inherited an Access front-end referencing a SQL 2005 database that worked OK for about 5000 records, but is failing miserably for 800k records...
Behind the scenes in the SQL profiler & activity manager I see some kind of Access query like:
SELECT "MS1"."id" FROM "dbo"."customer" "MS1" ORDER BY "MS1"."id"
The MS prefix doesn't appear in any Access code I can see. I'm suspicious of the built-in Access navigation code:
DoCmd.GoToRecord , , acNext
The GoToRecord has AcRecord constant, which includes things like acFirst, acLast, acNext, acPrevious and acGoTo.
What does it mean in a database context to move to the "next" record? This particular table uses an identity column as the PK, so is it internally grabbing all the IDs and then moving to the one that is the next highest???
If so, how would it work if a table was comprised of three different fields for the PK?
Or am I on the wrong track, and something else in Access is calling that statement? Unfortunately I see a ton of prepared statements in the profiler.
THanks!
First is literally the first row in the Recordset. In general, Access accesses data via the equivalent of cursors. So, Next and Previous are moving forward and backwards in the Recordset one row at a time just as you can with SQL Server's cursors. Be careful about depending on the sequence of the rows without an ORDER BY statement in the construction of the Recordset. Although Access is an ISAM, you should not rely on the rows coming in any particular order. Depending on the cursor type, Access will not pull the entire table down but will generally ask for one record at a time. That said, I have seen Access pull entire tables for whatever reason.
You have to distinguish between automating Access objects and working with recordsets in code.
In a form, this command has meaning:
DoCmd.GoToRecord , , acNext
It is nonspecific, and it is not predictable what record it will go to unless you know the ordering of the underlying recordset in the form and the starting record. It navigates you through the recordset stored in the form's edit buffer (which is loaded in the OnOpen event of the form). The command would be used, for instance, in the code behind a command button whose purpose is to navigate records loaded into the form that currentlyl has the focus. I would never leave out the optional arguments if I were to use that command (I almost never do). Instead, I'd identify the specific form I wanted it to apply to:
DoCmd.GoToRecord acForm, "MyForm", acNext
In traversing a DAO recordset, .MoveNext likewise has no predefined meaning except if you know the ordering and starting record. When you are walking a recordset (something you shouldn't do very often, since it's pretty inefficient; but it depends on the task you need to perform) and need to hit each record, you'd certainly call .MoveNext as part of your loop:
With rs
.MoveFirst ' technically not required, as it's the default starting point
Do Until .EOF
[do something]
.MoveNext
Loop
End With
Nothing mysterious there. It's most likely going to be used in code with small numbers of records (large recordsets really oughtn't be navigated sequentially).
In answer to your specific question:
What does it mean in a database
context to move to the "next" record?
This particular table uses an identity
column as the PK, so is it internally
grabbing all the IDs and then moving
to the one that is the next highest???
...as I said, the next record is determined by the ordering of the recordset being traversed and the starting position. In the case of the form, it's the edit buffer that's being traversed, and as the current record bookmark changes in the edit buffer, the form is updated to load the data for that record. The dynaset is bound to the underlying data table, and when the form's edit buffer is saved, the edited data is written back to the server. While it's being edited, locks may or may not be maintained on the record on the server, but Access/Jet/ACE does keep track of the state of the existing record on the server and the record in the edit buffer and will inform you at save time in Access if the record on the server has been changed since it was loaded into the form's edit buffer.
Now, in a comment, you say the form is bound to the whole table. This is a terrible design no matter whether your data is stored in a Jet/ACE back end data file or in a server database like SQL Server. The only reason Access can get away with it is because it and Jet are rather efficient about pulling data from the data source.
I properly-designed client/server Access front end will not load full tables in forms, but instead ask what filtered recordset you want to load, either 1 or more records at a time. This is only marginally more complicated than binding to a whole table.
As to knowing what cursor types are being used, you shouldn't be worrying about it. By default, Access forms use what Access/Jet/ACE calls dynasets. Each form has a RecordsetType property, and it's set to dynaset by default (read the help file on the meaning of the different recordset types). If you want more control of that, you can (but likely shouldn't) create your recordsets in code and assign them to the form's .Recordset property. This is useful in a few circumstances, such as when you'd like to bind a form to a disconnected recordset, but the point of Access is leveraging its capabilities working with bound data. Assigning your own recordsets still gets you bound controls, and the form events, but is more work than is usually necessary.
Basically, change your forms to load only the subset of records the user needs to work with (that may be one record at a time), and then let everything else get done with Access's default behaviors. If something causes a bottleneck, then troubleshoot that and replace the default behavior with something more efficient.
In other words, avoid premature optimization -- let Access be Access.
And don't worry about what Access is doing behind the scenes unless/until Access does something inappropriate.