How to rename primary key value in Access DB - ms-access

I have a MS Access DB with a primary key on the parent table and 82 other tables.
New data will be coming with new name for the primary key
Before New
RS182 X182RS
RS188 X188RS
RD301 X301RD
Is there a way to rename the primary key value in bulk on all the tables in the DB because I want to associate all previous historical data to the new name value.

Based on that sample, it seems there is a consistent pattern between the new and old primary key values.
? "X" & Right("RS182", 3) & Left("RS182", 2)
X182RS
If that is true, then use a series of UPDATE statements to replace the old values with the new. But first make a backup copy of your database for safekeeping.
For example, if the primary key field for YourTable is named ID:
UPDATE YourTable
Set ID = "X" & Right(ID, 3) & Left(ID, 2);
If YourTable is included in any defined relationships, you will first need to drop those relationships (or at least uncheck the "enforce referential integrity" option for them), then restore the relationships after updating the primary key values.
Also removing the primary key property from ID should allow the UPDATE to complete faster. Re-assign the primary key afterward.
Since you have 82 tables which require this conversion, you could create a VBA procedure to do it.
Public Sub ConvertPKeyValues(ByVal pTable As String, _
ByVal pField As String)
Dim db As DAO.Database
Dim strSql As String
strSql = "UPDATE [" & pTable & "]" & vbCrLf & _
"Set [" & pField & "] = 'X' & " & _
"Right([" & pField & "], 3) & " & _
"Left([" & pField & "], 2);"
Set db = CurrentDb
db.Execute strSql, dbFailOnError
Set db = Nothing
End Sub
Call the procedure with each table name and the name of the relevant field in that table. You should also add an error handler for any problems which dbFailOnError exposes.

