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.
Related
So I working on an Access form and ive noticed that when i dont fill out all the fields and close the form, the fields that i did fill out populate into my table as a sort of "incomplete" record which messes up my numbering increment system that i have going. Is there any way of discarding all data entered on form close without making certain fields required in the table properties? I dont want to do this because it gives a message when trying to close and i want to avoid that if possible
The issue of autonumbering incrementing, and that of not wanting to save record with missing fields or values are TWO HUGE different issues.
Try going to a new record, type in a few things, hit esc 2 times (or go to ribbon and use un-do and now exit form.
The record will now be blank and if you exit, then no record is created (however, the auto number will have incremented) So auto number incrementing and saving of records are two VERY DIFFERENT and separate things.
When you start typing into the blank new record on the form - the record becomes "dirty", and a auto number is incremented and assigned to that record.
if you don't want the record to be saved with missing things, then in the forms before update event, you can check for missing fields, or even bad or wrong values, and if you set cancel = true, the record will NOT be saved, nor created.
So, you don't need to set required in the table design. There are often ALL kinds of things you might want to check for. Say you might require first name or last name (one or the other - such things can't be done at table level. So you can have as complex record verification as you want. And as noted, in most cases, you can and would use the forms before update event.
However, preventing a blank record, or a record being saved with missing information? Sure, not problem - a common requirement.
The above has ZERO, but absolute ZERO to do with the autonumber issue.
Access will issue and set the autonumber as SOON as the first key is typed into that form. As noted, you can use undo, and upon exit, NO REOCRD will be created nor saved.
However, the autonumber created will be tossed out - and skipped. The reason for this is for multi-user operation. If two users move to a new blank record, then when one starts typing, the autonumber is assigned. (even if you don't save the record). when the 2nd user starts typing, they also get a autonumber. Now, if both users hit undo (or esc key), then both can exit, and you note that 2 autonumbers will have been skipped. As noted, access works this way so two users can't be assigned the same PK value when adding records.
As a result, autonumbers can NEVER be deemed to not skip. And of course what about deleting records - again, you have gaps.
Autonumbers cannot be used for say invoice numbers, or some kind of external value. Autonumbers can NEVER be assigned ANY more meaning then that of just some "random" like PK value. You can't use those numbers for external business use if such numbers are to say only be sequential without gaps.
If you need a business incrementing number, such as invoice, or job number or whatever? The you need to create your own column, and manage that incrementing in code. (such as assigning that number in the before update event - but as I noted, yo can also CANCEL that before update event for any old reason - including missing fields or even if this is a odd day of the week (any criteria that floats your boat can be used here, and thus when they exit you can prompt them and tell them that the record is incomplete. Or in those special cases, you can even let them exit without a save, but some kind of message probably is better.
So, if you don't want some incomplete record to be saved? Then put whatever you wish into the forms before update event - and set cancel = true (that event has a cancel option built EXACTLY for this purpose). However, that goal of saving, or preventing a record save has VERY LITTLE to do with the issue of not having gaps in the autonumber - that you can't control, and that you cannot prevent. (prevent gaps).
If you need some external business numbering system, you can't use autonumbers. In fact users should never even see autonumbers, and they are for you the developer to build relations between tables.
As such, these internal house keeping auto numbers cannot be used for external business process(s) that require some kind of sequential number without gaps, since that is not their purpose, and worse yet, you can't control or avoid gaps in such numbers anyway.
You can undo, but autonumbers increments means numbers will and are boing to be skipped as a normal operation of the database system.
Since that number is automatic and under control of the database system, and NOT you the developer, the you can only accept how it works and use it - but you can't change the behavour of that auto number. This applies to all and any database systems. In fact later versions of sql server will skip forward by 10,000 when you reboot.
If you don't want to save a record, then put in your code to prevent as such.
If you need a separate issue of some incrementing number for business use? Then add a column to the database and design the increment system to work whatever way you want - but you can't control the built in system, and its not designed with the goal you have here in mind anyway.
I am having to redevelop an MS Access 2003/2010 ADP project, that uses SQL Server views as the RecordSource for all its forms, into an MS Access 2016 ACCDB.
I have tried using pass through queries to get the data and this works fine for readonly columns, however when I want to change the value in one of the bound columns, it says that the RecordSet is not updateable, which is what you might expect if using a view.
But I have now read that if you define the view with SCHEMABINDING like this:
ALTER VIEW [dbo].[vwQuote_MinibusesDetails]
WITH SCHEMABINDING
AS
SELECT ...
and add a UNIQUE CLUSTERED INDEX like this:
CREATE UNIQUE CLUSTERED INDEX CIX_vwQuote_MinibusesDetails
ON vwQuote_MinibusesDetails (txtQuoteNo, txtVersion, txtVehicleNo);
and then add the view to your project as a DSN-less TableDef like this
stConnect = "ODBC;Driver=SQL Server;Server=" & SERVER_NAME & ";Database=" & APP_DATABASE & ";Trusted_Connection=Yes"
Set td = CurrentDb.CreateTableDef(stLocalTableName, dbAttachSavePWD, stRemoteTableName, stConnect)
CurrentDb.TableDefs.Append td
it becomes editable as if it were a table.
However, when I open the TableDef in Access it shows all the rows and columns as if it were editable, but if I try to edit a column, it says that there is a Write Conflict with another user's changes, when I am 100% sure that there isn't because I am the only person using it.
Any ideas? (I am using Access 2010 at the moment)
But why do all that truckloads of work? There is zero reason to do all that extra work.
Simply bind and set the forms data source to a view. It is assumed that you will simply link all of the existing views on the client side. Access will thus see all the views as simply tables, and views (as opposed to Pass through queries) are read/write.
So, there is no need to:
Have ANY connection strings in your code. If you ARE using connection strings in code, then you approach is all wrong. Just link to the views, and set the forms data source as that view.
At this point, the forms can edit data.
As for a pass-through quires to drive combo boxes? Do NOT do this. While a PT query is likely the fastest way to pull data, the access client cannot filter PT queries. So you ONLY EVER want to use a PT query for cases in which the client side does NOT have to filter the results. If you bind a combo box to a PT query, then Access needs and wants to ONLY pull the one value from that table for display. And since the access client can’t filter a PT query, then it will scan the WHOLE data source for that combo box to get the one value that is currently displayed. If you use a linked table (or even a view) to that source that drives the combo box, then the access client can filter that dataset to the ONE row. So, do NOT use a PT query for ANY case in which client side filtering is required. A linked view (or linked table) is fine in these cases.
So, just bind the forms directly to the linked table, or the linked view.
If the table has 1 million rows of data, and you say do this:
Docmd.OpenForm "frmInvoice",,,"InvoiceNum = 1234"
Access will open the form to the ONE row of data, and ONLY pull one row down the network pipe. This is despite that the form in question is bound to a table of 1 million rows. The form will load instant, and you not had to write any sql, any connection stuff, and not really do anything different then how you developed typical access applications.
Bound forms are how and why Access saves huge development dollars. If you jump to .net, then you have all kinds of tools and wizards that can help you around this issue. So in .net, you could adopt the dataset designer, or use the newer entity framework.
In Access, we don’t have this huge tool box of data designers and tools, so if you attempt to hand code, and code up the data sources for a form, then you get the worst possible outcome (you write truckloads of code, and don’t have all those cool tools for un-bound forms).
Simply link your forms to linked tables (or views) and you now have a working data bound form without any code. If you need to load the form, then use the 20+ year standard approach of the “where” clause of the open form command to open the form to the one record. The access client will ONLY pull what you put in the “where” clause.
So, to wire up a form for editing of data, ZERO code is required. The only developer efforts will then to ensure that the form does not pull un-necessary data to the client.
I am working on a very complex legacy ADP project in Microsoft Access that connects to a 2008 SQL Server back end. A common usage scenario is a form in datasheet view that contains an editable recordset.
The approach used through most of the database is to load the form, then build the SQL dynamically in VBA using form parameters, and apply the SQL as the RecordSource for the form. Many of the queries are quite complex, so this makes for really ugly code and a maintenance nightmare.
For the read-only recordsets, I have converted the dynamic sql to parameterized stored proceedures, which works great. Nice and clean, and easy to maintain going forward.
But for the editable recordsets, I am trying to determine the best approach:
1. Dynamic SQL - As mentioned above, I would really like to avoid this approach.
2. SQL View - The challenge here is that some of the tables are very large, so if I try to load the view and then filter it on the form, it has to pull the entire recordset from the SQL server, even though I only need a small number of rows. (Negative performance and IO impact.)
3. Use context_info - This sounds intriguing, but does not sound like a recommended approach based on discussion here: Create parameterized VIEW in SQL Server 2008 If I was developing against SQL Server 2016 I might look more into SESSION_CONTEXT.
4. Parameter Table with View - This is an idea that I am leaning towards. I would create a Parameters table in SQL, and set the parameter value (as a key/value pair) with the session ID. The view would then filter based on the current value in the parameter table. This would allow me to use a view as my RecordSource to support the edits, but the filtering would take place on the SQL Server side.
Is the parameter table indeed the best approach to take with this project, or is there another way that I could access a parameterized read-write recordset that is filtered on the server side?
I assume you talking about a non adp project now.
Even if you don’t use a view, and say bind a form directly to a linked table of 1 million rows, then access will ONLY pull down the records you requests. You simply just use the forms “where” clause of the open Form command.
So you don’t even have to use dynamic sql here.
However, you don’t want to launch a form bound to large table UNLESS you set the where clause.
You can certainly open a form without a reordsource, have the user enter some parameter values into a text box, and then go:
Dim strSQL as string
strSQL = "select * from tblCustomers where InvoiceNum = " & me.MyInvoiceTextBox
Me.RecordSoruce = strSQL
However, in most cases you better to create some type of search form. Let the user enter some values, display the results like this:
So in above, the person type in "smi". You display the results
(and in above I did use the above approach of stuffing the sql directly into the forms reocrdsource
Now on the edit buttons along the side to launch + edit one record,
I simply go:
Docmd.OpenForm "frmCustomer",,,id = & me!id
Once again, EVEN if the form is bound directly to the linked SQL server table, only the ONE record will be pulled from SQL server. So no messing with sql, no messing with parameters etc. is required.
So a regular non ADP access application with linked tables DOES NOT pull the whole table.
You can also after opening a form set the forms filter – and again access will ONLY pull the reocrds in question from the linked sql table. It is a “common” myth that access pulls all records – it does not if you provide a filter, and I recommend you open a form to one record, let the user work then close the form and return back to some search screen in which you ready to do battle with the next customer etc.
so provide a search form - don't recommend having the form to edit data be all cluttered up with the ability to search records. Let the user edit, and then close the form - this also promotes the record being saved after the user done working.
edit:
For a form that has any kind of complex joins etc., then create a view, and bind the form to that view. You use the forms "where" clause, then once again access will only pull down the one record. So for complex joins etc., yes access can often mess that query up and it runs slow. So if the form is bound to one table (that is MOST cases), then bind the form directly to the linked table. If the sql is complex, then bind the form to the linked view, and as noted in either case ALWAYS provide a "where clause" to the openform command - it will in these cases ONLY pull the one record into the form. And once again, no messy parameters, no messy sql is required on the access side - you will save MASSIVE amounts of coding if you adopt this approach, and you also get stellar performance since you limiting the reocrds pulled into that form from sql server.
I would like to delete a Column / Field from an existing ADODB.Recordset object that is already populated with data. A Recordset.Fields.Delete method does exist:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms675290%28v=vs.85%29.aspx
...but if called on a populated Recordset one gets the error: "operation is not allowed".
This is documented at the above link:
Calling the Fields.Delete method on an open Recordset causes a
run-time error.
Doing a Recordset.Close clears the fields collection entirely, so that won't work - it almost seems like this method exists only for dealing with hand-rolled and manually populated recordsets rather than ones retrieved from databases?
Does anyone know of a way this is possible, even a fairly performant means of copying to a new recordset instance would be better than nothing.
NOTE: Please resist the temptation to point out "you shouldn't be doing this, you should only retrieve the columns you actually want from the database!!!" I am well aware of this.
UPDATE: As for the why: I have these existing extremely complicated datasets to which I don't have access to the source code that physically creates them, and I simply want to drop some of the columns from them as they clutter up the UI where they are being viewed. I explicitly care not at all about the .1 ms of time or .00000136 Mw of electricity wasted by bringing these unwanted columns back and then discarding them.
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.