4 27 Klasy baz danych MFC (2)


Rozdział 27.
Klasy baz danych MFC


W tym rozdziale:

Obiekt CDatabase
Obiekt CRecordset
Opis klas baz danych MFC
Tworzenie prostej narzędziowej aplikacji wykorzystującej klasy baz danych MFC
Nauka dostępu do parametryzowanego rekordsetu
Nauka wywoływania parametryzowanej kwerendy Accessa zwracającej dane


W rozdziale 26. zapoznałeś się z ODBC (Open Database Connectivity) i poznałeś przyczyny, które doprowadziły do jego powstania, czyli konieczność tworzenia jednolitego interfejsu API do dostępu do różnych baz danych. Choć ODBC z pewnością ma wiele zalet, jednak nie jest pozbawione również wad. Jedną z takich wad jest ilość kodu konieczna do przeprowadzenia nawet najprostszej operacji na bazie danych. Załóżmy, że Twoja aplikacja ma zrobić coś tak prostego jak odczytanie wszystkich wierszy ze wskazanej tabeli. Zanim aplikacja korzystająca z ODBC będzie mogła w ogóle odczytać dane z tabeli, musi uzyskać uchwyt środowiska, poinformować ODBC o wersji ODBC, z której chce korzystać, uzyskać uchwyt połączenia z bazą danych oraz uzyskać uchwyt polecenia. Dorzuć do tego jeszcze kod zwalniający wszystkie te uchwyty i sprawdzający poprawność wywołań, a otrzymasz około stu linii kodu, mimo że jeszcze nawet nie zacząłeś odczytywać danych!
Z tego powodu Microsoft bardzo szybko zorientował się, co się dzieje i, poczynając od MFC w wersji 1.5, wprowadził zestaw klas MFC reprezentujących funkcje ODBC. Te klasy noszą ogólną nazwę klas baz danych i stanowią temat tego rozdziału. Zwróć uwagę, że ponieważ klasy baz danych MFC reprezentują interfejs ODBC SDK, dobrze byłoby, jeśli nie znasz ODBC, abyś zapoznał się z treścią rozdziału 26., przynajmniej po to, aby opanować terminologię, której będziemy używali w tym rozdziale.
W tym rozdziale poznasz najpierw cztery główne klasy baz danych, CDatabase, CRecordset, CRecordView oraz CDatabaseException. Gdy zapoznasz się z tymi klasami i ich funkcjami, przejdziemy do omawiania dwóch przykładowych programów. Pierwszy z nich to w pełni funkcjonalna aplikacja narzędziowa wykorzystująca klasy baz danych MFC. Ta aplikacja ma wszelkie możliwości tradycyjnych aplikacji narzędziowych
(tworzenie, odczytywanie, aktualizacja i usuwanie) przeznaczonych do manipulowania tabelami. Drugi program to w rzeczywistości dwa programy demonstracyjne w jednym. W pierwszej części nauczysz się używać parametryzowanych rekordsetów w celu pobierania danych z baz danych Microsoft Access, zaś w drugiej części poznasz sposób wywoływania parametryzowanej kwerendy Accessa zwracającej dane.
Klasa CDatabase
Obiekt CDatabase reprezentuje połączenie ze źródłem danych. Po skonstruowaniu obiektu CDatabase w wywołaniu funkcji składowej open ( ) lub openEx ( } należy podać nazwę źródła danych (DSN, data source name). Połączenie ze źródłem danych odpowiadającej DSN-owi następuje w momencie wywołania którejś z tych funkcji. Obiekt CDatabase jest używany zwykle w połączeniu z jednym lub kilkoma rekordsetami (które omówimy za chwilę), lecz może być wykorzystywany również samodzielnie. Przykładem użycia obiektu CDatabase bez korzystania z obiektu CRecordset może być wydawanie takich poleceń SQL dla źródła danych, które nie powodują zwrócenia żadnych danych. Właśnie do tego służy funkcja CDatabase: :ExecuteSQL (). Oto przykład pokazujący, jak łatwo jest za pomocą klasy CDatabase wstawić rekord do tabeli:
try
{
CDatabase db;
if (db.Open("Visual C++ 6 Bibie"))
{
db.ExecuteSQL("INSERT INTO UserMaster "
"VALUES ( 'TestlD' , 'Testowa nazwa użytkownika', 0)");

db.Close () ;
}
}
catch(CDBException* pe) {
AfxMessageBox (pe->m_strError) ;
pe->Delete () ;
}
Porównaj ten fragment kodu z ponad 50 liniami kodu ODBC SDK potrzebnymi do osiągnięcia tego samego zadania w rozdziale 26.! Klasy baz danych, choć nie tak wydajne jak bezpośrednie użycie ODBC SDK, z punktu widzenia programisty są dużo prostsze i szybsze w użyciu niż ODBC SDK. Poświęćmy minutę i przyjrzyjmy się dokładniej poprzedniemu fragmentowi kodu. Widzimy blok try/catch.
try
{
...
}
catch(CDBException* pe)
{
AfxMessageBox(pe->m_strError); pe->Delete();
}
Prawie wszystkie funkcje składowe klasy CDatabase zgłaszają w przypadku błędu wyjątki typu CDBException. Klasa CDBException, wyprowadzona z klasy CEKception, nie wprowadza do niej prawie niczego nowego. W rzeczywistości, poza zmiennymi składowymi odziedziczonymi od klasy bazowej, klasa CDBEKCeption nie definiuje żadnej funkcji składowej. Trzy zmienne składowe służą jedynie poinformowaniu aplikacji o przyczynie zgłoszenia wyjątku. Oto te trzy zmienne składowe oraz ich zastosowanie przy wyznaczaniu przyczyny wyjątku:
m_nRetCode - przyczyna zgłoszenia wyjątku w formie kodu zwrotnego ODBC
(typu SQLRETURN).
m_strError - łańcuch opisujący błąd, który spowodował zgłoszenie wyjątku.
m_strStateNativeOrigin - łańcuch opisujący błąd, który spowodował zgłoszenie wyjątku w formie opisu kodu błędu ODBC.
Ponieważ większość funkcji składowych klasy CDatabase może zgłaszać wyjątki typu CDBEKCeption, dobrze jest w którymś miejscu kodu zastosować blok try/catch wychwytujący wyjątki tej klasy (lub jej klasy bazowej CEKception).
Następna linia w poprzednim przykładzie konstruuje obiekt CDatabase i otwiera połączenie ze źródłem danych reprezentowanym przez DSN. Jak widać, nazwą źródła danych jest "Visual C++ 6 Bibie". Ta wartość, w celu zachowania prostoty, została zaszyta w kodzie, ale zwykle będziesz j ą podawał jako typedef, const lub zasób łańcucha.
CDatabase db;
if(db.Open("Yisual C++ 6 Bibie"))
...
Konstruktor klasy CDatabase nie robi zbyt wiele. Jak już wspominaliśmy, połączenie ze źródłem danych jest tworzone dopiero w momencie wywołania funkcji Open() lub OpenEK (). Tak więc skoncentrujmy się na funkcji Open ().
virtual BOOL Open( LPCTSTR IpszDSN,
BOOL bEKclusive = FALSE, BOOL bReadOnly = FALSE, LPCTSTR IpszConnect = "ODBC:", BOOL bUseCursorLib = TRUE );

throw( CDBEKCeption, CMemoryEKception );
Jak widać z tego prototypu, aplikacja może przekazać funkcji kilka argumentów, ale tak wymagany jest tylko pierwszy z nich. We wcześniejszym przykładzie pierwszy argument był jedynym i określał nazwę źródła danych.
^Argument bEKclusive określa, czy mogą być otwierane inne połączenia ze źródłem "danych. Ten argument nie był obsługiwany we wcześniejszych wersjach Visual C++, a dopiero wersja 6.0 obsługuje wartość TRUE dla tego argumentu.
Argument bReadOnly pozwala aplikacji na wskazanie, czy podczas połączenia będzie możliwe aktualizowanie danych w bazie. Domyślną wartością tego argumentu jest FALSE, oznaczająca, że poprzez to połączenie będzie możliwe dokonywanie zmian w bazie danych. Tę wartość automatycznie przejmują wszystkie obiekty CRecordset tworzone i dołączone do tego obiektu CDatabase.
Argument ipszConnect umożliwia aplikacji wskazanie łańcucha połączenia ODBC, używanego, gdy kod MFC klasy CDatabase próbuje połączyć się ze wskazanym źródłem danych poprzez ODBC. Jeśli ten argument ma być użyty, jako argument ipszDSN należy przekazać wartość NULL. Zaletą korzystania z tego argumentu jest elastyczność, gdyż w ten sposób można na przykład przekazać nazwę użytkownika i hasło dostępu do bazy danych. Format tego argumentu określa używany sterownik ODBC.
Na koniec, argument bUseCursorLib określa, czy ma być użyta biblioteka kursora ODBC. Jeśli ten argument ma wartość TRUE (domyślną), dla tego połączenia są dozwolone jedynie statyczne obrazy danych lub jedynie przewijanie w przód. Aby móc użyć dynastów, należy w tym argumencie przekazać wartość FALSE. Po otwarciu bazy danych następne linie wykonuj ą polecenie SQL i zamykają bazę:
db.ExecuteSQL("INSERT INTO UserMaster "
"VALUES('TestlD', 'Testowa nazwa użytkownika',0)");