It's not that hard to code, at least if I understand the following correct from your question:
the primary key column exists in every table
it has the same name in every table (I'll use ID in my example)
the existing values are all in the same format ("RS182" --> two letters and three numbers)
To get a list of all tables in your database, you can take a look at the hidden table MSysObjects.
And then you just have to loop through the tables and update the ID column.
A quick example (works on my machine):
Public Function Test()
Dim RS As DAO.Recordset
Dim SQL As String
SQL = "select name from msysobjects where type = 1 and name not like 'msys*'"
Set RS = CurrentDb.OpenRecordset(SQL)
Do While Not RS.EOF
SQL = "update " & RS("name") & " set ID = 'X' & Mid([ID],3) & Left([ID],2);"
CurrentDb.Execute SQL, dbFailOnError
RS.MoveNext
Loop
RS.Close
Set RS = Nothing
End Function

You cannot modify a PK if you have related records in the other tables.
So the trick here is to temporarily modify all those relationships (I suppose they exist and that Referential Integrity is enabled - otherwise I wouldn't even talk to you :), and enable the Cascade Update option.
Don't forget to turn that option off once your data goes back in prod !!

Related

How to avoid a "You must enter a value" error message in Access VBA

I'm having issues avoiding a "You must enter a value in the __ field" error message in Access 2016. I have three tables, Tasks, Users, and TaskAssignments, and a split form that looks like:
User Task Assigned?
User1 Task1 True
User1 Task2 False
User1 Task3 True
User2 Task1 False
User2 Task2 False
User2 Task3 True
User3 Task1 True
User3 Task2 True
User3 Task3 True
Each task can have multiple users assigned to it, and each user is assigned to multiple tasks. I want my form to display every possible value, then use a checkbox, so that I can click and add a user to that task. The TaskAssignments table has a primary key and a unique constraint on both TaskID and UserID.
The recordsource for my form is a query:
select x.UserName, x.TaskName, ta.is_assigned
from (select * from Tasks, Users) x
left join TaskAssignments ta on (ta.TaskID = x.TaskID and ta.UserID = x.UserID)
I have an on click event that checks if a record exists in TaskAssignments and either updates or inserts into TaskAssignments. When I debug.print and manually run my queries, they both do what's expected. When I manually insert a record into my TaskAssignments table, my form behaves how I expect. When I need to insert a new record, however, I receive a message stating that I must enter a TaskID in TaskAssignments.
I've tried requerying the form, but I still receive the error message. Why can't it find the record that I just inserted?
Help please?!? Do I need to drastically rethink my approach here?
Here's the VBA:
Private Sub is_assigned_Click()
Dim CurrentUser, AssignmentQuery As String, SelectedUserID, SelectedTaskID As Integer
Dim ShouldInsert, IsAssigned As Boolean
CurrentUser = Environ$("Username")
SelectedUserID = Me.UserID
SelectedTaskID = Me.TaskID
IsAssigned = Me.is_assigned
Dim db As DAO.Database, rs As DAO.Recordset, strSQL As String
Set db = CurrentDb
strSQL = "select UserID, taskID from TaskAssignments where UserID=" & SelectedUserID & " and taskID =" & SelectedTaskID & ";"
Set rs = db.OpenRecordset(strSQL)
If rs.EOF = True Then
ShouldInsert = True
Else: ShouldInsert = False
End If
If ShouldInsert = True Then
AssignmentQuery = "insert into TaskAssignments (UserID, taskID, DateAssignmentUpdated, AssignmentUpdatedBy, is_assigned) values " _
& vbCrLf & "(" & SelectedUserID & "," & SelectedTaskID & ",#" & Now & "#,'" & CurrentUser & "'," & IsAssigned & ");"
ElseIf ShouldInsert = False Then
AssignmentQuery = "update TaskAssignments set UserID=" & SelectedUserID & ", DateAssignmentUpdated=#" & Now & "#, AssignmentUpdatedBy='" & CurrentUser & "',is_assigned=" & IsAssigned _
& vbCrLf & " where taskID = " & SelectedTaskID & " And UserID = " & SelectedUserID & ";"
End If
MsgBox AssignmentQuery
db.Execute (AssignmentQuery)
Forms("Task Assignments").Requery
Set rs = Nothing
Set db = Nothing
End Sub
Edit - here are the queries produced:
Insert
insert into TaskAssignments
(UserID, TaskID, DateAssignmentUpdated, AssignmentUpdatedBy, is_assigned)
values (301,4,Now(),'mylogin',True);
Update
update TaskAssignments
set UserID=270, DateAssignmentUpdated=Now(), AssignmentUpdatedBy='mylogin', is_assigned=False
where TaskID = 1 And UserID = 270;
And a constraint on my TaskAssignments table. Both TaskID and UserID are set as required in my table design (which was my whole goal - I was hoping to avoid adding records to TaskAssignments until the user has actually been assigned to a task).
alter table TaskAssignments add constraint TaskAssignmentsConstraint unique (TaskID, UserID);
Beware of wrong datatypes, each Dim needs its own datatype!
Dim CurrentUser As String, AssignmentQuery As String
Dim SelectedUserID As Long, SelectedTaskID As Long ' don't use 16-bit Integer for ID columns
Dim ShouldInsert As Boolean, IsAssigned As Boolean
To avoid troubles with date/time formatting: the database engine knows Now(), so you can directly use this in the Insert SQL:
AssignmentQuery = "insert into TaskAssignments (UserID, taskID, DateAssignmentUpdated, AssignmentUpdatedBy, is_assigned) values " _
& vbCrLf & "(" & SelectedUserID & "," & SelectedTaskID & ", Now(), '" & CurrentUser & "'," & IsAssigned & ");"
If it still doesn't work, use Debug.Print AssignmentQuery instead of MsgBox and add the actual SQL to your question (Ctrl+G shows the output).
Edit
Re-reading the question and comment, I think the problem is:
You are editing a bound form, and are updating/inserting in the same table the form is based on. That's where the Write conflict on Update comes from, the other error is probably because the bound form is trying to insert a record when you click is_assigned, but can't.
So yes, you need to rethink your approach, at least partially.
One solution is to insert the recordsource into a temp table, and base your form on that. Then the rest of the code will probably work.
It may be over-complicating things, though.
I got this problem trying to update a field that use to be a primary field in my table. When I altered what was considered the primary field I assume access would automatically stop enforcing is not null, but for some reason it didn't.
I fixed it by deleting the field, saving the table recreating the field and saving the table and the problem went away. Of course this wouldn't be an ideal solution if you have data in that table you don't want to lose so you might want to try backing it up first, before you give the solution a try, then reinserting the values.

MS Access add/update query result to an existing table base on its ID

I followed the tips by others to produce an access query.
I have two tables. See figure1. And the result is figure2.
Figure1
http://img.libk.info/f1.png http://img.libk.info/f1.png
Figure2
http://img.libk.info/f2.png http://img.libk.info/f2.png
The method to generate the result query is solved in another question.
The query script :
TRANSFORM Nz(Count([number]),0) AS CountValue
SELECT Table1.ID
FROM Table1, Table2
WHERE (((Table2.number) Between [table1].[start] And [table1].[end]))
GROUP BY Table1.ID
PIVOT DatePart("yyyy",[ndate]);
My question is:
Is there anyway to write back the query result to table 1?
I want to add two new columns in table 1. And be able to add or update the query value to the field base on its "ID".
I'm not sure my description is clear or not. Hope you may understand and thanks for your help!
You won't be able to do it directly. However, here are two ways it could be done indirectly.
Method 1: Temp Table
This method is best for a quick-and-dirty one-time solution.
Create a Make-Table query based on your query and use it to make a temporary table.
Use the temporary table joined to [Table 1] to update your two new fields.
Delete the temporary table
Method 2: VBA Routine
This method is best when you want a repeatable method. It's overkill if you're only going to do it once. However, if you want calculated values for every year, you'll need to run it again.
Read the query into a recordset
Loop through the Recordset and for each ID, either
Run a sql statement to update table 1, or
open a second recordset querying by the ID and Edit/Update
Here's a simple version that updates the value for a single year.
Public Sub UpdateAnnualTotal(ByVal nYear As Long)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim sSQL As String
Dim sId As String
Dim nTotal As Long
Set db = CurrentDb
sSQL = "SELECT [ID],[" & nYear & "_count"] FROM AnnualTotalsQuery"
Set rs = db.OpenRecordset(sSQL)
With rs
Do Until .EOF
sId = .Fields("ID").Value
nTotal = .Fields(nYear & "_count").Value
sSQL = "UPDATE [Table 1] SET [" & nYear & "_count"] = " & nTotal _
& " WHERE [ID] = '" & sId & "'"
db.Execute sSQL
.MoveNext
Loop
.Close
End With
End Sub

