Dieser und noch weitere Artikel wurde von estartu erstellt.


Folgende Themen werden von diesem Artikel berührt:


Druckversion des Artikels


1 Einleitung

Wer mit der MFC arbeitet und eine Datenbank nutzen will, der hat mehrere Möglichkeiten, dies zu tun. Eine davon ist CRecordset und darum geht es in diesem Artikel.

Online ist kaum etwas zu finden und eine der wenigen (offline) Anleitungen für Anfänger ist Kapitel 14 aus "Visual C++6 in 21 Tagen".
Sie ist aber maximal für einen kleinen Einstieg geeignet und man wird schnell mit vielen Fragen alleingelassen. Dann gibt man entweder auf, oder man kämpft sich durch die MSDN und das Internet, um es nach und nach zu lernen.
Ich habe die zweite Möglichkeit gewählt und möchte meine Erkenntnisse jetzt an Sie weitergeben.

Im Folgenden lernen Sie u.a.

Da ich nicht alles erklären kann und will, habe ich häufig Links zur MSDN eingebaut, so dass Sie dort weiterlesen können.

2 Grundlegendes

Eine von CRecordset abgeleitete Klasse repräsentiert immer eine Tabelle bzw. einen View.
Views sind allerdings schreibgeschützt, daher verwende ich sie so selten wie möglich. Auch wenn Sie beim Erstellen der Recordsetklasse mehrere Tabellen auswählen, ist dies ein View.
Noch dazu kann es passieren, dass Sie viel mehr Datensätze erhalten als es eigentlich wären.
Das passiert, wenn Sie in der Datenbank keine Beziehungen zwischen den Tabellen gesetzt haben.

3 Was bedeutet der automatisch generierte Code?

Wenn Sie eine CRecordset-Klasse mit Hilfe des Assistenten erzeugen, finden Sie bereits eine Menge Code vor.
Damit Sie effektiv damit arbeiten können, sollten Sie aber auch verstehen, was Ihnen da serviert wird, denn nur so können Sie später auch selbst etwas verändern.

3.1 Klassenkopf

C++:
class CDemoSet : public CRecordset
Diese Klasse ist von CRecordset abgeleitet.
Wenn Sie später eine eigene Basisklasse haben, ist es trotzdem einfacher, neue Recordsets per Assistent erzeugen zu lassen und dann den Code anzupassen.

Falls Sie die Recordsetklassen in einer Dll erstellen, wird wahrscheinlich der Include in der stdafx.h fehlen. Sie müssen ihn selbst eintragen:
C++:
#include <afxdb.h>


3.2 Die Membervariablen für die Spalten

VC 2003:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
// Feld-/Parameterdaten
 
// Die Zeichenfolgentypen reflektieren den eigentlichen Datentyp des Datenbankfelds
// (CStringA für ANSI-Datentypen und CStringW für Unicode-Datentypen),
// um zu verhindern, dass der ODBC-Treiber nicht erforderliche
// Konvertierungen ausführt. Sie können diese Member zu  CString-Typen ändern
//, damit der ODBC-Treiber alle erforderlichen Konvertierungen ausführt.
// Hinweis: Sie müssen mindenstens die ODBC-Treiberversion 3.5 verwenden,
// um Unicode und die Konvertierungen zu unterstützen.
 
    long    m_ID;
    CStringA    m_Text;
    CTime   m_Erstellt;

bzw. bei VC6
C++:
    // Feld-/Parameterdaten
    //{{AFX_FIELD(CHideSet, CRecordset)
    long    m_ID;
    CString m_Text;
    CTime   m_Erstellt;
    //}}AFX_FIELD

Ich persönlich finde den Code von VC6 übersichtlicher, da er aufgeräumter ist. Bei VC2003 muss man selbst für Ordnung sorgen.
Beachten Sie bitte auch den Kommentar von VC2003.

3.3 Der Konstruktor MSDN
C++:
1
2
3
4
5
6
7
8
9
CDemoSet::CDemoSet(CDatabase* pdb)
    : CRecordset(pdb)
{
    m_ID = 0;
    m_Text = "";
    m_Erstellt;
    m_nFields = 3;
    m_nDefaultType = snapshot;
}

Hier werden die Datenmember der einzelnen Spalten initialisiert.
m_nFields ist die Anzahl der Spalten des Recordsets. Hier muss man besonders aufpassen, wenn man die Tabelle später von Hand verändert. Vergisst man m_nFields, funktioniert es nicht.

3.4 GetDefaultConnect MSDN
C++:
CString CDemoSet::GetDefaultConnect()
{
    return _T("DSN=Datenquelle;UID=Username;PWD=passwort;APP=Microsoft\x00ae Visual Studio .NET;WSID=PC-NAME;DATABASE=Datenbankname;LANGUAGE=Deutsch");
}