db.Close();

Funkcja CDatabase: :ExecuteSQL () ma bardzo prostą składnię. Aplikacja nie musi robić niczego więcej poza zwykłym przekazaniem łańcucha polecenia SQL, które ma być wykonane na źródle danych. Zwróć uwagę, że ta funkcja powinna być używana tylko wtedy, gdy aplikacja przewiduje, że w wyniku danego polecenia SQL nie zostaną zwrócone żadne dane.
Jak już wspominaliśmy, alternatywną metodą otwierania bazy danych jest zastosowanie funkcji składowej CDatabase: :OpenEx (). W rzeczywistości, właśnie ta metoda jest zalecana w dokumentacji MFC jako sposób otwierania połączenia z bazą danych:
virtual BOOL OpenEx(LPCTSTR IpszConnectString, DWORD dwOptions = 0 ) ;

throw( CDBException, CMemoryException );

Funkcja CDatabase: : OpenEx () wymaga jedynie dwóch argumentów: łańcucha połączenia oraz wartości DWORD reprezentującej opcje połączenia. Argument IpszConnectString jest używany do przekazywania funkcji łańcucha połączenia ODBC, podczas gdy argument dwOptions określa sposób tworzenia tego połączenia. Jako opcji otwierania bazy można użyć poniższych wartości, łącząc je za pomocą operatora logicznego LUB:

CDatabase::openExclusive CDatabase::openReadOnly CDatabase::useCursorLib CDatabase::noODBCDialog CDatabase::forceODBCDialog

Te wartości działają dokładnie tak jak w funkcji CDatabase: : Open (). Jedyną różnicą jest istnienie znaczników CDatabase: : noODBCDialog oraz CDatabase: : f orceODBCDialog. Te znaczniki określają, czy menedżer sterowników ODBC ma wyświetlać okno dialogowe połączenia ODBC w momencie próby nawiązania połączenia.
Kolejnym użytecznym zestawem funkcji zawartych w klasie CDatabase są funkcje obsługujące transakcje. Transakcje dają aplikacji możliwość łączenia zestawów wywołań funkcji w logiczne grupy. Jeśli wywołanie którejkolwiek z funkcji w grupie logicznej się nie powiedzie, wszystkie zmiany w bazie danych, dokonane przez poprzednio wywoływane funkcje grupy, są anulowane.
Jako przykład, załóżmy, że aplikacja księgowa musi przenieść pewną kwotę z jednego konta do innego. Co się stanie, jeśli po odjęciu kwoty z jednego konta nie powiedzie się próba dodania jej do innego konta? Spójność bazy danych zostanie oczywiście naruszona, gdyż pieniądze znikną z jednego konta, ale nie pojawią się na innym. Właśnie w takich przypadkach pomocne stają się transakcje. Dzięki transakcjom aplikacja może nakazać, że musi powieść się wykonanie albo wszystkich funkcji, albo ma nie być wykonywana żadna z nich. Tak więc transakcję stanowią jeden ze sposobów zapewnienia spójności danych.
Oto jak działają transakcje: wywołanie funkcji CDatabase: :BeginTrans () powoduje ustawienie "granicy transakcji". Wszelkie zmiany dokonane od tego momentu w źródle danych podlegają ewentualnemu anulowaniu. Gdy zostanie wywołana funkcja CDatabase: : commitTrans (), zatwierdzane są wszelkie zmiany dokonane w bazie danych od czasu ostatniego ustawienia granicy transakcji. Jeśli z jakiegoś powodu aplikacja uzna, że konieczne jest ręczne anulowanie wszystkich zmian od ustawienia ostatniej granicy transakcji, może to uczynić, wywołując funkcję CDatabase: :Rollback{). Pamiętaj tylko, że przed próbą użycia transakcji powinieneś wywołać funkcję CDatabase: -.CanTransact () w celu sprawdzenia, czy używany sterownik ODBC obsługuje transakcje.
Klasa CRecordset
O ile obiekt CDatabase reprezentuje połączenie z bazą danych, o tyle obiekt CRecordset (zwany rekordsetem) reprezentuje zestaw rekordów otrzymanych od źródła danych. Choć klasa CRecordset obsługuje kilka rodzajów rekordsetów, najczęściej wykorzystywane są tylko dwa z nich: dynaset i snapshot. Dynaset to dynamiczny zestaw danych obsługujący dwukierunkowe przewijanie i pozostający w synchronizacji ze zmianami dokonywanymi w źródle danych przez innych użytkowników. Z drugiej strony, snapshot to statyczny obraz danych otrzymany w momencie wypełniania rekordsetu. Na ten zestaw danych nie wpływają zmiany dokonywane przez innych użytkowników. Aby aplikacja używająca snapshotu mogła ujrzeć zmiany dokonane przez innych użytkowników, konieczne jest zamknięcie i ponowne otwarcie rekordsetu. Snapshot również obsługuje dwukierunkowe przewijanie.
Najprostszym sposobem zilustrowania wykorzystania rekordsetów jest stworzenie jednego z nich za pomocą ClassWizarda. Użyty rekordset będzie oparty na tabeli UserMaster znajdującej się w bazie danych Accessa, a pliku vc6bib.mdb. Ten plik znajdziesz na dołączonej do książki płytce CD-ROM, w folderze Rozdz26\Database. Tabela UserMaster, otwarta w programie Microsoft Access, została przedstawiona na rysunku 27.1.
Zacznij od otwarcia ClassWizarda. Po pojawieniu się okna dialogowego kliknij przycisk Add Class. W kolejnym menu wybierz polecenie New. Pojawi się okno dialogowe New Class, pokazane na rysunku 27.2.
Jako klasę bazową wybierz klasę CRecordset. Class Wizard wyświetli następnie prośbę o wskazanie rodzaju dostępu do bazy danych w nowej klasie rekordsetu. To okno dialogowe jest pokazane na rysunku 27.3.

Po wybraniu nazwy źródła danych po prostu wskaż tabelę UserMaster (tak jak pokazano na rysunku 27.4) i kliknij przycisk OK. I to już wszystko co jest potrzebne do stworzenia klasy wyprowadzonej z klasy CRecorcłset! W tym momencie ClassWizard automatycznie wygeneruje około 125 linii kodu, dzięki którym dostęp do tabeli UserMaster staje się dużo łatwiejszy.
Listingi 27.1 i 27.2 zawierają kod utworzony przez ClassWizarda. W tej części rozdziału zajmiemy się więc omówieniem, do czego służą poszczególne zmienne i funkcje składowe.
Listing 27.1. UserMasterSet.h ________________________________________
#if _MSC_VER > 1000
#pragma once
ttendif // _MSC_VER > 1000
// UserMasterSet.h : header file
/////////////////////////////////////////////////////////////
// CUserMasterSet recordset
class CUserMasterSet : public CRecordset
{
public :
CUserMasterSet (CDatabase* pDatabase = NULL) ;
DECLARE_DYNAMIC (CUserMasterSet)
// Field/Param Data
// { {AFX_FIELD (CUserMasterSet, CRecordset) CString m_sUserID; CString m_sUserName; int m_iStatus; //} }AFX_FIELD
// Overrides
// ClassWizard generated yirtual function overrides
//{ {AFX_VIRTUAL (CUserMasterSet)
public :
// Domyślny łańcuch połączenia
virtual CString GetDefaultConnect ( ) ;
virtual CString GetDefaultSQL ( ) ; // Domyślne SQL dla rekordsetu
virtual void DoFieldExchange (CFieldExchange* pFX) ; // Obsługa RFX
//} }AFX VIRTUAL
// Implementation ftifdef _DEBUG
virtual void AssertValid ( ) const;
virtual void Dump (CDumpContext& dc) const; #endif
};
// { {AFX_INSERT_LOCATION} }
// Microsoft Visual C++ will insert additionaldeclarations
// irranediately before the previous linę.

Listing 27.2. UserMasterSet.cpp
// UserMasterSet.cpp : implementation file
//
#include "stdafx.h"
#include "UserMaintenance .h"
#include "UserMasterSet .h"
#ifdef _DEBUG
ttdefine new DEBUG_NEW
ftundef THIS_FILE
static char THIS_FILE[] = _ FILE
#endif
/////////////
// CUserMasterSet

IMPLEMENT_DYNAMIC(CUserMasterSet, CRecordset)