Ms Access lock table while inserting data

In a Access 2003 database, I have an "Inscriptions" (subscription) database with a primary key on 2 fields idPersonnel (employee) and idSession.
I have made a form so that user can select a session (in a listbox), then one or more employee (another listbox) and suscribe them to that session by using a button, which, on VBA side, first check that there is enough room on the session (defined by "MaxParticipants" field on "Sessions" table, linked to "Inscriptions" table on idSession), then insert data in "Inscriptions" table
This is working fine in a single-user environnement, but fails if 2 people want to join some employees on the same session at the same time, as I have a confirmation message between check and insertion. Therefore 2 users can select employees, get the confirmation message (at this point both are told there is enough room), resulting in having more people than expected joined to the session.
Fortuneatly, if both users try to insert the same employee(s) to that table, one will get a duplicate error, but insertion will be made if employees are different.
On another DB engine, such as SQL server, I would use a stored procedure that would lock the table, do the check and the insertion then unlock the table.
But it does not seem to be possible in MS Access.
What are the possibilities in MS Access to prevent a session from having more than maximum number of participants ? Any help is appreciated.
One way to accomplish your goal would be to do the INSERT in a transaction, count the participants for that session, and roll back the transaction if the new total exceeds the limit:
Option Compare Database
Option Explicit
Sub AddParticipant()
Dim cdb As DAO.Database, cws As DAO.Workspace, _
qdf As DAO.QueryDef, rst As DAO.Recordset
' test data
Const idPersonnelToAdd = 4
Const idSessionToAdd = 2
Set cdb = CurrentDb
Set cws = DBEngine.Workspaces(0)
cws.BeginTrans
Set qdf = cdb.CreateQueryDef("", _
"PARAMETERS prmIdPersonnel Long, prmIdSession Long; " & _
"INSERT INTO Inscriptions (idPersonnel, idSession) " & _
"VALUES (prmIdPersonnel, prmIdSession)")
qdf!prmIdPersonnel = idPersonnelToAdd
qdf!prmIdSession = idSessionToAdd
qdf.Execute dbFailOnError
Set qdf = Nothing
Set qdf = cdb.CreateQueryDef("", _
"PARAMETERS prmIdSession Long; " & _
"SELECT " & _
"Count(*) AS NumParticipants, " & _
"First(MaxParticipants) AS Limit " & _
"FROM Inscriptions INNER JOIN Sessions " & _
"ON Inscriptions.idSession = Sessions.idSession " & _
"WHERE Sessions.idSession = prmIdSession")
qdf!prmIdSession = idSessionToAdd
Set rst = qdf.OpenRecordset(dbOpenSnapshot)
If rst!NumParticipants <= rst!Limit Then
cws.CommitTrans
Debug.Print "INSERT committed"
Else
cws.Rollback
Debug.Print "INSERT rolled back"
End If
rst.Close
Set rst = Nothing
Set qdf = Nothing
Set cws = Nothing
Set cdb = Nothing
End Sub

