infobase: EDV - MS-Access


Tabellen

Beziehungen   Quelle: dmt   Datum: 03.2004   nach oben

BEZIEHUNGEN:

sind wie im wirklichen Leben auch in MS-ACCESS nicht immer ein Grund zur Freude.

Abgesehen davon, daß die Segnungen der referentiellen Integrität auch schon mal des Öfteren an mir vorbeigegangen sind, kann das Arbeiten mit Beziehungen zu einem mittleren Desaster führen, wenn später eine solche Tabelle umbenannt wird und in ferner Zukunft einmal gelöscht werden soll. Die Meldung 'Tabelle kann nicht gelöscht werden. Sie ist Teil einer Beziehung' ist absolut hartnäckig. Die Beziehung, auf die sich diese Meldung beruft, ist nach dem Umbenennen der Tabelle aber nicht mehr im Beziehungsfenster zu finden.

Erst mit einer typischen Arschloch-Aktion konnte dieses Problem behoben werden:

Vorausgesetzt, daß der alte Name der Tabelle bekannt ist, kann nach Wieder-Zurückbenennen der Tabelle bzw. dem Erstellen einer Dummy-Tabelle mit einem gleichlautendem Schlüssel-Feld eine der mittlerweile unsichtbaren Beziehungen wieder hergestellt werden. Wenn diese Dummy-Beziehung im Beziehungsfenster von Hand gelöscht wird, lassen sich anschließend auch die Tabellen löschen.

Unerschrockene Hardliner können sich alternativ auch an den System-Tabellen zu schaffen machen. Naja, vielleicht wird einem in einer der nächsten Versionen der Anwendungs-Quelltext nebst Compiler oder besser gleich eine Schrotflinte zur Verfügung gestellt.


Datensatz, Existenz prüfen   Quelle: dmt   Datum: 03.2004   nach oben

DATENSATZ AUF EXISTENZ PRÜFEN:

Der Aufruf von

ExistsId("ID", "Leistungsrapport", Me!ID)

gibt zurück, ob ein angegebener Wert im Feld einer Tabelle gefunden werden kann.
Die Funktion erkennt auch den Null-Zustand von Zählerfeldern bei neuen Datensätzen.

Function ExistsId (Feld As String, Tabelle As String, Wert As Variant) As Integer

    On Error GoTo err_ExistsId

    Dim sTrennzeichen As String

    If IsNull(Wert) Then Exit Function

    If IsNumeric(Wert) Then
       sTrennzeichen = ""
    Else
       sTrennzeichen = "'"
    End If

    If IsNull(DLookup(Feld, Tabelle, Feld & "=" & sTrennzeichen & Wert & sTrennzeichen)) Then
       Exit Function
    Else
       ExistsId = True
    End If

    Exit Function


err_ExistsId:

    Fehler "ExistsId"
    Exit Function

End Function


Datentypen   Quelle: dmt   Datum: 03.2004   nach oben

ACCESS-DATENTYPEN (Version 2.0):

Konstantenname Wert Zugehörigkeit

DB_BOOLEAN     1    Access
DB_BYTE        2    Microsoft Jet Datenbank-Engine-Datentypen
DB_INTEGER     3    Access-Basic
DB_LONG        4    Access-Basic
DB_CURRENCY    5    Access-Basic
DB_SINGLE      6    Access-Basic
DB_DOUBLE      7    Access-Basic
DB_DATE        8    Access
DB_TEXT       10    Access
LONG_BINARY   11    Microsoft Jet Datenbank-Engine-Datentypen, eigentlich DB_LONGBINARY
DB_MEMO       12    Access

Ach ja, und dann gibt es auch noch den GAR NIX - Datentyp, doch dazu mehr unter Fehler.


default   Quelle: dmt   Datum: 02.2006   nach oben

STANDARDWERT / DEFAULTVALUE eines Tabellenfeldes:

Ist immer ein String und kann eigentlich sehr einfach innerhalb einer Anwendung gesetzt werden: FeldVariable("Feldname").DefaultValue = "='saukasse88'"

Klappt aber nur, wenn zu diesem Zeitpunkt kein Zugriff auf die Tabelle durch z.B. Formulare erfolgt.


STANDARDNAMEN für Felder in neuen Tabellen:

Klassisches Szenario:
In loser Form liegen Daten in zig Excel-Dateien verstreut.
Manuell werden diese Daten in eine vergleichbare Form gebracht und ab dann besser per Datenbank verarbeitet.
Super unkompliziert läuft das u.a. bereits im alten Access 2.0 (mittlerweile kann so was auch locker per phpMyAdmin in einer MySql-Datenbank abgewickelt werden, aber manchmal ist's halt schnell geklickt, vor allem in so dankbaren Anwendungen wie den älteren Accessversionen).
Locker die Importfunktion anwerfen, mit Trennzeichen getrennter Text anwählen, ein bißchen was zum Thema Text- und Feldbegrenzer sagen und schwupps sind die Daten da, wo sie hingehören (in einer Datenbanktabelle).
Die Felder sind in einfachster Form als Textfelder mit einer maximalen Länge von 255 Zeichen definiert und werden der Reihe nach numerisch benannt.
Jetzt würde man nur noch per SQL ein paar aufräumende Statements absetzen und gut ist.
Denkste !
Bereits so simple Sachen wie

SELECT * FROM tabelle WHERE 1 = ""
liefern dann ganz wichtige Fehlermeldungen.
Doch warum sollte ich ausgerechnet ein Textfeld nicht gegen "" prüfen dürfen ?
Klar gibt es dafür (noch) keinen vernünftigen Grund.
Es ist vielmehr so, daß die numerischen Default-Feldnamen innerhalb von SQL-Statements Ärger machen, weil evtl. der SQL-Interpreter versucht, mathematische Sachen auszuführen. Selbst ein explizites tabelle.1 rief nur weitere Fehlermeldungen hervor.
Abhilfe:
Den Feldern flugs neue Namen gegeben (Alpha!) und die Kacke dampft nicht mehr.

Anmerkung für Leute, die nicht verstehen, was mit Alpha gemeint ist:
Gemeint sind Namen, die NUR aus Buchstaben bestehen.
Wenn Sie schon mit so komplizierten Maschinen wie Computern und noch komplizierteren Systemen wie den darauf installierten Betriebssystemen umeinander hantieren, dann tun Sie sich und anderen einen Gefallen und vermeiden Sie bitte Fehlerquellen, wo es nur geht.
Geben Sie bei Namens-Bezeichnern für Computer-technische Objekte wie Dateien, Datenbanken, Tabellen und Feldern etc. als Namen NUR Buchstaben an und vermeiden Sie ALLES, was auch nur im entferntesten kein Buchstabe ist (z.B. Leerzeichen und ähnliche Spielereien). Nennen Sie ein Feld, daß z.B. Daten einer Maß-Nummer enthält, bitte "massnr" oder meinetwegen auch "mass_nr". Spielen Sie bitte nicht mit Großbuchstaben, sprachbezogenen Sonderzeichen und anderem Quatsch herum, wenn Sie es mit einem EDV-System zu tun haben, dessen Eigen- und Unarten Sie selbst nicht überschauen können.

Ein typisches Beispiel für solche Aufräumarbeiten ist das Entfernen von leeren Zeilen, die manche Anwender in ihre Excel-Listen reinmosten, damit's übersichtlicher für's Auge wird.
Wenn die Felder gescheite Alpha-Namen haben, kann man nachschauen, welche Leerzeilen sich eingeschlichen haben:

SELECT * FROM matnr WHERE
entwicklungsnr IS NULL AND
entwicklungsnr_sap IS NULL AND
bestellnr IS NULL AND
aznr IS NULL AND
freigabenr IS NULL

Wenn es sicher ist, daß die gefundenen Zeilen wirklich Asche sind, dann machen wir ihnen mit folgendem Statement den Garaus:

DELETE * FROM matnr WHERE
entwicklungsnr IS NULL AND
entwicklungsnr_sap IS NULL AND
bestellnr IS NULL AND
aznr IS NULL AND
freigabenr IS NULL

Weitere Infos zur Schreibweise von Löschabfragen siehe auch unter LÖSCHABFRAGEN.


eingebundene   Quelle: dmt   Datum: 04.2005   nach oben

EINGEBUNDENE TABELLEN

können mitunter ziemlich nützlich sein, so wie z.Bsp. im Falle der 'Runtime'-Version des HP/VHP-Managers. Hier besteht für den Anwender die Möglichkeit, zu den nicht bearbeitbaren Ventildatensätzen Extra-Memo's (Text sowie OLE) anzulegen. Die Schwierigkeit besteht darin, daß bei einem Update der Ventildatensätze die Extra-Memo's des Anwenders nicht verloren gehen sollten.

Entweder man löst dieses Problem über eine Aus- und Einlese-Software, die das während einer Update-Installation abcheckt, oder die Extra-Memo-Daten werden in einer anderen mdb-Datei abgelegt, deren Daten als 'eingebundene Tabelle' in der 'Runtime'-mdb zugänglich sind.

Diese scheinbar elegante Lösung mittels eines der tollen Features von MS-ACCESS erwies sich jedoch bereits beim ersten Problelauf bei BOSCH als Schlag ins Wasser, da auf hinterfotzige Weise der komplette Pfad der x_memo.mdb auf meinem System hinterlegt wurde.

Intensives Nachforschen ergab, daß dieser Pfad in mehreren Stellen innerhalb von MS-ACCESS hinterlegt ist:
- In der Tabelle MySysObjects
- Im Eigenschaftenfenster der eingebundenen Tabelle

Diese Einträge können allerdings nicht bearbeitet werden, da z. Bsp. die Tabelle MySys-Objects zum Eigentümer 'engine' gehört. Dies zu ändern erschien mir dann doch als zu riskant, schließlich wird EXZESS nicht mal mit seinen eigenen Geschichten (so wie sie sind) fertig.

Eine Lösung wird in der (lächerlich kleinen) Dokumentation des Developer's-Toolkit angeboten. Die mehrere Bildschirm-Seiten füllende Routine konnten für meinen Bedarf drastisch abgespeckt werden:

Sub ReAttachTables ()

    Dim DB As Database, T As TableDef

    Set DB = DBEngine.Workspaces(0).Databases(0)
    Set T = DB("Extra_Memo")

    T.Connect = ";DATABASE=X_MEMO.MDB"
    T.RefreshLink
    ' Debug.Print T.Connect
    DB.Close

End Sub

Man beachte, daß in der Connect-Anweisung der eigentliche Pfad nicht mehr enthalten ist, aber dann von Access nach Ausführen der Anweisung wieder gebildet wird.

Das geht aber auch nur dann gut, wenn diese Datei auch im aktuellen ACCESS-Verzeichnis steht. Notfalls kann dieses manuell mit 'Datei öffnen' zurechtgeklickt werden.

Diese Information wird dauerhaft in der Tabelle 'MySysObjects' hinterlegt und kann über das Eigenschaften-Fenster im Tabellen-Entwurfsmodus angezeigt werden.

Doch leider muß sich MS-ACCESS auch hier wieder Wermutstropfen-mäßig einmischen, um diesen Schritt wieder zu versauen. Beim Ausführen der RefreshLink-Anweisung zieht sich MS-ACCESS nämlich wieder den Betriebssystem-Pfad, unter dem die einzubindende Tabelle zu finden ist, rein und legt diesen dann auch brav ab.

Die oben beschriebene ReAttach-Routine muß also (am besten durch eine erweiterte Setup-Routine) auf dem Ziel-Rechner erfolgen, da ein Pfad-loses Verwalten eingebundener Tabellen ums Verrecken nicht möglich zu sein scheint.

Wenn's hierbei Probleme geben sollte, kann man es auch einmal mit

TransferDatabase A_ATTACH
versuchen.

Leider gibt es auch an dieser Stelle einen Nachschlag der negativen Art:

Als die ReAttach-Routine in der 'erweiterte Setup'-Datenbank 'transact.mdb' ausgeführt wurde, funktionierte das, wie üblich, bei mir und, ebenfalls wie üblich, beim Kunden nicht.

Der Grund liegt ( verständlicherweise ) darin, daß ein ReAttachen nur in der Datenbank ausgeführt werden kann, in der die eingebundenen Tabellen auch enthalten sind. Witzigerweise erzeugt das Ausführen der Routine in einer fremden Tabelle keinen Fehler. So wird als nächstes versucht, im erweiterten Setup, indem eh' schon zwischen 2 anderen MDB's geswitcht wird, per speziellem "/X Makroname"-Parameter-Aufruf auch noch als drittes die Anwender-Datenbank direkt aufzurufen.

Auch in dieser Beziehung sind wir mittlerweile etwas weiter:

Im LaserJob-Manager führt eine laserjob.mdb, die nur Tabellen enthält, Code aus, der in lib_lj.mda enthalten ist. Eine MessageBox, die den Code in lib_lj.mda unterbricht, zeigt, daß für MS-Access die aktuelle Datenbank immer noch laserjob.mdb ist, obwohl der Code mit der Anweisung DBEngine.Workspaces(0).Databases(0) in lib_lj.mda ausgeführt wird.

Nach einem trockenem Durchschlucken erinnerte ich mich an die Funktion CodeDB(), die so freundlich ist, die Datenbank als Objekt zurückzugeben, die den gerade laufenden Code auch wirklich enthält. Mit dieser Objekt-Zuweisung ließ sich eine Lösung realisieren, in der eingebundene Tabellen innerhalb einer Bibliotheksdatenbank auf gültige Connect-Einträge hin überprüft und bei Bedarf automatisch wieder eingebunden werden, wenn sich die Herkunftsdatenbank im selben Verzeichnis befindet. Die Routine AreTableAttached stammt im Wesentlichen aus der MS-ACCESS 2.0 - Vorlagen.mdb und kann ihre Stärken immerhin bei ALLEN eingebundenen Tabellen der wahren Code-Datenbank ausspielen.

Und so gehts:

Zu Anfang werden in zwei Konstanten die Namen der bezogenen Datenbank sowie einer eingebundenen 'Test'-Tabelle vereinbart. Läßt sich eine OpenRecordset-Anweisung fehlerfrei ausführen, war der Connect-String in Ordnung. Das gilt dann auch für alle anderen eingebundenen Tabellen. Tritt ein Fehler auf, werden für alle Tabellen, die einen Connect-String enthalten, diese Strings gelöscht und die DATABASE-Zuweisung neu gesetzt. Ein RefreshLink sorgt dafür, daß Access seine benötigten Informationen selbst aktualisiert. Leider werden hierbei explizite Laufwerks- und Pfadangaben festgeschrieben. Pfadlose Informationen werden nicht behalten. Schade. Und obendrein gibt es auch noch weitere Probleme, wenn eingebundene Tabellen auf mehrere, verschiedene Datenbanken an verschiedenen Orten zeigen, aber für den Normalfall sollte ein AreTablesAttached ausreichen.

Die neueste Version schafft es sogar, Tabellen, die auf mehrere verschiedene Datenbanken verweisen, wieder automatisch einzubinden. Die einzige Voraussetzung ist, daß diese Datenbank-Dateien im selben Verzeichnis wie die Anwendung stehen.

Private Function AreTablesAttached() As Boolean

    On Error GoTo err_AreTablesAttached

    ' **** Aktualisiert bei Bedarf den Connect eingebundener Tabellen ****

    Dim TableCount As Integer, MaxTables As Integer, i As Integer
    Dim FileName As String, SearchPath As String, Temp As String
    Dim v As Variant, AccDir As String, DbBackend As String, DbDir As String
    Dim DB As Database, RS As Recordset, TD As TableDef

    Const TST_TABLE_NAME = "webmanagement"

    Const NONEXISTENT_TABLE = 3011
    Const NWIND_NOT_FOUND = 3024
    Const ACCESS_DENIED = 3051
    Const READ_ONLY_DATABASE = 3027

    Set DB = CurrentDb

    AreTablesAttached = True

    MaxTables = DB.TableDefs.Count

    On Error Resume Next                ' Weiter bei ungültigen Einbindungspfaden

    ' **** Öffne eingebundene Tabelle; Bindungs-Informationen ok ? ****

    Set RS = DB.OpenRecordset(TST_TABLE_NAME)

    If Err = 0 Then
       RS.Close                         ' Beende, wenn die eingebundenen
       Exit Function                    ' Informationen korrekt sind.
    End If

    ' **** Initialisiere das Wiedereinbindungsfortschritts-Meter ****

    v = SysCmd(SYSCMD_INITMETER, "Tabellen werden eingebunden", MaxTables)

    ' **** Binde alle Tabellen ohne Nullängen-Zeichenfolgen erneut ein ****

    TableCount = 1                      ' Initialisiere Statuswert
    DbDir = Database_Dir()              ' und das Verzeichnis der aktuellen Anwendung

    For i = 0 To DB.TableDefs.Count - 1
        Set TD = DB.TableDefs(i)
        If TD.Connect <> "" Then
           FileName = DbDir & "\" & Get_Filename(TD.Connect)
           TD.Connect = ";DATABASE=" & FileName
           Err = 0
           TD.RefreshLink
           If Err <> 0 Then
              If Err = NONEXISTENT_TABLE Then
                MsgBox "Die Datei '" & FileName & "' enthält nicht die benötigten Tabellen '" & TD.SourceTableName & "'", 16, "Applikation nicht ausführbar"
              ElseIf Err = NWIND_NOT_FOUND Then
               MsgBox "Windige Nordmänner legen fragwürdiges vor.", 16, "Applikation nicht ausführbar"
              ElseIf Err = ACCESS_DENIED Then
                MsgBox "Die Datei '" & FileName & "' konnte nicht geöffnet werden, da sie entweder schreibgeschützt ist, oder sich auf einem schreibgeschützten Speichermedium befindet.", 16, "Applikation nicht ausführbar"
              ElseIf Err = READ_ONLY_DATABASE Then
                MsgBox "Die Tabellen können nicht erneut eingebunden werden, da '" & DbBackend & "' entweder schreibgeschützt ist, oder sich auf einem schreibgeschützten Speichermedium befindet.", 16, "Applikation nicht ausführbar"
              Else
                MsgBox Error, 16, "Applikation nicht ausführbar"
              End If
              AreTablesAttached = False
              GoTo exit_AreTablesAttached
           End If
           TableCount = TableCount + 1
           v = SysCmd(SYSCMD_UPDATEMETER, TableCount)
        End If
    Next i


exit_AreTablesAttached:

    Set RS = Nothing
    Set TD = Nothing
    Set DB = Nothing
    v = SysCmd(SYSCMD_REMOVEMETER)
    Exit Function


err_AreTablesAttached:

    Fehler "AreTablesAttached"
    Exit Function


End Function

Im schlimmsten Fall müssen halt alle Tabellen durchlaufen werden, bei enthaltenen Connect-Strings wird die Zugänglichkeit einzeln überprüft (allein das kann dauern) und bei Mißlingen sollte dem Anwender in einem Dialog (evtl. auch noch mit DateiÖffnen) Pfad und Name der letzten Connect-Anweisung genannt werden.

Es besteht aber auch die Möglichkeit, den Access-Dialog zum manuellen Wiederherstellen von Einbindungs-Informationen in eine Anwendung einzubauen s. bestatt.mdb. Da muß meines Wissens aber noch eine soa200.dll oder so mitgeliefert werden.

Eine manuelle Lösung, die mit wenig Aufwand dem Benutzer per InputBox-Eingabe die Möglichkeit gibt, einen fehlerhaften Connect-String zu korrigieren, kann so aussehen (auch die Übergabe eines Tabellennamens per Parameter ist denkbar):

Private Function Check_LogDir() As Boolean

    On Error GoTo err_Check_LogDir

    Dim DB As Database, RS As Recordset, TD As TableDef, s As String

    Const WEBLOG_TABLE_NAME = "logfiles"

    Set DB = CurrentDb

    On Error Resume Next                ' Weiter bei ungültigen Einbindungspfaden

    ' **** Öffne eingebundene Tabelle; Bindungs-Informationen ok ? ****

    Set RS = DB.OpenRecordset(WEBLOG_TABLE_NAME)

    If Err = 0 Then
       RS.Close                         ' Beende, wenn die eingebundenen
       Check_LogDir = True
       Exit Function                    ' Informationen korrekt sind.
    End If

    Set TD = DB.TableDefs(WEBLOG_TABLE_NAME)

    Beep
    s = "Der Verbindung-String der Tabelle " & WEBLOG_TABLE_NAME & ": " & TD.Connect & " ist ungültig !"
    s = s & Chr$(13) & Chr$(10) & Chr$(13) & Chr$(10)
    s = s & "Geben Sie einen gültigen Verbindungs-String ein."
    s = s & Chr$(13) & Chr$(10) & Chr$(13) & Chr$(10)
    s = s & "Abbrechen umgeht diese Prüfung."
    s = InputBox(s, "Check_LogDir", TD.Connect)

    If s = "" Then
       Exit Function
    Else
       TD.Connect = s
       Err = 0
       TD.RefreshLink
       If Err <> 0 Then
          Call Check_LogDir
       Else
          Beep
          Check_LogDir = True
          MsgBox "Die Weblogs-Tabelle wurde erfolgreich eingebunden !", vbInformation, "Check_LogDir"
       End If
    End If

    Exit Function


err_Check_LogDir:

    Fehler "Check_LogDir"
    Exit Function

End Function

* * * *


Externer Access-Tip: Pfade von verknüpften Tabellen

In Datenbanken mit vielen eingebundenen Tabellen, ist es nicht immer möglich zu erkennen, auf welche Datenbank die jeweilige Tabellen zurückgreift. Die nachfolgende Funktion liest die Pfade aller eingebundenen Tabellen einer Datenbank aus und zeigt diese im Direktfenster an.

   Public Function LinkPfadAuslesen()

   ' ---------------------------------------------------------------
   '  mit dieser Funktion werden alle eingebundenen
   '  Tabellen gelöscht
   ' ---------------------------------------------------------------

   On Error GoTo fehler

   Dim DB As Database
   Dim rs As Recordset
   Dim td As TableDef

       Set DB = CurrentDb()
       Set rs = DB.OpenRecordset("SELECT Name FROM MSysObjects " & _
       "WHERE Name Not Like 'MSys*' AND Type=6;", dbOpenSnapshot)

       rs.MoveFirst
       Do Until rs.EOF
           Set td = DB.TableDefs(rs!Name)
           Debug.Print rs!Name & " - " & Mid(td.Connect, 11)
           rs.MoveNext
       Loop
       rs.Close
       DB.Close

   ende:
       Exit Function

   fehler:
       Resume ende

   End Function

Anmerkung:
In den Verweisen muss die Bibliothek "Microsoft DAO 3.6 Object Libary" aktiviert sein.
Um in Access einen Verweis auf eine andere Bibliothek einzustellen gehen Sie bitte folgendermaßen vor:
1. öffnen Sie in Ihrer Access-Datenbank ein beliebiges Modul
2. über das Menü [Extras][Verweise] können Sie den Dialog zum Einstellen der Verweise öffnen
3. suchen Sie in der Liste den Verweis auf die Bibliothek "Microsoft DAO 3.6 Object Libary" und aktivieren diesen
4. schieben Sie den Verweis an die oberste mögliche Stelle in der Liste (mit den Schaltflächen 'Priorität')
5. schließen Sie den Dialog

* * * *

Mini-Routine zum Auslesen eines AttachedTable-Verweises:

Function GetConnectDb(sTable As String) As String

    On Error GoTo err_GetConnectDb

    Dim DB As Database, TD As TableDef

    Set DB = CurrentDb
    Set TD = DB.TableDefs(sTable)
    GetConnectDb = ReplaceInString(CStr(TD.Connect), ";DATABASE=", "")
    Set TD = Nothing
    Set DB = Nothing

    Exit Function


err_GetConnectDb:

    Fehler "GetConnectDb für Tabelle " & sTable
    Exit Function

End Function


Gültigkeitsregeln   Quelle: dmt   Datum: 02.2006   nach oben

GÜLTIGKEITSREGEL / VALIDATION RULE:

Eigentlich eine feine Sache, bereits bei der Tabellen-Feld-Definition oder im Formular ungültige Werte für ein Feld innerhalb der Eigenschaften abzufangen und sogar eine aussagekräftige Meldung zu präsentieren. In einer Formularlösung ergab sich jedoch, daß, wenn mittels einer ungültigen Eingabe die Verletzungsmeldung kam, selbst die Korrektur des Wertes nach Bestätigung wieder die Meldung hervorbrachte. Da ich keine Lust hatte, zu testen, ob dieser Verhaltensfehler auch bei Implementierung in den Tabellenfeld-Eigenschaften auftritt, habe ich es wie so oft vorgezogen, das Problem per Code zu lösen.

In den späteren 32-Fick-Access-Varianten kann dann mit <Esc>-Spielereien eine formulargebundene Gültigkeitsregel umgangen werden. Bravo, ihr Schwachköpfe !

Angeblich sind ValidationsRules in Tabellen besser aufgehoben.


relational   Quelle: dmt   Datum: 02.2006   nach oben

ANLEGEN RELATIONAL VERKNÜPFTER DATEN:

Gesetzt den Fall, zwei Tabellen ( z.Bsp. Lieferanten und Adressen ) sind über ACCESS-Beziehungen miteinander referentiell 1:1 verknüpft. Der Anwender arbeitet mit einem Formular, dessen Abfrage ebenfalls eine SQL-Verknüpfung der beiden Tabellen enthält.

Wird nun ein Datensatz angelegt, wird, wenn der Anwender mit dem Lieferanten-Formular arbeitet, aber nur ein Lieferanten-Datensatz angelegt. Vom Adressendatensatz trotz referentieller Integrität weit und breit keine Spur. Wird aber im Lieferantenformular für ein Feld, das in beiden Tabellen vorkommt ( z.Bsp. Suchname ) die Herkunftseigenschaft der anderen ( hier Adresse ) Tabelle eingestellt, wird

1. Im Lieferanten.Suchname der Name übernommen und
2. ein neuer Adressen-Datensatz mit dem angegebenem Suchnamen angelegt.

Allerdings ist damit noch nicht alles erledigt.

Bis man einen Anwender auf diese Formulare loslassen kann, muß doch noch einiges eingestellt werden ( siehe Formulare KV_Lizenznehmer und KV_Adressen in hp_vhp.mdb ). So mußte z. Bsp. die Eigenschaft DefaultEditing auf 'Bearbeiten' eingestellt werden, damit beim Ändern eines Datensatzes das Formular automatisch geschlossen werden kann. Somit war es aber möglich, in einen neuen Unter-Adress-Datensatz zu gehen ( unerlaubt ), wenn der aktive nicht geändert wurde. Jegliches Umstellen der DefaultEditing-Eigenschaft zur Laufzeit stellte alle Datensätze anstatt des einen per Bedingung übergebenen dar. So wurde versucht, bei Form_Current die IstNull-Eigenschaft des Feldes 'Suchname' zu überprüfen, um dann das Formular zu schließen. Hierbei tritt ein Laufzeitfehler auf. Führt man statt 'DoCmd Close' ein 'SendKeys "^{F4}" aus, klappt die Sache (besser DoCmd Close A_FORM, Me.Name).

Nach einer Nacht Bedenkzeit habe ich mich dazu entschlossen, die relationale Konstruktion fallen zu lassen, und durch eine 1-Tabellen-Lösung zu ersetzen. Die getrennte Darstellung der Daten mit zwei verschiedenen Formularen habe ich beibehalten. Das hatte allerdings leider zum Ergebnis, daß ich die Ereignis-Kacke mit dem Thema automatisches Hin- und Herblenden immer noch hatte.

Auch das Darstellen relationaler Daten in Formularen will erkämpft sein.

Eine Basistabelle soll durch ein Formular bearbeitet werden können, indem zu dem jeweils aktiven Datensatz dazugehörende Daten in anderen Tabellen angezeigt werden sollen. Normalerweise kann das mit Unterformularen gut gelöst werden, die aber in der Listen- bzw. Endlosformular-Darstellung des Hauptformulares nicht angezeigt werden können ( die Endlosformular-Darstellung verbietet sich sogar, wenn das Hauptformular Unterformulare enthält ). Eine hart erkämpfte Lösung besteht darin, einem Kombinationsfeld, das an ein Feld der Basistabelle gebunden sein muß, über das Ereignis 'FormCurrent' des Formulares einen SQL-String als RowSource zuzuweisen, der eine relationale Verknüpfung zu den Daten einer anderen Tabelle darstellt. Dieses Kombinationsfeld erfüllt seinen Zweck in ALLEN drei Formularansichten und gibt die enthaltenen Daten Listen-mäßig nur dann preis, wenn dessen Schaltfläche betätigt wird. Somit wird der entsprechende Datensatz automatisch zum aktiven und die Kombinationsfeld-Liste zeigt die gewünschten Daten. Über die Ereignisse 'DblClick' und 'Key_Down' kann auch ein anderes Formular mit einer Bedingung geöffnet werden, die den Datensatz des Hauptformulares mit den Daten der anderen Tabelle darstellt.

SPLITTING von Rahmen- und Detail-Daten:

Bei der klassischen Trennung von z.B. Auftragsdaten und Auftragspositionen konnte es trotz referentieller Beziehung passieren, daß Unterdaten angelegt wurden, obwohl kein Master-Datensatz besteht. Wurde der Vorgang abgebrochen, blieben die Unterdaten als Geister-Datensätze übrig. Man kann aber in BeforeInsert im Unterformular-Formular prüfen, ob ein Masterdatensatz existiert (z.B. mit Isnull(ZählerID)) und mittels Cancel=true und einer Meldung schlimmstes verhindern.

Hier sollte aber auch die Art der Verknüpfung (1:n, Left Jion etc. eine Rolle spielen).


Struktur auswerten   Quelle: dmt   Datum: 10.2004   nach oben

Feststellen, ob ein Objekt ein SYSTEMOBJEKT ist:

Beim Durchnudeln z. B. aller in einer Datenbank enthaltenen Tabellen kann es vonnöten sein, daß die ebenfalls enthaltenen Systemtabellen ausgeblendet werden.

Hierfür wird in verschiedenen Dokumentationen der obskure Prüfvergleich

If (TD.Attributes And DB_SYSTEMOBJECT) then

angeboten. Da aber in TableDef.Attributes auch Flags enthalten sind, die auch andere als nur Systemtabellen-Zeiger enthalten, kann davon ausgegangen werden, daß bei diesem Vergleich evtl. auch Nicht-Systemobjekte unterschlagen werden. Das könnten z.B. eingebundene oder andere anormale Tabellen sein, bei den ein entsprechendes DB_ARSCHFLAG gesetzt ist und der oben beschriebene Vergleich evtl. dann negativ ausfällt. So ganz trau' ich der Sache nicht, aber erste Erfolge können bewundert werden in ANALYZER.MDB im Formular 'Analyze_Tabellen_Update2'.

Mehr dazu siehe unten ...

Ebenfalls fündig wird man in den Routinen des Formulares 'Check_Tables', das enthaltene Tabellen, deren Eigenschaften, darin enthaltene Felder sowie deren Eigenschaften auflistet.


LIST_TABLES liefert einen Listen-/Klappfeld-kompatiblen Rowsource-String und gibt je nach Parameter die gewünschte Art von Tabellen zurück:

Function List_Tables (Modus As String) As String

    On Error GoTo err_List_Tables

    ' **** Füllt die Werte-Liste des übergebenen Kombifeldes ****
    ' **** mit den Namen der Tabellen der angegebenen Art    ****

    Dim DB As Database, i As Integer, s As String, v As Variant

    v = SysCmd(SYSCMD_SETSTATUS, "lese Tabellennamen ein ...")

    Set DB = DBEngine.Workspaces(0).Databases(0)

    Select Case Modus

        Case "Eingebundene":            For i = 0 To DB.TableDefs.Count - 1
                                            If (DB.TableDefs(i).Attributes And DB_ATTACHEDTABLE) Then
                                               s = s & DB.TableDefs(i).Name & ";"
                                            End If
                                        Next i

        Case "Alle":                    For i = 0 To DB.TableDefs.Count - 1
                                            s = s & DB.TableDefs(i).Name & ";"
                                        Next i

        Case "System":                  For i = 0 To DB.TableDefs.Count - 1
                                            If (DB.TableDefs(i).Attributes And DB_SYSTEMOBJECT) Then
                                               s = s & DB.TableDefs(i).Name & ";"
                                            End If
                                        Next i

        Case "Eingebundene und System": For i = 0 To DB.TableDefs.Count - 1
                                            If (DB.TableDefs(i).Attributes) Then
                                               s = s & DB.TableDefs(i).Name & ";"
                                            End If
                                        Next i

        Case "Normale":                 For i = 0 To DB.TableDefs.Count - 1
                                            If (DB.TableDefs(i).Attributes) = 0 Then
                                               s = s & DB.TableDefs(i).Name & ";"
                                            End If
                                        Next i

        Case "Keine System":            For i = 0 To DB.TableDefs.Count - 1
                                            If (DB.TableDefs(i).Attributes And DB_SYSTEMOBJECT) = 0 Then
                                               s = s & DB.TableDefs(i).Name & ";"
                                            End If
                                        Next i

        Case Else:                      Beep
                                        MsgBox "Unbekannter Modus '" & Modus & "' !", 16, "List_Tables"
                                        v = SysCmd(SYSCMD_CLEARSTATUS)
                                        Exit Function

    End Select

    s = Left$(s, Len(s) - 1)

    List_Tables = s

    v = SysCmd(SYSCMD_CLEARSTATUS)

    Exit Function


err_List_Tables:

    v = SysCmd(SYSCMD_CLEARSTATUS)

    Fehler "List_Tables"
    Exit Function

End Function

Um Tabellen zu 'fangen', die nicht eingebundene oder Systemtabelle sind, muß
If (DB.TableDefs(i).Attributes)
in
If (DB.TableDefs(i).Attributes)=false
umformuliert werden. Ein If Not (... geht nicht !


Und jetzt das ganze mit LIST_TABLEPROPERTIES für die 'internen' Eigenschaften der Tabellen. Obendrein gibt es sog. 'benutzerdefinierte' Eigenschaften wie 'DataSheetHeight' o.s.ä. und echte benutzerdefinierte Eigenschaften (z.B. Tabelle.beschissen=true). Laut Doku müssen diese explizit benannt werden. In der Praxis war da bisher noch nichts zu holen.

Function List_TableProperties (TN As Variant) As String

    On Error GoTo err_List_TableProperties

    ' **** Füllt die Werte-Liste des übergebenen Kombifeldes ****
    ' **** mit den Eigenschaften der übergebenen Tabelle     ****

    Dim DB As Database, TD As TableDef, i As Integer, s As String

    If IsNull(TN) Then
       List_TableProperties = ""
       Exit Function
    End If

    Set DB = DBEngine.Workspaces(0).Databases(0)

    Set TD = DB.TableDefs(TN)

    ' 'eingebaute' Eigenschaften

    For i = 0 To TD.Properties.Count - 1
        s = s & TD.Properties(i).Name & ";'" & TD.Properties(i) & "';"
    Next i

    s = Left$(s, Len(s) - 1)

    List_TableProperties = s

    Exit Function


err_List_TableProperties:


    Fehler "List_TableProperties"
    Exit Function

End Function


LIST_FIELDS macht das, was der Name schon sagt:

Function List_Fields (TN As Variant) As String

    On Error GoTo err_List_Fields

    ' **** Füllt die Werte-Liste des übergebenen Kombifeldes ****
    ' **** mit den Feldnamen der übergebenen Tabelle         ****

    Dim DB As Database, TD As TableDef, i As Integer, s As String

    If IsNull(TN) Then
       List_Fields = ""
       Exit Function
    End If

    Set DB = DBEngine.Workspaces(0).Databases(0)

    Set TD = DB.TableDefs(TN)

    For i = 0 To TD.Fields.Count - 1
        s = s & TD.Fields(i).Name & ";"
    Next i

    s = Left$(s, Len(s) - 1)

    List_Fields = s

    Exit Function


err_List_Fields:


    Fehler "List_Fields"
    Exit Function

End Function


LIST_INDEXES ist auch noch nicht das Ende der Fahnenstange:

Function List_Indexes (TN As Variant) As String

    On Error GoTo err_List_Indexes

    ' **** Füllt die Werte-Liste des übergebenen Kombifeldes ****
    ' **** mit den Namen der Indizes der übergebenen Tabelle ****

    Dim DB As Database, TD As TableDef, i As Integer, s As String

    If IsNull(TN) Then
       List_Indexes = ""
       Exit Function
    End If

    Set DB = DBEngine.Workspaces(0).Databases(0)

    Set TD = DB.TableDefs(TN)

    For i = 0 To TD.Indexes.Count - 1
        s = s & TD.Indexes(i).Name & ";"
    Next i

    If Len(s) > 0 Then
       s = Left$(s, Len(s) - 1)
    End If

    List_Indexes = s

    Exit Function


err_List_Indexes:


    Fehler "List_Indexes"
    Exit Function

End Function

LIST_FIELDPROPERTIES; auch hier das Spiel mit den eingebauten Eigenschaften:

Private Function List_FieldProperties (TN As Variant, FN As Variant) As String

    On Error GoTo err_List_FieldProperties

    ' **** Füllt die Werte-Liste des übergebenen Kombifeldes ****
    ' **** mit den Eigenschaften der überg. Tabelle und Feld ****

    Dim DB As Database, TD As TableDef, FD As Field, i As Integer, s As String, s1 As String

    If IsNull(TN) Or IsNull(FN) Then
       List_FieldProperties = ""
       Exit Function
    End If

    Set DB = DBEngine.Workspaces(0).Databases(0)

    Set TD = DB.TableDefs(TN)
    Set FD = TD.Fields(FN)

    ' 'eingebaute' Eigenschaften

    For i = 0 To FD.Properties.Count - 1

        Select Case FD.Properties(i).Name

            Case "Attributes":  Select Case FD.Properties(i)
                                    Case DB_FIXEDFIELD:     s1 = "fixe Größe, def. bei num."
                                    Case DB_VARIABLEFIELD:  s1 = "variable Größe, Textfelder"
                                    Case DB_AUTOINCRFIELD:  s1 = "Zähler, Long"
                                    Case DB_UPDATABLEFIELD: s1 = "Wert veränderbar"
                                    Case DB_DESCENDING:     s1 = "Feld wird in absteigender Reihenfolge sortiert"
                                End Select

            Case "Type":        Select Case FD.Properties(i)
                                    Case DB_DATE:           s1 = "Datum/Uhrzeit"
                                    Case DB_TEXT:           s1 = "Text"
                                    Case DB_MEMO:           s1 = "Memo"
                                    Case DB_UPDATABLEFIELD: s1 = "Wert veränderbar"
                                    Case DB_BOOLEAN:        s1 = "Ja/Nein"
                                    Case DB_INTEGER:        s1 = "Integer"
                                    Case DB_LONG:           s1 = "Long"
                                    Case DB_CURRENCY:       s1 = "Currency"
                                    Case DB_SINGLE:         s1 = "Single"
                                    Case DB_DOUBLE:         s1 = "Double"
                                End Select

            Case Else:          s1 = ""

        End Select

        s = s & FD.Properties(i).Name & ";'" & FD.Properties(i) & "';" & s1 & ";"

    Next i

    s = Left$(s, Len(s) - 1)

    List_FieldProperties = s

    Exit Function


err_List_FieldProperties:

    If Err = 3219 Then
       Resume Next
    Else
       Fehler "List_FieldProperties"
       Exit Function
    End If

End Function

nach oben
zur Startseite dieses Webangebotes zur infobase-Hauptseite