Dies ist der Connectionstring.
Hier werden alle Daten festgelegt, die notwendig sind, um sich mit der Datenquelle zu verbinden. In meinem Fall ist es eine MSDE (Microsoft SQL Server).

3.5 GetDefaultSQL MSDN
C++:
CString CDemoSet::GetDefaultSQL()
{
    return _T("[dbo].[Tabellenname]");
}

Hier findet man den Tabellennamen der Tabelle(n), mit der (denen) das Recordset verbunden ist.
Das dbo.Tabellenname ist nur bei MS SQL so, daher kann es bei Ihnen etwas anders aussehen.

3.6 DoFieldExchange MSDN
C++:
1
2
3
4
5
6
7
8
9
10
11
void CDemoSet::DoFieldExchange(CFieldExchange* pFX)
{
    pFX->SetFieldType(CFieldExchange::outputColumn);
// Makros, z.B. RFX_Text() und RFX_Int(), sind vom Typ
// der Membervariablen abhängig, nicht vom Typ des Felds in der Datenbank.
// ODBC konvertiert den Spaltenwert automatisch in den angeforderten Typ.
    RFX_Long(pFX, _T("[ID]"), m_ID);
    RFX_Text(pFX, _T("[Text]"), m_Text);
    RFX_Date(pFX, _T("[Erstellt]"), m_Erstellt);
    RFX_Text(pFX, _T("[Bemerkung]"), m_strBemerkung, 500);
}

DoFieldExchange regelt, welche Spalte in welche Membervariable übertragen wird und wie.
Beachten Sie bitte die letzte Zeile: Bei mehr als 255 Zeichen in einem String muss man die Maximallänge angeben, sonst wird automatisch abgeschnitten.

4 Zugriffsfunktionen

Der Assistent legt die Membervariablen als public an.
Das ist soweit ganz praktisch, wenn man etwas quick & dirty fertig bekommen will.
Bei kleinen Hilfstools lasse ich das auch oft so, da ich sonst für die Zugriffsfunktionen mehr Zeit aufwenden würde als für den Rest des Projektes.

Bei größeren Projekten sollte man sich aber die Zeit für die Zugriffsfunktionen nehmen, da man sie oft genug damit später einsparen kann. Vor allem bei der Fehlersuche helfen sie enorm.

Was eine Get- bzw. Set-Funktion ist, muss ich Ihnen nicht mehr erklären, aber ich möchte Ihnen einige kleine Tricks zeigen, mit denen man sich viel erleichtern kann.

4.1 Get-/SetZahl mit möglichem NULL-Wert

Wenn eine Spalte NULL sein kann, müssen Sie darauf auf jeden Fall achten.
Ich habe das mit einem Hilfswert (-1) gelöst, den man der besseren Lesbarkeit halber als define ablegen sollte.
C++:
#define DB_NULL -1

C++:
1
2
3
4
5
6
7
8
inline long CDemoSet::GetZahl()
{
    if (IsFieldNull(&m_lZahl)) // Ist es NULL?
    {
        return DB_NULL; // Hilfswert zurückgeben
    }
    return m_lZahl;
}
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
inline bool CDemoSet::SetZahl(long f_lZahl)
{
    if (f_lZahl == DB_NULL) // Ist es der Hilfswert?
    {
        SetFieldNull(&m_lZahl); // Zahl auf NULL setzen
    }
    else // es ist eine reguläre Zahl
    {
        if (f_lZahl > 30) // ist sie im Wertebereich? (opt)
        {
            return false; // Fehler!
        }
        m_lZahl = f_lZahl;
    }
    return true;
}

In der Set-Funktion wird gleich noch eine weitere Aufgabe übernommen:
Der Wertebereich wird überprüft. Sollte dieser egal sein, können Sie die if-Anweisung natürlich weglassen.

4.2 SetZahl mit Controlparameter

Wenn Sie je nachdem, was aus einer ComboBox (Dropdown-Listenfeld) gewählt wurde, einen Wert speichern möchten, kann es sehr nervig werden, ständig die Abfragen wieder und wieder zu tippen. Angenehmer wird es z.B. so:
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool CDemoSet::SetZahl(CComboBox& f_cbxZahl)
{
    int nSel = f_cbxZahl.GetCurSel();
    if (nSel == -1) // Ist nichts gewählt?
    {
        SetFieldNull(&m_lZahl); // Zahl auf NULL setzen
    }
    else // es ist etwas gewählt
    {
        // Je nach Anwendung der Combobox entweder
        m_lZahl = nSel;
        // oder
        m_lZahl = f_cbxZahl.GetItemData(nSel);
    }
    return true;
}