CUserMasterSet::CUserMasterSet(CDatabase* pdb) : CRecordset(pdb)
{
//{{AFX_FIELD_INIT(CUserMasterSet) m_sUserID = _T(""); m_sUserName = _T(""); m_iStatus = 0; m_nFields = 3; //}}AFX_FIELD_INIT m_nDefaultType = snapshot; }
CString CUserMasterSet::GetDefaultConnect()
{
return _T("ODBC;DSN=Visual C++ 6 Bibie"); }
CString CUserMasterSet::GetDefaultSQL()
{
return _T("[UserMaster]"); }

void CUserMasterSet::DoFieldExchange(CFieldExchange* pFX) {
//{{AFX_FIELD_MAP(CUserMasterSet)
pFX->SetFieldType(CFieldExchange::outputColumn);
RFX_Text(pFX, _T("[sUserlD]") , m_sUserID);
RFX_Text(pFX, _T("[sUserName]"), m_sUserName);
RFX_Int(pFX, _T("[iStatus]"), m_iStatus);
//}}AFX FIELD MAP
}
////////////////
// CUserMasterSet diagnostics

#ifdef_DEBUG
void CUserMasterSet::AssertValid(} const
{
CRecordset: :AssertValid() ;
}
void CUserMasterSet::Dump(CDumpContext& dc) const
{ CRecordset::Dump(dc);
}
#endif //_DEBUG
Gdy już masz klasę wyprowadzoną z CRecordset, aplikacja może użyć jej do następujących celów:
Skonstruowania rekordsetu
Otwarcia rekorsdsetu
Odczytu i zapisu danych z użyciem RFX (record field exchange)
Filtrowania rekordów
Sortowania rekordów zwróconych jako wynik polecenia SQL
Nawigacji w zestawie wyników (zwróconych danych)
Zapisywania rekordów
Usuwania rekordów
Konstruowanie rekordsetu
Konstruktor klasy CRecordset wymaga podania tylko jednego parametru, wskaźnika do obiektu CDatabase. Ten argument ma domyślną wartość NULL. Jeśli aplikacja tworząca obiekt CRecordset pozostawi tę wartość jako NULL, MFC automatycznie stworzy tymczasowy obiekt CDatabase, używając informacji podanych podczas tworzenia klasy rekordsetu za pomocą ClassWizarda. Te informacje znajdują się wewnątrz funkcji CRecordset: : GetDef aultConnect () oraz CRecordset: : GetDef aultSQL (). Jeśli jednak aplikacja ma zamiar korzystać z kilku rekordsetów, dużo efektywniejsze jest stworzenie jednego obiektu CDatabase i użycie go do tworzenia tych rekordsetów. W ten sposób dla wszystkich tworzonych rekordsetów zostanie stworzony tylko jeden obiekt CDatabase zamiast tworzenia takiego obiektu dla każdego rekordsetu.
Otwieranie rekordsetu
Po skonstruowaniu rekordsetu do jego otwarcia służy funkcja CRecordset: :0pen(). Jej składnia jest następująca:
virtual BOOL Open(
UONT nOpenType = AFX_DB_USE_DEFAULT_TYPE, LPCTSTR lpSzSQL = NULL, DWORD dwOptions = none );

throw( CDBException, CMemoryException );

Argument nOpenType służy do wskazania typu otwieranego rekordsetu: dynasetu, snaps-hotu, przewijanego jedynie w przód lub dynamicznego. Każdy z tych typów rekordsetów jest reprezentowany przez wartość wyliczeniową w klasie CRecordset. Domyślnym typem rekordsetu dla argumentu nOpenType jest CRecordset: : snapshot.
class CRecordset:CObject
{
...
public:
enum OpenType {
dynaset, snapshot, forwardOnly, dynamie
};
...
Argument lpszSQL pozwala aplikacji na określenie polecenie SQL, które zostanie wydane dla źródła danych po otwarciu rekordsetu. Jeśli ten argument będzie miał domyślną wartość NULL, zostanie użyty kod z funkcji CRecordset: : GetDef aultSQL (). W większości przypadków zobaczysz, że ta funkcja zwraca jedynie nazwę tabeli lub kwerendy. W takiej sytuacji, rekordset pobiera wszystkie kolumny z podanej tabeli i kwerendy, nie filtrując danych ani w żaden sposób ich nie sortując. Zagadnieniem filtrowania i sortowania danych zwróconych przez rekordset zajmiemy się jednak już wkrótce.
Odczyt i zapis danych z użyciem RFX
Jak już wspominaliśmy, w celu odczytu wszystkich danych z tabeli bez względu na ich kolejność aplikacja może po prostu stworzyć obiekt klasy wyprowadzonej z klasy CRecordset utworzonej przez ClassWizarda i wywołać jego funkcję CRecordset: : Open (). Jednak w tym momencie nadszedł czas, aby bardziej zagłębić się w zagadnienie przenoszenia danych ze źródła danych do zmiennych w aplikacji. Odpowiedź leży w mechanizmie RFX (record field exhange, wymianie pól rekordu). Jeśli klasa wyprowadzona z CRecordset jest tworzona za pomocą ClassWizarda, ClassWizard wylicza wszystkie kolumny w podanej tabeli lub kwerendzie i w pliku nagłówkowym nowej klasy deklaruje zmienne odpowiednich typów. Oprócz tego tworzy funkcję DoFieldExchange (), służącą do powiązania tych zmiennych składowych z odpowiednimi kolumnami źródła danych. W czasie działania programu, gdy MFC chce przenieść dane ze źródła do zmiennych lub odwrotnie, wywołuje właśnie funkcję DoFieldExchange ().
Wszystkie konieczne do tego funkcje ODBC SDK (na przykład do powiązania kolumn, pobrania danych itd.), omawiane w rozdziale 26., wykonuje kod zawarty w bibliotece MFC. Jednak nie wszystko dzieje się automatycznie! Gdy aplikacja otwiera rekordset, funkcja CRecordset: : Open () przechodzi do jego pierwszego rekordu. Aplikacja może poruszać się po danych, używając funkcji składowych CRecordset: :MoveFirst o, CRecordset::MoveLast(), CRecordset::MovePrev() oraz CRecordset: : MoveNext (). Omówimy je już za moment.
Filtrowanie rekordów
Gdy już wiesz, jak użyć klas CDatabase i CRecordset do otwarcia i pobrania danych ze źródła, następnym krokiem jest nauczenie się filtrowania tych danych w celu otrzymania tylko tych rekordów, na których nam zależy. Można osiągnąć to na kilka sposobów. Jak widzieliśmy w poprzedniej sekcji, "Otwieranie rekordsetu", aplikacja może w parametrze lpszSQL funkcji CRecordset: :0pen () określić polecenie SQL dla źródła danych. Oto przykład filtrowania danych za pomocą tej funkcji. Ten fragment kodu odczytuje z tabeli UserMaster tylko te rekordy, których wartość istatus jest równa zeru:

try
{
CDatbase db;
if(db.Open("Visual C++ 6 Bibie"))
{
CUserMasterSet* pUserMasterSet = new CUserMasterSet();

pUserMasterSet->Open(CRecordset::snapshot,
"SELECT * from usermaster where iStatus = 0");

// wykorzystaj zwrócone dane
pUserMasterSet->Close();
db.Close ();
}
}
catch(CDBException* pe) {
AfxMessageBox(pe->m_strError);
pe->Delete ();
}
Drugim sposobem filtrowania danych zwróconych w rekordsecie jest użycie rekordsetu parametryzowanego. Używając klasy CUserMasterSet (listingi 27.1 i 27.2) stworzonej we wcześniejszej części tego rozdziału, spójrzmy, jakie zmiany są konieczne w kodzie tej klasy, tak aby po wywołaniu funkcji Open () zostały pobrane tylko te rekordy tabeli UserMaster, których pole istatus jest równe zeru.
1. Pierwszym krokiem jest zadeklarowanie zmiennej (lub zmiennych), która będzie zawierać "dane parametru". Dane parametru to dane używane w klauzuli WHERE polecenia SQL budowanego przez MFC w momencie otwierania rekordsetu.
Ponieważ rekordset będzie filtrowany względem wartości w kolumnie istatus, powinniśmy stworzyć zmienną właśnie dla tej kolumny. Powszechnie przyjęta konwencja nazw mówi, że powinniśmy po prostu dołączyć słówko Param do nazwy kolumny, według której będzie przeprowadzane filtrowanie. Odszukaj poniższy blok kodu:
// Field/Param Data
//{{AFX_FIELD(CUserMasterSet, CRecordset)
CString m_sUserID;
CString m_sUserName;
int m_iStatus;
//}}AFX_FIELD
i dopisz poniższą deklarację zmiennej:
int m_iStatusParam;
2. Następnym krokiem jest inicjalizacja tej zmiennej w konstruktorze rekordsetu i przypisanie wartości m_nParams ilości parametrów, które zostaną użyte. To bardzo ważne. Jeśli spróbujesz użyć parametryzowanego rekordsetu bez wcześniejszego ustawienia tej zmiennej, podczas wywołania funkcji DoFieldEx-change () funkcja Open () zgłosi asercję. Odszukaj poniższy blok kodu:
//{{AFX_FIELD_INIT(CUserMasterSet)
m_sUserID = _T("");
m_sUserName = _T ("")/'
m_iStatus = 0;
m_nFields = 3;
//}}AFX_FIELD_INIT
i zmodyfikuj go tak, aby wyglądał następująco:
//{{AFX_FIELD_INIT(CUserMasterSet)
m_sUserID = _T("");
m_sUserName = _T ( " " ) ;
m_iStatus = 0;
m_nFields = 3;
m_iStatusParam = 0;
m_nParams =1;
/!}}AFX_FIELD_INIT
3. Zaktualizuj kod funkcji DoFieldExchange () tak, aby wyglądał jak przedstawiony poniżej. Funkcja CFieldExchange: : SetFieldType () musi ZOStaĆ wywołana z argumentem CFieidExchange: :param. Informuje on MFC, że zmienna m_istatusParam (wskazana w następnym wywołaniu) zawiera dane, według których powinna zostać odfiltrowana kolumna istatus.
//{{AFX_FIELD_MAP(CUserMasterSet) pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Text(pFX, _T("[sUserlD]"), m_sUserID); RFX_Text(pFX, _T("[sUserName]"), m_sUserName); RFX_Int(pFX, _T("[istatus]"), m_iStatus); pFX->SetFieldType(CFieldExchange::param); RFX_Int(pFX, _T(" [iStatus] ") , m_iStatusParam) ; //}}AFX FIELD MAP
1. Gdy wprowadzisz opisane zmiany, pozostanie Ci jeszcze jedynie zmiana samego kodu otwierającego rekordset. Przy otwieraniu parametryzowanego rekordsetu aplikacja powinna odpowiednio ustawić zmienną CRecordset: :m_strFilter. Gdy to uczyni, może następnie przypisać zmiennej parametru wartość, według której chce filtrować rekordy. Na koniec, wywołanie funkcji CRecordset: : Open () zwróci tylko te rekordy, które spełniają warunek zawarty w zmiennych parametrów.
try {
CDatbase db;
if(db.Open("Yisual C++ 6 Bibie"))
{
CUserMasterSet* pUserMasterSet = new CUserMasterSet();

pUserMasterSet->m_strFilter = "iStatus = ?"; pUserMasterSet->m_iStatusParam = 0; pUserMasterSet->Open();

// wykorzystaj zwrócone dane
pUserMasterSet->Close();

db.Close();
} }
catch(CDBException* pe) {
AfxMessageBox(pe->m_strError) ;
pe->Delete(); }
Jak widać, korzystanie z parametryzowanych rekordsetów nie jest zbyt trudne. Największą przeszkodą jest konieczność ręcznego tworzenia kodu, a jeśli zapomnisz o którymś z kroków, możesz stracić wiele czasu, analizując kod i zastanawiając się dlaczego rekordset nie chce poprawnie działać.
Sortowanie rekordów zwróconych przez rekordset
Jak widzieliśmy w sekcji "Filtrowanie rekordów", klasa CRecordset udostępnia zmienną składową (m_strFilter), pozwalającą na filtrowanie rekordów. Klasa CRecordset posiada także inną zmienną składową, przeznaczoną do określania kolejności, w jakiej są sortowane rekordy zwracane podczas otwierania rekordsetu. Ta zmienna nosi nazwę m_strsort. Aby z niej skorzystać, aplikacja musi jedynie przypisać jej nazwę kolumny, według której dane mają być sortowane. Na przykład w poprzednim kodzie, jeśli chciałbyś, by aplikacja posortowała dane według kolumny sUserid, kod otwierający rekordset powinien wyglądać następująco:
try
{
CDatbase db;
if(db.Open("Visual C++ 6 Bibie"))
{
CUserMasterSet* pUserMasterSet = new CUserMasterSet();

pUserMasterSet->m_strFilter = "iStatus = ?"; pUserMasterSet->m_iStatusParam = 0;

pUserMasterSet->m_strSort = "sUserld"; pUserMasterSet->Open();

// wykorzystaj zwrócone dane pUserMasterSet->Close();
db.Close(); }
}
catch(CDBException* pe)
AfxMessageBox(pe->m_strError) ; pe->Delete(); }
Jeśli aplikacja musi sortować dane według więcej niż jednej kolumny, po prostu rozdziel nazwy kolumn przecinkiem.
Poruszanie się w zestawie wyników
Teraz gdy wiesz, jak pobierać zestaw odfiltrowanych i posortowanych rekordów, poświęćmy chwilę na omówienie sposobów poruszania się po tym zestawie. Dzięki funkcjom składowym klasy CRecordset, MoveFirst(), MovePrev(), MoveNext () oraz Move-Last (), jest to bardzo łatwe. Z nazw funkcji łatwo można się zorientować, do czego służą, jednak wykorzystując je w swoim kodzie, powinieneś pamiętać o kilku rzeczach:
Jeśli aplikacja spróbuje wywołać którąś z tych funkcji dla rekordsetu, który nie zwrócił żadnych rekordów, zostanie zgłoszony wyjątek CDBException. W związku z tym aplikacja powinna zawsze przed ich wywołaniem wywołać funkcję CRecordset: : IsBOF () lub CRecordset: : IsEOF ().
Funkcje MoveFirst() i MovePrev() nie są obsługiwane przez rekordsety umożliwiające jedynie przewijanie danych w przód.
Jeśli aplikacja porusza się po rekordsecie, dla którego istnieje możliwość, że jakiś inny użytkownik usunął rekordy, aplikacja powinna wywołać funkcję CRecordset : : isDeleted () w celu sprawdzenia, czy dany rekord jest wciąż poprawny.
Jeśli aplikacja przejdzie do ostatniego rekordu w rekordsecie, po czym wywoła funkcję MoveNext (), zostanie zgłoszony wyjątek CDBException. Tak więc w celu sekwencyjnego przejścia przez cały rekordset przed wywołaniem funkcji MoveNext () aplikacja powinna wywołać funkcję ISEOF ().
Oto prosty przykład przechodzenia przez rekordset za pomocą pętli while. Jak widać, po każdym ruchu jest wywoływana funkcja CRecordset: : ISEOF (), dzięki czemu po dojściu do ostatniego rekordu nie jest błędnie wywoływana funkcja CRecordset: :MoveNext() .

