Zur Hauptseite ... Zum Onlinearchiv ... Zum Abonnement ... Zum Newsletter ... Zu den Tools ... Zum Impressum ... Zum Login ...

Gedrucktes Heft

Diesen Beitrag finden Sie in Ausgabe 5/2015.

Unser Angebot für Sie!

Lesen Sie diesen Beitrag und 500 andere sofort im Onlinearchiv, und erhalten Sie alle zwei Monate brandheißes Access-Know-how auf 72 gedruckten Seiten! Plus attraktive Präsente, zum Beispiel das bald erscheinende Buch 'Access 2010 - Das Grundlagenbuch für Entwickler'!

Diesen Beitrag twittern

Kundendatensätze zusammenführen

Wer eine Kundendatenbank pflegt, wird früher oder später Dubletten in seiner Datenbank vorfinden. Sei es, weil Kunden sich mit neuer E-Mail und neuer Adresse erneut im Onlineshop anmelden und von dort importiert werden oder weil man bei der Suche nach einem vorhandenen Konto für einen Kunden wegen eines Tippfehlers keinen Treffer landet – langfristig lassen sich doppelte Kundendatensätze nicht verhindern. Aber das ist kein Problem: In Datenbanken lässt sich zum Glück alles nachträglich ändern. Wie dies bei Kundendaten und den damit verknüpften Daten wie etwa Bestellungen funktioniert, erklärt dieser Beitrag.

Das Problem nach der Erstellen eines doppelten Kundendatensatzes lautet: Wie mache ich aus den beiden Kundendatensätzen einen, und vor allem: Wie sorge ich dafür, dass die Daten, die mit dem zu löschenden Kundendatensatz verknüpft sind, mit dem verbleibenden Kundendatensatz zusammengeführt werden?

Es wäre ja leicht, wenn man einfach den »alten« Kundendatensatz löschen könnte und dann Ruhe hätte. Aber in der Regel erstellt man ja einen Kundendatensatz erst, wenn für diesen auch eine Bestellung vorliegt. Wenn wir ein Datenmodell wie in Bild 1 zugrunde legen, bei dem ein Bestelldatensatz auf der einen Seite mit dem bestellenden Kunden, auf der anderen Seite mit der Tabelle Bestelldetails verknüpft wird, ist klar: Wir können nicht einfach den alten Datensatz löschen, sondern müssen auch alle Daten der Tabelle tblBestelldetails auf den Kundendatensatz übertragen, der beibehalten werden soll. Das bedeutet eigentlich nur, dass der Wert des Fremdschlüsselfeldes KundeID mit dem entsprechenden Primärschlüsselwert des neuen Kundendatensatzes gefüllt werden muss. Das allein lässt sich mit einer einfachen UPDATE-Abfrage erledigen. Danach noch den nicht mehr benötigten Kundendatensatz löschen oder als inaktiv markieren, schon ist man fertig.

Tabellen der Beispieldatenbank

Bild 1: Tabellen der Beispieldatenbank

Um solche Änderungen nachher wieder rückgängig machen oder zumindest nachvollziehen zu können, sollten Sie die geänderten oder gelöschten Daten archivieren. Wenn Sie Access ab Version 2010 verwenden, können Sie dies etwa gemäß dem Beitrag Geänderte Daten archivieren (www.access-im-unternehmen.de/925) erledigen. Beim SQL Server würden Sie entsprechende Trigger nutzen oder in die gespeicherten Prozeduren, welche die Daten ändern, passende Anweisungen zum Sichern der Datensätze einbringen.

Benutzeroberfläche

Nun hätten wir die Theorie bereits erledigt. Wie aber sieht die Praxis aus? Otto Normalverbraucher kann leider meist nicht mal eben eine UPDATE-Anweisung ins Direktfenster schmeißen, sodass wir für die gewünschte Funktion eine entsprechende Benutzeroberfläche bereitstellen müssen. Nehmen wir doch ein herkömmliches Formular mit Unterformular als Basis, wie es zur Darstellung von Daten aus zwei Tabellen einer 1:n-Beziehung aussieht. Im Beispiel aus Bild 2 finden Sie die Tabelle tblKunden im Hauptformular und die Bestellungen des aktuell angezeigten Kunden aus tblBestellungen im Unterformular vor.

Formular zur Anzeige der Bestellungen eines Kunden

Bild 2: Formular zur Anzeige der Bestellungen eines Kunden

Im Gegensatz zum Standard-Bestellformular, das eine Bestellung samt Bestellpositionen anzeigt, fallen hier die Bestelldetails unter den Tisch. Wir können diese mit einem zweiten Unterformular nachreichen, das die Bestellpositionen zu der jeweils im ersten Unterformular ausgewählten Bestellung anzeigt. Dies ist allerdings unnötig, denn wir wollen ja nur die Bestellungen des nicht mehr benötigten Duplikats eines Kunden auf einen anderen Kunden übertragen. Die Bestellpositionen sind ja ohnehin mit der Tabelle tblBestellungen verknüpft und werden quasi »mit übertragen«.

