{"id":55000499,"date":"2026-02-01T00:00:00","date_gmt":"2026-03-10T11:58:24","guid":{"rendered":"http:\/\/access-im-unternehmen.aix-dev.de\/aiu\/?p=499"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-30T00:00:00","slug":"SQL_ServerMigration_Anpassen_des_VBACodes","status":"publish","type":"post","link":"https:\/\/vbentwickler.de\/SQL_ServerMigration_Anpassen_des_VBACodes\/","title":{"rendered":"SQL Server-Migration: Anpassen des VBA-Codes"},"content":{"rendered":"<p><b>Wenn Du Deine Datenbank zum SQL Server migriert hast, also alle Tabellen in einer SQL Server-Datenbank liegen und mit dem Frontend verkn&uuml;pft sind, kannst Du ohne Probleme &uuml;ber die Tabellenverkn&uuml;pfungen auf die Tabellen im SQL Server zugreifen. Wenn Du jedoch per VBA, speziell mit den Methoden OpenRecordset und Execute, auf diese Tabellen zugreifen m&ouml;chtest, kann es zu Fehlermeldungen kommen. In diesem Artikel erl&auml;utern wir, welche Fehler dies sind, warum sie auftreten und wie Du Deine Datenzugriffe auf die Tabellen der SQL Server-Datenbank so anpasst, dass sie fehlerfrei laufen. Au&szlig;erdem schauen wir uns eine Besonderheit an, die beim Anlegen neuer Datens&auml;tze mit der AddNew-Methode des Recordset-Objekts auftritt &#8211; und schlie&szlig;lich gibt es noch eine Besonderheit bei Formularen, die wir ebenfalls ber&uuml;cksichtigen.<\/b><\/p>\n<p>Wenn Du die Migration zum SQL Server zum Beispiel mit dem SQL Server Migration Assistant durchgef&uuml;hrt hast, steht als n&auml;chster Schritt in der Regel der Test an, wie sich das Access-Frontend mit den Tabellen aus dem neuen SQL Server-Backend vertr&auml;gt.<\/p>\n<p>Dabei schauen wir uns in diesem Artikel prim&auml;r die VBA-Seite an. Hier treten verschiedene Fehler auf, die wir uns zun&auml;chst einmal ansehen.<\/p>\n<h2>Fehler beim &Ouml;ffnen eines Recordsets<\/h2>\n<p>Der erste Fehler, der auftreten kann und vermutlich auch auftreten wird, wenn Du die <b>OpenRecordset<\/b>-Methode des <b>Database<\/b>-Objekts verwendest, ist der aus Bild 1.<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_499_001.png\" alt=\"Fehler in der OpenRecordset-Methode\" width=\"649,627\" height=\"487,6776\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 1: Fehler in der OpenRecordset-Methode<\/span><\/b><\/p>\n<p>Die Fehlermeldung ist aussagekr&auml;ftig genug, um den Fehler selbst zu beheben: Wir ben&ouml;tigen einen weiteren Parameter namens <b>dbSeeChanges<\/b>.<\/p>\n<p>Diesen f&uuml;gen wir wie folgt zur <b>OpenRecordset<\/b>-Methode hinzu:<\/p>\n<pre><span style=\"color:blue;\">Set<\/span> rst = db.OpenRecordset(\"tblBuecher\", _\r\n    dbOpenDynaset, dbSeeChanges)<\/pre>\n<p>Diese Konstanten m&uuml;ssen wir immer als dritten Parameter angeben, wenn wir mit <b>OpenRecordset <\/b>eine Tabelle oder Abfrage &ouml;ffnen wollen.<\/p>\n<p>Wenn wir bisher wie in folgendem Beispiel &uuml;berhaupt keinen Parameter angegeben haben, erscheint die gleiche Fehlermeldung:<\/p>\n<pre><span style=\"color:blue;\">Set<\/span> rst = db.OpenRecordset(\"tblBuecher\")<\/pre>\n<p>Wir k&ouml;nnen dann aber nicht einfach <b>dbSeeChanges<\/b> wie hier als dritten Parameter setzen und den zweiten Parameter leer lassen:<\/p>\n<pre><span style=\"color:blue;\">Set<\/span> rst = db.OpenRecordset(\"tblBuecher\", , dbSeeChanges)<\/pre>\n<p>Dies w&uuml;rde bedeuten, dass beim <b>OpenRecordset <\/b>der Standardwert f&uuml;r den zweiten Parameter verwendet wird, der <b>dbOpenTable <\/b>lautet.<\/p>\n<p>Und <b>dbOpenTable <\/b>funktioniert mit ODBC-Tabellenverkn&uuml;pfungen gar nicht. Wir m&uuml;ssen also einen der &uuml;brigen Werte f&uuml;r den zweiten Parameter angeben.<\/p>\n<p>Wenn wir hingegen <b>dbOpenSnapshot <\/b>oder <b>dbOpenForwardOnly <\/b>nutzen, brauchen wir <b>dbSeeChanges <\/b>nicht anzugeben. Diese beiden Methoden &ouml;ffnen das Recordset n&auml;mlich schreibgesch&uuml;tzt.<\/p>\n<p>Dies liefert bereits einen wichtigen Hinweis auf den Grund f&uuml;r die Fehlermeldung. Wenn wir diese nur erhalten, wenn wir das Recordset in dem Modus &ouml;ffnen, in dem wir es bearbeiten k&ouml;nnen, muss es etwas mit dem Bearbeiten zu tun haben.<\/p>\n<p>Au&szlig;erdem tritt dieser Fehler nur auf, wenn die zum SQL Server migrierte Tabelle ein Autowert-Feld als Prim&auml;rschl&uuml;sselfeld enth&auml;lt. Dieses wird im SQL Server bei der Migration als <b>IDENTITY<\/b>-Spalte abgebildet, was im Grunde nichts anderes ist als ein Autowert.<\/p>\n<p>Der entscheidende Unterschied ist jedoch, dass die Identit&auml;tsspalte nun vom SQL Server gef&uuml;llt und verwaltet wird und nicht mehr direkt von der Access-Datenbank aus.<\/p>\n<p>Nun stellen wir uns vor, dass wir im Recordset mit <b>AddNew <\/b>einen neuen Datensatz anlegen. Dies sorgt daf&uuml;r, dass beim Speichern zwar ein neuer Identit&auml;tswert in der SQL Server-Tabelle angelegt wird, aber dieser wird nicht automatisch an Access zur&uuml;ckgegeben.<\/p>\n<p>Um genau dies zu gew&auml;hrleisten, muss man den Parameter <b>dbSeeChanges <\/b>setzen. Er sorgt daf&uuml;r, dass bei Anlegen neuer Datens&auml;tze das Recordset direkt aktualisiert wird und SQL Server die ID des neuen Datensatzes an Access zur&uuml;ckmeldet.<\/p>\n<p>Zusammengefasst: <b>dbSeeChanges <\/b>zwingt DAO\/ODBC dazu, nach einem <b>Insert<\/b>\/<b>Update <\/b>die serverseitig erzeugten beziehungsweise ge&auml;nderten Werte (zum Beispiel <b>IDENTITY<\/b>) wieder einzulesen. Ohne dieses Flag bricht DAO bei schreibenden Zugriffen auf verkn&uuml;pfte SQL-Server-Tabellen mit Fehler <b>3622 <\/b>ab.<\/p>\n<h2>OpenRecordset f&uuml;r ein QueryDef-Objekt<\/h2>\n<p>Das Gleiche gilt, wenn wir <b>OpenRecordset <\/b>als Methode eines <b>QueryDef<\/b>-Objekts ausf&uuml;hren. Im folgenden Beispiel erstellen wir ein <b>QueryDef<\/b>-Objekt auf Basis einer Abfrage.<\/p>\n<p>Wenn wir ein Recordset f&uuml;r dieses <b>QueryDef<\/b>-Objekt &ouml;ffnen wollen, m&uuml;ssen wir ebenfalls dbSeeChanges &uuml;bergeben &#8211; dieses Mal allerdings mit dem zweiten Parameter:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_Recordset_1()\r\n    <span style=\"color:blue;\">Dim <\/span>db<span style=\"color:blue;\"> As <\/span>DAO.Database\r\n    <span style=\"color:blue;\">Dim <\/span>qdf<span style=\"color:blue;\"> As <\/span>DAO.QueryDef\r\n    <span style=\"color:blue;\">Dim <\/span>rst<span style=\"color:blue;\"> As <\/span>DAO.Recordset\r\n    <span style=\"color:blue;\">Set<\/span> db = CurrentDb\r\n    <span style=\"color:blue;\">Set<\/span> qdf = db.CreateQueryDef(\"\", _\r\n        \"SELECT * FROM tblBuecher\")\r\n    <span style=\"color:blue;\">Set<\/span> rst = qdf.OpenRecordset(dbOpenDynaset, _\r\n        dbSeeChanges)\r\n    <span style=\"color:blue;\">Do While<\/span> <span style=\"color:blue;\">Not<\/span> rst.EOF\r\n        <span style=\"color:blue;\">Debug.Print<\/span> rst!Buchtitel\r\n        rst.Move<span style=\"color:blue;\">Next<\/span>\r\n    <span style=\"color:blue;\">Loop<\/span>\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<h2>Fehler beim Ausf&uuml;hren von Aktionsabfragen<\/h2>\n<p>Aktionsabfragen wie <b>INSERT INTO<\/b>, <b>UPDATE <\/b>oder <b>DELETE <\/b>k&ouml;nnen wir auf zwei Arten ausf&uuml;hren:<\/p>\n<ul>\n<li><b>Docmd.RunSQL<\/b><\/li>\n<li><b>Execute<\/b>-Methode des <b>Database<\/b>-Objekts<\/li>\n<\/ul>\n<h2>Aktionsabfragen mit DoCmd.RunSQL<\/h2>\n<p>Wenn wir die <b>DoCmd.RunSQL<\/b>-Methode verwenden, k&ouml;nnen jede der folgenden Aktionsabfragen ohne Fehler ausf&uuml;hren:<\/p>\n<pre>DoCmd.RunSQL \"INSERT INTO tblBuecher(BuchID, Buchtitel) VALUES(38, ''Buch'')\"\r\nDoCmd.RunSQL \"UPDATE tblBuecher SET Buchtitel = ''Buch 1'' WHERE BuchID = 38\"\r\nDoCmd.RunSQL \"DELETE FROM tblBuecher WHERE BuchID = 38\"<\/pre>\n<h2>Aktionsabfragen mit Execute<\/h2>\n<p>Bei Verwendung der <b>Execute<\/b>-Methode sieht dies anders aus. Allein das Anlegen eines Datensatzes wie folgt gelingt ohne Fehlermeldung:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_DbExecute_INSERTINTO()\r\n    <span style=\"color:blue;\">Dim <\/span>db<span style=\"color:blue;\"> As <\/span>DAO.Database\r\n    <span style=\"color:blue;\">Set<\/span> db = CurrentDb\r\n    db.Execute \"INSERT INTO tblBuecher(BuchID, Buchtitel) \" _\r\n        & \"VALUES(37, ''Buch'')\", dbFailOnError\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>Wenn wir jedoch versuchen, diesen Datensatz zu aktualisieren, erhalten wir wieder den Fehler mit der Nummer <b>3622<\/b>:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_DbExecute_UPDATE()\r\n    <span style=\"color:blue;\">Dim <\/span>db<span style=\"color:blue;\"> As <\/span>DAO.Database\r\n    <span style=\"color:blue;\">Set<\/span> db = CurrentDb\r\n    db.Execute \"UPDATE tblBuecher SET Buchtitel = ''Buch'' \" _\r\n        & \"WHERE BuchID = 36\", dbFailOnError\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>Und auch beim L&ouml;schen eines Datensatzes tritt dieser Fehler auf:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_DbExecute_DELETE()\r\n    <span style=\"color:blue;\">Dim <\/span>db<span style=\"color:blue;\"> As <\/span>DAO.Database\r\n    <span style=\"color:blue;\">Set<\/span> db = CurrentDb\r\n    db.Execute \"DELETE FROM tblBuecher WHERE BuchID = 36\",  _\r\n        dbFailOnError\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>Diesen k&ouml;nnen wir &auml;hnlich beheben wie bei der OpenRecordset-Methode. Dieses Mal m&uuml;ssen wir <b>dbSeeChanges <\/b>jedoch nicht als weiteren Parameter anh&auml;ngen, sondern wir m&uuml;ssen diesen mit dem vorhandenen (und immer dringend empfohlenen) <b>dbFailOnError <\/b>in einem Parameter vereinen. Das gelingt &uuml;ber den Plus-Operator <b>(+<\/b>) etwa wie folgt:<\/p>\n<pre>db.Execute \"UPDATE tblBuecher SET Buchtitel = ''Buch'' \" _\r\n    & \"WHERE BuchID = 36\", dbFailOnError + dbSeeChanges<\/pre>\n<p>Warum aber funktioniert<b> INSERT INTO <\/b>ohne die Angabe von <b>dbSeeChanges<\/b>, <b>UPDATE <\/b>und <b>DELETE <\/b>nicht? Weil <b>INSERT INTO <\/b>in diesem Fall direkt an den SQL Server durchgereicht wird. F&uuml;r die anderen Abfragen sind jedoch &Auml;nderungen an bestehenden Datens&auml;tzen n&ouml;tig &#8211; und daf&uuml;r m&uuml;ssen die Prim&auml;rschl&uuml;ssel wieder zwischen Access und dem SQL Server synchron gehalten werden.<\/p>\n<h2>Probleme mit Ja\/Nein-Feldern<\/h2>\n<p>Ein weiteres Problem, das dieses Mal eher inhaltliche Fehler produziert statt Laufzeitfehler, sind <b>Ja\/Nein<\/b>-Felder. Wenn wir eine Tabelle mit <b>Ja\/Nein<\/b>-Feldern zum SQL Server &uuml;bertragen, wird daraus standardm&auml;&szlig;ig der Datentyp <b>bit <\/b>erzeugt.<\/p>\n<p>Hier gibt es zun&auml;chst einen wichtigen Unterschied zu ber&uuml;cksichtigen: W&auml;hrend in Access der Wert f&uuml;r <b>Ja<\/b>\/<b>True<\/b> intern als <b>-1 <\/b>und der Wert f&uuml;r <b>Nein<\/b>\/<b>False <\/b>als <b>0 <\/b>gespeichert wird, kann das <b>bit<\/b>-Feld lediglich die Werte <b>1 <\/b>und <b>0 <\/b>speichern.<\/p>\n<p>Im SQL Server haben die Werte <b>1 <\/b>und <b>0 <\/b>auch keine tiefergehende Bedeutung &#8211; sie entsprechen lediglich den Zahlenwerten.<\/p>\n<p>Dass Access die Werte dieser Felder korrekt als <b>True <\/b>oder <b>False <\/b>interpretiert, stellt der ODBC-Treiber sicher. Mit Tabellen, die per ODBC verkn&uuml;pft sind, &auml;ndert sich auch erst einmal nichts: <b>1 <\/b>entspricht <b>True <\/b>und <b>0 <\/b>entspricht <b>False<\/b>.<\/p>\n<p>Wenn wir also zum Beispiel in der Beispieldatenbank alle gelesenen B&uuml;cher abfragen wollen, funktioniert die folgende Abfrage auf der entsprechenden Tabellenverkn&uuml;pfung korrekt:<\/p>\n<pre>SELECT * FROM tblBuecher WHERE Gelesen = <span style=\"color:blue;\">True<\/span><\/pre>\n<p>Auch <b>False <\/b>funktioniert hier einwandfrei.<\/p>\n<p>Gerade wenn wir solche Abfragen mit dem Abfrageentwurf zusammenstellen, ist sichergestellt, dass diese funktionieren, da dieser f&uuml;r <b>Ja\/Nein<\/b>-Felder auch die Werte <b>-1 <\/b>und <b>0 <\/b>immer in <b>Wahr <\/b>und <b>Falsch <\/b>&uuml;bersetzt.<\/p>\n<p>Nun schauen wir uns aber ein Recordset an, das wir per VBA &ouml;ffnen. Hier verwenden wir nicht den Wert <b>True <\/b>als Vergleichswert, sondern den Wert <b>-1<\/b>:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_TrueFalse()\r\n    <span style=\"color:blue;\">Dim <\/span>db<span style=\"color:blue;\"> As <\/span>DAO.Database\r\n    <span style=\"color:blue;\">Dim <\/span>rst<span style=\"color:blue;\"> As <\/span>DAO.Recordset\r\n    <span style=\"color:blue;\">Set<\/span> db = CurrentDb\r\n    <span style=\"color:blue;\">Set<\/span> rst = db.OpenRecordset(\"SELECT * FROM tblBuecher \" _\r\n        & \"WHERE Gelesen = -1\", dbOpenDynaset, dbSeeChanges)\r\n    <span style=\"color:blue;\">Do While<\/span> <span style=\"color:blue;\">Not<\/span> rst.EOF\r\n        <span style=\"color:blue;\">Debug.Print<\/span> rst!BuchID, rst!Buchtitel\r\n        rst.Move<span style=\"color:blue;\">Next<\/span>\r\n    <span style=\"color:blue;\">Loop<\/span>\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>F&uuml;hren wir diese Abfrage aus, erhalten wir &uuml;berraschenderweise keine Daten im Direktbereich!<\/p>\n<p>Auch eine <b>DCount<\/b>-Funktion, mit der wir die Anzahl der gelesenen B&uuml;cher pr&uuml;fen wollen, liefert den Wert <b>0 <\/b>im Direktbereich:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_TrueFalseDLookup()\r\n    <span style=\"color:blue;\">Debug.Print<\/span> DCount(\"BuchID\", \"tblBuecher\", \"Gelesen = -1\")\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>Damit scheint festzustehen: In Abfragen, in denen nicht durch den Abfrageentwurf der Wert <b>-1 <\/b>durch den Wert <b>Wahr<\/b>\/<b>True <\/b>ersetzt wird, erhalten wir nicht die korrekten Ergebnisse, weil hier tats&auml;chlich der Access-Wert <b>-1<\/b> mit dem im SQL Server gespeicherten Wert <b>1 <\/b>verglichen wird &#8211; und das liefert keine Ergebnisse.<\/p>\n<p>Korrekt w&auml;re hier also f&uuml;r das Recordset:<\/p>\n<pre><span style=\"color:blue;\">Set<\/span> rst = db.OpenRecordset(\"SELECT * FROM tblBuecher \" _\r\n    & \"WHERE Gelesen = 1\", dbOpenDynaset, dbSeeChanges)<\/pre>\n<p>Und in der <b>DCount<\/b>-Anweisung m&uuml;ssten wir Folgendes schreiben, um das korrekte Ergebnis zu erhalten:<\/p>\n<pre><span style=\"color:blue;\">Debug.Print<\/span> DCount(\"BuchID\", \"tblBuecher\", \"Gelesen = 1\")<\/pre>\n<p>M&uuml;ssen wir nun in allen Abfragen <b>-1 <\/b>durch <b>1 <\/b>ersetzen?<\/p>\n<p>Dies w&uuml;rde tats&auml;chlich funktionieren, aber nur, wenn wir ausschlie&szlig;lich mit SQL Server-Tabellen arbeiten w&uuml;rden.<\/p>\n<p>Wenn wir sp&auml;ter einmal, aus welchem Grund auch immer, noch einmal auf Access-Tabellen umstellen, liefern die Abfragen wieder falsche Werte, weil Access beim Vergleich eines <b>Ja\/Nein<\/b>-Feldes mit dem Wert <b>-1<\/b>\/<b>True <\/b>mit dem Wert <b>1 <\/b>wieder falsche Ergebnisse liefern w&uuml;rde.<\/p>\n<p>Damit liegt die L&ouml;sung auf der Hand: Wir m&uuml;ssen einfach immer <b>True <\/b>oder <b>False <\/b>als Kriterium f&uuml;r <b>Ja\/Nein<\/b>-Felder verwenden, solange diese Abfragen sich auf per ODBC verkn&uuml;pfte Tabellen aus einer SQL Server-Datenbank beziehen.<\/p>\n<h2>Vorsicht beim Wechsel zu ADODB<\/h2>\n<p>Wenn wir jedoch im Code Zugriffe auf die SQL Server-Datenbank per ADODB einstreuen, m&uuml;ssen wir wiederum aufpassen: ADODB-Recordsets werden direkt vom SQL Server eingelesen.<\/p>\n<p>Dann m&uuml;ssen wir uns endg&uuml;ltig von <b>True <\/b>und <b>False <\/b>verabschieden und die in SQL Server verwendeten Werte f&uuml;r <b>bit<\/b>-Felder als Kriterien nutzen, n&auml;mlich <b>1 <\/b>f&uuml;r <b>True <\/b>und <b>0 <\/b>f&uuml;r <b>False<\/b>.<\/p>\n<p>Ein Beispiel sieht wie folgt aus:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_ADODB()\r\n    <span style=\"color:blue;\">Dim <\/span>cnn<span style=\"color:blue;\"> As <\/span>ADODB.Connection\r\n    <span style=\"color:blue;\">Dim <\/span>strConnection<span style=\"color:blue;\"> As String<\/span>\r\n    <span style=\"color:blue;\">Dim <\/span>rst<span style=\"color:blue;\"> As <\/span>ADODB.Recordset\r\n    <span style=\"color:blue;\">Set<\/span> cnn = <span style=\"color:blue;\">New<\/span> ADODB.Connection\r\n    strConnection = \"Provider=MSOLEDBSQL;\" _\r\n        & \"Server=amvDesktop2023;Database=FE_Test;\" _\r\n        & \"Trusted_Connection=Yes\"\r\n    cnn.ConnectionString = strConnection\r\n    cnn.Open\r\n    <span style=\"color:blue;\">Set<\/span> rst = <span style=\"color:blue;\">New<\/span> ADODB.Recordset\r\n    rst.Open \"SELECT * FROM tblBuecher WHERE Gelesen = 1\", cnn\r\n    <span style=\"color:blue;\">Do While<\/span> <span style=\"color:blue;\">Not<\/span> rst.EOF\r\n        <span style=\"color:blue;\">Debug.Print<\/span> rst!BuchID, rst!Buchtitel\r\n        rst.Move<span style=\"color:blue;\">Next<\/span>\r\n    <span style=\"color:blue;\">Loop<\/span>\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>Wenn wir hier <b>True <\/b>oder <b>False <\/b>als Vergleichswert nutzen, erhalten wir den Fehler <b>Ung&uuml;ltiger Spaltenname<\/b> &#8211; der SQL Server kann diesen Wert nicht korrekt interpretieren. Und das Problem l&auml;sst sich hier auch mit dem Wert <b>-1 <\/b>f&uuml;r <b>True <\/b>nicht l&ouml;sen &#8211; SQL Server verlangt hier nach dem Wert <b>1<\/b>, wenn die Abfrage sinnvolle Ergebnisse liefern soll.<\/p>\n<h2>Unter ADODB und SQL Server gilt T-SQL-Syntax<\/h2>\n<p>Grunds&auml;tzlich gilt f&uuml;r den Zugriff von Access aus mit ADODB auf eine SQL Server-Datenbank, dass wir hier nicht mehr mit Access-SQL arbeiten k&ouml;nnen, sondern mit T-SQL.<\/p>\n<p>Das spiegelt sich zuerst in einer anderen Verwendung von Platzhaltern in Abfragen mit dem Vergleichsoperator <b>LIKE <\/b>wider &#8211; statt des Sternchens f&uuml;r beliebig viele Zeichen nutzen wir hier das Prozentzeichen und statt des Fragezeichens f&uuml;r ein beliebiges Zeichen den Unterstrich.<\/p>\n<p>Das Auflisten aller wichtigen Unterschiede zwischen Access-SQL und T-SQL w&uuml;rde an dieser Stelle allerdings den Rahmen sprengen.<\/p>\n<h2>Autowert-ID auslesen bei AddNew funktioniert nicht mehr<\/h2>\n<p>Zur&uuml;ck zum Datenzugriff per DAO: Unter Access kommt es oft vor, dass wir per VBA einen neuen Datensatz nicht mit <b>INSERT INTO <\/b>anlegen, sondern mit der <b>AddNew<\/b>-Methode des <b>Recordset<\/b>-Objekts. Anschlie&szlig;end tragen wir die Werte f&uuml;r die einzelnen Felder des neuen Datensatzes ein und speichern diesen mit der Methode <b>Update<\/b>.<\/p>\n<p>Wenn wir danach eine Aktion durchf&uuml;hren wollen, f&uuml;r die wir den Prim&auml;rschl&uuml;sselwert des als Autowert deklarierten Access-Feldes ben&ouml;tigen &#8211; beispielsweise, um diesem Buch gleich einen Autor zuzuweisen -, greifen wir typischerweise auf das Prim&auml;rschl&uuml;sselfeld des Recordsets zu und speichern diesen in einer entsprechenden Variablen.<\/p>\n<p>Im folgenden Beispiel erstellen wir ein neues Buch in der Tabelle <b>tblBuecher <\/b>und lesen den automatisch generierten Wert des Feldes <b>BuchID <\/b>in die Variable <b>lngBuchID <\/b>ein:<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_Autowert()\r\n    <span style=\"color:blue;\">Dim <\/span>db<span style=\"color:blue;\"> As <\/span>DAO.Database\r\n    <span style=\"color:blue;\">Dim <\/span>rst<span style=\"color:blue;\"> As <\/span>DAO.Recordset\r\n    <span style=\"color:blue;\">Dim <\/span>lngBuchID<span style=\"color:blue;\"> As Long<\/span>\r\n    <span style=\"color:blue;\">Set<\/span> db = CurrentDb\r\n    <span style=\"color:blue;\">Set<\/span> rst = db.OpenRecordset(\"tblBuecher\", _\r\n        dbOpenDynaset)\r\n    <span style=\"color:blue;\">With<\/span> rst\r\n        .Add<span style=\"color:blue;\">New<\/span>\r\n        !Buchtitel = \"Neues Buch\"\r\n        lngBuchID = !BuchID\r\n        .Update\r\n    End <span style=\"color:blue;\">With<\/span>\r\n    ''... etwas mit lngBuchID machen\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p>Wenn wir mit verkn&uuml;pften SQL Server-Tabellen arbeiten, funktioniert dies nicht mehr. Statt des Wertes erhalten wir Fehler <b>94<\/b>, <b>Unzul&auml;ssige Verwendung von Null <\/b>(siehe Bild 2).<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_499_002.png\" alt=\"Fehler beim Zugriff auf den Autowert eines neuen Datensatzes\" width=\"649,627\" height=\"375,1146\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 2: Fehler beim Zugriff auf den Autowert eines neuen Datensatzes<\/span><\/b><\/p>\n<p>Der Grund ist, dass der neue Prim&auml;rschl&uuml;sselwert erst auf dem SQL Server erzeugt wird, wenn wir den Datensatz mit der <b>Update<\/b>-Methode speichern. Wir k&ouml;nnen nun versuchen, den Wert nach dem Aufruf von <b>Update <\/b>einzulesen:<\/p>\n<pre>...\r\n<span style=\"color:blue;\">Set<\/span> rst = db.OpenRecordset(\"tblBuecher\", dbOpenDynaset, _\r\n    dbSeeChanges)\r\n<span style=\"color:blue;\">With<\/span> rst\r\n    .Add<span style=\"color:blue;\">New<\/span>\r\n    !Buchtitel = \"Neues Buch\"\r\n    .Update\r\n    lngBuchID = !BuchID\r\nEnd <span style=\"color:blue;\">With<\/span>\r\n''... etwas mit lngBuchID machen\r\n<span style=\"color:blue;\">Debug.Print<\/span> \"Neuer Autowert: \" & lngBuchID\r\n...<\/pre>\n<p>Dies liefert allerdings immer den Wert des Prim&auml;rschl&uuml;sselfeldes des ersten Datensatzes im Recordset.<\/p>\n<p>Das liegt daran, dass der zu bearbeitende Datensatz nach dem Aufrufen von Update verlassen und der Datensatzzeiger wieder auf den ersten Datensatz des Recordsets gesetzt wird.<\/p>\n<p>Man k&ouml;nnte nun versuchen, zum Beispiel mit <b>DMax(&#8220;BuchID&#8221;, &#8220;tblBuecher&#8221;) <\/b>den gr&ouml;&szlig;ten Wert f&uuml;r das Autowert-Feld zu ermitteln.<\/p>\n<p>Das ist allerdings nicht zuverl&auml;ssig, denn es kann sein, dass in der Zwischenzeit ein anderer Benutzer ebenfalls einen neuen Datensatz angelegt hat und wir dann den Prim&auml;rschl&uuml;sselwert dieses Datensatzes erhalten und eine falsche Zuordnung durchf&uuml;hren.<\/p>\n<p>Zum Gl&uuml;ck gibt es eine andere in der Praxis funktionierende Methode. Dabei stellen wir die Bookmark-Eigenschaft des Recordsets mit <b>rst.LastModified <\/b>auf den zuletzt bearbeiteten Datensatz ein.<\/p>\n<p>Warum ist das zuverl&auml;ssig? Weil <b>rst.LastModified <\/b>sich immer auf die Datens&auml;tze in der aktuellen Session beschr&auml;nkt. Danach k&ouml;nnen wir den Prim&auml;rschl&uuml;sselwert des neuen Datensatzes wie gewohnt auslesen und weiterverarbeiten:<\/p>\n<pre>...\r\n<span style=\"color:blue;\">Set<\/span> rst = db.OpenRecordset(\"tblBuecher\", dbOpenDynaset, _\r\n    dbSeeChanges)\r\n<span style=\"color:blue;\">With<\/span> rst\r\n    .Add<span style=\"color:blue;\">New<\/span>\r\n    !Buchtitel = \"Neues Buch\"\r\n    .Update\r\n    .Bookmark = rst.LastModified\r\n    lngBuchID = !BuchID\r\nEnd <span style=\"color:blue;\">With<\/span>\r\n''... etwas mit lngBuchID machen\r\n<span style=\"color:blue;\">Debug.Print<\/span> \"Neuer Autowert: \" & lngBuchID\r\n...<\/pre>\n<h2>Autowert-ID steht in Formularen nicht sofort zur Verf&uuml;gung<\/h2>\n<p>Wir machen auch noch einen kleinen Ausflug in die Welt der Formulare.<\/p>\n<p>Im Formular aus Bild 3 haben wir dem Formular die Tabelle <b>tblBuecher <\/b>als Datensatzquelle zugewiesen und einige Felder dieser Tabelle zum Detailbereich hinzugef&uuml;gt.<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_499_004.png\" alt=\"Formular direkt nach dem Hinzuf&uuml;gen eines neuen Datensatzes\" width=\"424,6267\" height=\"209,0326\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 3: Formular direkt nach dem Hinzuf&uuml;gen eines neuen Datensatzes<\/span><\/b><\/p>\n<p>Wenn wir nun einen neuen Datensatz hinzuf&uuml;gen, liefert das Formular zun&auml;chst das gewohnte Bild &#8211; das Autowert-Feld zeigt f&uuml;r das Prim&auml;rschl&uuml;sselfeld den Wert <b>(Neu) <\/b>an.<\/p>\n<p>Beginnen wir die Bearbeitung, w&uuml;rden wir wie bei Verwendung reiner Access-Tabellen direkt die Anzeige des neu vergebenen Autowertes im Prim&auml;rschl&uuml;sselfeld erwarten. Das ist jedoch nicht der Fall (siehe Bild 4).<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_499_003.png\" alt=\"Formular direkt nach dem Start der Bearbeitung\" width=\"424,6267\" height=\"209,0326\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 4: Formular direkt nach dem Start der Bearbeitung<\/span><\/b><\/p>\n<p>Der Grund ist der gleiche wie beim Anlegen eines neuen Datensatzes mit der <b>AddNew<\/b>-Methode des <b>Recordset<\/b>-Objekts: Der neue Prim&auml;rschl&uuml;sselwert wird erst generiert, wenn der Datensatz gespeichert wird und SQL Server den automatisch generierten Wert zur&uuml;ckliefert.<\/p>\n<p>Hier gibt es keine kleine, zuverl&auml;ssig funktionierende Technik. Wir m&uuml;ssten den Datensatz nach Beginn der Bearbeitung speichern, zum Beispiel im Ereignis Bei Ge&auml;ndert des Formulars.<\/p>\n<p>Dieses wird nach dem ersten &Auml;ndern eines Feldes ausgel&ouml;st, also auch nach dem Eintippen des ersten Zeichens in ein Textfeld, und ist gleichbedeutend mit dem Versetzen des Formulars in den Zustand &#8220;In Bearbeitung&#8221;, was an der Anzeige des Stift-Symbols im Datensatzmarkierer zu erkennen ist.<\/p>\n<p>Wir k&ouml;nnten den Datensatz dann speichern, indem wir zum Beispiel <b>Me.Dirty <\/b>auf <b>False <\/b>einstellen. Dies f&uuml;hrt aber zum Speichern des Datensatzes mit all seinen Folgen &#8211; zum Beispiel werden alle Restriktionen gepr&uuml;ft, etwa ob ein Feld einen Wert enthalten muss. Ist das nicht der Fall, kann der Datensatz nicht gespeichert werden und wir erhalten einen Fehler.<\/p>\n<h2>Fehlerbehandlung mit ODBC und SQL Server<\/h2>\n<p>Ein wichtiger Punkt ist die Fehlerbehandlung. Wenn wir in VBA arbeiten, erhalten wir f&uuml;r jeden Laufzeitfehler genau eine Fehlernummer und eine Fehlermeldung.<\/p>\n<p>Wenn wir zum Beispiel versuchen, einen Datensatz mit <b>INSERT INTO <\/b>zur Tabelle <b>tblBuecher <\/b>hinzuzuf&uuml;gen und dabei einen Prim&auml;rschl&uuml;sselwert angeben, der bereits vorhanden ist, erhalten wir die wenig aussagekr&auml;ftige Fehlermeldung aus Bild 5.<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_499_005.png\" alt=\"Fehlermeldung beim Ausf&uuml;hren einer SQL-Aktionsabfrage &uuml;ber ODBC\" width=\"699,627\" height=\"360,5135\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 5: Fehlermeldung beim Ausf&uuml;hren einer SQL-Aktionsabfrage &uuml;ber ODBC<\/span><\/b><\/p>\n<p>F&uuml;r Fehler beim Zugriff auf ODBC-verkn&uuml;pfte Tabellen gibt es jedoch eine Besonderheit. Wir k&ouml;nnen auch mehr als eine Fehlermeldung zur&uuml;ckerhalten. Das Err-Objekt liefert dabei immer nur die allgemeine Fehlermeldung, wie in diesem Beispiel Fehler <b>3146 <\/b>mit der Meldung <b>ODBC-Aufruf fehlgeschlagen<\/b>.<\/p>\n<p>Es gibt jedoch in der Fehlerbehandlung unter VBA nicht nur das <b>Err<\/b>-Objekt, das zwar die meisten Informationen zum aktuellsten Fehler liefert.<\/p>\n<p>Wir finden, wenn wir den Objektkatalog durchst&ouml;bern, in den Bibliotheken <b>DAO <\/b>und <b>ADODB <\/b>jeweils eine <b>Errors<\/b>-Auflistung und auch noch eine <b>Error<\/b>-Klasse (siehe Bild 6).<\/p>\n<p class=\"image\"><img decoding=\"async\" src=\"..\/fileadmin\/_temp_\/2026_01\/pic_499_006.png\" alt=\"Weitere Fehler-Objekte im Objektkatalog\" width=\"499,6267\" height=\"438,5213\" \/><\/p>\n<p><b><span style=\"color:darkgrey;\">Bild 6: Weitere Fehler-Objekte im Objektkatalog<\/span><\/b><\/p>\n<p>Diese k&ouml;nnen wir wie in Listing 1 nutzen, um alle Fehler in Zusammenhang mit einer DAO- oder ADODB-Anweisung auszulesen.<\/p>\n<pre><span style=\"color:blue;\">Public Sub <\/span>Test_Fehler()\r\n    <span style=\"color:blue;\">Dim <\/span>db<span style=\"color:blue;\"> As <\/span>DAO.Database\r\n    <span style=\"color:blue;\">Dim <\/span>objError<span style=\"color:blue;\"> As <\/span>Error\r\n    <span style=\"color:blue;\">Set<\/span> db = CurrentDb\r\n    <span style=\"color:blue;\">On Error GoTo<\/span> Fehler\r\n    db.Execute \"INSERT INTO tblBuecher(BuchID, Buchtitel) VALUES(1, ''Buch'')\", dbFailOnError\r\n    <span style=\"color:blue;\">Exit Sub<\/span>\r\nFehler:\r\n    For Each objError In Errors\r\n        <span style=\"color:blue;\">Debug.Print<\/span> objError.Number, objError.Description\r\n    <span style=\"color:blue;\">Next<\/span> objError\r\n<span style=\"color:blue;\">End Sub<\/span><\/pre>\n<p><b><span style=\"color:darkgrey;\">Listing 1: Auslesen von ODBC-Fehlern<\/span><\/b><\/p>\n<p>Wenn wir nun beispielsweise versuchen, einen bereits vorhandenen Wert in das Prim&auml;rschl&uuml;sselfeld <b>BuchID <\/b>zu schreiben, erhalten wir die folgenden Fehlermeldungen:<\/p>\n<ul>\n<li>Fehler <b>2627<\/b>: <b>[Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Verletzung der PRIMARY KEY-Einschr&auml;nkung &#8220;tblBuecher$PrimaryKey&#8221;. Ein doppelter Schl&uuml;ssel kann in das dbo.tblBuecher-Objekt nicht eingef&uuml;gt werden. Der doppelte Schl&uuml;sselwert ist (1).<\/b><\/li>\n<li>Fehler <b>3621<\/b>: <b>[Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Die Anweisung wurde beendet.<\/b><\/li>\n<li>Fehler <b>3146<\/b>: <b>ODBC-Aufruf fehlgeschlagen.<\/b><\/li>\n<\/ul>\n<p>Damit erhalten wir neben der Fehlermeldung <b>ODBC-Aufruf fehlgeschlagen <\/b>auch noch die Fehlermeldungen, wie sie bei Eingabe der gleichen <b>INSERT INTO<\/b>-Abfrage auch direkt im SQL Server Management Studio erscheinen w&uuml;rden.<\/p>\n<h2>Zusammenfassung und Ausblick<\/h2>\n<p>Dieser Artikel liefert einen &Uuml;berblick &uuml;ber die g&auml;ngigen Probleme, die in bestehenden VBA-Projekten nach der Migration der Tabellen einer Datenbank zum SQL Server auftreten. Au&szlig;erdem bietet er L&ouml;sungen f&uuml;r all diese Probleme an.<\/p>\n<h2>Downloads zu diesem Beitrag<\/h2>\n<p>Enthaltene Beispieldateien:<\/p>\n<p>SQLServerAnpassenDesVBACodes.accdb<\/p>\n<p><a href=\"..\/fileadmin\/beispiele\/17B27A3E-E923-4264-9DCC-73F1CB6ADCD1\/vbe_499.zip\">Download<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Wenn Du Deine Datenbank zum SQL Server migriert hast, also alle Tabellen in einer SQL Server-Datenbank liegen und mit dem Frontend verkn&uuml;pft sind, kannst Du ohne Probleme &uuml;ber die Tabellenverkn&uuml;pfungen auf die Tabellen im SQL Server zugreifen. Wenn Du jedoch per VBA, speziell mit den Methoden OpenRecordset und Execute, auf diese Tabellen zugreifen m&ouml;chtest, kann es zu Fehlermeldungen kommen. In diesem Artikel erl&auml;utern wir, welche Fehler dies sind, warum sie auftreten und wie Du Deine Datenzugriffe auf die Tabellen der SQL Server-Datenbank so anpasst, dass sie fehlerfrei laufen. Au&szlig;erdem schauen wir uns eine Besonderheit an, die beim Anlegen neuer Datens&auml;tze mit der AddNew-Methode des Recordset-Objekts auftritt &#8211; und schlie&szlig;lich gibt es noch eine Besonderheit bei Formularen, die wir ebenfalls ber&uuml;cksichtigen.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_uf_show_specific_survey":0,"_uf_disable_surveys":false,"footnotes":""},"categories":[66012026,662026,44000006],"tags":[],"yst_prominent_words":[],"class_list":["post-55000499","post","type-post","status-publish","format-standard","hentry","category-66012026","category-662026","category-SQL_Server_und_Co"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/posts\/55000499","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/comments?post=55000499"}],"version-history":[{"count":0,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/posts\/55000499\/revisions"}],"wp:attachment":[{"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/media?parent=55000499"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/categories?post=55000499"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/tags?post=55000499"},{"taxonomy":"yst_prominent_words","embeddable":true,"href":"https:\/\/vbentwickler.de\/data\/wp\/v2\/yst_prominent_words?post=55000499"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}