Ihnen ist sicherlich aufgefallen, dass ich einmal mit ItemData arbeite und einmal nicht...
Da GetCurSel oft unzuverlässig ist (z.B. bei Sortierungen oder Umstellungen), arbeite ich lieber mit ItemData. Das kann ich besser kontrollieren.
Natürlich können Sie auch eine passende GetZahl-Funktion schreiben, die dann den richtigen Eintrag aus der Liste wählt.
Da dies aber nur eine kleine Fingerübung mit einer Schleife und einer if ist, überlasse ich das Ihnen. ;)

5 Datenzugriffe

Nun haben Sie genug Grundlagen um mit CRecordset zu arbeiten.
Sie werden sehen, es ist nicht schwer.

Zuerst brauchen Sie eine Instanz Ihrer Recordsetklasse. Das kann eine lokale oder auch eine Membervariable der Klasse sein.
Falls Sie mit CRecordView arbeiten, funktioniert alles so ähnlich. Sie werden es wiedererkennen. Ich mag CRecordView nicht, weil er mich in der Anwendung des Recordsets zu sehr einschränkt und alles so kompliziert erscheinen lässt.

Eine der größten Hürden für Anfänger ist es, sich von CRecordView zu lösen.
Einige denken sogar, man könne CRecordset nicht außerhalb von CRecordView verwenden. Das ist völliger Unsinn, Sie können CRecordset verwenden, wo Sie es brauchen bzw. wo Sie wollen.

5.1 Eine Tabelle einlesen

Auch wenn Sie vermutlich mit einer leeren Tabelle starten werden, möchte ich doch zuerst erklären, wie Sie Daten aus einer Tabelle abfragen können.

In diesem kleinen Beispiel haben wir eine Tabelle (Farben) mit zwei Spalten: ID (Zahl) und Bezeichnung (Text).
Den Inhalt dieser Tabelle möchten wir in einer ComboBox (m_cbxFarben) anzeigen.
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
CFarbenSet farbSet; // Instanz anlegen
farbSet.Open(); // Die Daten holen
 
if (!farbSet.IsBOF()) // Ist es nicht leer?
{
    while (!farbSet.IsEOF()) // Sind wir noch nicht fertig?
    {
        int nIdx = m_cbxFarben.AddString(farbSet.GetBezeichnung()); // Die Bezeichnung in die Liste packen
        m_cbxFarben.SetItemData(nIdx, farbSet.GetID()); // Die ID in ItemData merken
 
        farbSet.MoveNext(); // Nächste Zeile
    }
}

Das ist eine absolute Minimallösung, aber sie funktioniert und die gröbsten Fehler sind abgefangen.
Wenn Sie eine bessere Fehlerbehandlung einbauen möchten, lesen Sie diese Zusammenfassung von Artchi und schauen Sie dann in die MSDN, welche Exceptions Sie fangen müssen.

5.1.1 Filtern

In SQL kennen Sie sicher:
Code:
SELECT * FROM Tabelle_Soundso WHERE Bedingung

Mit CRecordset geht das natürlich auch, nur heißt es hier m_strFilter und ist ein CString.

Füllen Sie diesen String vor dem Datenholen einfach mit dem, was Sie bei SQL hinter das WHERE schreiben würden.
C++:
1
2
3
4
5
6
7
8
9
10
CFarbenSet farbSet;
long lID = 5;
farbSet.m_strFilter.Format(_T("[ID] = %d"), lID); // Bedingung setzen
// WHERE ID = 5
// oder
CString strBezeichnung = _T("Gelb");
farbSet.m_strFilter.Format(_T("[Bezeichnung] = \'%s\'"), strBezeichnung);
// WHERE Bezeichnung = 'Gelb'
farbSet.Open();
//...

Die eckigen Klammern können Sie auch weglassen. Es ist aber besser, sie zu verwenden.
Es ist ganz einfach, m_strFilter muss wirklich nur genau so aussehen, wie Sie es aus SQL kennen.
Sie können alle Befehle verwenden, die Sie kennen.
Wildcards und Unterabfragen sind ebenfalls kein Problem.

5.1.2 Sortieren

Das Sortieren funktioniert ähnlich wie das Filtern, nur dass hier die Variable m_strSort heißt und Sie den Teil aus dem SQL-Statement hinter ORDER BY verwenden.
C++:
CFarbenSet farbSet;
farbSet.m_strSort = _T("[Bezeichnung]");
// ORDER BY Bezeichnung (aufsteigend)
farbSet.m_strSort = _T("[Bezeichnung] DESC");
// ORDER BY Bezeichnung (absteigend)
farbSet.Open();
//...

Auch hier gilt: Sie dürfen alles verwenden, was SQL Ihnen an Möglichkeiten bietet.