Gibt es nun eine sinnvolle Variante, um diesen Datensatz mit einer Dublette zusammenzuführen? Nun, eigentlich nicht – denn diese müssten wir ja auf jeden Fall erst einmal ausfindig machen. Gelegentlich wird sich vielleicht ein Kunde melden, der vielleicht Kundennewsletter an die beiden unterschiedlichen E-Mail-Adressen seiner beiden Accounts erhält und somit das Vorhandensein eines Duplikats aufdecken. In der Regel sollten Sie sich allerdings, je nach der Anzahl der zu verwaltenden Kunden, von Zeit zu Zeit selbst auf die Suche nach Duplikaten machen.

Aufmerksame und langjährige Access im Unternehmen-Leser werden jetzt aufhorchen: War da nicht mal was? Ja, genau: Im Beitrag Duplikatsuche in Access (www.access-im-unternehmen.de/744) haben wir ein Formular vorgestellt, mit dem Sie flexibel Duplikate in Ihren Datenbeständen finden können. Die Lösung aus diesem Beitrag werden wir für unsere Zwecke nutzen und entsprechend aufbohren.

Integration der Lösung in eigene Datenbanken

Wenn Sie die Lösung in einer eigenen Datenbank nutzen möchten, müssen Sie zunächst die folgenden Objekte aus der Beispieldatenbank in Ihre Datenbank importieren:

  • frmDuplikatmanager
  • sfmDuplikatfelder
  • sfmFlex
  • frmDuplikatdetails
  • tblDuplikatfelder
  • clsDatasheetForm
  • clsDatasheetControl
  • clsColumnWidths
  • mdlTools

Damit erhalten Sie schon einmal das Formular aus Bild 3 mit allen benötigten Unterformularen, Modulen und Klassen. Außerdem fügen Sie so eine Tabelle hinzu, welche die Konstellation für das Auffinden der Duplikate speichert.

Das Formular zum Ermitteln und Abgleichen der Duplikate einer Tabelle

Bild 3: Das Formular zum Ermitteln und Abgleichen der Duplikate einer Tabelle

Außerdem müssen Sie das Formular frmDuplikatdetails noch an die Gegebenheiten der Zieldatenbank anpassen. Das Formular sieht in der Beispieldatenbank im Entwurf wie in Bild 4 aus. Dieses Formular soll einen der zusammenzufassenden Datensätze mit den notwendigsten Informationen für den Abgleich darstellen. Das Hauptaugenmerk liegt dabei darauf, dass die verknüpften Daten angezeigt werden, die beim Zusammenführen zweier (oder auch mehrerer) Duplikate berücksichtigt werden sollen. Auf diese Weise kann der Benutzer sich nochmals versichern, dass dort auch die richtigen Daten zusammengeführt werden.

Formular zur Anzeige der Details zu einem der Duplikate

Bild 4: Formular zur Anzeige der Details zu einem der Duplikate

Das Formular soll mehrfach geöffnet werden. Mit einem Klick auf die Schaltfläche Als Ziel übernehmen übernimmt der Benutzer dann den entsprechenden Datensatz als Zieldatensatz für das Zusammenführen der Daten – die bis dahin geöffneten Detailformulare werden dann geschlossen.

Ablauf der Zusammenführung zweier Datensätze

Das Formular frmDuplikatmanager bietet in einem Kombinationsfeld alle Tabellen der aktuellen Datenbank zur Auswahl an. Wenn der Benutzer eine Tabelle ausgewählt hat, erscheinen alle Felder im linken, oberen Unterformular. Dort finden Sie neben der Spalte mit den Feldnamen noch zwei weitere Spalten – eine mit den für die Duplikatsuche zu verwendenden Felder und eine mit den Feldern, die im Ergebnis angezeigt werden sollen.

Nachdem der Benutzer diese festgelegt hat (im Screen­shot sollen nur die E-Mail-Adressen abgeglichen werden und alle Felder in der Ergebnisliste erscheinen), klickt er auf die Schaltfläche Duplikate suchen. Findet die Lösung mindestens ein Duplikat, zeigt es die Anzahl der gefundenen Exemplare sowie den Wert des Vergleichsfeldes im Unterformular rechts oben an.

Hier kann der Benutzer nun wiederum auf einen Eintrag klicken und so alle Duplikate zu diesem Eintrag im unteren Unterformular einblenden. Dies hat den Vorteil, dass Sie direkt prüfen können, ob sich die Inhalte der übrigen Felder unterscheiden. Wenn Sie sich entschieden haben, welcher der Datensätze beibehalten werden soll, können Sie in diesem gegebenenfalls Korrekturen vornehmen oder Informationen aus den zu löschenden Datensätzen übernehmen.

Nun kommt auch unser Formular für die Anzeige der Duplikatdetails ins Spiel. Der Hauptgrund für die Erstellung der vorliegenden Lösung ist ja, nicht nur einen von mehreren Datensätzen (im Beispiel Kunden) zu übernehmen und die übrigen zu löschen, sondern auch noch die Daten, die mit den zu löschenden Datensätzen verknüpft sind, auf den verbleibenden Datensatz zu übertragen.

Deshalb können Sie mit dem Formular frmDuplikatdetails in diesem Beispiel die Kundendaten plus die Bestellungen der Kunden anzeigen, und zwar per Doppelklick auf einen der Einträge im unteren Unterformular. Im Gegensatz zu üblichen Formularen, von denen Sie nur jeweils eine einzige Instanz öffnen, können Sie hier für jeden der in der Liste enthaltenen Kunden ein Detailformular öffnen. In Bild 5 sehen Sie beispielsweise zwei Formulare mit verschiedenen Datensätzen zum gleichen Kunden. Klicken Sie hier auf die Schaltfläche Als Ziel übernehmen, werden alle Formulare geschlossen. Außerdem markiert das Formular frmDuplikatmanager den zu übernehmenden Datensatz im unteren Unterformular.

