[X] Datenbankzugriff mit CRecordset (und CDatabase) - Teil 1
-
Da es aktuell im MFC Forum wieder einige Fragen darüber gab und ich schon öfters Intensivkurse per Forum, Mail oder Chat gegeben habe, dachte ich mir, es wäre doch was, einen Artikel zu CRecordset zu haben.
Das Kapitel aus VC in 21 Tagen ist ja ganz nett - aber man kommt schnell nicht weiter.
Der Recordview macht auch mehr Probleme als dass er hilft...Und da ich jetzt 2 Jahre intensiv damit gearbeitet habe, wollte ich meinen Kenntnisstand mal weitergeben.
Es fehlt definitiv viel try & catch, weil ich mich da noch nicht eingearbeitet habe - aber ich prüfe anders und würde das auch erklären.Ich würde in dem Artikel (wenns zu lang wird, mache ich ne Serie)
- die Grundlagen vermitteln (lesen, schreiben, löschen)
- eine eigene Basisklasse erstellen, die oft Benötigtes übernimmt
- zeigen, wie man eine DB-Instanz für das komplette Programm nutzt
- zeigen, was man alles Modifizieren kann und wo
- Probleme zeigen, die auftreten und wenn bekannt auch die Lösung
So, das fällt mir jetzt spontan ein.
Was haltet ihr davon?
PS: Da ich mit SQL-Server arbeite, wird es darauf spezialisiert sein, aber auf "Umzüge" zwischen DBMS werde ich auch zu sprechen kommen. (Hab ich dreimal hinter mir.)
-
Schau mal auf meine Website unter Fehlerbehandlung, da hab ich einen relativ kurzen und doch informativen Bericht zu try/catch bzw. Exceptions geschrieben.
http://www.kharchi.de/cppratgeber3.htm
Über Exceptions sollte man schon bescheid wissen, da sie wirklich einem das Leben erleichtern, da sie den Code verbessern. Ist wirklich nicht schwer die Sache mit den Exceptions zu verstehen.
Ansonst: Datenbank-Anbindung ist immer ein Thema, was in jeder Business-Anwendung gebraucht wird. Ist auf jeden Fall ein Artikel der für viele interessant wäre!
-
Den Link guck ich nachher mal an.
Es ist nicht so, dass ich nicht weiß, wie das funktioniert, so Grundlagen kann ich und nutze es auch - ich mag es nur nicht und nutze es nicht überall da, wo man könnte (oder sollte?).
Vielleicht schaut ja jemand mit auf den Artikel und wir ergänzen dann so lange, bis es wirklich komplett und bombensicher ist.
Eigentlich spricht ja nichts gegen 2 Autoren, oder?
Und ich lerne gerne dazu - auch wenn ich es vielleicht nicht mehr überall im Projekt nacharbeite.
-
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
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
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
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
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
Open
Ö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
Läd die Daten aus der Datenbank neu in das Recordset.
Schlägt fehl, wenn das Recordset (noch) geschlossen ist.Close
Schließt das Recordset.
AddNew
Fügt eine neue Zeile an das Recordset an, die man danach füllen und speichern kann.
Das Recordset muss offen sein.Edit
Schaltet die aktuelle Zeile zum Ändern frei, danach kann man ihre Inhalte ändern und speichern.
Das Recordset muss offen sein.Delete
Löscht die aktuelle Zeile.
Danach muss noch gespeichert werden.
Das Recordset muss offen sein.Update
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
Gibt TRUE zurück, wenn der Cursor vor dem ersten Datensatz steht.
Das Recordset muss offen sein.IsEOF
Gibt TRUE zurück, wenn der Cursor hinter dem letzten Datensatz steht.
Das Recordset muss offen sein.IsOpen
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
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
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
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
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
Open
Ö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
Läd die Daten aus der Datenbank neu in das Recordset.
Schlägt fehl, wenn das Recordset (noch) geschlossen ist.Close
Schließt das Recordset.
AddNew
Fügt eine neue Zeile an das Recordset an, die man danach füllen und speichern kann.
Das Recordset muss offen sein.Edit
Schaltet die aktuelle Zeile zum Ändern frei, danach kann man ihre Inhalte ändern und speichern.
Das Recordset muss offen sein.Delete
Löscht die aktuelle Zeile.
Danach muss noch gespeichert werden.
Das Recordset muss offen sein.Update
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
Gibt TRUE zurück, wenn der Cursor vor dem ersten Datensatz steht.
Das Recordset muss offen sein.IsEOF
Gibt TRUE zurück, wenn der Cursor hinter dem letzten Datensatz steht.
Das Recordset muss offen sein.IsOpen
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.