5.2 Eine neue Zeile schreiben

Nun soll die Tabelle aber nicht ewig leer bleiben.
Um eine neue Zeile hinzuzufügen brauchen Sie eine geöffnete Instanz Ihrer Recordsetklasse.
C++:
1
2
3
4
5
6
7
8
9
10
11
CFarbenSet farbSet; // Instanz anlegen
farbSet.Open(); // Die Daten holen
 
farbSet.AddNew(); // Neue leere Zeile anfügen
// Zeile füllen
farbSet.SetBezeichnung(_T("Blau"));
if (!farbSet.Update()) // Die Änderungen schreiben
{
    AfxMessageBox(_T("Es wurde nicht gespeichert."));
}
farbSet.Requery(); // Bei Bedarf neu laden


5.3 Eine Zeile verändern

Wenn Sie eine Zeile verändern möchten, müssen Sie zuerst ihre Daten in das Recordset laden. Der Rest funktioniert wie in Kapitel 5.2.
C++:
1
2
3
4
5
6
7
8
9
10
11
12
CFarbenSet farbSet;
farbSet.Open();
farbSet.Suche("Gruen"); // Cursor positionieren (siehe 5.5)
 
farbSet.Edit(); // Diese Zeile bearbeiten
// Daten verändern
farbSet.SetBezeichnung(_T("Grün"));
if (!farbSet.Update())
{
    AfxMessageBox(_T("Es wurde nicht gespeichert."));
}
farbSet.Requery();


5.4 Eine Zeile löschen

Eine Zeile löschen ist dem Verändern sehr ähnlich.
C++:
1
2
3
4
5
6
7
8
9
10
CFarbenSet farbSet;
farbSet.Open();
farbSet.Suche("Grün");
 
farbSet.Delete(); // Diese Zeile löschen
if (!farbSet.Update())
{
    AfxMessageBox(_T("Es wurde nicht gespeichert."));
}
farbSet.Requery();


5.5 Eine Zeile suchen

Oft müssen Sie den Cursor auf eine bestimmte Zeile positionieren. Daher bietet es sich an, eine Funktion dafür zu schreiben.
C++:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bool CFarbenSet::Suche(CString strBezeichnung)
{
    // Stimmt die Kombination?
    if ((m_strBezeichnung == strBezeichnung) && (!IsEOF()))
    {
        // Schon gefunden
        return true;
    }
    else
    {
        // Einmal über alle Datensätze laufen
        if (IsBOF())
        {
            // Das Recordset ist leer
        }
        else
        {
            // Vorne anfangen
            MoveFirst();
            while(!IsEOF())
            {
                // Stimmt die Kombination?
                if (m_strBezeichnung == strBezeichnung)
                {
                    // Fertig und raus
                    return true;
                }
                MoveNext(); // Weiter
            }
        }
    }
}


6 Funktionsübersicht MSDN

Open MSDN

Öffnet das Recordset und lädt die Daten aus der Datenbank.
Der Cursor steht auf dem ersten Datensatz, sofern einer vorhanden ist.
Schlägt fehl, wenn das Recordset bereits offen ist.

Requery MSDN

Lädt die Daten aus der Datenbank neu in das Recordset.
Schlägt fehl, wenn das Recordset (noch) geschlossen ist.

Close MSDN

Schließt das Recordset.

AddNew MSDN

Fügt eine neue Zeile an das Recordset an, die man danach füllen und speichern kann.
Das Recordset muss offen sein.

Edit MSDN

Schaltet die aktuelle Zeile zum Ändern frei, danach kann man ihre Inhalte ändern und speichern.
Das Recordset muss offen sein.

Delete MSDN

Löscht die aktuelle Zeile.
Danach muss noch gespeichert werden.
Das Recordset muss offen sein.

Update MSDN

Speichert das aktuelle Recordset in der Datenbank.
Das Recordset muss offen sein.

Move

Das Recordset muss offen sein.
Ist das Recordset leer, gibt es einen Fehler.


IsBOF MSDN

Gibt TRUE zurück, wenn der Cursor vor dem ersten Datensatz steht.
Das Recordset muss offen sein.

IsEOF MSDN

Gibt TRUE zurück, wenn der Cursor hinter dem letzten Datensatz steht.
Das Recordset muss offen sein.

IsOpen MSDN

Gibt TRUE zurück, wenn das Recordset offen ist.

Literatur:

ISBN:382668107X S. 629 ff
ISBN:3827220351 Kapitel 14
Grundlagen zu Exceptions

Sie können Kommentare zu diesem Artikel im Forum schreiben. (Eine Registrierung ist nicht notwendig.)

Logo-Design: MastaMind Webdesign