Vergleich zweier Datensätze für den gleichen Kunden

Bild 5: Vergleich zweier Datensätze für den gleichen Kunden

Nun folgt der interessante Teil: Das untere Listenfeld des Formulars frmDuplikatmanager zeigt alle mit der zusammenzuführenden Tabelle per 1:n-Beziehung verknüpften Tabellen an, in diesem Fall tblBestellungen und tblNotizen (s. Bild 6).

Auswahl der zu übernehmenden Daten aus den verknüpften Tabellen

Bild 6: Auswahl der zu übernehmenden Daten aus den verknüpften Tabellen

Sie können nun einen oder mehrere Einträge auswählen, damit die enthaltenen Datensätze auf den zu übernehmenden Kundendatensatz übertragen werden. Um die Duplikate letztlich zusammenzuführen, klicken Sie auf die Schaltfläche rechts neben dem Listenfeld.

Danach sollte der übernommene Datensatz rasch vom unteren Unterformular verschwinden. Über einen Doppelklick auf den verbleibenden Datensatz können Sie sich im Detailformular vergewissern, dass die verknüpften Daten wie gewünscht übernommen wurden.

Sicher ist sicher

Zur Sicherheit sollten Sie solche Aktionen nicht durchführen, ohne zuvor eine Kopie der Datenbank angelegt zu haben. Noch besser wäre es, wenn Sie Access 2010 oder höher verwenden und die geänderten oder gelöschten Datensätze in entsprechenden Archivtabellen sichern. Eine geeignete Lösung finden Sie in den Beiträgen Geänderte Daten archivieren auf (www.access-im-unternehmen.de/925) und Änderungshistorie implantieren (www.access-im-unternehmen.de/995).

Aufbau der benötigten Formulare

Die folgenden Abschnitte erläutern die Zusammenhänge zwischen dem Haupt- und den Unterformularen und wie diese gefüllt werden und auf Benutzeraktionen reagieren.

Laden des Formulars

Beim Laden des Formulars frmDuplikatmanager müssen einige Aktionen ausgeführt werden, um das Formular vorzubereiten. Dies geschieht in der Ereignisprozedur, die durch das Ereignis Beim Laden ausgelöst wird (s. Listing 1).

Private Sub Form_Load()
     Dim db As DAO.Database
     Set db = CurrentDb
     db.Execute "DELETE FROM tblDuplikatfelder", dbFailOnError
     Me!sfmDuplikatfelder.Form.Requery
     Set frm_sfmDuplikate = Me!sfmDuplikate.Form
     frm_sfmDuplikate.OnCurrent = "[Event Procedure]"
     Set objCW_Duplikatfelder = New clsColumnWidths
     Set objCW_Duplikatfelder.DataSheetForm = Me!sfmDuplikatfelder.Form
     Set objCW_Duplikate = New clsColumnWidths
     Set objCW_Duplikate.DataSheetForm = Me!sfmDuplikate.Form
     Set objCW_DuplikateDetail = New clsColumnWidths
     Set objCW_DuplikateDetail.DataSheetForm = Me!sfmDuplikateDetails.Form
     DatasheetFormInstanzieren
     Set colForms = New Collection
End Sub

Listing 1: Vorbereitung des Formulars und seiner Elemente

Diese leert zunächst die Tabelle tblDuplikatfelder, welche später mit je einem Datensatz für jedes Feld der zu untersuchenden Tabelle gefüllt wird (s. Bild 7). Danach aktualisiert sie den Inhalt des Unterformulars sfmDuplikatfelder, damit dieses den aktualisierten Inhalt der nun leeren Tabelle tblDuplikatfelder anzeigt. Anschließend füllt sie die folgende Variable mit einem Verweis auf das Unterformular sfmDuplikate:

Tabelle zum Speichern der zu verwendenden Felder

Bild 7: Tabelle zum Speichern der zu verwendenden Felder

Dim WithEvents frm_sfmDuplikate As Form

Dies geschieht mit dem Schlüsselwort WithEvents, weil wir im Klassenmodul des Hauptformulars Ereignisse für dieses Unterformular implementieren wollen – zum Beispiel für die Auswahl eines der enthaltenen Datensätze, um dann alle passenden Duplikate im Unterformular sfmDuplikatdetails anzuzeigen. Dabei handelt es sich um das Ereignis Beim Anzeigen, wozu wir noch mitteilen müssen, dass das aktuelle Klassenmodul auf solche Ereignisse lauschen soll (OnCurrent = [Event Procedure]).

Die drei Unterformulare sfmDuplikatfelder, sfmDuplikate und sfmDuplikateDetails sollen mit optimierter Spaltenbreite angezeigt werden. Dazu verwenden wir die Klasse clsColumnWidths, die wir ausführlich im Beitrag Spaltenbreiten optimieren mit Klasse vorstellen (siehe www.access-im-unternehmen.de/998).

Wir wollen jedes der drei Unterformulare mit der Funktion zur optimalen Anpassung der Spaltenbreiten ausstatten, also legen wir drei Objektvariablen für die entsprechenden Objekte fest:

Dim objCW_Duplikatfelder As clsColumnWidths
Dim objCW_Duplikate As clsColumnWidths
Dim objCW_DuplikateDetail As clsColumnWidths

Diese instanziert die Prozedur Form_Load dann und stellt mit der Eigenschaft DataSheetForm jeweils das betroffene Unterformular ein.

Danach ruft sie noch die Prozedur DatasheetFormInstanzieren auf, die dem unteren Unterformular einige Funktionen hinzufügt, und instanziert ein Collection-Objekt, das wir im Kopf des Klassenmoduls deklarieren:

Dim colForms As Collection

Den Zweck dieser Collection erläutern wir weiter unten.

Instanzieren der Datenblattfunktionen von sfmDuplikateDetails

Das untere Unterformular namens sfmDuplikateDetails soll bei einem Doppelklick auf einen der Datensätze ein Ereignis auslösen, um einen Detaildatensatz in einem eigenen Formular anzuzeigen. Damit der Benutzer dabei nur auf eine beliebige Stelle im Datensatz zu klicken braucht, müssen wir theoretisch für jedes Steuerelement eine Beim Klicken-Ereignisprozedur anlegen.

Dies können wir uns jedoch sparen, wenn wir die beiden im Beitrag Datenblattereignisse mit Klasse vorgestellten Klassen nutzen. Damit brauchen wir als Erstes nur ein Element mit folgendem Typ zu deklarieren:

Dim WithEvents objDS As clsDataSheetForm