pUserMasterSet->Open() ;
while (!pUserMasterSet->IsEOF())
{
AfxMessageBox(pUserMasterSet->m_sUserID);
pUserMasterSet->MoveNext();
}
Zapisywanie rekordów
Użycie klasy CRecordset do zapisywania rekordów jest łatwe. Istnieją dwa główne sposoby zapisywania danych: dodawanie nowych rekordów oraz aktualizacja istniejących rekordów.
Aby dodać nowy rekord, aplikacja po prostu musi wykonać poniższe kroki:
1. Wywołać funkcję CRecordset: : AddNew (). Spowoduje to stworzenie nowego, pustego rekordu.
2. Przenieść dane do zmiennych składowych rekordsetu.
3. Wywołać funkcję CRecordset: :Update() w celu zapisania nowego rekordu w źródle danych.
Aby zaktualizować istniejący rekord, aplikacja musi wykonać poniższe kroki:
1. Wywołać funkcję CRecordset: : Edit () .
2. Przenieść dane do zmiennych składowych rekordsetu.
3. Wywołać funkcję CRecordset: :Update () w celu zaktualizowania bieżącego rekordu w źródle danych.
Bardzo ważna jest kolejność wywoływania tych funkcji. Dane muszą zostać przypisane zmiennym składowym już po wywołaniu funkcji Edit (). W przeciwnym razie MFC nie zaktualizuje źródła danych. Powodem jest to, że klasy baz danych MFC buforują dane i wykonują porównanie pamięci w momencie wywołania funkcji update (). Jeśli od czasu wywołania funkcji Edit () nie została dokonana żadna zmiana w zmiennych składowych, źródło danych nie jest aktualizowane, nawet jeśli aplikacja jawnie wywoła funkcję Update ().
Usuwanie rekordów
Aby usunąć rekord z rekordsetu, dany rekord musi być bieżącym rekordem rekordsetu. W tym momencie aplikacja musi wywołać funkcję CRecordset: : Delete (). Po usunięciu rekordu konieczne jest wywołanie jednej z funkcji nawigacyjnych rekordsetu w celu przejścia do innego rekordu, gdyż bieżący rekord nie jest już poprawny.
Użycie klas baz danych MFC
Program demonstracyjny UserMaintenace ilustruje w jaki sposób można wykorzystać klasy baz danych MFC do wykonywania podstawowych operacji na bazie danych, takich jak odczyt, zapis i usuwanie rekordów. Kod źródłowy tej aplikacji znajduje się na dołączonej do książki płytce CD-ROM, w kartotece Rozdz27\UserMaintenance. Jeśli jednak masz zamiar uruchomić ten program lub stworzyć kod zgodnie z treścią książki, musisz najpierw stworzyć odpowiednią nazwę źródła danych. Tą nazwą jest "Visual C++ 6 Bibie". Jeśli nie wiesz, jak stworzyć nazwę źródła danych, wróć do opisu w rozdziale 26. Następnie za pomocą AppWizarda stwórz opartą na oknie dialogowym aplikację o nazwie UserMaintenance.
Dodanie obsługi klas baz danych MFC
Po stworzeniu aplikacji musisz dodać do niej dyrektywę włączającą plik nagłówkowy afodb.h. Ten plik zawiera deklaracje klas baz danych MFC. Choć ta dyrektywa ttinclude może być umieszczona w każdym z plików odwołujących się do klas baz danych, jednak zwykle najwygodniej jest umieścić ją w pliku nagłówkowym stdafa.h.
Tworzenie interfejsu użytkownika dla programu demonstracyjnego
1. Ponieważ nasz program jest aplikacją opartą na oknie dialogowym, cała interakcja z użytkownikiem będzie odbywać się przez okno dialogowe stworzone automatycznie przez AppWizarda. Projekt powinien już posiadać okno dialogowe o identyfikatorze zasobu IDD_USERMAINTENANCE_DIALOG. Odszukaj je i zmodyfikuj tak, aby przypominało okno dialogowe z rysunku 27.5.