Microsoft Access - Merging content with identical keys into a new table

In Microsoft Access, I would like to merge a specific column from tables having an identical key (=being duplicates) into a new table.
The source table would look like this
Key Content
Apple X
Banana B
Banana D
Banana E
Banana G
Lemon A
Lemon X
Orange A
I would like to create a new table where there is only one entry per key, each key having a field consisting of all corresponding “content” fields accumulated into one field. Each “content” value should be delimited with something, example:
Apple X
Banana B<x>D<x>E<x>G
Lemon A<x>X
Orange A
I would prefer to have it like above, but it could also work if they are in different fields/columns like below:
Apple X
Banana B D E G
Lemon A X
Orange A
I would really appreciate help with this. When googling on this I have found a quit a few of third party add-ons (like this one http://www.tucows.com/preview/1586663/MS-Access-Join-Two-Tables-Software) that seems to be solving this, but surely this can be done with MS Access itself….or…?
Another approach would be to create the table with the two columns (Key, Content) then run the function below to copy the data into the new table. You'll have to replace "ExistingTableName" and "NewTableName" with your table names.
Sub CreateNewTable()
Dim rs As Recordset
Dim rsContent As Recordset
Dim strContent As String
'Select and loop through all keys
Set rs = CurrentDb.OpenRecordset("SELECT DISTINCT Key FROM [ExistingTableName]")
Do Until rs.EOF
'Select all content records for this key and combine into a string
Set rsContent = CurrentDb.OpenRecordset("SELECT Content FROM [ExistingTableName] WHERE Key = " & Chr(34) & Nz(rs!Key) & Chr(34))
strContent = ""
Do Until rsContent.EOF
strContent = strContent & rsContent!Content & ","
rsContent.MoveNext
Loop
If Len(strContent) > 0 Then
strContent = Mid(strContent, 1, Len(strContent) - 1)
End If
CurrentDb.Execute "INSERT INTO [NewTableName] (Key, Content) VALUES (" & Chr(34) & Nz(rs!Key) & Chr(34) & ", " & Chr(34) & Nz(strContent) & Chr(34) & ")"
rs.MoveNext
Loop
Set rs = Nothing
Set rsContent = Nothing
End Sub
I don't think there is a way to do this without VBA.
Seeing as you are using a relational database you should consider using one table to store keys and another table to store content (one content per row/record), then link them up either by using a third table or by adding the 'key' as a foreign key in the content table. I would also always use an autonumber as the primary key in all MS Access tables, if not for every other reason this is a good idea, simply to avoid corruption and enable you to change a spelling mistake like 'aple' to 'apple' without breaking your relationships.
One version would be to use a UDF and this query:
SELECT Distinct Fruit.Key,
ConcatADO("SELECT Content FROM Fruit WHERE Key='" & [Key] & "'","<x>","<x>")
AS AllRows
INTO NewFruit
FROM Fruit
User Defined Function (UDF)
Function ConcatADO(strSQL As String, strColDelim, strRowDelim)
Dim rs As New ADODB.Recordset
Dim strList As String
On Error GoTo Proc_Err
If strSQL <> "" Then
rs.Open strSQL, CurrentProject.Connection
strList = rs.GetString(, , strColDelim, strRowDelim)
strList = Mid(strList, 1, Len(strList) - Len(strRowDelim))
End If
ConcatADO = strList
Exit Function
Proc_Err:
ConcatADO = "***" & UCase(Err.Description)
End Function
Working within the query design window, you can create a crosstab
TRANSFORM Min(Fruit.Content) AS MinOfContent
SELECT Fruit.Key
FROM Fruit
GROUP BY Fruit.Key
PIVOT Fruit.Content;
Which would return
Key A B D E G X
Apple X
Banana B D E G
Lemon A X
Orange A
You could then save the crosstab and create a new query based on the cross tab. This query could be a Make Table query to get the new table, but as you can see, you have several columns.
If you have a predetermined number of possible rows for each key, there are other approaches.
Finally, you must ask yourself, is de-normalizing really the way to go?

Loop through Access tables

I'm creating a database in Access that will have many similar tables with identical and the names of them will all be named in sequential order, such as table_0, table_1...
I wanted to know if there is a way to make a loop in VBA to first make the tables, and them altering them, such as adding a field easier.
So if I were to make the tables, this would be the process, but in VBA
while(i=0, i<20,i++){
CREATE TABLE table_i(
field_1 int PRIMARY KEY,
field_2 CHAR(255));
}
In VBA, you can loop through your numbered table names like this ...
Dim strTable As String
Dim i As Long
For i = 0 To 20
strTable = "table_" & CStr(i)
Debug.Print strTable
Next i
But you want to do something with each table name other than print it. So say you've already worked out your CREATE TABLE statement. Maybe it looks like this ...
CREATE TABLE table_name (
field_1 INTEGER PRIMARY KEY,
field_2 TEXT(255));
So you could load that DDL statement into a string variable, then each time through your For loop replace table_name with the current value of strTable, and execute that DDL statement.
Dim strTable As String
Dim i As Long
Dim strSql As String
strSql = "CREATE TABLE table_name (" & vbCrLf & _
"field_1 INTEGER PRIMARY KEY," & vbCrLf & _
"field_2 TEXT(255));"
For i = 0 To 20
strTable = "table_" & CStr(i)
CurrentProject.Connection.Execute Replace(strSql, _
"table_name", strTable)
Next i
That Replace() function first became available with Access 2000. If you're using an older Access version, you'll need a different method to handle your SQL string.
If you want to execute an ALTER TABLE statement, you could do that instead.
So I think doing what you want can be easily done with VBA. However I'm uncertain whether you should do it. Multiple tables with identical structure is generally a red flag signalling a design error. There is no evidence to conclude it's a design error in your case. OTOH, there is no evidence to conclude it's not a design error. ;-)