[X] 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 und lernt es nach und nach.
    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, wenn Sie in der Datenbank keine Beziehungen 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 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.

    inline long CDemoSet::GetZahl()
    {
    	if (IsFieldNull(&m_lZahl)) // Ist es NULL?
    	{
    		return -1; // Hilfswert zurückgeben
    	}
    	return m_lZahl;
    }
    
    inline bool CDemoSet::SetZahl(long f_lZahl)
    {
    	if (f_lZahl == -1) // 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 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, da ich es besser kontrollieren kann.
    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äd 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äd 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: 382668107XS. 629 ff
    Visual C++ 6 in 21 Tagen | ISBN: 3827220351Kapitel 14
    Grundlagen zu Exceptions



  • So, da ich jetzt auch mal wieder dazu komme, geht es hier weiter. 🙂
    Das, was bis jetzt da steht, wäre soweit fertig - nur falls es jemand mal kommentieren möchte.

    Es geht dann weiter mit Lesen und Schreiben und dann kommen solche Leckerlis wie eigene Basisklasse oder Controlfüttern (evtl.).



  • Mag sich jetzt jemand dazu äußern?
    Ich guck auch nochmal drüber und sonst geht er in die Korrekturen.

    PS: Die Basisklasse kommt wirklich zusammen mit anderen Sachen in Teil 2.
    Dafür fehlt noch die Anwendung selbst.
    Der muss wohl doch nochmal auf A. 🙄



  • So, Teil 1 ist meiner Meinung nach soweit fertig.
    Bitte sagt was dazu. 🙂



  • So, hab zwar keine Ahnung von den MFC, aber das sag ich dazu:
    1. Der Link zu dem Kapitel 14 von VC in 21 Tagen geht nicht, anscheinend hab ich nicht die benötigten Berechtigungen.
    2. Bei 3.2 solltest du vllt. noch dazuschreiben, dass der erste Codeblock für VC 03 ist.
    3. Wieso initialisierst du bei 3.3 nicht alles im Elementinitializer?
    4. Bei 4.1 komm ich nicht ganz mit... Also Absicht ist wohl, zu prüfen, ob Spalten NULL sein können, ja? Wieso dann nen Hilfswert wie -1 verwenden, wenn du das auch durch NULL signalisieren könntest? 😕

    Ansonsten 👍



  • Arg, ich hab Filtern und Sortieren vergessen. 😮
    Mach ich nachher noch rein.

    GPC schrieb:

    So, hab zwar keine Ahnung von den MFC, aber das sag ich dazu:
    1. Der Link zu dem Kapitel 14 von VC in 21 Tagen geht nicht, anscheinend hab ich nicht die benötigten Berechtigungen.

    Arg, der muss raus, das Buch ist nur noch für Studenten, hab schon mit dem Webmaster da gemailt. 😞

    2. Bei 3.2 solltest du vllt. noch dazuschreiben, dass der erste Codeblock für VC 03 ist.

    Ups, da war wohl ein Textfresser unterwegs, danke.

    3. Wieso initialisierst du bei 3.3 nicht alles im Elementinitializer?

    Weil das der Assistent so hinschreibt, wie es da steht.

    4. Bei 4.1 komm ich nicht ganz mit... Also Absicht ist wohl, zu prüfen, ob Spalten NULL sein können, ja? Wieso dann nen Hilfswert wie -1 verwenden, wenn du das auch durch NULL signalisieren könntest? 😕

    Bei einem long? Wie?

    Ansonsten 👍

    Danke 🙂



  • estartu schrieb:

    3. Wieso initialisierst du bei 3.3 nicht alles im Elementinitializer?

    Weil das der Assistent so hinschreibt, wie es da steht.

    okay, is aber auch nich die feine englische Art von dem Assi 😉

    4. Bei 4.1 komm ich nicht ganz mit... Also Absicht ist wohl, zu prüfen, ob Spalten NULL sein können, ja? Wieso dann nen Hilfswert wie -1 verwenden, wenn du das auch durch NULL signalisieren könntest? 😕

    Bei einem long? Wie?

    Hm, du könntest 0 zurückgeben, aber dann müsstest du ja trotzdem prüfen und dann wären wir gleich weit wie jetzt... denkfehler meinerseits 😞



  • Bitte nimm noch was zu SQLConfigDataSource dazu damit die Leute auch wissen wie man ne Datenquelle registriert. Nach der funktion sucht man (jeden falls hab ich es) stunden wenn man irgend was mit ODBC registrieren sucht und nicht mit SQL... 🙄



  • GPC schrieb:

    estartu schrieb:

    3. Wieso initialisierst du bei 3.3 nicht alles im Elementinitializer?

    Weil das der Assistent so hinschreibt, wie es da steht.

    okay, is aber auch nich die feine englische Art von dem Assi 😉

    public Datenmember ja auch nicht, macht er trotzdem. 😉

    4. Bei 4.1 komm ich nicht ganz mit... Also Absicht ist wohl, zu prüfen, ob Spalten NULL sein können, ja? Wieso dann nen Hilfswert wie -1 verwenden, wenn du das auch durch NULL signalisieren könntest? 😕

    Bei einem long? Wie?

    Hm, du könntest 0 zurückgeben, aber dann müsstest du ja trotzdem prüfen und dann wären wir gleich weit wie jetzt... denkfehler meinerseits 😞

    Schade, ich dachte ich könnte was lernen.

    Ich habe deswegen -1 genommen (und nicht 0), weil ich afair nur mit positiven Zahlen arbeite. IDs, Comboboxenindexe usw.
    An den Stellen, wo das so nicht geht, muss man sich mit anderen Funktionen (SetBlablaNULL()) behelfen - aber ich hoffe darauf kommen die Leute selber. 😉



  • CMatt schrieb:

    Bitte nimm noch was zu SQLConfigDataSource dazu damit die Leute auch wissen wie man ne Datenquelle registriert. Nach der funktion sucht man (jeden falls hab ich es) stunden wenn man irgend was mit ODBC registrieren sucht und nicht mit SQL... 🙄

    In Teil 2, okay? 🙂
    Hier sollen erstmal nur die absoluten Grundlagen hin, dass man so halbwegs damit arbeiten kann.



  • So, ich müsste alle Anmerkungen drin haben und Filtern und Sortieren auch (Kapitel 5.1.1 + 5.1.2).
    Reicht das? Besonders zum Filtern wird ja viel gefragt. 🙄



  • estartu schrieb:

    GPC schrieb:

    4. Bei 4.1 komm ich nicht ganz mit... Also Absicht ist wohl, zu prüfen, ob Spalten NULL sein können, ja? Wieso dann nen Hilfswert wie -1 verwenden, wenn du das auch durch NULL signalisieren könntest? 😕

    Bei einem long? Wie?

    Hm, du könntest 0 zurückgeben, aber dann müsstest du ja trotzdem prüfen und dann wären wir gleich weit wie jetzt... denkfehler meinerseits 😞

    Schade, ich dachte ich könnte was lernen.

    Na ja, also NULL kann man nem long zuweisen, ich weiß nur nicht, inwiefern das dann praktikabel bleibt, kenn mich wie gesagt damit nicht aus...

    inline long CDemoSet::GetZahl()
    {
        return m_lZahl;  //Entweder wird 0 oder ne "richtige" Zahl zurückgegeben
    }
    
    inline bool CDemoSet::SetZahl(long f_lZahl)
    {
        if (f_lZahl == 0) // In dem Fall mal auf null prüfen
        {
            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;
    }
    


  • Also, wenn du ohne IsFieldNull Prüfung einfach die Zahl rausgibst und die ist NULL, dann ist die nicht 0.
    Die ist dann ein wirrer Zahlensalat. (Im Prinzip wie bei nem Zeiger.)

    Und wenn du 0 als reservierten Wert für NULL nimmst, wie arbeitest du dann mit ner echten 0? 😕

    Man könnte das noch insoweit verändern, dass man sich ein DB_NULL macht.

    #define DB_NULL -1
    //...
        if (f_lZahl == DB_NULL)
        {
            SetFieldNull(&m_lZahl);
        }
    

    Besser? 🙂



  • estartu schrieb:

    Also, wenn du ohne IsFieldNull Prüfung einfach die Zahl rausgibst und die ist NULL, dann ist die nicht 0.
    Die ist dann ein wirrer Zahlensalat. (Im Prinzip wie bei nem Zeiger.)

    ah ja, dachte es wäre 'n "echtes NULL.

    Und wenn du 0 als reservierten Wert für NULL nimmst, wie arbeitest du dann mit ner echten 0? 😕

    Wenn 0 als reguläre Zahl vorkommt, wird das schwierig. Allerdings kann ich mir das jetzt schwerlich vorstellen, da in C(++) NULL und 0 so gut wie immer "äquivalent" waren, pointer z.B. kann man ja sowohl auf NULL und 0 testen.

    Man könnte das noch insoweit verändern, dass man sich ein DB_NULL macht.

    #define DB_NULL -1
    //...
        if (f_lZahl == DB_NULL)
        {
            SetFieldNull(&m_lZahl);
        }
    

    Besser? 🙂

    Das ist cool, und erhöht die Lesbarkeit 👍



  • GPC schrieb:

    Und wenn du 0 als reservierten Wert für NULL nimmst, wie arbeitest du dann mit ner echten 0? 😕

    Wenn 0 als reguläre Zahl vorkommt, wird das schwierig. Allerdings kann ich mir das jetzt schwerlich vorstellen, da in C(++) NULL und 0 so gut wie immer "äquivalent" waren, pointer z.B. kann man ja sowohl auf NULL und 0 testen.

    Ja, kommt drauf an, wie man es verwendet.

    Nimm eine Anzahl, die optional ist.
    Oder einen Arrayindex.
    Es gibt genug Stellen, wo man die 0 braucht, ist erstaunlich.
    (Ich hab die Anpassung von 0 auf -1 hinter mir... wünsche ich keinem!)



  • 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 und lernt es nach und nach.
    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, wenn Sie in der Datenbank keine Beziehungen 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 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 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, da ich es besser kontrollieren kann.
    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äd 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äd 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



  • Es gibt in C++ kein NULL-Pointer, oder besser gesagt ein syntaktisches NULL. Es gibt nur integer 0. Beispiel:

    void foo(int *a);
    void foo(int a);
    

    Und jetzt:

    foo(NULL);
    

    Was meint ihr was da passiert? Es wird einen error geben, weil hinter NULL eine 0 steckt und der Compiler nicht weiß, welches foo gemeint ist.

    Im nächsten C++ Standard wird es wohl ein echtes NULL-Schlüsselwort geben, welches auch eine Semantik hätte.

    foo(nullptr);
    

    Der Compiler würde das erste foo aufrufen.



  • Da wohl keiner mehr was zu sagen hatte, geht der Artikel jetzt auf R. 🙂



  • `Hab ein wenig korregiert:

    ===============================`

    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, wenn Sie in der Datenbank keine Beziehungen gesetzt haben.
    ~Der Satz ist unverständlich~

    3 Was bedeutet der automatisch generierte Code?

    [...]

    3.1 Klassenkopf

    [...]

    [...]

    3.2 Die Membervariablen für die Spalten

    [...]
    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

    [...]
    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

    [...]

    [...]

    3.5 GetDefaultSQL

    [...]

    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

    [...]

    [...]

    4 Zugriffsfunktionen

    [...]

    Bei größeren Projekten sollte man sich aber die Zeit nehmen, ~wofür Zeit 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

    [...]

    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

    [...]

    [...]

    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

    [...]

    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.

    [...]

    5.1 Eine Tabelle einlesen

    [...]

    [...]

    [...]

    5.1.1 Filtern

    [...]

    [...]

    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

    [...]

    5.2 Eine neue Zeile schreiben

    [...]

    5.3 Eine Zeile verändern

    [...]

    5.4 Eine Zeile löschen

    [...]

    5.5 Eine Zeile suchen

    [...]

    6 Funktionsübersicht

    MSDN

    Open

    MSDN

    [...]

    Requery

    MSDN

    [...]

    Close

    MSDN

    [...]

    AddNew

    MSDN

    [...]

    Edit

    MSDN

    [...]

    Delete

    MSDN

    [...]

    Update

    MSDN

    [...]

    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[/quote]



  • 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äd 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äd 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


Anmelden zum Antworten