Rozdział 27. Klasy baz danych MFC
753
2. Po dodaniu kontrolek przypisz im następujące identyfikatory:
Kontrolka
Identyfikator kontrolki
Lista Użytkownicy
IDC_LBX_USERS
Pole ID użytkownika
IDC_EDT_USERID
Pole Nazwa użytkownika
IDC_EDT_USERNAME
Pole Status
IDC_EDT_STATUS
Przycisk Zapisz
IDC_BTN_SAVE
Przycisk Usuń
IDC_BTN_DELETE Przycisk Zamknij
IDC_BTN_CLOSĘ



3. Po przypisaniu identyfikatorów stwórz dla każdej z kontrolek zmienną składową o następujących właściwościach:
Kontrolka Kategoria (Category) Typ zmiennej (Variable type) Nazwa zmiennej (Variable name)
Lista Użytkownicy Control CListBox m lbxUsers
Pole ID użytkownika Value CString m strUserlD
Pole Nazwa użytkownika Value CString m strUserName
Pole Status Value int m iStatus
Przycisk Zapisz Control CButton m btnSave
Przycisk Usuń Control CButton m btnDelete
Przycisk Zamknij Control CButton m btnClose
Dodawanie klasy pomocniczej reprezentującej dane użytkownika
W celu reprezentowania danych rekordu konieczne będzie stworzenie klasy pomocniczej. Jak zobaczymy za chwilę, po odczytaniu rekordu będzie tworzony obiekt typu cuser. Następnie do listy będzie dopisywany łańcuch (userio) reprezentujący dany rekord. Każda pozycja listy będzie posiadać także wskaźnik do obiektu cuser zawierającego dane użytkownika. Definicja klasy cuser znajduje się na listingu 27.3.
Listing 27.3. User, h - kod źródłowy klasy CUser____________________________
#pragma once

