Datenbankzugriff mit CRecordset (und CDatabase) - Teil 1



  • 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.

    • was der ganze automatisch erstellte Code bedeutet.
    • wie sie statische Recordsets benutzen um Daten zu lesen und zu manipulieren.
    • welche Stolperfallen es gibt und wie Sie sie vermeiden können.
    • wie sie Recordsets im Nachinein verändern können. (Teil 2)
    • wie Sie sich das Leben mit einer eigenen Basisklasse vereinfachen können. (Teil 2)

    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

    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:

    #include <afxdb.h>
    

    3.2 Die Membervariablen für die Spalten

    VC 2003:

    // 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

    // 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

    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

    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

    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

    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.

    #define DB_NULL -1
    
    inline long CDemoSet::GetZahl()
    {
    	if (IsFieldNull(&m_lZahl)) // Ist es NULL?
    	{
    		return DB_NULL; // Hilfswert zurückgeben
    	}
    	return m_lZahl;
    }
    
    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:

    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.

    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:

    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.

    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.

    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.

    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.

    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.

    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.

    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.

    • Move MSDN
      Bewegt den Cursor wie angegeben.
    • MoveFirst MSDN
      Setzt den Cursor auf die erste Zeile.
    • MovePrev MSDN
      Setzt den Cursor auf die voherige Zeile.
    • MoveNext MSDN
      Setzt den Cursor auf die nächste Zeile.
    • MoveLast MSDN
      Setzt den Cursor auf die letzte Zeile.

    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:

    Visual C++ .NET | ISBN: 382668107X S. 629 ff
    Visual C++ 6 in 21 Tagen | ISBN: 3827220351 Kapitel 14
    Grundlagen zu Exceptions



  • Moin,
    nachdem ich jetzt Teil 1 verstanden habe, würde ich gern weiterlesen.

    Wann wird das wohl gehen ??

    Mfg



  • Verwegener schrieb:

    Wann wird das wohl gehen ??

    Irgendwann nächstes Jahr.
    Dieses Jahr komme ich wohl nicht mehr zum Schreiben. 😞

    Du kannst mir aber trotzdem gerne mal mailen, was so vorkommen sollte. (Das dürfen alle anderen auch.)
    Es soll ja so vollständig wie möglich sein. 🙂



  • Ich wünsche mir auch einen zweiten Teil 🙂

    Vielen vielen Dank für diesen Artikel. Er hat mir unglaublich geholfen
    den Weg in Richtung Datenbank zu gehen und zu verstehen.

    👍



  • Im MFC Forum meinten vor ein paar Tagen 2 Experten das CRecordset nicht zu empfehlen ist.



  • :o schrieb:

    Im MFC Forum meinten vor ein paar Tagen 2 Experten das CRecordset nicht zu empfehlen ist.

    Tja, wer was anderes kann, darf gerne darüber schreiben. 😉



  • Hallo, ich würde auch gerne eine Anwendung erstellen ohne CRecordview, aber wenn ich mit dem Klassenassistenten erstelle und am Ende statt Recordview eine andere View erstelle dann ist leider keine Set Klasse mehr vorhanden die von CRecordset abgeleitet wird.

    Wenn ich es nachträglich erstelle die Klasse funzt gar nicht´s mehr. gibt es eine Lösung?

    danke im Voraus.

    Mfg

    Özkan



  • Oka81 schrieb:

    Wenn ich es nachträglich erstelle die Klasse funzt gar nicht´s mehr. gibt es eine Lösung?

    Klar, aber stell die Frage bitte im MFC Forum, denn die Lösung könnte länger werden.

    Zuerst einmal solltest du "funzt gar nicht´s mehr" genauer erklären, bei mir geht es nämlich.



  • [quote="estartu"]

    Verwegener schrieb:

    Irgendwann nächstes Jahr.

    Dein nächstes Jahr ist bald zuende 😉 Wann kommt denn nun was???? *grins*

    Liebe Grüsse,

    Asmo



  • Momentan keine Chance. 😞
    Ich komme zu nix und zu dem Thema was ich gerne behandeln würde bin ich immer noch nicht schlauer: Dynamisch Tabellen anbinden.
    Sollte sich ein anderer Autor finden würde ich mich aber freuen.



  • Ich finde deinen -1 Hack für DB_NULL nicht gerade toll. Ich kann zwar verstehen, dass man mit der MFC zwangsweise in den Frickelmode schaltet, aber ich will trotzdem eine Alternative aufzeigen: boost::optional, das ist ein Wrapper um einen Typ welchen man überprüfen kann ob der gewrappte Typ sinnvoll initialisiert ist, d.h. man kann damit den NULL-Wert aus der DB sehr schön nachbilden im Programm 🙂



  • Könnte mir jemand sagen, wie ich in den ConnectionString eine vorher initialisierte Variable einfüge?

    Beispiel:

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

    Das klappt z.b. nicht. Auch mit & klappt es nicht. Danke schonmal im voraus!



  • Ich weiß nicht mehr ganz sicher, ob das hier klappt:

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

    Ansonsten: halte den Connectionstring als CString vor und füge den Datenbanknamen mit der CString::Format-Methode ein



  • Vielen Dank! Mit der CString::Format-Methode funktioniert es 🙂


Log in to reply