Danach benötigen wir noch die Anweisungen aus der Routine aus Listing 2. Diese instanziert das Objekt auf Basis von clsDatasheetForm und weist diesem das Unterformular sfmDuplikateDetails als Formular zu. Außerdem stellt sie die Eigenschaft PrimaryKey auf den Primärschlüsselnamen der zu untersuchenden Tabelle ein, den wir mit der Funktion GetSinglePrimaryKey einlesen (siehe Primärschlüsselfelder ermitteln, www.access-im-unternehmen.de/1004. Mit ZeileBeiKlickMarkieren = False legen wir fest, dass beim Anklicken nicht die komplette Zeile des Datensatzes markiert werden soll.

Private Sub DatasheetFormInstanzieren()
     Set objDS = New clsDatasheetForm
     With objDS
         Set .DatasheetForm = Me!sfmDuplikateDetails.Form
         If Len(Me!cboTabellen) > 0 Then
             .PrimaryKey = GetSinglePrimaryKey(Nz(Me!cboTabellen))
             .ZeileBeiKlickMarkieren = False
         End If
     End With
End Sub

Listing 2: Funktionen für das Unterformular sfmDuplikateDetails einrichten

Auswahl der Tabelle

Das Hauptformular frmDuplikatmanager verwendet das Kombinationsfeld cboTabellen, um dem Benutzer die Auswahl der zu untersuchenden Tabelle zu ermöglichen. Diese füllen wir mit der folgenden Abfrage:

SELECT MSysObjects.Name FROM MSysObjects WHERE Name Not Like 'MSys*' And Name Not Like 'USys*' And Type=1

Nach dem Auswählen eines der Einträge löst das Ereignis Nach Aktualisierung des Kombinationsfeldes die Prozedur aus Listing 3 aus. Die Prozedur leert die Tabelle tblDuplikatfelder, welche die Konfiguration für die Duplikatsuche speichert, und füllt diese dann neu, indem sie in einer For Each-Schleife alle Felder der zu untersuchenden Tabelle durchläuft und für jedes einen neuen Datensatz zur Tabelle tblDuplikatfelder hinzufügt. Dann aktualisiert sie das Unterformular sfmDuplikatfelder und optimiert mit der Methode OptimizeColumnWidths die Spaltenbreiten.

Private Sub cboTabellen_AfterUpdate()
     Dim db As DAO.Database
     Dim fld As DAO.Field
     Dim tdf As DAO.TableDef
     Dim strVerknuepfteTabellen() As String
     Dim strFremdschluesselfelder() As String
     Dim strRowSource As String
     Dim i As Integer, intAnzahlVerknuepfungen As Integer
     Set db = CurrentDb
     Set tdf = db.TableDefs(Me!cboTabellen)
     db.Execute "DELETE FROM tblDuplikatfelder", dbFailOnError
     For Each fld In tdf.Fields
         db.Execute "INSERT INTO tblDuplikatfelder(Feldname, FeldAnzeigen) VALUES('" & fld.Name & "', True)", _
             dbFailOnError
     Next fld
     Me!sfmDuplikatfelder.Form.Requery
     objCW_Duplikatfelder.OptimizeColumnWidths
     intAnzahlVerknuepfungen = VerknuepfteTabellen(Me!cboTabellen, strVerknuepfteTabellen(), strFremdschluesselfelder())
     Me!lstVerknuepfteTabellen.RowSourceType = "Value List"
     For i = 0 To intAnzahlVerknuepfungen - 1
         strRowSource = strRowSource & strVerknuepfteTabellen(i) & ";" & strFremdschluesselfelder(i) & ";"
     Next i
     Me!lstVerknuepfteTabellen.RowSource = strRowSource
End Sub

Listing 3: Aktionen nach der Auswahl einer Tabelle

Dann ruft sie eine Funktion namens VerknuepfteTabellen auf, die alle per 1:n-Beziehung verknüpften Tabellen sowie die Namen der Fremdschlüsselfelder liefert (siehe Verknüpfte Tabellen ermitteln, www.access-im-unternehmen.de/1005). Sie erwartet den Namen der Tabelle und liefert zwei Arrays mit den Ergebnissen zurück – sowie als Funktionswert die Anzahl der Ergebnisse. Dies durchläuft die folgende For...Next-Schleife und stellt dabei die Datensatzherkunft für das Listenfeld lstVerknuepfteTabellen im Fuß des Formulars zusammen (zum Beispiel tblBestellungen;tblNotizen;).

Duplikatfelder auswählen

Der nächste Schritt ist die Auswahl der als Duplikatfelder zu nutzenden Felder im Unterformular sfmDuplikatfelder und das anschließende Betätigen der Schaltfläche cmdDuplikateSuchen. Dies löst die Prozedur aus Listing 4 aus. Die Prozedur liest ein Recordset mit allen Datensätzen der soeben frisch gefüllten Tabelle tblDuplikatfelder ein, die als Vergleichsfelder ausgewählt wurden (Duplikatfeld = True). Sie durchläuft diese Tabelle und stellt eine kommaseparierte Liste der Feldnamen zusammen, also etwa EMail, Nachname, PLZ,, und entfernt anschließend das abschließende Komma. Das Feld aus dem ersten Datensatz speichert die Prozedur in der Variablen strAnzahlfeld. Damit stellt die Prozedur eine Abfrage zusammen, welche die Anzahl der gefundenen Duplikate enthält – hier für eine Duplikatsuche nur nach dem Feld EMail:

Private Sub cmdDuplikateSuchen_Click()
     Dim db As DAO.Database
     Dim qdf As DAO.QueryDef
     Dim strSQL As String
     Dim strDuplikatfelder As String
     Dim strAnzahlfeld As String
     Dim rstDuplikatfelder As DAO.Recordset
     Set db = CurrentDb
     Set rstDuplikatfelder = _
         db.OpenRecordset("SELECT * FROM tblDuplikatfelder WHERE Duplikatfeld = True", dbOpenDynaset)
     If rstDuplikatfelder.EOF Then
         MsgBox "Bitte wählen Sie mindestens ein Feld aus, das als Kriterium für die Duplikatsuche dienen soll."
         Exit Sub
     End If
     strAnzahlfeld = rstDuplikatfelder!Feldname
     Do While Not rstDuplikatfelder.EOF
         strDuplikatfelder = strDuplikatfelder & rstDuplikatfelder!Feldname & ","
         rstDuplikatfelder.MoveNext
     Loop
     strDuplikatfelder = Left(strDuplikatfelder, Len(strDuplikatfelder) - 1)
     strSQL = "SELECT Count(" & strAnzahlfeld & ") AS Duplikatanzahl, "
     strSQL = strSQL & strDuplikatfelder & " FROM " & Me!cboTabellen
     strSQL = strSQL & " GROUP BY " & strDuplikatfelder & " HAVING Count(" & strAnzahlfeld & ") > 1"
     If Not IsNull(Me!cboTabellen) Then
         On Error Resume Next
         db.QueryDefs.Delete "_qryDuplikate"
         On Error GoTo 0
         db.QueryDefs.Refresh
         Set qdf = db.CreateQueryDef("_qryDuplikate", strSQL)
         Me!sfmDuplikate.Form.FillFlexForm "_qryDuplikate"
         Me!sfmDuplikate.Form.Requery
         objCW_Duplikate.OptimizeColumnWidths
         DatasheetFormInstanzieren
     End If
End Sub

Listing 4: Suchen der Duplikate

SELECT Count(EMail) AS Duplikatanzahl, EMail 
FROM tblKunden GROUP BY EMail HAVING Count(EMail) > 1

Die Abfrage gruppiert die Datensätze der Tabelle tblKunden nach dem Feld EMail und zählt die Anzahl der jeweils gefundenen Datensätze. Diejenigen, deren Anzahl größer als 1 ist, sind logischerweise Duplikate und werden von der Abfrage zurückgeliefert.

Mit diesem SQL-Ausdruck im Gepäck löscht die Prozedur eine eventuell noch vorhandene Abfrage namens _qryDuplikate und erstellt diese neu.

Dann ruft sie die Methode FillFlexForm des Unterformulars sfmDuplikate auf und übergibt dieser den Namen der soeben erstellten Abfrage _qryDuplikate. Hier hängt noch ein kleiner Rattenschwanz an Aktionen dran: Das Aktualisieren des Unterformulars zieht nämlich noch das Auslösen des Ereignisses Beim Anzeigen des Unterformulars nach sich, das wir im Hauptformular implementiert haben. Mehr dazu weiter unten.

Nachdem dies geschehen ist, aktualisiert die Prozedur die Datenherkunft des Formulars und ruft die Methode OptimizeColumnWidths auf, um die Spaltenbreiten für die nun angezeigten Daten zu optimieren.

Mehr über die Methode FillFlexForm lesen Sie im Beitrag Flexibles Unterformular (http://www.access-im-unternehmen.de/743).

Es handelt sich bei dem Unterformular sfmDuplikate um ein Formular, das eine Anzahl vorgefertigter Textfelder, Kombinationsfelder und Kontrollkästchen enthält, die dann je nach der Gestalt der anzuzeigenden Datenherkunft eingeblendet werden.

Nach dem Füllen des Formulars ruft die Prozedur noch die Methode DatasheetFormInstanzieren auf, welche das neu gefüllte Unterformular sfmDuplikateDetails mit den Funktionen zum Weiterleiten von Mausklicks an das Hauptformular ausstattet (siehe oben).

Anzeige der Duplikate im Unterformular

Das Unterformular sfmDuplikate zeigt die Basisdaten der gefundenen Duplikate samt der Anzahl der Exemplare an. Wenn der Benutzer auf einen der Einträge klickt, soll das untere Unterformular sfmDuplikateDetails die jeweiligen Datensätze der Tabelle anzeigen (s. Bild 8).

Aktualisieren von einem Unterformular zum nächsten

Bild 8: Aktualisieren von einem Unterformular zum nächsten

Die dazu notwendige Ereignisprozedur wollen wir auch im Klassenmodul des Hauptformulars abbilden und nicht im Unterformular. Dazu haben wir ja die Objektvariable frm_sfmDuplikate deklariert und im Ereignis Form_Load eingestellt, dass wir das Ereignis OnCurrent im Klassenmodul des Hauptformulars implementieren wollen.

Die passende Ereignisprozedur legen Sie an, indem Sie im Klassenmodul des Hauptformulars im linken Kombinationsfeld den Eintrag frm_sfmDuplikate und im rechten den Eintrag OnCurrent auswählen. Die dann automatisch erstellte Prozedur ergänzen Sie wie in Listing 5.

Private Sub frm_sfmDuplikate_Current()
     Dim db As DAO.Database, qdf As DAO.QueryDef
     Dim strSQL As String, strAnzeigefelder As String
     Dim rstDuplikatfelder As DAO.Recordset, rstAnzeigefelder As DAO.Recordset
     Dim strWhere As String
     Set db = CurrentDb
     Set rstAnzeigefelder = db.OpenRecordset( _
         "SELECT Feldname FROM tblDuplikatfelder WHERE FeldAnzeigen = True", dbOpenDynaset)
     If rstAnzeigefelder.EOF Then
         MsgBox "Bitte wählen Sie mindestens ein Feld aus, das in der Detailliste der Duplikate erscheinen soll."
         Exit Sub
     End If
     Do While Not rstAnzeigefelder.EOF
         strAnzeigefelder = strAnzeigefelder & rstAnzeigefelder!Feldname & ","
         rstAnzeigefelder.MoveNext
     Loop
     strAnzeigefelder = Left(strAnzeigefelder, Len(strAnzeigefelder) - 1)
     Set rstDuplikatfelder = db.OpenRecordset("SELECT Feldname FROM tblDuplikatfelder WHERE Duplikatfeld = True", _
         dbOpenDynaset)
     Do While Not rstDuplikatfelder.EOF
         On Error Resume Next
         strWhere = strWhere & rstDuplikatfelder!Feldname & "='" _
             & Me!sfmDuplikate. Form.Recordset.Fields(rstDuplikatfelder!Feldname) & "' AND "
         If Not Err.Number = 0 Then
             On Error GoTo 0
             Exit Sub
         End If
         rstDuplikatfelder.MoveNext
     Loop
     If Len(strWhere) > 0 Then
         strWhere = Left(strWhere, Len(strWhere) - 4)
     End If
     On Error Resume Next
     db.QueryDefs.Delete "_qryDuplikateDetail"
     On Error GoTo 0
     db.QueryDefs.Refresh
     strSQL = "SELECT " & strAnzeigefelder & " FROM " & Me!cboTabellen & " WHERE " & strWhere
     Set qdf = db.CreateQueryDef("_qryDuplikateDetail", strSQL)
     Me!sfmDuplikateDetails.Form.FillFlexForm "_qryDuplikateDetail"
     Me!sfmDuplikateDetails.Form.Requery
     Set objCW_DuplikateDetail.DataSheetForm = Me!sfmDuplikateDetails.Form
     objCW_DuplikateDetail.OptimizeColumnWidths
     Set db = Nothing
End Sub

Listing 5: Aktualisieren des Unterformulars zur Anzeige der Duplikate

Die Prozedur erstellt ein Recordset mit den anzuzeigenden Feldern aus der Tabelle tblDuplikatfelder (FeldAnzeigen = True). Sie durchläuft dann alle Felder in einer Do While-Schleife und stellt eine kommaseparierte Liste der Felder zusammen (KundeID, Firma, Vorname, Nachname,), von der sie wieder das letzte Komma entfernt.

Ein zweites Recordset nimmt alle Felder der Tabelle tblDuplikatfelder auf, die für den Abgleich der Duplikate markiert wurden (Duplikatfeld = True). Auch dieses Recordset durchläuft die Prozedur in einer Do While-Schleife. Dabei stellt sie eine Reihe von Bedingungen zusammen, die durch den AND-Operator getrennt werden. Der Feldname ist der erste Teil der Bedingung, der Wert des Duplikatfeldes des aktuell markierten Datensatzes im Unterformular sfmDuplikate ist der Vergleichswert. Heraus kommt so etwas: EMail = 'andre@minhorst.com' AND Vorname = 'André' AND, wobei das abschließende AND wieder abgeschnitten wird.

Dann fügt die Prozedur die beiden Listen mit ein paar SQL-Schlüsselwörtern zusammen, sodass eine Abfrage wie die folgende herauskommt:

SELECT KundeID, Firma, Vorname, Nachname 
FROM tblKunden 
WHERE EMail = 'andre@minhorst.com' AND Vorname = 'André'

Damit erstellt die Prozedur ein neues Abfrageobjekt namens _qryDuplikateDetail und füllt das Unterformular sfmDuplikateDetails über die Methode FillFlexForm mit den Datensätzen der Abfrage. Nach dem Aktualisieren der Datenherkunft des Unterformulars weist die Prozedur dem Objekt objCW_DuplikateDetail das Unterformular zu und optimiert mit dessen Methode OptimizeColumnWidths die Spaltenbreiten.

Das Ergebnis ist die Anzeige der doppelten Datensätze im Unterformular sfmDuplikateDetails.

Details zu einem Datensatz anzeigen

Die Klasse objDS soll uns ja ermöglichen, Ereignisse, die durch einen Klick oder Doppelklick auf einen der Datensätze des Unterformulars sfmDuplikateDetails ausgelöst werden, im Hauptformular auszuwerten. Dies erledigen wir mit einer Ereignisprozedur, die durch das Ereignis DblClick des Objekts objDS ausgelöst wird.

Diese Anzeige hat die Eigenart, dass wir das gleiche Formular gleich mehrfach instanzieren, um mehrere Datensätze gleichzeitig anzeigen zu können. Aus technischen Gründen benötigen wir eine Objektvariable namens frmDuplikatdetails. Damit können wir die geöffneten Formulare später steuern:

Dim frmDuplikatdetails As Form_frmDuplikatdetails

Die Prozedur liefert einen Verweis auf das im Unterformular angeklickte Steuerelement sowie den Primärschlüsselwert des Datensatzes als Parameter. Wir speichern den Primärschlüsselwert in der Variablen lngID und öffnen dann das Detailformular. Dies geschieht nicht wie sonst mit der DoCmd.OpenForm-Methode, sondern durch Instanzieren der Formularklasse mit dem New-Schlüsselwort. Das Ergebnis referenzieren wir dann mit der Variablen frmDuplikatdetails. Dieses filtern wir dann nach dem Primärschlüsselwert. Den Filterausdruck setzen wir mithilfe der Funktion GetSinglePrimaryKey, die den Namen des Primärschlüsselfeldes liefert, und dem Wert aus lngID zusammen (KundeID = 123).

Diesen Ausdruck weisen wir der Eigenschaft Filter zu und aktivieren den Filter durch Einstellen von FilternOn auf den Wert True. Schließlich speichern wir den Wert des Primärschlüsselfeldes in der Tag-Eigenschaft des Formulars und blenden dieses mit Visible = True ein.

Da wir auf diese Weise nicht nur eines, sondern mehrere Formulare öffnen wollen, müssen wir die Referenz auf das Formular aus der Variablen frmDuplikatdetails noch sichern. Dazu nutzen wir die Collection colForms und weisen dieser den Verweis auf das Formular sowie den Wert des Primärschlüsselfeldes als Key zu:

Private Sub objDS_DblClick(ctl As Control, _
         varPKValue As Variant)
     Dim lngID As Long
     lngID = objDS.PrimaryKeyValue
     Set frmDuplikatdetails = New Form_frmDuplikatdetails
     With frmDuplikatdetails
         .Filter = GetSinglePrimaryKey(Me!cboTabellen) _
             & " = " & lngID
         .FilterOn = True
         .Tag = lngID
         .Visible = True
     End With
     colForms.Add frmDuplikatdetails, CStr(lngID)
End Sub

Duplikatdetailformulare ausblenden

Irgendwann hat der Benutzer die Details der Duplikatdatensätze zu Ende studiert und möchte diese entweder einfach schließen oder aber er klickt auf die Schaltfläche cmdAlsZielUebernehmen, um einen der im Formular frmDuplikatdetails angezeigten Datensätze gleich als zu übernehmenden Datensatz zu markieren.

Wenn er das Detailformular einfach schließt, löst er damit automatisch das Ereignis Beim Schließen des Formulars aus. Diese ruft die Methode DuplikatformularAusblenden des Formulars frmDuplikatmanager auf und übergibt den Wert der Tag-Eigenschaft des Formulars, die den Primärschlüsselwert des aktuellen Datensatzes enthält:

Private Sub Form_Close()
     Forms!frmDuplikatmanager.DuplikatformularAusblenden  Me.Tag
End Sub

Damit machen wir einen kleinen Sprung zurück zum Klassenmodul des Formulars frmDuplikatmanager, und zwar zur Methode DuplikatformularAusblenden. Diese sucht das Element aus der Collection colForms heraus, das dem auszublendenden Formular entspricht, und entfernt es aus der Collection:

Public Sub DuplikatformularAusblenden(strTag As String)
     On Error Resume Next
     colForms.Item(strTag).Tag = ""
     colForms.Remove strTag
End Sub

Auf diese Weise verbleiben nur die Einträge in der Collection, für die auch noch ein geöffnetes Formular vorliegt.

Die zweite Variante, mit welcher der Benutzer ein Detailformular schließen kann, ist ein Klick auf die Schaltfläche cmdAlsZielUebernehmen des Formulars frmDuplikatdetails. Diese liest den Inhalt der Tag-Eigenschaft des Formulars in die Variable strTag ein. Dann referenziert sie das aufrufende Formular frmDuplikatmanager und ruft die Methode ZieldatensatzSetzen mit dem Wert aus strTag auf:

Private Sub cmdAlsZielUebernehmen_Click()
     Dim frm As Form
     Dim strTag As String
     strTag = Me.Tag
     Set frm = Forms!frmDuplikatmanager
     If Not frm Is Nothing Then
         frm.ZieldatensatzSetzen strTag
     End If
End Sub

Diese Methode stellt den Datensatzzeiger im Unterformular sfmDuplikateDetails auf den entsprechenden Datensatz ein und ruft die Prozedur Duplikatformular­Ausblenden auf:

Public Sub ZieldatensatzSetzen(varZieldatensatzID As  Variant)
     Me!sfmDuplikateDetails.Form.Recordset.FindFirst  GetSinglePrimaryKey(Me!cboTabellen)  & "=" & varZieldatensatzID
     DuplikatformulareAusblenden
End Sub

Die Methode DuplikatformulareAusblenden durchläuft alle aktuell geöffneten Formulare über die Forms-Auflistung. Dabei ruft sie zuerst die Prozedur DuplikatformularAusblenden auf (man beachte Plural und Singular bei der Benennung der beiden Prozeduren), die, wie oben beschrieben, den Eintrag für das aktuelle Formular aus der Collection colForms entfernt.

Danach prüft es, ob das Formular frmDuplikatdetails heißt, und schließt es dann:

Private Sub DuplikatformulareAusblenden()
     Dim frm As Form
     Dim i As Integer
     For i = Forms.Count - 1 To 0 Step -1
         Set frm = Forms(i)
         DuplikatformularAusblenden frm.Tag
         If frm.Name = "frmDuplikatdetails" Then
             DoCmd.Close acForm, frm.Name
         End If
     Next i
End Sub

Duplikate zusammenführen

Fehlt nur noch die Prozedur, mit der Sie die mit den zu löschenden Duplikaten verknüpften Datensätze an das zu übernehmende Duplikat binden und mit der Sie die überflüssigen Datensätze löschen.

Dies erledigt die Prozedur aus Listing 6. Sie ermittelt das Primärschlüsselfeld und den passenden Wert für das beizubehaltende Duplikat und erstellt dann ein Recordset, das alle Duplikate enthält, die gelöscht werden sollen. Dieses durchläuft sie in einer Do While-Schleife und trägt dabei zunächst die Inhalte des Listenfeldes lstVerknuepf­te­Tabellen in die Variablen strForeignTable und strFK ein. So erstellt sie eine UPDATE-Abfrage, die den Fremdschlüsselwert der betroffenen Datensätze der verknüpften Tabellen mit der beizubehaltenden Tabelle verknüpft. Anschließend löscht die Prozedur das nun überflüssige Duplikat. Dies erledigt die Prozedur für alle zu löschenden Duplikate.

Private Sub cmdUebernehmen_Click()
     Dim db As DAO.Database, rst As DAO.Recordset
     Dim strPK As String, var As Variant, lngPK As Long
     Dim strForeignTable As String, strFK As String, lngFK As Long
     Dim strSQL As String
     Set db = CurrentDb
     strPK = GetSinglePrimaryKey(Me!cboTabellen)
     lngPK = Me!sfmDuplikateDetails.Form.Controls(strPK)
     Set rst = db.OpenRecordset("SELECT * FROM " & Me!sfmDuplikateDetails.Form.RecordSource & " WHERE NOT " _
         & strPK & " = " & lngPK, dbOpenDynaset)
     Do While Not rst.EOF
         For Each var In Me!lstVerknuepfteTabellen.ItemsSelected
             strForeignTable = Me!lstVerknuepfteTabellen.Column(0, var)
             strFK = Me!lstVerknuepfteTabellen.Column(1, var)
             lngFK = rst(strFK)
             strSQL = "UPDATE " & strForeignTable & " SET " & strFK & " = " & lngPK & " WHERE " & strFK & " = " & lngFK
             db.Execute strSQL, dbFailOnError
         Next var
         strSQL = "DELETE FROM " & Me!cboTabellen & " WHERE " & strPK & " = " & rst(strPK)
         db.Execute strSQL, dbFailOnError
         rst.MoveNext
     Loop
     DuplikatformulareAusblenden
     Me!sfmDuplikateDetails.Requery
     Set colForms = Nothing
     Set colForms = New Collection
End Sub

Listing 6: Überführen verknüpfter Daten und Löschen überflüssiger Datensätze

Sofern noch eingeblendet, werden noch offene Detailformulare geschlossen und das Unterformular sfmDuplikateDetails aktualisiert. Dieses hat nun vermutlich einige Detaildatensätze in verknüpften Tabellen mehr.

Kompletten Artikel lesen?

Einfach für den Newsletter anmelden, dann lesen Sie schon in einer Minute den kompletten Artikel und erhalten die Beispieldatenbanken.

E-Mail:

Download

Download

Die .zip-Datei enthält folgende Dateien:

KundendatensaetzeZusammenfuehren.mdb

Beispieldateien downloaden

© 2003-2015 André Minhorst Alle Rechte vorbehalten.