class CUser
{
public:
CString m_strUserID;
CString m_strUserName;
int miStatus;
};
Tworzenie klasy rekordsetu dla tabeli UserMaster
Teraz musisz stworzyć klasę rekordsetu potrzebną przy dostępie do tabeli UserMaster. Nazwij jącuserMasterSet. Jeśli nie pamiętasz jak to się robiło, przy jej tworzeniu postępuj zgodnie z instrukcjami z poprzedniej sekcji zatytułowanej "Klasa CRecordset".
Modyfikowanie pliku nagłówkowego dialogu
Dla potrzeb programu musisz dokonać kilku zmian w pliku nagłówkowym okna dialogowego:
1. Otwórz plik UserMaintenanceDlg.h i przed deklaracją klasy dialogu umieść poniższą dyrektywę:
#include "User.h"
2. Dodaj poniższe deklaracje zmiennych składowych:
void FillListboxWithUsers();
BOOL GetSelectedUser(int* piIndex,CUser**ppUser);
void InitControls();
BOOL SaveUser(CUserpUser);
BOOLDeleteUser(CUser*pUser);
Modyfikowanie pliku implementującego dialog
Ostatnią rzeczą, jaką musisz zrobić, jest zmodyfikowanie kodu dialogu w celu odwołania się do bazy danych.
1. Otwórz plik UserMaintenanceDlg.cpp i dopisz do niego poniższą dyrektywę
#include'.
#include "UserMasterSet.h"
2. Zlokalizuj funkcję OninitDialog () i umieść na jej końcu, przed instrukcją return, poniższy kod. Funkcja FillListboxWithUsers () wypełnia listę nazwami użytkowników odczytanymi z tabeli UserMaster. Funkcja init-Controis () inicjuje stan przycisków poleceń.
FillListboxWithUsers(); InitControls();
3. Na końcu pliku dopisz poniższą funkcję:
void CUserMaintenanceDlg::FillListboxWithUsers() {
int ilndex;
CDatabase db;
CUserMasterSet* pUserMasterSet = NULL;
try
{
if (db.Open("Visual C++ 6 Bibie")) {
pUserMasterSet = new CUserMasterSet();
p(JserMasterSet->Open () ;
while (!pUserMasterSet->IsEOF()) {
CUser* pUser = new CUser();
pUser->m_strUserID = pUserMasterSet->m_sUserID;
pUser->m_strUserName = pUserMasterSet->m_sUserName;
pUser->m_iStatus = pUserMasterSet->m_iStatus;
ilndex = m_lbxUsers.AddString( pUserMasterSet->m_sUserName);
m_lbxUsers.SetItemData(ilndex, (DWORD)pUser); pUserMasterSet->MoveNext();
}
pUserMasterSet->Close( delete pUserMasterSet; db.CloseO ;
}
}
catch(CDBException* pe)
{ AfxMessageBox(pe->m_strError) if (pUserMasterSet)
{ if (pUserMasterSet->IsOpen()
{
pUserMasterSet->Close();
} delete pUserMasterSet;
}
if (db.IsOpent))
{
db.Close();
}
pe->Delete();
}
}
1. Po zdefiniowaniu funkcji FiiiListboxwithUsers () dopisz kolejną funkcję inicjalizującą:
void CUserMaintenanceDlg::InitControls() { m_lbxUsers.SetCurSel(-1);
m_strUserID = ""; m_strUserName = ""; m iStatus = -1;
m_btnSave.EnableWindow(FALSE); m_btnDelete.EnableWindow(FALSE); m_btnClose.EnableWindow(TRUE);
m_lbxUsers.SetFocus(); UpdateData(FALSE);
}
5. Teraz musisz dodać funkcję, dzięki której po wybraniu użytkownika na liście w polach tekstowych pojawią się odpowiednie informacje. W tym celu za pomocą ClassWizarda zaimplementuj funkcję obsługi komunikatu LBN_SELCHANGE dla kontrolki listy (IDC_LBX_USERS). Następnie wypełnij ją poniższym kodem:
void CUserMaintenanceDlg::OnSelchangeLbxUsers()
{
int ilndex; CUser* pUser;

if (GetSelectedUser(&ilndex, spUser))
{
m_strUserID = pUser->m_strUserID; m_strUserName = pUser->m_strUserName; m_iStatus = pUser->m_iStatus;

m_btnSave.EnableWindow(TRUE); m_btnDelete.EnableWindow(TRUE);

UpdateData(FALSE);
}
}
6. Dopisz poniższą funkcję pomocniczą. Będzie ona wywoływana w momencie kliknięcia przycisk Zapisz lub Usuń w celu pobrania danych użytkownika zaznaczonego na liście.
BOOL CUserMaintenanceDlg::GetSelectedUser(
int* pilndex, CUser** ppUser) {
BOOL bSuccess = FALSE;

int ilndex;
if (LB_ERR != (ilndex = m_lbxUsers.GetCurSel()))
{
*pilndex = ilndex;
*ppUser = (CUser*)m_lbxUsers.GetltemData(ilndex); bSuccess = TRUE;
}
return bSuccess;
}
7. Dodaj funkcję obsługi komunikatu BN_CLICKED dla przycisku Zamknij (IDC_ BTN_CLOSE). Ta funkcja po prostu zamyka okno dialogowe.
void CUserMaintenanceDlg::OnBtnClose() { CDialog::OnOK();
}
8. W tym momencie aplikacja posiada już funkcje potrzebne do odczytania wszystkich użytkowników z tabeli UserMaster i wyświetlenia w oknie dialogowym odpowiednich informacji. Nadszedł więc czas, aby dodać możliwość "aktualizacji" bazy danych. Dodaj funkcję obsługi komunikatu BN_CLICKED dla przycisku Zapisz (IDC_BTN_SAVE) i wypełnij ją poniższym kodem:
void CUserMaintenanceDlg::OnBtnSave() { int iCurr!ndex;

CUser* pUser = NULL;
if (GetSelectedUser(&iCurr!ndex, &pUser))
{
ASSERT(pUser);
if (pUser)
{ UpdateData() ;

CString strPrevUserID; strPrevUserID = m_strUserID; CString strPrevUserName; strPrevUserName = m_strUserName; int iPrevStatus = m_iStatus;

pUser->m_strUserID = m_strUserID; pUser->m_strUserName = m_strUserName; pUser->m_iStatus = m_iStatus;

if (SaveUser(pUser))
{
if (LB_ERR == m_lbxUsers.DeleteString(iCurr!ndex)} {
AfxMessageBox("ID użytkownika zostało zapisane, " "ale nie powiodło się usunięcie z listy poprzedniego "ID użytkownika."); }
else { int iNew!ndex = m_lbxUsers.AddString(
pUser->m_strUserName); if ((LB_ERR == iNew!ndex) M (LB_ERRSPACE ==
iNew!ndex)) {
AfxMessageBox("ID użytkownika zostało zapisane, " "ale nie powiodło się dodanie do listy nowego " "ID użytkownika.");
}
if (LB_ERR == m_lbxUsers.SetItemData(iNew!ndex,
(DWORD)pUser))
{
AfxMessageBox("SetltemData zwróciło LB_ERR. " "Prawdopodobnie spowoduje to poważne problemy "jeśli spróbujesz zaktualizować lub usunąć" "ten element z listy.");
}
}
InitControls ( ) ;

UpdateData (FALSE) ; }
else {
pUser->m_strUserID = strPrevUserID;
pUser->m_strUserName = strPrevUserName;
pUser->m_iStatus = iPrevStatus;
AfxMessageBox ( "Funkcja SaveUser się nie powiodła")
}
}
}
else {
// Nie powinno nas tu nigdy być, z powodu
// włączania/wyłączania przycisku na podstawie
// elementu zaznaczonego na liście.
AfxMessageBox ( "Musisz najpierw wybrać ID użytkownika
"przeznaczonego do zapisania");
}
}
9. Dodaj poniższą funkcję pomocniczą. Będzie ona wywoływana, gdy wystąpi konieczność zapisania informacji znajdujących się w oknie dialogowym.
BOOL CUserMaintenanceDlg::SaveUser(CUser* pUser)
{
BOOL bSuccess = FALSE; CDatabase db;

try
{ if (db.Open("Visual C++ 6 Bibie"))
{
CString strSQL = CString("UPDATE UserMaster SET "); strSQL += CString("sUserName = '") + pUser->m_strUserName + CString("', ");

strSQL += CString("iStatus = "); char szStatus[10];
itoa(pUser->m_iStatus, szStatus, 10); strSQL += szStatus;
strSQL += CString("WHERE sUserlD = ");
strSQL += CString("'") + pUser->m strUserlD + CString("'");



db.ExecuteSQL(strSQL);
bSuccess = TRUE;
}
}
catch(CDBException* pe)
{ AfxMessageBox(pe->m_strError) ;
if (db.IsOpen())
{ db.Close();
}
pe->Delete() ;
}
return bSuccess; }
10. Dodaj funkcję obsługi komunikatu BN_CLICKED dla przycisku Usuń (IDC BTN_DELETE) i wypełnij j ą poniższym kodem:
void CUserMaintenanceDlg::OnBtnDelete()
{
int ilndex; CUser* pUser = NULL;
if (GetSelectedUser(&ilndex, SpUser)} {
ASSERT(pUser); if (pUser) {
UpdateData(); CString strMsg;

strMsg = "Czy jesteś pewien, że chcesz usunąć "
"użytkownika "; strMsg += "'";
strMsg += pUser->m_strUserName; strMsg += "'?";
if (IDYES == AfxMessageBox(strMsg, MB_YESNOCANCEL) {
if (DeleteUser(pUser))
{
if (LB_ERR == m_lbxUsers.DeleteString (ilndex) ) {
AfxMessageBox ( "Usunięcie użytkownika się powiodło, " "lecz nie udało się usunąć go z listy.");
}
InitControls ( ) ;
UpdateData (FALSE) ;
}
else
{ AfxMessageBox("Funkcja DeleteUser się nie powiodła");
}
}
}
}
else {
// Nie powinno nas tu nigdy być, z powodu
// włączania/wyłączania przycisku na podstawie
// elementu zaznaczonego na liście.
AfxMessageBox("Musisz najpierw wybrać ID użytkownika
"przeznaczonego do usunięcia");
}
}
11. Na końcu pliku dopisz poniższą funkcję pomocniczą, wywoływaną w momencie konieczności usunięcia rekordu:
BOOL CUserMaintenanceDlg::DeleteUser (CUser* pUser) {
BOOL bSuccess = FALSE;
CDatabase db;

try {
if (db.Open("Visual C++ 6 Bibie")) {
CString strSQL = CString("DELETE FROM UserMaster " "WHERE ");

strSQL += CString("sUserlD = '") + pUser->m_strUserID + CString("'");

db.ExecuteSQL(strSQL);
bSuccess = TRUE;
}
}
catch(CDBException* pe)
{
AfxMessageBox(pe->m_strError) ;

if (db.IsOpen())
{
db.CloseO ; }
} return bSuccess;
}
Zbuduj i uruchom aplikację. Powinieneś ujrzeć okno podobne do pokazanego na rysunku 27.6.
Parametryzowane rekordsety i kwerendy
Drugi program demonstracyjny, Parametrized, jest podzielony na dwie części. Pierwsza z nich ilustruje użycie parametryzowanego rekordsetu w praktycznej aplikacji. Wcześniej w tym rozdziale dowiedziałeś się, że parametryzowane rekordsety to rekordsety, które umożliwiają odfiltrowanie rekordów zwracanych podczas otwierania rekordsetu. Podobnie jak w poprzednim programie demonstracyjnym, także i w tym przypadku posługujemy się oknem dialogowym wyświetlającym informacje o użytkownikach z tabeli UserMaster. Jednak obecny program ma jedną zasadniczą różnicę. W poprzednim programie, podczas inicjalizacji dialogu, wszyscy użytkownicy z tabeli byli odczytywani i umieszczani na liście, z której można ich było wskazywać w celu przeprowadzenia różnych operacji na bazie danych (takich jak aktualizacja, usuwanie itd.). W obecnym programie demonstracyjnym można wpisać identyfikator UseriD, który zostanie użyty do wyszukania w tabeli UserMaster podanego użytkownika. Jeśli użytkownik zostanie znaleziony, w oknie dialogowym pojawią się informacje na jego temat. Jeśli nie, program wyświetli odpowiednie okno dialogowe.
Druga część programu demonstracyjnego ilustruje użycie parametryzowanej kwerendy zwracającej dane. Ta kwerenda będzie oparta na połączeniu kilku tabel w demonstracyjnej bazie danych Accessa. Kwerenda będzie wymagać tylko jednego parametru: identyfikatora UseriD. Gdy identyfikator UseriD wprowadzony w oknie dialogowym zostanie znaleziony w tabeli UserMaster, zostanie wywołana kwerenda pobierająca pozostałe dane na temat użytkownika. Te informacje zostaną wyświetlone w oknie dialogowym.
Zacznij od stworzenia aplikacji opartej na oknie dialogowym, nazwanej Parametrized. Gdy AppWizard stworzy projekt i jego pliki, to do pliku stdafo.h dopisz dyrektywę #include dołączającą plik afodb.h.
Tworzenie interfejsu użytkownika dla programu
Ponieważ nasz program jest aplikacją opartą na oknie dialogowym, cała interakcja z użytkownikiem będzie odbywać się przez okno dialogowe stworzone automatycznie przez AppWizarda. Projekt powinien już posiadać okno dialogowe o identyfikatorze zasobu IDD PARAMETRIZED DIALOG.
2. Po dodaniu kontrolek przypisz im następujące identyfikatory:
Kontrolka
Identyfikator kontrolki
Pole Id użytkownika
IDC_EDT_USERID
Pole Nazwa użytkownika
IDC_EDT_USERNAME
Pole Status użytkownika
IDC_EDT_STATUS
Drzewo Uprawnienia
IDC_TREE_PERMISSIONS
Przycisk Pobierz użytkownika
IDC_BTN_GET_USER
Przycisk Zamknij
IDCANCEL

3. Dla drzewa Uprawnienia włącz następujące style:
Has buttons Has lines Hines at root
4. Po przypisaniu identyfikatorów stwórz dla każdej z kontrolek zmienną składową o następujących właściwościach:
Kontrolka Kategoria (Category) Typ zmiennej (Variable type) Nazwa zmiennej (Variable name)
Pole ID użytkownika Value CString m_ strUserlD
Pole Nazwa użytkownika Value CString m_ strUserName
Pole Status użytkownika Value int m_ iStatus
Drzewo Uprawnienia Control CTreeCtrl m_ treePermissions
Tworzenie klasy rekordsetu dla tabeli UserMaster
Teraz musisz stworzyć klasę rekordsetu potrzebną przy dostępie do tabeli UserMaster. Nazwij ją cuserMasterSet. Jeśli nie pamiętasz jak to się robiło, przy jej tworzeniu postępuj zgodnie z instrukcjami w jednej z poprzednich sekcji, zatytułowanej "Klasa CRecordset.".
Modyfikowanie klasy CRecordset, tak aby akceptowała parametry
Po stworzeniu klasy wyprowadzonej z klasy CRecordset, powinieneś zmodyfikować ją tak, aby mogła przyjmować parametry.
1. Otwórz plik UserMasterSet.h i dodaj do klasy cuserMasterSet poniższą zmienną składową. Wartość tej zmiennej zostanie użyta do filtrowania rekordów w momencie otwierania rekordsetu.
CString m_sUserIDParam;
2. Otwórz plik UserMasterSet.cpp i znajdź w nim konstruktor klasy cuserMaster-set. Na końcu tej funkcji dopisz poniższe linie. Pierwsza z nich po prostu inicjuje zadeklarowaną przed chwilą zmienną składową. Druga linia jest bardzo ważna. Określa, jak wiele parametrów zostanie użytych dla rekordsetu. Jeśli byś ją pominął, w momencie wywołania funkcji CRecordset: : Open () zostałaby zgłoszona asercja.
m_sUserIDParam = _T(""); m_nParams = 1;
3. Następnie zlokalizuj funkcję CuserMasterSet:: DoFieldExchange () i tuż przed jej końcem dopisz poniższe linie. Te linie wiążą zmienną parametru z odpowiednią kolumną tabeli UserMaster. Zwróć szczególną uwagę, że nazwą kolumny jest sUserlD. W przypadku parametryzowanego rekordsetu, dla każdego parametru zdefiniowanego dla rekordsetu ogólnie masz do czynienia z dwiema zmiennymi wskazującymi tę samą kolumnę. Jedna z tych zmiennych jest używana do zapisu i odczytu danych do i z kolumny w tabeli (w naszym programie jest to zmienna m_sUseriD), zaś druga jest używana jedynie jako klucz do wyszukiwania (w naszym programie jest to zmienna m_sUseriDParam).
pFX->SetFieldType(CFieldExchange::param);
RFX Text(pFX, T("[sUserlD]"), m sUserlDParam);
Dodawanie możliwości wyszukiwania
Teraz nadszedł czas, aby dodać kod umożliwiający wyszukiwanie użytkowników w tabeli UserMaster.
1. Otwórz plik ParametrizedDlg.cpp i dopisz do niego poniższą dyrektywę ttinclude: ftinclude "UserMasterSet.h"
2. Teraz musisz dodać funkcję, która po kliknięciu przycisku Pobierz użytkownika wstawi odpowiednie informacje do kontrolek w oknie dialogowym. W tym celu dodaj funkcję obsługi komunikatu BN_CLICKED dla przycisku Pobierz użytkownika (IDC_BTN_GET_USER). Wypełnij j ą poniższym kodem:
void CParameterizedDlg : : OnBtnGetUser ( ) { CUserMasterSet* pUserMasterSet = NULL;

try {
pUserMasterSet = new CUserMasterSet (); }
catch (CMemoryException* pe) {
TRACĘ ("CParameterizedDlg: : OnOk - Wyjątek " "pamięci przy tworzeniu rekordsetu\n" ) ;

pUserMasterSet = NULL;
pe->Delete ( ) ; }
if (pUserMasterSet) { UpdateData(TRUE) ;
try {
pUserMasterSet->m_strFilter = "sUserlD = ?"; pUserMasterSet->m_sUserIDParam = m_strUserID; if (pUserMasterSet->Open( ) ) {
if ( !pUserMasterSet->IsEOF() ) {
m_strUserName = pUserMasterSet->m_sUserName; m_iStatus = pUserMasterSet->m_iStatus;
UpdateData(FALSE) ;
} else
{
CString str = m_strUserID + CString(" nie został "znaleziony" ) ;

AfxMessageBox (str) ; }
pUserMasterSet->Close ( ) ; }
delete pUserMasterSet; }
catch (CDBException* pe) {
if (pUserMasterSet->IsOpen( ) ) {
pUserMasterSet->Close ( ) ; } delete pUserMasterSet;
TRACĘ("CParameterizedDlg::0n0k - Wyjątek "bazy danych - %s\n", pe->m_strError);

pe->Delete();
}
}
}
Budowanie aplikacji
Gdy dodasz kod umożliwiający wyszukiwanie w bazie danych określonego rekordu, po prostu zbuduj aplikację i uruchom ją. Na tym etapie powinieneś otrzymać okno podobne do przedstawionego na rysunku 27.8.
Tworzenie parametryzowanych kwerend
Gdy zakończyłeś już pierwszą część programu demonstracyjnego, nadszedł czas, aby dodać kod dla parametryzowanej kwerendy. Użyjemy tabeli UserMaster wykorzystywanej w całym rozdziale oraz kilku nowych tabel. W tym przykładzie będziemy udawać, że piszemy wielki system zawierający kilka podsystemów - na przykład system dystrybucji zawierający podsystemy takie jak składanie zamówień, fakturowanie, odbiory, nabycia itd. Ten system może także obsługiwać kilka firm.
Wiele dużych korporacji jest podzielonych na kilka mniejszych firm lub oddziałów. Ponieważ użytkownicy zwykle nie mają dostępu do wszystkich podsystemów we wszystkich firmach, system powinien obsługiwać pewien rodzaj zabezpieczeń. Użytkownik Bob może mieć dostęp do systemu składania zamówień i fakturowania firmy A, jednak może nie posiadać żadnych uprawnień dostępu do firmy B. Do ograniczenia dostępu do różnych części systemu można użyć tabeli uprawnień (ang. permission). We wspomnianym przykładzie, Bob miałby dwa rekordy w tabeli Permissions (po jednym dla każdej pary firma-podsystem, dla którego posiada uprawnienia dostępu). Ten scenariusz stanowi podstawę drugiej części programu demonstracyjnego.
Demonstracyjna baza danych
Poza wykorzystywaną już tabelą UserMaster, do działania programu konieczne są trzy dodatkowe tabele (znajdujące się już w dostarczonym pliku .mdb):
Tabela CompanyMaster - ta tabela zawiera listę firm w systemie. Jej jedyne kolumny to CompanylD (identyfikator firmy) oraz Company Name (nazwa firmy).
Tabela SubsystemMaster - ta tabela zawiera listę podsystemów w systemie (na przykład Oreder Entry, Receiving itd.). Jej jedyne kolumny to SubsystemID (identyfikator podsystemu) oraz Subsystem Name (nazwa podsystemu).
Tabela Permissions - ponieważ użytkownik może posiadać uprawnienia dostępu do dowolnej liczby podsystemów w różnych firmach, w celu obsługi tej relacji (typu "wiele do wielu") konieczna jest tabela wiążąca ze sobą użytkownika, firmę oraz podsystem. Tabela posiada więc trzy kolumny: User ID (identyfikator użytkownika), Company ID (identyfikator firmy) oraz Subsystem ID (identyfikator podsystemu). Te kolumny reprezentują podstawowe klucze odpowiednich tabel. W tabeli występują rekordy dla wszystkich użytkowników, firm i podsystemów, dla których użytkownicy mają prawo dostępu.
Demonstracyjna baza danych zawiera również kwerendę (GetPermissionsByUserlD), stanowiącą połączenie pomiędzy wszystkimi czterema tabelami w bazie danych. Ta kwerenda pobiera jako argument identyfikator użytkownika (UserlD) i zwraca nazwę firmy (Company Name) oraz nazwę podsystemu (Subsystem Name) dla wszystkich firm i podsystemów, dla których ten użytkownik ma prawa dostępu.
Użycie parametryzowanej kwerendy w rekordsecie
W Yisual C++ 6.0 tworzenie klasy wyprowadzonej z CRecordset, opartej na parametryzowanej kwerendzie, jest dużo łatwiejsze niż w poprzednich wersjach Yisual C++. Poniższe kroki są podobne do tworzenia dowolnego rekordsetu i zawierają jedynie parę różnic, zaznaczonych z użyciem pogrubionej czcionki. Tak jak w przypadku pierwszej części tego programu demonstracyjnego każdy z kroków zostanie za chwilę dodatkowo omówiony.
1. Za pomocą ClassWizarda stwórz wyprowadzoną z klasy CRecordset klasę CPermissionsQry.
2. Zmień funkcję składową rekordsetu, GetDefaultsoM), tak aby używała składni CALL. Oto przykład tej składni:
{CALL GetPermissionsByUserlD(?)}
3. W pliku nagłówkowym rekordsetu zadeklaruj zmienną (zmienne) parametru.
4. W konstruktorze rekordsetu zainicjuj zmienną (zmienne) parametru.
5. Ustaw zmienną składową m_nParams rekordsetu na ilość parametrów, których chcesz użyć.
6. Zaktualizuj funkcję DoFieldExchange () rekordsetu. Używając parametryzowanej kwerendy, nie potrzebujesz ustawiać zmiennej m_strFilter.
7. Przypisz wartość (wartości) zmiennej (zmiennym) parametrów rekordsetu.
8. Wywołaj funkcję open () rekordsetu w trybie tylko do odczytu.
Tworzenie rekordsetu uprawnień
Gdy już wiemy, jak stworzyć rekordset oparty na parametryzowanej kwerendzie, spróbujmy stworzyć podobny dla naszej demonstracyjnej aplikacji. Ten rekordset będzie oparty na pojedynczej kwerendzie zawartej w przykładowej bazie danych: GetPermissionsByUserlD.
1. Stwórz wyprowadzoną z klasy CRecordset klasę CPermissionsQry. Postępuj zgodnie z zaleceniami zawartymi w jednej z wcześniejszych sekcji tego rozdziału, "Klasa CRecordset". Pamiętaj, by w oknie dialogowym Select Database Tables wskazać kwerendę GetPermissionsByUserlD.
2. Otwórz plik PermissionsQry.cpp i zlokalizuj funkcję składowąGetDefaultSQL ( ) . Odwołując się do parametryzowanych kwerend poprzez sterowniki ODBC Microsoftu, musisz używać składni CALL. Gdy aplikacja wywołuje funkcję CRecordset: : Open ( ) , MFC sprawdza, czy łańcuch zwrócony przez funkcję CRecordset : : GetDef aultSQL () rozpoczyna się od łańcucha {CALL. Jeśli nie, MFC próbuje zbudować instrukcję SQL SELECT. Znak zapytania reprezentuje przekazywany parametr. W przypadku kilku parametrów po prostu zastosuj kilka znaków zapytania, zaś poszczególne parametry rozdziel przecinkami. Zmodyfikuj funkcję GetPermissionsByUserlD ( ) tak, aby wyglądała następująco:
CString CPermissionsQry : : GetDef aultSQL () {
return _T("{CALL GetPermissionsByUserlD (?)}");
3. Dołącz poniższe dwie linie do konstruktora klasy CPermissionsQry:
m_sUserIDParam = _T(""); m_nParams = 1;
4. Na końcu funkcji DoFiiedExchange ( ) dopisz poniższe linie:
pFX->SetFieldType (CFieldExchange : : param) ; RFX_Text (pFX, _T ( " [sUserlD] " ) , m_sUserIDParam) ;
5. Otwórz plik PermissionsQry.h i zadeklaruj zmienną składową:
CString m_sUserIDParam;
6. W pliku ParametrizedDlg.h zadeklaruj poniższą funkcję składową. Ta funkcja będzie wywoływana w celu odczytania wszystkich uprawnień dla użytkownika. Odczytane uprawnienia zostaną wyświetlone w kontrolce drzewa Uprawnienia w oknie dialogowym aplikacji.
protected:
void ShowPermissions ( ) ;
7. Otwórz plik Parametr izedDlg.cpp i włącz poniższy plik nagłówkowy:
#include "PermissionsQry.h"
8. Zlokalizuj w funkcji OnBtnGetUser () poniższą linię kodu:
UpdateData (FALSE) ;
Tuż przed tą linią dopisz wywołanie funkcji wyświetlającej uprawnienia:
ShowPermissions() ;
9. Na końcu pliku ParametrizedDlg.cpp dopisz poniższą funkcję. Ta funkcja wyświetla w kontrolce widoku drzewa uprawnienia posiadane przez użytkownika. Jeśli nie wiesz, jak działają kontrolki widoku drzewa (lub kontrolki widoku listy), zajrzyj do rozdziału 24.
void CParameterizedDlg::ShowPermissions() { m_treePermissions.DeleteAllltems(};

CPermissionsCjry* pPermissionsQry = NULL;

try {
pPermissionsQry = new CPermissionsQry(); }
catch(CMemoryException* pe) {
TRACĘ("CParameterizedDlg::0n0k - Wyjątek pamięci " "przy tworzeniu rekordsetu\n");

pPermissionsCjry = NULL; pe->Delete();
}
if (pPermissionsQry)
{
try
{
pPermissionsQry->m_sUserIDParam = m_strUserID; if (pPermissionsQry->Open ( ) ) {
if ( !pPermissionsQry->IsEOF() ) {
TV_INSERTSTRUCT tvinsert; HTREEITEM hCompany = NULL;

CString strPrevCompanyID;

while ( !pPermissionsQry->IsEOF ( ) ) {
tvinsert .hParent = NULL; tvinsert .hlnsertAfter = TVI_LAST; tvinsert.item.mask = TVIF_TEXT; tvinsert . item. hitem = NULL; tvinsert . item. state = 0; tvinsert . item. stateMask = 0; tvinsert . item. cchTextMax =
pPermissionsQry->m_sCompanyName . GetLength
+1;
tvinsert . item. iSelectedlmage = 0; tvinsert . item. cChildren = 0; tvinsert . item. IParam = 0;
tvinsert.item.pszText =
pPermissionsQry->m_sCompanyName.GetBuffer( tvinsert.item.cchTextMax);

hCompany = m_treePermissions.Insertltem(&tvinsert);

strPrevCompanyID = pPermissionsQry->m_sCompanyID;

while ((!pPermissionsQry->IsEOF()) && (strPrevCompanyID == pPermissionsQry->m_sCompanyID))
{
if (hCompany)
{
tvinsert.hParent = hCompany; tvinsert.item.cchTextMax =
pPermissionsQry->m_sSubsystemName.GetLength()
+ 1; tvinsert.item.pszText =
pPermissionsQry->m_sSubsystemName.GetBuffer(
tvinsert.item.cchTextMax); m_treePermissions.Insertltem(&tvinsert;
}
pPermissionsQry->MoveNext
}
}
}
else
{
CString str = m_strUserID + CString(" nie
"został znaleziony"); AfxMessageBox(str);
}
pPermissionsQry->Close (); }
delete pPermissionsQry; }
catch(CDBException* pe) {
if (pPermissionsQry->IsOpen()) {
pPermissionsQry->Close() ; }
delete pPermissionsQry;
TRACĘ("CParameterizedDlg::0n0k - Wyjątek " "bazy danych " "- %s\n", pe->m strError);
}
}
}
10. Zbuduj i uruchom aplikację. Po uruchomieniu powinieneś ujrzeć okno podobne do przedstawionego na rysunku 27.9.
Podsumowanie
W rozdziałach 26. i 27. zapoznałeś się z ODBC SDK oraz klasami baz danych MFC, łącznie z zaletami i wadami obu technologii. W tym momencie powstaje zasadnicze pytanie. Co jest lepsze - klasy baz danych MFC czy ODBC SDK? Jak widzieliśmy, zarówno MFC jak i ODBC SDK mają swoje zalety. Zaletą korzystania z ODBC SDK jest wydajność i elastyczność, podczas gdy główne zalety klas baz danych MFC płyną z łatwości ich użycia. Tak więc ODBC SDK doskonale nadaje się do tworzenia stabilnych, skalowalnych systemów odwołujących się do różnych DBMS-ów, zaś MFC lepiej sprawdza się w przypadku mniejszych systemów, odwołujących się tylko do pojedynczego DBMS-a. Tak więc o wyborze pomiędzy obiema technologiami decyduje sytuacja i Twoja osobista decyzja jako programisty aplikacji bazy danych.

Wyszukiwarka

Podobne podstrony:
Wprowadzenie do baz danych
Podstawy baz danych zajecia 2 z SQL Tabela Biblioteka
2004 11 Porównanie serwerów relacyjnych baz danych Open Source [Bazy Danych]
IWZ 2 Podstawy baz danych
Analiza baz danych na temat materiałów betonopodobnych
wprowadzenie do baz danych
system baz danych
Administrator baz danych!3101
22 Część VII Udostępnianie baz danych w sieci WWW Podsta
18 Część VI Przegląd baz danych Oracle
Podstawy baz danych zajecia 3 z sql
projektowanie baz danych PRZYKŁAD

więcej podobnych podstron