OLE DB: Programmierung mit Consumer Templates [1st ed.] 3827318335 [PDF]


161 3 3MB

German Pages 473 Year 2001

Report DMCA / Copyright

DOWNLOAD PDF FILE

Table of contents :
OLE DB - Programmierung mit Consumer Templates......Page 3
2.3 UDA-Komponenten......Page 5
4.3 Zusammenfassung......Page 6
6.5.2 Datenquelle schließen......Page 7
8.8 Die Client-Cursor-Dienstkomponente......Page 8
10.3.2 Verwendung von verzögerten Aktualisierungen......Page 9
12.4.3 Beibehaltung von Zeilenmengen......Page 10
13.5 Zusammenfassung......Page 11
15.8 Zusammenfassung......Page 12
Index......Page 13
Vorwort der Übersetzer......Page 15
Vorwort......Page 17
Systemvoraussetzungen......Page 19
Danksagung......Page 21
1.1 Verteilte Internet- Anwendungen......Page 23
1.2 Die DNA-Architektur von Windows......Page 24
1.2.2 Die Geschäftslogikschicht......Page 25
1.2.3 Die Datenschicht......Page 26
1.2.4 Die Hauptmerkmale von DNA noch einmal zusammengefasst......Page 27
1.3.2 Das World Wide Web......Page 28
1.4.1 Wiederverwendung......Page 30
1.4.3 Die COM-Lösung......Page 31
1.4.4 COM+ und Attribute......Page 34
1.4.5 Die Vorteile von COM......Page 35
1.4.6 Teile/Ganzes-Beziehung bei COM......Page 38
1.5 ATL zusammen mit COM-Objekten verwenden......Page 43
1.6 Kapselung von COM-Komponenten......Page 44
1.7 Zusammenfassung......Page 47
2 Universeller Datenzugriff......Page 49
2.1 Universeller Daten-Server......Page 50
2.2 Näheres zum universellen Datenzugriff (UDA)......Page 51
2.3 UDA-Komponenten......Page 52
2.3.1 ODBC (Open DataBase Connectivity)......Page 53
2.3.3 OLE DB......Page 54
2.3.5 OLE DB-Versionen......Page 61
2.3.6 ADO (ActiveX Data Object)......Page 63
2.4 OLE DB Templates......Page 65
2.4.1 Verwendung von OLE DB direkt......Page 66
2.4.2 Verwendung der OLE DB Consumer Templates......Page 70
2.5 Erweiterungen der OLE DB Consumer Templates......Page 71
2.6 Zusammenfassung......Page 72
3.1 Wie nützlich ist der Assistent?......Page 73
3.2.1 ATL-Projekt erstellen......Page 74
3.2.2 Den Assistenten aufrufen......Page 76
3.3 Generierter Code......Page 80
3.3.1 Tabelle öffnen......Page 81
3.3.2 Befehl öffnen......Page 84
3.3.3 Unterstützung für Änderungen hinzufügen......Page 85
3.3.4 Stored Procedures......Page 87
3.3.5 Verbesserungen am generierten Code......Page 88
3.5 Die Klassen der OLE DB Consumer Templates im Überblick......Page 89
Zeilenmengen......Page 90
3.6 CAccessorRowset......Page 92
3.6.1 CAccessorRowset schließen......Page 94
3.7 Zusammenfassung......Page 95
4.1 HRESULT......Page 97
4.1.1 Interpretation von HRESULT......Page 98
4.1.2 Bedeutung von HRESULT in der Praxis......Page 99
4.2.1 Ablaufverfolgung für Fehler......Page 100
4.2.3 IErrorRecords......Page 102
4.2.4 Die OLE DB-Klassen......Page 104
4.2.5 Die grundlegenden Fehlerinformationen abrufen......Page 105
4.2.6 Benutzerdefiniertes Fehlerobjekt abrufen......Page 106
4.2.7 Unterstützung für ErrorInfo......Page 109
4.3 Zusammenfassung......Page 110
5.1 Der Eigenschaftsbegriff......Page 113
5.1.2 GUIDs von Eigenschaften-Sets......Page 114
5.1.3 Eigenschaftengruppen......Page 115
5.1.4 Eigenschaftsbezogene Operationen......Page 116
5.2.2 DBPROP......Page 117
5.2.3 Verwendung unspezifischer Strukturen......Page 118
5.2.4 CDBPropSet......Page 119
5.2.5 Eine Eigenschaft setzen......Page 122
5.2.6 Erforderliche und optionale Eigenschaften......Page 123
5.2.7 Spaltenbezeichner......Page 124
5.2.9 Eigenschaftenstatus prüfen......Page 125
5.3.1 DBPROPIDSET......Page 127
5.3.3 Speicherverwaltung......Page 129
5.3.4 Eine Eigenschaft abrufen......Page 130
5.4 Eigenschaften aufzählen......Page 131
5.4.1 Fehler aufzählen......Page 132
5.5 Eigenschafteninformationen abrufen......Page 133
5.5.2 DBPROPINFO......Page 134
5.6 Zusammenfassung......Page 136
6 Datenquellen und Sitzungen......Page 137
6.1.2 ODBC......Page 138
6.2.1 ProgID und CLSID......Page 139
6.2.2 Datenquelle mit Benutzername und Kennwort öffnen......Page 140
6.2.3 Datenquelle über ein Eigenschaften-Set öffnen......Page 143
6.2.4 Datenquelle mit unvollständigen Informationen öffnen......Page 144
6.2.5 Sicherheit......Page 145
6.3 Datenverknüpfungen und Initialisierungs-Strings......Page 147
6.4 Datenquelle interaktiv öffnen......Page 150
6.4.1 Das Dialogfenster......Page 151
6.5 Dienstkomponenten und Ressourcen-Pooling......Page 152
6.5.1 Ressourcen-Pooling......Page 156
6.6 Enumeratoren......Page 157
6.6.2 Cursorbewegung im Enumerator......Page 159
6.6.4 Provider-Enumeratoren......Page 160
6.7.1 Verbindungs-String in einer Datei speichern......Page 161
6.7.2 Datenquelle bearbeiten......Page 162
6.8 Sitzungen......Page 163
6.9 Zusammenfassung......Page 164
7.1 Soll eine Tabelle oder ein Befehl verwendet werden?......Page 165
7.2 Tabellen......Page 166
7.2.1 Tabelle öffnen......Page 167
7.3 Befehle......Page 169
7.3.1 CCommand......Page 170
7.3.2 Befehl öffnen......Page 171
7.3.3 Befehl schließen......Page 177
7.3.4 Befehl vorbereiten......Page 179
7.3.5 Befehle mit Parametern......Page 182
7.3.6 Befehle mit Mehrfachergebnis......Page 183
7.4 Erweiterungen für Befehle......Page 186
7.4.1 Befehle mit mehreren Parameter- Zugriffsobjekten......Page 187
7.4.2 Befehle mit mehreren Parameter- Sets......Page 191
7.5 Zusammenfassung......Page 192
8.1 Zeilenmengen-Objekte......Page 193
8.2 Zeilenmengen-Klassen......Page 195
8.3 Zeilenmengen und Zeilen......Page 196
8.3.2 Zeilenanzahl und -position......Page 197
8.3.3 Cursorbewegung......Page 198
8.4 Positionsmarken......Page 203
8.4.1 Typen von Positionsmarken......Page 205
8.4.3 Standard-Positionsmarken......Page 208
8.5.2 Eigenschaften abrufen......Page 209
8.6 Zeilenmengen-»Sammelpackungen«......Page 210
8.7 Zeilenmengen-Arrays......Page 214
8.8 Die Client-Cursor-Dienstkomponente......Page 217
8.9 Zeilen suchen......Page 218
8.9.2 Verwendung der Suche mit CRowsetFind......Page 219
8.10 Zusammenfassung......Page 222
9.1 Übersicht......Page 223
9.1.1 Spalten- und Parameter- Zugriffsobjekte......Page 224
9.1.3 Bindungen......Page 225
9.1.4 Zugriffsobjekte als Gruppen von Bindungen......Page 230
9.2 Zugriffsobjekt-Klassen......Page 231
9.2.2 Unterscheidung zwischen den Zugriffsobjekt-Klassen......Page 232
9.2.3 CNoAccessor......Page 233
9.3 Verwendung von CAccessor......Page 234
9.3.1 Makros zur Spaltenbindung......Page 235
9.3.2 Makros zur Längen- und Statusbindung......Page 238
9.3.4 Makros für Spaltentyp und Größe......Page 240
9.3.6 Mehrere Zugriffsobjekte......Page 241
9.3.7 Binden über Referenz......Page 242
9.3.8 Provider-Speicher......Page 243
9.3.9 Client-Speicher......Page 244
9.3.10 BSTR......Page 245
9.3.12 Parameterbindung......Page 246
9.4 Dynamische Zugriffsobjekte......Page 247
9.4.1 Verwendung von dynamischen Zugriffsobjekten......Page 248
9.4.2 Datenbestandteile abrufen und setzen......Page 251
9.4.3 Verwendung von CDynamicParameterAccessor......Page 253
9.4.4 Erweiterungen für dynamische Zugriffsobjekte......Page 254
9.5 Zusammenfassung......Page 255
10.1 Daten abrufen......Page 257
10.1.1 Automatische Zugriffsobjekte......Page 258
10.1.2 GetDataHere......Page 260
10.2 Daten vom Consumer zum Datenspeicher übertragen......Page 261
10.2.1 Zeile löschen......Page 262
10.2.2 Daten setzen......Page 263
10.2.3 Zeilen einfügen......Page 264
10.2.4 Soll ich Zeilenmengen-Methoden oder SQL-Befehle verwenden?......Page 265
10.3 Verzögerte Aktualisierungen......Page 266
10.3.3 Der Änderungsstatus bei verzögerter Aktualisierung......Page 267
Cache......Page 269
10.3.5 Die Änderungen übertragen......Page 270
10.3.6 Alle Änderungen übertragen......Page 272
10.3.7 Änderungen rückgängig machen......Page 273
10.3.9 Ursprüngliche und zuletzt sichtbare Daten......Page 274
10.3.10Verzögerte Aktualisierungen und Transaktionen......Page 275
10.4 Benachrichtigungen......Page 276
10.4.1 Zeilenmengen-Listener......Page 278
10.4.2 Gründe für Ereignisse......Page 279
10.4.3 Ereignisphasen......Page 280
10.4.4 Beispiel für Benachrichtigungen......Page 282
10.5 Zusammenfassung......Page 283
11.1.1 Inhalt......Page 285
11.1.3 Lange und kurze BLOBs......Page 286
11.2 BLOBs als Daten im Speicher......Page 287
11.3 BLOBs als Daten im Speicher mit Puffer-Referenz......Page 288
11.4.1 Speicherschnittstellen......Page 289
11.4.2 BLOB-Daten über ein Speicherobjekt abrufen......Page 291
11.4.4 BLOBs in ein Speicherobjekt schreiben......Page 296
11.4.5 BLOBs in ein vom Consumer erzeugtes Speicherobjekt schreiben......Page 297
11.4.6 Eigenschaften von BLOBs......Page 298
11.4.7 Mit mehreren BLOBs arbeiten......Page 299
11.4.8 Schnittstellen, Speicher und Dateien konvertieren......Page 302
11.5 Auf persistente COM-Objekte zugreifen......Page 305
11.6 Zusammenfassung......Page 306
12.1 Warum werden Transaktionen benötigt?......Page 307
12.2 Transaktionen als Lösung......Page 309
12.3.3 Isolation......Page 311
12.4.1 Einfache Transaktionen......Page 312
12.4.2 Erneuerung von Transaktionen......Page 313
12.4.3 Beibehaltung von Zeilenmengen......Page 314
12.5 Isolationsstufen......Page 316
12.5.1 Demoprogramme für die Transaktionsisolation......Page 318
Daten lesen«......Page 320
lesen«......Page 322
12.5.4 Transaktionen mit der Isolationsstufe »Wiederholbares Lesen«......Page 323
12.5.6 Isolationsstufen und Sperrungen......Page 325
12.5.7 Sitzungen ohne Transaktionen......Page 328
12.6 Asynchrone Transaktionen......Page 329
12.6.1 Verwendung des Transaktionsergebnisses......Page 330
12.7.1 Verwendung von verschachtelten Transaktionen......Page 331
12.8.1 Microsoft Distributed Transaction Coordinator......Page 333
12.8.2 Verwendung von MS DTC......Page 334
12.9.1 Granularität von Komponenten......Page 336
12.9.2 Microsoft Transaction Server und COM+-Komponenten......Page 338
12.9.3 Transaktionseinbindung......Page 342
12.10 Zusammenfassung......Page 344
13.1 Indizes......Page 345
13.1.2 Verwendung von Indizes......Page 346
13.1.3 Index öffnen......Page 348
13.1.4 Indexsuche nach einer Zeile......Page 349
13.1.5 Indexbereich beschränken......Page 351
13.2 Kapitelmarken......Page 353
13.2.1 Verwendung von Kapitelmarken......Page 354
13.2.2 CRowsetEx......Page 356
13.3 Hierarchische Zeilenmengen......Page 358
13.3.1 Beispiel......Page 359
13.3.2 Herkömmlicher Ansatz......Page 360
13.3.4 Der Data Shape-Dienst......Page 361
13.3.5 Cursorbewegung in hierarchischen Zeilenmengen......Page 363
13.4 Sichten......Page 368
13.4.2 Sicht öffnen......Page 369
13.4.3 Der Sichtentyp......Page 370
13.4.5 Sicht filtern......Page 372
13.4.6 Kapitelmarken für Sichten......Page 373
13.4.7 Sichten stapeln......Page 374
13.5 Zusammenfassung......Page 375
14.1 Das Schema abfragen und durch dessen Daten navigieren......Page 377
14.1.1 Restriktionen......Page 378
14.2.1 Kataloge......Page 380
14.2.2 Tabellen......Page 381
14.2.3 Spalten......Page 382
14.2.5 Prozeduren......Page 383
14.2.6 Indizes......Page 384
14.2.7 Schlüssel......Page 385
14.2.8 Berechtigungen......Page 386
14.2.9 Provider-Typen......Page 387
14.3 Schema bearbeiten......Page 389
14.3.1 CTableDefinition......Page 390
14.3.2 CIndexDefinition......Page 393
14.4 Schema mit Datenbank synchronisieren......Page 394
14.4.1 Prüfung der Bindungen zum Zeitpunkt der Kompilierung......Page 395
14.4.2 Spalten anhand von Namen binden......Page 397
14.4.3 Tabellen über den Programmcode erstellen......Page 399
14.5 Zusammenfassung......Page 402
15.1 Tabellendaten und mehrdimensionale Daten......Page 403
15.2 Mehrdimensionale Sitzungen und Befehle......Page 407
15.3.1 Das Datenmengen-Objekt......Page 408
15.3.2 CDataset......Page 409
15.4 Metadaten: Informationen zu den Achsen......Page 410
15.4.1 Durch die Achseninformationen navigieren......Page 411
15.4.2 Achsen mit mehreren Dimensionen......Page 414
Dimensionen......Page 415
15.5.1 Wie werden Zellen identifiziert?......Page 417
15.5.2 Zugriffsobjekte für Zellen......Page 419
15.5.3 Zellendaten abrufen......Page 420
15.5.4 Zellenbereiche......Page 421
zugreifen......Page 424
15.8 Zusammenfassung......Page 425
16 Hierarchische Daten......Page 427
16.1 Übersicht über die verwendeten Objekte......Page 429
16.1.2 Mit direkter Bindung arbeiten......Page 430
16.2.1 Zeile öffnen......Page 432
16.2.3 Spaltenwerte abrufen und setzen......Page 433
16.2.4 Operationen mit definiertem Bereich......Page 436
16.3 Dokumente......Page 440
16.4 Das Zeilenordner- Objekt......Page 442
16.4.1 Zeilenordner öffnen und erstellen......Page 443
16.4.2 Untergeordnete Zeilen (Kind- Zeilen) aufzählen......Page 445
16.5.1 Datenstrom öffnen......Page 446
16.7 Nachwort......Page 448
A. 1 Vererbung ausgehend von einem Template-Parameter......Page 451
A. 2 Template-Standardparameter......Page 452
A. 3 Überladene Typendefinitionen......Page 453
B OLE DB-Typen und Konvertierungen......Page 455
B.1 Konvertierungstabelle......Page 456
A......Page 459
B......Page 460
C......Page 461
D......Page 462
E......Page 463
G......Page 464
I......Page 465
M......Page 466
O......Page 467
R......Page 468
S......Page 469
T......Page 470
Z......Page 471
Ins Internet: Weitere Infos zum Buch, Downloads, etc.......Page 0
© Copyright......Page 473
Papiere empfehlen

OLE DB: Programmierung mit Consumer Templates [1st ed.]
 3827318335 [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

Sandini Bib

OLE DB

Sandini Bib

Programmer’s Choice

Sandini Bib

Pierre Nallet

OLE DB Programmierung mit Consumer Templates

An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam

Sandini Bib

Die Deutsche Bibliothek – CIP-Einheitsaufnahme Ein Titeldatensatz für diese Publikation ist bei Der Deutschen Bibliothek erhältlich. Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Abbildungen und Texten wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen, die in diesem Buch erwähnt werden, sind gleichzeitig eingetragene Warenzeichen oder sollten als solche betrachtet werden. Umwelthinweis: Dieses Produkt wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material. Die amerikanische Orginalausgabe ist erschienen bei Addison-Wesley USA unter dem Titel „OLE DB Consumer Templates” ISBN 0-201-65792-9 © 2001 by Addison-Wesley 5 05

4 04

3

2

1

03

02

01

ISBN 3-8273-1833-5

© 2001 by Addison-Wesley Verlag, ein Imprint der Pearson Education Deutschland GmbH Martin-Kollar-Straße 10–12, D-81829 München/Germany Alle Rechte vorbehalten Einbandgestaltung: Christine Rechl, München Titelbild: Adiantum pedatum, Haarfarn. © Karl Blossfeldt Archiv – Ann und Jürgen Wilde, Zülpich/VG Bild-Kunst Bonn, 2001. Lektorat: Martin Asbach, [email protected] Korrektorat: Anne Spranger, München Herstellung: TYPisch Müller, Arcevia, [email protected] Satz: reemers publishing services gmbh, Krefeld, www.reemers.de Druck und Verarbeitung: Wiener Verlag, Wien Printed in Germany

Sandini Bib

Inhalt

Vorwort der Übersetzer

15

Vorwort

17

Die Zukunft von OLE DB Systemvoraussetzungen

19 19

Danksagung

21

1

Windows DNA und COM

23

1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.3 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.5 1.6 1.7

Verteilte Internet-Anwendungen Die DNA-Architektur von Windows Die Präsentationsschicht Die Geschäftslogikschicht Die Datenschicht Die Hauptmerkmale von DNA noch einmal zusammengefasst DNA-Szenarien Die Client/Server-Architektur Das World Wide Web Das Komponentenobjektmodell Wiederverwendung Herausforderungen Die COM-Lösung COM+ und Attribute Die Vorteile von COM Teile/Ganzes-Beziehung bei COM ATL zusammen mit COM-Objekten verwenden Kapselung von COM-Komponenten Zusammenfassung

23 24 25 25 26 27 28 28 28 30 30 31 31 34 35 38 43 44 47

2

Universeller Datenzugriff

49

2.1 2.2 2.3

Universeller Daten-Server Näheres zum universellen Datenzugriff (UDA) UDA-Komponenten

50 51 52

Sandini Bib 6

Inhalt

2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.4 2.4.1 2.4.2 2.5 2.6

ODBC (Open DataBase Connectivity) Nachteile von ODBC OLE DB Vergleichende Betrachtung von ODBC und OLE DB OLE DB-Versionen ADO (ActiveX Data Object) OLE DB Templates Verwendung von OLE DB direkt Verwendung der OLE DB Consumer Templates Erweiterungen der OLE DB Consumer Templates Zusammenfassung

53 54 54 61 61 63 65 66 70 71 72

3

Der OLE DB-Assistent

73

3.1 3.2 3.2.1 3.2.2 3.3 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.4 3.5 3.5.1

73 74 74 76 80 81 84 85 87 88 89 89

3.5.2 3.6 3.6.1 3.7

Wie nützlich ist der Assistent? Verwendung des Assistenten ATL-Projekt erstellen Den Assistenten aufrufen Generierter Code Tabelle öffnen Befehl öffnen Unterstützung für Änderungen hinzufügen Stored Procedures Verbesserungen am generierten Code Der MFC-Assistent Die Klassen der OLE DB Consumer Templates im Überblick Architektur der Zugriffsobjekt-Klassen für Tabellen, Befehle und Zeilenmengen Klassen kombinieren CAccessorRowset CAccessorRowset schließen Zusammenfassung

90 92 92 94 95

4

Fehler und Fehlermechanismen

97

4.1 4.1.1 4.1.2 4.2 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.3

HRESULT Interpretation von HRESULT Bedeutung von HRESULT in der Praxis Fehlerinfo- und Fehlerdatensatz-Objekte Ablaufverfolgung für Fehler IErrorInfo IErrorRecords Die OLE DB-Klassen Die grundlegenden Fehlerinformationen abrufen Benutzerdefiniertes Fehlerobjekt abrufen Unterstützung für ErrorInfo Das Puzzle zusammenfügen Zusammenfassung

97 98 99 100 100 102 102 104 105 106 109 110 110

Sandini Bib Inhalt

7

5

OLE DB-Eigenschaften

5.1 5.1.1 5.1.2 5.1.3 5.1.4 5.2 5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3 5.3.1 5.3.2 5.3.3 5.3.4 5.4 5.4.1 5.5 5.5.1 5.5.2 5.5.3 5.6

Der Eigenschaftsbegriff Eigenschaften und Eigenschaften-Sets GUIDs von Eigenschaften-Sets Eigenschaftengruppen Eigenschaftsbezogene Operationen Eigenschaften setzen DBPROPSET DBPROP Verwendung unspezifischer Strukturen CDBPropSet Eine Eigenschaft setzen Erforderliche und optionale Eigenschaften Spaltenbezeichner Benutzerdefinierte Eigenschaften-Klassen Eigenschaftenstatus prüfen Eigenschaften abrufen DBPROPIDSET CDBPropIDSet Speicherverwaltung Eine Eigenschaft abrufen Eigenschaften aufzählen Fehler aufzählen Eigenschafteninformationen abrufen DBPROPINFOSET DBPROPINFO CDBPropertyInfoSet Zusammenfassung

6

Datenquellen und Sitzungen

6.1 6.1.1 6.1.2 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2

Geschichtlicher Rückblick Proprietäre Datenbanken ODBC Datenquelle über die Programmierung öffnen ProgID und CLSID Datenquelle mit Benutzername und Kennwort öffnen Datenquelle über ein Eigenschaften-Set öffnen Datenquelle mit unvollständigen Informationen öffnen Sicherheit Datenverknüpfungen und Initialisierungs-Strings Verbindungs-String speichern Datenquelle interaktiv öffnen Das Dialogfenster DATENLINKEIGENSCHAFTEN Das Dialogfenster Datenverknüpfungsdatei auswählen Dienstkomponenten und Ressourcen-Pooling Ressourcen-Pooling Datenquelle schließen

113 113 114 114 115 116 117 117 117 118 119 122 123 124 125 125 127 127 129 129 130 131 132 133 134 134 136 136

137 138 138 138 139 139 140 143 144 145 147 150 150

151 152 152 156 157

Sandini Bib 8

Inhalt

6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.7 6.7.1 6.7.2 6.7.3 6.8 6.9

Enumeratoren Der Stamm-Enumerator Cursorbewegung im Enumerator Datenquellen und Enumeratoren Provider-Enumeratoren Erweiterungen für Datenquellen Verbindungs-String in einer Datei speichern Datenquelle bearbeiten Die CLSID abrufen Sitzungen Zusammenfassung

7

Tabellen und Befehle

7.1 7.2 7.2.1 7.3 7.3.1 7.3.2 7.3.3 7.3.4 7.3.5 7.3.6 7.4 7.4.1 7.4.2 7.5

Soll eine Tabelle oder ein Befehl verwendet werden? Tabellen Tabelle öffnen Befehle CCommand Befehl öffnen Befehl schließen Befehl vorbereiten Befehle mit Parametern Befehle mit Mehrfachergebnis Erweiterungen für Befehle Befehle mit mehreren Parameter-Zugriffsobjekten Befehle mit mehreren Parameter-Sets Zusammenfassung

8

Zeilenmengen

8.1 8.2 8.3 8.3.1 8.3.2 8.3.3 8.3.4 8.4 8.4.1 8.4.2 8.4.3 8.5 8.5.1 8.5.2 8.6 8.7 8.8

Zeilenmengen-Objekte Zeilenmengen-Klassen Zeilenmengen und Zeilen Zeilenverwaltung Zeilenanzahl und -position Cursorbewegung Zeilenidentität Positionsmarken Typen von Positionsmarken Position von Positionsmarken Standard-Positionsmarken Zeilenmengen-Eigenschaften Eigenschaften bei der Erzeugung setzen Eigenschaften abrufen Zeilenmengen-»Sammelpackungen« Zeilenmengen-Arrays Die Client-Cursor-Dienstkomponente

157 159 159 160 160 161 161 162 163 163 164

165 165 166 167 169 170 171 177 179 182 183 186 187 191 192

193 193 195 196 197 197 198 203 203 205 208 208 209 209 209 210 214 217

Sandini Bib Inhalt

9

8.9 8.9.1 8.9.2 8.10

Zeilen suchen Beispiel für eine Suchoperation Verwendung der Suche mit CRowsetFind Zusammenfassung

9

Zugriffsobjekte

9.1 9.1.1 9.1.2 9.1.3 9.1.4 9.2 9.2.1 9.2.2 9.2.3 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.3.7 9.3.8 9.3.9 9.3.10 9.3.11 9.3.12 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5

Übersicht Spalten- und Parameter-Zugriffsobjekte Eingabe- und Ausgabe-Zugriffsobjekte Bindungen Zugriffsobjekte als Gruppen von Bindungen Zugriffsobjekt-Klassen CAccessorBase Unterscheidung zwischen den Zugriffsobjekt-Klassen CNoAccessor Verwendung von CAccessor Makros zur Spaltenbindung Makros zur Längen- und Statusbindung Makros für Genauigkeit und Skalierung Makros für Spaltentyp und Größe Automatische Zugriffsobjekte Mehrere Zugriffsobjekte Binden über Referenz Provider-Speicher Client-Speicher BSTR Zusammenfassung der Bindungsverfahren Parameterbindung Dynamische Zugriffsobjekte Verwendung von dynamischen Zugriffsobjekten Datenbestandteile abrufen und setzen Verwendung von CDynamicParameterAccessor Erweiterungen für dynamische Zugriffsobjekte Zusammenfassung

10

Datenaustausch

10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.2.4 10.3 10.3.1 10.3.2

Daten abrufen Automatische Zugriffsobjekte GetDataHere Daten vom Consumer zum Datenspeicher übertragen Zeile löschen Daten setzen Zeilen einfügen Soll ich Zeilenmengen-Methoden oder SQL-Befehle verwenden? Verzögerte Aktualisierungen Verzögerte Änderungen definieren Verwendung von verzögerten Aktualisierungen

218 219 219 222

223 223 224 225 225 230 231 232 232 233 234 235 238 240 240 241 241 242 243 244 245 246 246 247 248 251 253 254 255

257 257 258 260 261 262 263 264 265 266 267 267

Sandini Bib 10

Inhalt

10.3.3 10.3.4 10.3.5 10.3.6 10.3.7 10.3.8 10.3.9 10.3.10 10.4 10.4.1 10.4.2 10.4.3 10.4.4 10.5

Der Änderungsstatus bei verzögerter Aktualisierung Zeilen mit bestimmtem Änderungsstatus abrufen: Navigation im Cache Die Änderungen übertragen Alle Änderungen übertragen Änderungen rückgängig machen Alle Änderungen rückgängig machen Ursprüngliche und zuletzt sichtbare Daten Verzögerte Aktualisierungen und Transaktionen Benachrichtigungen Zeilenmengen-Listener Gründe für Ereignisse Ereignisphasen Beispiel für Benachrichtigungen Zusammenfassung

11

BLOBs

11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.3 11.4 11.4.1 11.4.2 11.4.3 11.4.4 11.4.5 11.4.6 11.4.7 11.4.8 11.5 11.6

Die verschiedenen BLOB-Typen Inhalt Zugriffsverfahren Lange und kurze BLOBs BLOBs mit fester und variabler Größe BLOBs als Daten im Speicher BLOBs als Daten im Speicher mit Puffer-Referenz BLOBs als Daten in einem Speicherobjekt Speicherschnittstellen BLOB-Daten über ein Speicherobjekt abrufen BLOB-Daten mit Hilfe eines Speicherobjekts speichern BLOBs in ein Speicherobjekt schreiben BLOBs in ein vom Consumer erzeugtes Speicherobjekt schreiben Eigenschaften von BLOBs Mit mehreren BLOBs arbeiten Schnittstellen, Speicher und Dateien konvertieren Auf persistente COM-Objekte zugreifen Zusammenfassung

12

Transaktionen

12.1 12.2 12.3 12.3.1 12.3.2 12.3.3 12.3.4 12.4 12.4.1 12.4.2 12.4.3

Warum werden Transaktionen benötigt? Transaktionen als Lösung Merkmale von Transaktionen Atomicity (Alles-oder-nichts-Prinzip) Consistency (Konsistenz) Isolation Durability (Dauerhaftigkeit) Verwendung von Transaktionen Einfache Transaktionen Erneuerung von Transaktionen Beibehaltung von Zeilenmengen

267 269 270 272 273 274 274 275 276 278 279 280 282 283

285 285 285 286 286 287 287 288 289 289 291 296 296 297 298 299 302 305 306

307 307 309 311 311 311 311 312 312 312 313 314

Sandini Bib Inhalt

11

12.5 12.5.1 12.5.2 12.5.3 12.5.4 12.5.5 12.5.6 12.5.7 12.6 12.6.1 12.6.2 12.7 12.7.1 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.10

Isolationsstufen Demoprogramme für die Transaktionsisolation Transaktionen mit der Isolationsstufe »Nicht festgeschriebene Daten lesen« Transaktionen mit der Isolationsstufe »Festgeschriebene Daten lesen« Transaktionen mit der Isolationsstufe »Wiederholbares Lesen« Transaktionen mit der Isolationsstufe »Serialisierbar« Isolationsstufen und Sperrungen Sitzungen ohne Transaktionen Asynchrone Transaktionen Verwendung des Transaktionsergebnisses Ereignisse einer Transaktion abrufen Verschachtelte Transaktionen Verwendung von verschachtelten Transaktionen Verteilte Transaktionen Microsoft Distributed Transaction Coordinator Verwendung von MS DTC Transaktionen und Komponenten Granularität von Komponenten Microsoft Transaction Server und COM+-Komponenten Transaktionseinbindung Zusammenfassung

13

Indizes, Kapitelmarken und Sichten

13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.1.5 13.2 13.2.1 13.2.2 13.3 13.3.1 13.3.2 13.3.3 13.3.4 13.3.5 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.4.5 13.4.6 13.4.7 13.5

Indizes Der Indextyp Verwendung von Indizes Index öffnen Indexsuche nach einer Zeile Indexbereich beschränken Kapitelmarken Verwendung von Kapitelmarken CRowsetEx Hierarchische Zeilenmengen Beispiel Herkömmlicher Ansatz Ansatz mit Verwendung von Kapitelmarken Der Data Shape-Dienst Cursorbewegung in hierarchischen Zeilenmengen Sichten Wann sollten Sichten verwendet werden? Sicht öffnen Der Sichtentyp Sicht sortieren Sicht filtern Kapitelmarken für Sichten Sichten stapeln Zusammenfassung

316 318 320 322 323 325 325 328 329 330 331 331 331 333 333 334 336 336 338 342 344

345 345 346 346 348 349 351 353 354 356 358 359 360 361 361 363 368 369 369 370 372 372 373 374 375

Sandini Bib 12

Inhalt

14

Schemata, Definitionen und Synchronisation

14.1 14.1.1 14.2 14.2.1 14.2.2 14.2.3 14.2.4 14.2.5 14.2.6 14.2.7 14.2.8 14.2.9 14.3 14.3.1 14.3.2 14.4 14.4.1 14.4.2 14.4.3 14.5

Das Schema abfragen und durch dessen Daten navigieren Restriktionen Das Katalog/Schema-Modell Kataloge Tabellen Spalten Sichten Prozeduren Indizes Schlüssel Berechtigungen Provider-Typen Schema bearbeiten CTableDefinition CIndexDefinition Schema mit Datenbank synchronisieren Prüfung der Bindungen zum Zeitpunkt der Kompilierung Spalten anhand von Namen binden Tabellen über den Programmcode erstellen Zusammenfassung

15

OLAP

15.1 15.2 15.3 15.3.1 15.3.2 15.3.3 15.4 15.4.1 15.4.2 15.4.3 15.4.4

Tabellendaten und mehrdimensionale Daten Mehrdimensionale Sitzungen und Befehle Datenmengen Das Datenmengen-Objekt CDataset Verwendung von Datenmengen Metadaten: Informationen zu den Achsen Durch die Achseninformationen navigieren Achsen mit mehreren Dimensionen Achseninformationen für Datenmengen mit mehreren Dimensionen Verwendung von CAxisAccessor für Datenmengen mit mehreren Dimensionen Zellen Wie werden Zellen identifiziert? Zugriffsobjekte für Zellen Zellendaten abrufen Zellenbereiche Über Zeilenmengen auf mehrdimensionale Daten zugreifen OLAP-Schema-Daten Zusammenfassung

15.5 15.5.1 15.5.2 15.5.3 15.5.4 15.6 15.7 15.8

377 377 378 380 380 381 382 383 383 384 385 386 387 389 390 393 394 395 397 399 402

403 403 407 408 408 409 410 410 411 414 415 415 417 417 419 420 421 424 425 425

Sandini Bib Inhalt

13

16

Hierarchische Daten

16.1 16.1.1 16.1.2 16.2 16.2.1 16.2.2 16.2.3 16.2.4 16.3 16.4 16.4.1 16.4.2 16.5 16.5.1 16.5.2 16.6 16.7

Übersicht über die verwendeten Objekte Direktes Binden Mit direkter Bindung arbeiten Das Zeilenobjekt Zeile öffnen Zeile erstellen Spaltenwerte abrufen und setzen Operationen mit definiertem Bereich Dokumente Das Zeilenordner-Objekt Zeilenordner öffnen und erstellen Untergeordnete Zeilen (Kind-Zeilen) aufzählen Datenströme Datenstrom öffnen Daten aus einem Datenstrom lesen und in einen Datenstrom schreiben Zusammenfassung Nachwort

A

Erweiterte C++-Techniken

A.1 A.2 A.3

Vererbung ausgehend von einem Template-Parameter Template-Standardparameter Überladene Typendefinitionen

B

OLE DB-Typen und Konvertierungen

B.1

Konvertierungstabelle

Index

427 429 430 430 432 432 433 433 436 440 442 443 445 446 446 448 448 448

451 451 452 453

455 456

459

Sandini Bib

Sandini Bib

Vorwort der Übersetzer In fast jedem Buch zur professionellen Programmierung finden Sie ein Vorwort der Übersetzer, in dem diese erläutern, warum sie sich für eine mehr oder weniger enge Anlehnung an die häufig vorherrschende englischsprachige Terminologie entschieden haben. Wir machen dabei keine Ausnahme, denn diese Entscheidung hat einen ganz entscheidenden Einfluss auf die Formulierung – und Lesbarkeit – des deutschen Textes. Bei einer solchen Übersetzung gilt es, einen sinnvollen Mittelweg zwischen zwei Extremen zu finden: einerseits die kritiklose, durchgehende Übernahme der englischen Begriffe, bei der sich die Arbeit des Übersetzers – übertrieben gesagt – hauptsächlich im Einfügen deutscher Füllwörter erschöpft, und andererseits die vollständige Eindeutschung, die den großen Nachteil hat, dass die teilweise bereits etablierte englische Terminologie nirgends mehr zu finden ist und daher der Fachmann wenig mit dem Text anfangen kann. Da es unsere Aufgabe ist, einen gut lesbaren deutschen Text zu erstellen, haben wir uns in Absprache mit dem Verlag und mit dem Autor dafür entschieden, einige zentrale Begriffe in diesem Buch (wie z.B. »Rowset« oder »Accessor«) einzudeutschen. Der Bezug zum englischen Terminus bleibt dabei durch entsprechende Erwähnung und verweisende Indexeinträge gewahrt. Bei anderen Begriffen, für die sich bisher keine einheitliche deutsche Übersetzung durchgesetzt hat, haben wir die englischen Benennungen beibehalten. Dies trifft vor allem auf die zentralen Begriffe »Provider«, »Consumer« und »Consumer Templates« zu, die in den einschlägigen Publikationen teils mit unterschiedlichen Übersetzungen eingedeutscht, teils Englisch gelassen wurden. Auch in der MicrosoftDokumentation war hierzu keine einheitliche Linie zu finden, so dass die Entscheidung letztendlich bei uns lag. Eine Hilfe hierbei war, dass der Verlag den Titel des Buches bereits im Vorfeld festgelegt hatte. Wir möchten uns an dieser Stelle ganz herzlich beim Autor des Buches, Pierre Nallet, für seine tatkräftige Unterstützung bedanken. Ein weiterer Dank gilt unserem Betreuer Martin Asbach von der Pearson Education Deutschland GmbH für die gute Zusammenarbeit bei diesem Projekt. Mai 2001

Sandini Bib 16

Vorwort der Übersetzer

Anregungen und Kritik zu unserer Übersetzung sind jederzeit willkommen. Manfred Altmann und Annette Hunger Fachübersetzungen Hunger/Altmann Panger Str. 30 83026 Rosenheim Tel. 0 80 31 / 61 51 84 Mail: [email protected] http://www.pang30.de

Sandini Bib

Vorwort OLE DB, OLE DB Consumer Templates? Warum soll ich mich damit beschäftigen? Bei einer neuen Technologie herrscht zunächst einmal Unklarheit: Warum brauchen wir sie überhaupt? Was bewirkt sie? Wie funktioniert sie? Lohnt es sich, dass ich meine Zeit dafür opfere? OLE DB ist eine der vielen Datenzugriffstechnologien, die heutzutage zur Verfügung stehen. In diesem Bereich gibt es inzwischen zahlreiche Alternativen, eine davon ist z.B. ODBC von Microsoft. In der Regel deutet das Vorhandensein vieler konkurrierender Technologien darauf hin, dass keine auch nur annähernd perfekt ist. Die eine ist vielleicht anwenderfreundlich, aber langsam, die nächste mag zwar schnell sein, ist aber nicht portierbar, die dritte wiederum ist portierbar, dafür aber nicht anwenderfreundlich usw. OLE DB ist den anderen Technologien in vielen Bereichen überlegen: Diese Technologie basiert auf COM, sie ist in hohem Maße modular aufgebaut und effizient, sie bietet Lösungen für viele Probleme beim Datenzugriff und sie wird von zahlreichen Datenbankanbietern unterstützt. So ist es kein Wunder, dass Microsoft OLE DB innerhalb von SQL Server und für verteilte Abfragen verwendet. OLE DB weist jedoch auch einen Nachteil auf: eine hohe Komplexität. Das Schreiben eines einfachen Programms oder einer simplen Komponente mit OLE DB ist genauso aufwändig wie das Programmieren einer Anwendung mit graphischer Benutzeroberfläche (GUI) unter Verwendung der Windows-API: Es ist eine große Menge (nicht so einfachen) Codes notwendig um eine einfache Aufgabe durchzuführen. Dies ist ein Fall für die OLE DB Consumer Templates: Bei ihnen handelt es sich um eine äußerst leistungsfähige, höhere C++-Bibliothek, die auf OLE DB aufsetzt. Die OLE DB Consumer Templates sind für die Datenbankprogrammierung das, was ATL für die Programmierung graphischer Benutzeroberflächen ist. Dieser Ansatz bietet u.a. den großen Vorteil, dass der Programmierer es sich aussuchen kann, auf welcher Ebene er arbeiten möchte. Mit der Template-Bibliothek haben Sie die Möglichkeit, auf einer sehr abstrakten Ebene zu arbeiten, aber Sie können auch auf alle Details der darunter liegenden Ebenen zugreifen. Die Leistungsfähigkeit von OLE DB kann aus zwei verschiedenen Perspektiven betrachtet werden:

Sandini Bib 18

Vorwort

왘 Als Datenbankprogrammierer kennen Sie vermutlich die Nachteile aus erster Hand, die ein direkter Zugriff der Benutzer auf eine Datenbank mit sich bringt. Es ist eine Art schützender Zwischenschicht erforderlich. Mit geeigneten COM-Komponenten können Sie hier Abhilfe schaffen, und die OLE DB Consumer Templates sind das ideale Instrument um diese Komponenten in die Datenbank zu integrieren. 왘 Als C++-Entwickler brauchen Sie Unterstützung für den Umgang mit persistenten Daten. Datenbanken sind eine der möglichen Lösungen. Diese weisen aber bekanntermaßen den Nachteil auf, dass die Speicherung der Daten in einer Datenbank für einen Programmierer eine schwierige Aufgabe darstellt. Hier bieten die OLE DB Consumer Templates einen unkomplizierten Ansatz speziell für C++-Programmierer. Am Anfang war ich glücklich und zufrieden, mit den OLE DB Consumer Templates zu programmieren. Im Laufe der Zeit erkannte ich jedoch, dass diese Bibliothek nur unzureichend dokumentiert war und dass es auch kein Buch darüber gab. So war ich gezwungen mich selbst eingehender mit ihr zu beschäftigen. Zudem stellte ich fest, dass die OLE DB Consumer Templates nicht alle Fassetten von OLE DB berücksichtigten. So begann ich kleine Erweiterungen zu schreiben, um zusätzliche Aspekte von OLE DB abzudecken oder alternative Möglichkeiten anzubieten. Ich stellte diese in einer Bibliothek mit dem Namen »OLE DB Extensions« (OLE DB-Erweiterungen) zusammen. Als ich kurz davor war, den Code freizugeben, stellte sich heraus, dass diese Bibliothek inzwischen umfangreicher geworden war als die eigentlichen OLE DB Consumer Templates und daher dokumentiert werden musste. Dies war meine Motivation zum Schreiben dieses Buches. Hier ergab sich die Möglichkeit, zwei Fliegen mit einer Klappe zu schlagen: Einerseits die unzureichende Dokumentation über die OLE DB Consumer Templates zu ergänzen und andererseits meine OLE DB-Erweiterungen ausführlich zu dokumentieren. Wenn Sie zum ersten Mal mit OLE DB programmieren, haben Sie zwei Möglichkeiten zum Einstieg in die Materie: Sie können entweder auf der unteren Ebene mit dem OLE DB SDK von Microsoft beginnen und sich dann auf eine abstraktere Ebene hocharbeiten oder Sie beginnen auf der C++-Ebene mit den OLE DB Consumer Templates und steigen jeweils bei Bedarf auf die SDK-Ebene hinab. Thema dieses Buches ist die OLE DB-Programmierung mit den Consumer Templates und nicht mit dem SDK. Bei Microsoft ist OLE DB detailliert dokumentiert, daher wäre es unsinnig, an dieser Stelle ausführlich die Grundlagen zu wiederholen. Da dieses Buch jedoch eine mehr oder weniger eigenständige Dokumentation darstellt, werde ich auf SDK-Konzepte immer dann eingehen, wenn sie für das Verständnis des eigentlichen Themas notwendig sind. Dies bedeutet, dass Sie sicherlich nicht oft in der SDKDokumentation werden nachschlagen müssen. Damit soll aber keineswegs gesagt werden, dass Sie das OLE DB SDK links liegen lassen sollten. Diese Dokumentation ist als Referenz unersetzlich.

Sandini Bib Die Zukunft von OLE DB

19

Der Umgang mit OLE DB ähnelt ein wenig dem Arbeiten mit COM. Am Anfang werden Sie sich fragen, warum Sie sich mit dieser komplexen Technologie auseinander setzen sollen. Wenn Sie aber einige Zeit damit gearbeitet haben, werden Sie sich fragen, wie Sie früher ohne OLE DB auskommen konnten. Dazwischen muss ein Prozess liegen, in dem sich Ihre Wahrnehmung dieser Technologie ändert: Erstens sollten Sie über die Erfahrungen, die Sie mit OLE DB machen, ein Verständnis für diese Technologie entwickeln. Wenn Sie erkannt haben, warum es OLE DB gibt, verstehen Sie leichter, wie diese Technologie funktioniert. Zweitens sollten Sie zu der Überzeugung kommen, dass OLE DB wirklich einfach ist – einfach zu verstehen und einfach zu verwenden. Die OLE DB Consumer Templates werden Ihnen diesen Übergang erleichtern, da Sie nach und nach tiefer in die Bibliothek hineinfinden werden. Die Durchführung einfacher Aufgaben mit den Consumer Templates ist unkompliziert. Wenn die Aufgaben schwieriger werden, können Sie allmählich die zahlreichen Möglichkeiten erkunden, die diese Bibliothek bietet. Anders ausgedrückt: Ihr Verständnis für die OLE DB Consumer Templates wird mit Ihren Anforderungen wachsen. Das Ziel dieses Buches ist es, Sie durch diesen Lernprozess zu führen und Ihnen zu helfen, effiziente, skalierbare und überschaubare Datenbankanwendungen zu entwickeln.

Die Zukunft von OLE DB OLE DB ist eine Technologie, die sich rasant weiterentwickelt. Dieses Buch basiert auf Version 2.5, aber es ist wahrscheinlich, dass bereits eine neue Version zur Verfügung steht, wenn es in die Buchhandlungen kommt. Tatsächlich ist OLE DB 2.6 bereits in der Beta 2-Version von SQL Server verfügbar. In OLE DB 2.6 werden bei der Befehlsausführung jetzt nicht nur Zeilenmengen, sondern auch XML-Datenströme als Ergebnis zurückgeliefert. Auch die OLE DB-Spezifikation für Data Mining befindet sich in einem weit fortgeschrittenen Entwicklungsstadium. Was kommt als Nächstes? OLE DB wird sich weiter an den Bedürfnissen des Marktes orientieren. Ich rechne mit der Unterstützung komplexer Zeilen, weiterer XML-Unterstützung und der Einbindung zusätzlicher Befehlssprachen. Bleiben Sie am Ball!

Systemvoraussetzungen Was brauchen Sie, um auf Ihrem Computer mit den OLE DB Consumer Templates arbeiten zu können? Zunächst benötigen Sie einen C++-Compiler – Visual C++ ab Version 6.0. Visual C++ enthält die Dateien für die OLE DB Consumer Templates sowie eine OLE DB-Standardkonfiguration.

Sandini Bib 20

Vorwort

Es empfiehlt sich auch, das neueste Plattform-SDK von der Microsoft Website unter http://msdn.microsoft.com/ herunterzuladen. Die Dateien für die Bibliothek der OLE DBErweiterungen sowie die Beispieldateien finden Sie schließlich unter der Adresse http:/ /www.com-object.com. Beachten Sie, dass für einige der Beispiele die Windows Template Library (WTL) benötigt wird, die Bestandteil des Plattform-SDK ist.

Sandini Bib

Danksagung Das Schreiben eines Buches ähnelt ein wenig dem Schreiben einer Anwendung. Es sieht einfach aus, aber so viel kann danebengehen. Von den folgenden Leuten erhielt ich dabei wertvolle Hilfe: Ich möchte ganz besonders Bob Beauchemin für seine Hilfe danken. Während der Vorbereitung seines Kurses »Essential OLE DB und ADO« für DevelopMentor entwickelte er ein tief greifendes Verständnis für OLE DB, die dazugehörigen Objekte und Schnittstellen sowie die Implementierungen, die von den einzelnen Providern zur Verfügung gestellt werden. Er ließ mich an seinen Erkenntnissen teilhaben und erkannte viele Fehler. Seinen Beitrag an diesem Buch gebührend zu würdigen, würde den Rahmen dieser Danksagung sprengen. Mein Dank geht an Lyn Robison, die mich ermutigte, dieses Buch zu schreiben, und es anschließend lektorierte, sowie an Hubert Divoux für die Durchsicht des Manuskripts. Bei Addison-Wesley half man mir, wann immer ich Hilfe benötigte. Hierfür möchte ich Marisa Meltzer, Jenie Pak, Marilyn Rash und Michael Slaughter danken. Schließlich möchte ich meiner Frau Darcy dafür danken, dass sie mich glauben ließ, ich wäre schon fast fertig, noch bevor ich die erste Seite geschrieben hatte. Irgendwie wurde dadurch alles viel einfacher.

Sandini Bib

Sandini Bib

1 Windows DNA und COM COM, OLE DB, ADO, DNA, DNS – die Welt der Entwickler strotzt nur so vor Akronymen. Und ständig kommen neue hinzu, so dass es nicht einfach ist, immer auf dem Laufenden zu bleiben. In diesem Kapitel soll die Frage geklärt werden, in welchen Fällen der Einsatz von OLE DB angebracht ist. Aus diesem Grund möchte ich zunächst einen Überblick über das Thema geben.

1.1 Verteilte Internet-Anwendungen Welche grundsätzlichen Änderungen bringt das Internet für die Programmierung? Erstens weiß der Entwickler einer Website nicht, wie viele Benutzer später einmal darauf zugreifen werden. Daher ist es empfehlenswert, sich die Möglichkeit offen zu halten, die Site bei Bedarf auch für eine große Anzahl von Benutzern verfügbar zu machen. Zweitens besteht offensichtlich die Notwendigkeit einer Trennung zwischen Client- und Server-Maschine. In manchen Fällen wird ein »fetter« Client benötigt, während in anderen Fällen ein »dünner« Client vorteilhafter ist. Drittens müssen Internet-Anwendungen flexibel sein. Eine gute Idee für das Internet eignet sich in der Regel auch sehr gut für die Unternehmenswelt. Aber welche Richtung soll man einschlagen? Hier gibt es zahlreiche Möglichkeiten. Der Schwerpunkt dieses Buches liegt auf den von der Microsoft-Architektur gebotenen Möglichkeiten. Das Akronym DNA steht für Distributed Internet Applications Architecture (Architektur für verteilte Internet-Anwendungen). Diese Architektur stellt einen von Microsoft konzipierten Rahmen zur Entwicklung einer neuen Generation von Anwendungen zur Verfügung, die sowohl für das World Wide Web als auch für Intranets geeignet sind. Dabei gelten die nachfolgend beschriebenen Prinzipien: 왘 Die Stärken von Windows. In Windows stehen leistungsfähige Dienste zur Verfügung. Möchten Sie auf den vorhandenen Diensten aufbauen oder diese lieber neu programmieren? Zur Entwicklung und Bereitstellung erstklassiger DNA-Anwendungen müssen die Programmierer die Möglichkeiten von Windows, d.h. die Funktionen für Sicherheit, Transaktionen, Komponenten und Verzeichnisdienste,

Sandini Bib 24

1

Windows DNA und COM

voll ausschöpfen. Da Windows alle diese Dienste als Bestandteile des Betriebssystems bereits zur Verfügung stellt, brauchen sich die Programmierer um deren Bereitstellung, Verwaltung und Aktualisierung nicht zu kümmern. 왘 Die Stärken des Internets. Die Zeiten, in denen man Geschäftsanwendungen entwickeln konnte, ohne auf das Internet Rücksicht zu nehmen, sind längst vorbei. Unternehmen wie Einzelanwender wollen nicht auf die weltweite Reichweite des Internets verzichten. DNA nutzt das Internet als globale Plattform. 왘 Kompatibilität. Die Unternehmen wollen nicht jedes Mal, wenn eine neue Technologie aktuell wird, wieder von vorne beginnen, sondern mit den aktuell vorhandenen Systemen weiterarbeiten und diese um die Funktionalität der neuen Technologie erweitern. Dies bedeutet insbesondere, dass neue Komponenten in den bewährten Systemen funktionieren müssen. Zudem muss Interoperabilität mit den Komponenten anderer Anbieter gewährleistet sein. Daher werden neue Anwendungen ausgehend von Standardformaten, -protokollen und -schnittstellen von Windows und aus dem Internet erstellt.

1.2 Die DNA-Architektur von Windows Windows DNA ist keine Senkrechtstarter-Technologie, sondern eine nach vernunftgemäßen Grundsätzen entwickelte, bewährte Architektur, die nach dem Prinzip aufgebaut ist, dass Anwendungen in drei Schichten gegliedert sein sollten. Hierbei handelt es sich um die Präsentations-, die Geschäftslogik- und die Datenschicht, wie in Abbildung 1.1 gezeigt und nachfolgend beschrieben: 왘 Die Präsentationsschicht ist für die Benutzeroberfläche zuständig, d.h. sowohl für die Anzeige von Informationen am Bildschirm als auch für die Entgegennahme von Benutzereingaben (Maus, Tastatur usw.). 왘 Die Geschäftslogikschicht bildet den Kern des Systems. Diese Schicht implementiert die Geschäftsvorgänge und -regeln. 왘 Die Datenschicht verwaltet alle persistenten Daten der Anwendung. Diese Aufteilung ermöglicht mehr Flexibilität als das traditionelle Client/ServerModell. Beachten Sie, dass es logische und keine physischen Schichten sind. Sie können entweder auf ein und derselben Maschine laufen oder mehrere Maschinen bzw. mehrere Prozesse umfassen. Präsentation

Geschäftslogik

Abbildung 1.1: DNA-Architektur

Daten

Sandini Bib 1.2

Die DNA-Architektur von Windows

1.2.1

25

Die Präsentationsschicht

Die Präsentationsschicht implementiert die Anzeige von Informationen sowie die Interaktion mit dem Benutzer. Technologie

Anwendungsbereich Portabilität

Funktionalität

HTML

Internet

Beliebiger Web-Browser

Anzeige von Text/Bildern

Skripte

Internet

Skript-fähiger Browser

Basisinteraktion

Dynamisches HTML

Internet

Internet Explorer

Basisinteraktion

ActiveX-Komponenten

Windows

COM

Umfangreich

Windows-API

Windows

Windows

Sehr umfangreich

Tabelle 1.1: DNA-Präsentationstechnologien

Windows DNA bietet eine breite Palette von Präsentationsdiensten, aus denen der Entwickler die richtige Technologie für seine Anwendung auswählen kann (siehe Tabelle 1.1). Einige Komponenten basieren auf dem Internet, andere auf Windows. Die Internet-Komponenten zeichnen sich durch maximale Portabilität, die Windows-Komponenten durch maximale Funktionalität aus. Grundsätzlich gilt, dass eine erhöhte Portabilität zu Lasten der Funktionalität geht. Dies bedeutet aber nicht, dass die aufgezeigten Möglichkeiten sich gegenseitig ausschließen. Es ist möglich, verschiedene Präsentationsdienste zusammen einzusetzen. Beispielsweise arbeitet ein Produkt wie Microsoft Outlook, das auf der Win32-API basiert, mit ActiveX-Komponenten und eingebettetem dynamischem HTML.

1.2.2

Die Geschäftslogikschicht

Die Geschäftslogikschicht stellt den Kern aller Anwendungen dar. Sie definiert das anwendungsspezifische Verhalten und die entsprechenden Algorithmen. Die Aufgabe der Geschäftslogik ist schwer zu beschreiben, da ihre Funktionen von der jeweiligen Anwendung abhängen. Beispielsweise muss ein Internet-Laden Funktionen für Produkte, Bestellungen und Zahlungen enthalten, während für wissenschaftliche Anwendungen Matrix- und Optimierungsfunktionen benötigt werden. Grundsätzlich kann gesagt werden, dass die Geschäftslogikschicht nichts mit der Speicherung persistenter Daten oder mit ihrer Präsentation zu tun hat. Sie bildet vielmehr eine Brücke zwischen der Präsentations- und der Datenschicht. Die Anwendungsbereiche für die Geschäftslogik sind vielfältig. Dennoch stellt Windows eine Gruppe von Diensten zur Verfügung, die in zahlreichen Situationen nützlich sein können. Diese sind in Tabelle 1.2 zusammengefasst.

Sandini Bib 26

1

Windows DNA und COM

Technologie

Beschreibung

Komponentendienste

Dienste im Zusammenhang mit COM: Transaktionen, Sicherheit, Concurrency usw.

Messaging-Dienste

Dienste für Nachrichtenwarteschlangen

Web-Dienste

Entwicklung von Websites (Internet oder Intranet): IIS und ASP

Tabelle 1.2: Geschäftslogikdienste von Windows

1.2.3

Die Datenschicht

Die Datenschicht besteht aus den beiden folgenden Komponenten: universeller Datenzugriff (Universal Data Access, UDA) von Microsoft und XML. Der universelle Datenzugriff stellt jedem beliebigen Windows-Client eine Vielzahl von Datenquellen zur Verfügung, insbesondere relationale, hierarchische und mehrdimensionale Daten. Dieses Konzept ermöglicht es den Programmierern, sich auf die Geschäftslogik zu konzentrieren, anstatt sich mit den Besonderheiten einer bestimmten Datenquelle befassen zu müssen. Der universelle Datenzugriff umfasst die folgenden drei Teile: 왘 ODBC (Open DataBase Connectivity): Das Konzept der offenen Datenbankkonnektivität stellt den traditionellen Standard für den Zugriff auf relationale Daten dar. 왘 OLE DB (OLE for Data Bases): Das Konzept zum Verknüpfen und Einbinden von Objekten für Datenbanken stellt eine Reihe von COM-Schnittstellen (Component Object Model, Komponentenobjektmodell) für besonders leistungsfähigen Datenzugriff auf verschiedene Datenquellen zur Verfügung. 왘 ADO (ActiveX Data Objects): Bei den ActiveX-Datenobjekten handelt es sich um eine vereinfachte Schicht, die auf OLE DB aufsetzt und hauptsächlich auf Visual Basic-, Java- und Skriptprogramme ausgerichtet ist. In Kapitel 2 wird der universelle Datenzugriff ausführlicher beschrieben. XML (Extensible Markup Language) ist der Internet-Standard für strukturierte Informationen und bietet folgende Funktionen: 왘 Lieferung von Daten an Clients, die nicht unter Windows laufen 왘 Lieferung von Daten für lokale Verarbeitung und Präsentation 왘 Speicherung hierarchischer Daten 왘 Datenübertragung von Server zu Server ohne Behinderung durch Formatprobleme 왘 Austausch von Informationen zwischen Websites

Sandini Bib 1.2

Die DNA-Architektur von Windows

27

Zwischen XML und dem universellen Datenzugriff gibt es einige Verbindungspunkte. Beispielsweise ist es möglich, eine Zeilenmenge (Rowset) aus OLE DB in einen XMLDatenstrom (Stream) umzuwandeln und umgekehrt. Des Weiteren kann XML für Dienste wie SOAP (Simple Object Access Protocol) verwendet werden. SOAP stellt einen Standard für Methodenaufrufe auf fernen Websites dar. Schließlich bieten Datenspeicherprodukte eine umfassende XML-Integration. In Tabelle 1.3 sind die Komponenten der Datenschicht im Überblick dargestellt.

1.2.4

Die Hauptmerkmale von DNA noch einmal zusammengefasst

DNA ist eine vernünftige, bewährte Architektur und kein hypothetischer Ausblick auf die Zukunft. Ihre drei Schichten bieten zahlreiche Möglichkeiten, von denen sich einige ergänzen. Die Entwickler können dabei den Ansatz wählen, der zur Lösung ihrer Probleme am besten geeignet ist. Bei Kombination aller DNA-Elemente entsteht das in Abbildung 1.2 gezeigte Diagramm. Technologie

Anwendungsbereich

Beschreibung

ODBC

Universeller Datenzugriff von Microsoft

Altbewährtes Konzept zum Zugriff auf SQL-Datenbanken

OLE DB

Universeller Datenzugriff von Microsoft

COM-gestütztes Konzept zum Zugriff auf eine breite Palette von Datenquellen

ADO

Universeller Datenzugriff von Microsoft

Vereinfachte Version von OLE DB mit Ausrichtung auf Visual Basic, Java und Skripte

XML

Internet-Standard

Textformat für die Datenübertragung

Tabelle 1.3: Die Komponenten der Datenschicht

Präsentation

Geschäftslogik

Daten

HTML

COM-Dienste

XML

DHTML

Webdienste

Universeller Datenzugriff

Skripte

Nachrichtenwarteschlangen

OLE DB

ActiveX Windows-API

Abbildung 1.2: Elemente der DNA-Architektur

ADO ODBC

Sandini Bib 28

1

Windows DNA und COM

1.3 DNA-Szenarien Zum besseren Verständnis der DNA-Architektur möchte ich einige wichtige Einsatzszenarien darstellen.

1.3.1

Die Client/Server-Architektur

Bevor es DNA gab, war das Client/Server-Modell vorherrschend. Dieses besteht, kurz gesagt, aus einem Client, der für die Präsentation zuständig ist, und einem Server, auf dem die Daten gespeichert sind. Es ist nicht klar und auch umstritten, wo bei diesem Modell die Geschäftslogik angesiedelt ist. In gewissem Sinn stellt DNA ein »sauberes« Client/Server-Modell dar, bei dem die Geschäftslogik sowohl von der Client-/Präsentations-Seite als auch von der Server/Daten-Seite getrennt vorhanden ist. Ob sie physisch auf dem Server, dem Client oder sogar auf einer ganz anderen Maschine implementiert ist, hängt von der jeweiligen Anwendung ab.

1.3.2

Das World Wide Web

Die Erstellung einer Web-Anwendung besteht ganz grundlegend darin, dass WebSeiten dynamisch von ASP generiert werden. Da ASP (Active Server Pages, aktive Server-Seiten) sich sehr an Visual Basic anlehnt, ist ADO gut für den Zugriff auf die Datenschicht geeignet. Der Präsentationsteil kann aus HTML-Seiten bestehen, in denen HTML-, DHTML- und Client-seitiger Skript-Code enthalten ist (siehe Abbildung 1.3). Bei zunehmendem Umfang eines solchen Projekts wird es immer schwieriger, die Skripte und ADO zu verwalten, weil der Code nicht streng typisiert ist. Eine Lösung besteht darin, die Skripte durch ActiveX-Komponenten zu ersetzen und mit OLE DB (siehe Abbildung 1.4) auf die Daten zuzugreifen. Das dabei entstehende System ist jedoch schwieriger zu entwickeln und eignet sich nur für größere Projekte. Bei einer hoch entwickelten Benutzeroberfläche genügt ein seitengestütztes System mit einem Browser möglicherweise den Anforderungen der Präsentationsschicht nicht mehr. Dieses Problem kann dadurch gelöst werden, dass man den Browser durch eine Windows-gestützte Anwendung (siehe Abbildung 1.5) ersetzt. Wenn die Anwendung in einem Intranet läuft, kann die Benutzeroberfläche mit verteiltem COM auf die COM-Objekte zugreifen. Läuft die Anwendung im Internet, können Sie den DCOM HTTP-Transport über das Internet oder eine Lösung wie z.B. SOAP verwenden.

Sandini Bib 1.3

DNA-Szenarien

29

ADO

Web

Webserver: IIS, ASP

Client: HTML, DHTML, Skripte

DatenServer

Abbildung 1.3: Web-Szenario 1

COMKomponenten

Web

OLE DB

Webserver: IIS, ASP

Client: HTML, DHTML, Skripte

Datenserver

Abbildung 1.4: Web-Szenario 2

Benutzerschnittstelle

Intranet

COMKomponenten

Daten

Internet Abbildung 1.5: Web-Szenario 3

Präsentation Komponente 1

Komponente 2 Komponente 3 Andere Elemente

COM

Geschäftslogik

Daten Komponente 5

Komponente 3

COM

Komponente 6

Komponente 4

Komponente 7

Andere Elemente

Andere Elemente

Abbildung 1.6: Komponenten einer DNA-Anwendung

Sandini Bib 30

1

Windows DNA und COM

1.4 Das Komponentenobjektmodell Der Einsatz von Windows DNA bedeutet, dass man die Vorteile von Windows und des Internets nutzt und die Anwendungen in drei Schichten aufteilt. Dies ist ein praktischer Ansatz ohne unsinnige Komplexität oder futuristische Spielereien. Hierbei stellt sich die logische Frage, wie diese Elemente miteinander kommunizieren. Für Internet-Elemente stehen die Standards HTML oder XML zur Verfügung, doch was ist mit all den anderen Elementen (siehe Abbildung 1.6)? Für viele Komponenten lautet die Antwort COM, das die Grundlage für DNA und insbesondere für OLE DB darstellt. Bevor wir uns in die Datenschicht von DNA vertiefen, sollten wir uns vielleicht einige Merkmale von COM vor Augen führen. Denjenigen, die zum ersten Mal mit COM arbeiten, empfehle ich, sich ein Buch zu besorgen, in dem die COM-Grundlagen ausführlich beschrieben werden. Denjenigen, die sich bereits gut mit COM auskennen, dient der folgende Abschnitt zum Auffrischen ihrer Kenntnisse und zur eingehenden Behandlung einiger Punkte, die für OLE DB besonders wichtig sind.

1.4.1

Wiederverwendung

Heutzutage sind die Entwickler dem Druck ausgesetzt, Anwendungen zu erstellen, die alle Aufgaben besser und schneller erledigen. Es ist ganz normal, dass sie dabei mit wenig Zeit und beschränkten Ressourcen auskommen müssen. Die einzige Möglichkeit, hiermit fertig zu werden, besteht in der Wiederverwendung von Komponenten, die bereits früher geschrieben und getestet wurden. »Wiederverwendung von Komponenten« ist eine unscharfe Formulierung. Für manche bedeutet dies die Wiederverwendung von Quellcode durch Kopieren und Einfügen oder mit einem Include-Mechanismus für Dateien. Aus einer Reihe von Gründen ist die Wiederverwendung von Quellcode in großem Stil nicht der ideale Weg: 왘 Wirtschaftliche Gründe. In der Regel verlangen die Programmierer eine Vergütung für ihre Arbeit. Wenn sie ihren Quellcode weitergeben, haben sie kaum die Möglichkeit, auf die weitere Verwendung ihrer Arbeit und damit auch auf ihre Einkünfte Einfluss zu nehmen. 왘 Praktische Gründe. Die Wiederverwendung vom Code ist nicht immer einfach. Häufig treten Namenskonflikte auf, die nur schwer zu lösen sind. 왘 Technische Gründe. Die Wiederverwendung vom Code führt per definitionem zu mehr Codes, d.h. zu umfangreicheren Programmdateien. Die Lösung liegt in der anderen Interpretation der Formulierung »Wiederverwendung von Komponenten«, nämlich der Wiederverwendung von Binärdaten.

Sandini Bib 1.4

Das Komponentenobjektmodell

1.4.2

31

Herausforderungen

Wenn Anbieter in der ganzen Welt zu unterschiedlichen Zeiten Komponenten programmieren ohne zu wissen, was die jeweils anderen tun, ist es unausweichlich, dass mit Blick auf eine zuverlässige Kommunikation der einzelnen Komponenten untereinander Probleme entstehen. Beim Schreiben von wiederverwendbaren Komponenten sehen sich die Entwickler mit folgenden Herausforderungen konfrontiert: 왘 Leistung. Die Wiederverwendung von Komponenten darf nicht dazu führen, dass die Systeme weniger effizient werden. 왘 Interoperabilität. Die Interaktion der Komponenten muss einfach und effektiv erfolgen können, unabhängig davon, wer die jeweiligen Komponenten entwickelt hat. 왘 Aktualisierung. Die Entwickler brauchen eine Möglichkeit zur Aktualisierung bestimmter Komponenten ohne das gesamte System aktualisieren zu müssen. Komponentenaktualisierungen müssen abwärts kompatibel sein. 왘 Sprachenunabhängigkeit. Es ist ganz normal, dass die einzelnen Sprachen miteinander konkurrieren. Tatsächlich ist es jedoch so, dass die Sprachen kommen, entwickelt werden und wieder verschwinden. Die Systeme werden dann in der Regel aus einer Kombination von mehreren Sprachen erstellt. Die Komponenten müssen daher in der Lage sein, mit anderen Komponenten zusammenzuarbeiten, unabhängig von der Sprache, in der diese geschrieben wurden. 왘 Transparenz des Ausführungsortes. Einige Komponenten laufen in ein und demselben Prozess, während andere in einem anderen Prozess oder auf einer anderen Maschine aktiv sind. Idealerweise braucht der Entwickler nicht zu wissen, wo die Komponente im Endeffekt läuft.

1.4.3

Die COM-Lösung

COM stellt die Kombination zweier Konzepte dar. Beim ersten Konzept handelt es sich um einen Standard für Binärdaten, der eine Wiederverwendung von binären, d.h. kompilierten Komponenten ermöglicht. Dies löst das Problem der Sprachenunabhängigkeit, da COM nicht auf einer bestimmten Sprache basiert. Allerdings können zahlreiche Sprachen COM-Komponenten generieren. Das zweite Konzept besteht in einer vollständigen Trennung von Schnittstelle und Implementierung. Zwar ermöglichen auch Sprachen wie C++ eine Trennung von Schnittstelle (.h-Dateien) und Implementierung (.cpp-Dateien). Doch nur COM bietet eine effektive, praktische und systematische Möglichkeit einer solchen Trennung auch in großem Rahmen.

Sandini Bib 32

1

Windows DNA und COM

Schnittstellen Objekte weisen eine Schnittstelle und eine Implementierung auf. Die Schnittstelle beantwortet die Frage »Welche Funktion hat dieses Objekt?« (siehe Abbildung 1.7), die Implementierung dagegen die Frage »Wie führt das Objekt diese Funktion aus?«. COM definiert eine Schnittstelle als Sammlung semantisch verwandter Methoden. Genau gesagt stellt eine Schnittstelle ein Array aus Funktionszeigern dar, das eine Vereinbarung zwischen Software-Komponenten mit strenger Typisierung spezifiziert. Vereinbarungsgemäß beginnt der Name einer Schnittstelle immer mit »I«. In der Regel werden sie in der IDL (Interface Definition Language, Schnittstellendefinitionssprache) deklariert. Dabei entstehen .idl-Dateien. Bei zahlreichen Ansätzen zur objektorientierten Programmierung wird mit Schnittstellen gearbeitet. COM ist jedoch in zweierlei Hinsicht etwas Besonderes. Erstens wird hierbei nicht definiert, wie die Implementierung durchzuführen ist. Die Implementierung kann sogar in vielen Sprachen wie z.B. C++, Basic, oder Java erfolgen. Zweitens unterstützen COM-Objekte mehrere Schnittstellen. Vereinbarungsgemäß werden COM-Komponenten durch Rechtecke und ihre Schnittstellen durch kleine Kreise dargestellt (siehe Abbildung 1.8).

Funktionszeiger 1 Schnittstelle Funktionszeiger 2 Funktionszeiger 3 Funktionszeiger 4 Abbildung 1.7: Schnittstelle als Array aus Funktionszeigern

IInterface1 COMKomponente IInterface2 Abbildung 1.8: COM-Komponenten und -Schnittstellen

IUnknown ist eine von COM definierte Schnittstelle, welche die beiden folgenden Beson-

derheiten aufweist: 왘 Alle COM-Komponenten müssen sie unterstützen. 왘 Alle Schnittstellen erben ihre Eigenschaften.

Sandini Bib 1.4

Das Komponentenobjektmodell

33

IUnknown weist folgende drei Methoden auf: virtual virtual virtual

HRESULT QueryInterface(IID& iid, void** ppvObj) = 0; ULONG AddRef() = 0; ULONG Release() = 0;

QueryInterface wird zum Abfragen einer anderen, von der Komponente unterstützten Schnittstelle verwendet, wobei iid die angeforderte Schnittstelle angibt und ppvObj die zurückgelieferte Schnittstelle enthält. Wenn die angeforderte Schnittstelle nicht unterstützt wird, liefert QueryInterface einen Fehler zurück. AddRef und Release werden in einem späteren Abschnitt erläutert.

COM-Klassen Die Definition einer Komponentenschnittstelle reicht nicht aus, denn es ist erforderlich, auch die Implementierung der Komponente zu spezifizieren. Dies erfolgt über eine COM-Klasse. Zum Erstellen einer COM-Komponente rufen Sie einfach die API-Funktion CoCreateInstance auf. WINOLEAPI CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID FAR* ppv);

wobei rclsid ein Bezeichner zur Identifikation der COM-Klasse ist und ppv die angeforderte Schnittstelle enthält.

COM-Exemplare Wie bei C++ gibt es für Objektexemplare einen endlichen Lebenszyklus. Im Gegensatz zu C++ definiert COM einen einfachen Standardmechanismus zur Verwaltung von Objektexemplaren. COM-Objekte weisen einen internen Referenzzähler auf. Bei der Erstellung eines COM-Objekts wird der Referenzzähler auf 1 gesetzt. Beim Aufruf von AddRef wird der Referenzzähler inkrementiert, beim Aufruf von Release wird er dekrementiert. Erreicht er den Wert 0, geht das Objekt davon aus, dass keine gültigen Referenzen vorhanden sind, und es zerstört sich selbst. Somit ist der Programmierer dafür verantwortlich, dass beim Abrufen einer zusätzlichen Referenz auf eine COM-Schnittstelle AddRef und nach Abschluss dieses Vorgangs Release aufgerufen wird. Dieses Modell ist einfach, elegant und effektiv, allerdings auch fehlerträchtig. Wenn der Programmierer den Aufruf von AddRef an der richtigen Stelle vergisst, kann das COM-Objekt zu früh freigegeben werden. Vergisst er Release aufzurufen, bleibt das COM-Objekt auf unbestimmte Zeit bestehen. Im Abschnitt zu ATL wird erklärt, wie Sie mit intelligenten COM-Zeigern solche Fehler vermeiden können.

Sandini Bib 34

1

Windows DNA und COM

GUIDs In C++ werden Klassen und Variablen über ihren Namen, wie z.B. CMyClass oder m_MyVariable identifiziert, was gut funktioniert, solange kein Namenskonflikt vorliegt. Namenskonflikte entstehen, wenn zwei unterschiedliche Klassen denselben Namen ausweisen oder wenn zwei Variablen in einem bestimmten Gültigkeitsbereich gleich benannt sind. Wenn nur ein oder wenige Programmierer an einem Projekt arbeiten, kann der Konflikt mühelos durch Umbenennen einer der Entitäten oder durch entsprechende Mechanismen, z.B. Namensbereiche (Namespaces), aufgelöst werden. Wenn jedoch eine Vielzahl von Programmierern in verschiedenen Unternehmen aus mehreren Ländern an einem Projekt beteiligt sind, kann das Auflösen eines Namenskonflikts zu einem Problem werden. Zudem werden Namenskonflikte wahrscheinlicher, je mehr der Umfang des Systems zunimmt. Zur Vermeidung von Namenskonflikten werden in COM so genannte GUIDs (Global Unique Identifier, globaler eindeutiger Bezeichner) verwendet, bei denen es sich einfach um 128 Bit große, binäre IDs handelt. Für den Wert einer GUID stehen so viele Möglichkeiten zur Verfügung, dass Namenskonflikte sehr unwahrscheinlich, ja praktisch sogar unmöglich sind. GUIDs werden an zahlreichen verschiedenen Stellen verwendet, beispielsweise zur Identifikation einer COM-Schnittstelle oder einer COM-Klasse. Eine GUID, die eine COM-Schnittstelle identifiziert, wird Schnittstellenbezeichner (IID) genannt, eine GUID, die eine COM-Klasse identifiziert, heißt Klassenbezeichner (CLSID). GUIDs können auch andere Elemente identifizieren. Vor allem bei OLE DB werden GUIDs an mehreren Stellen verwendet.

Beim Arbeiten mit C++ ist es praktisch, eine GUID einer C++-Variablen zuzuordnen. Wenn beispielsweise die Schnittstelle IMyInterface die GUID { D9542ED2-705E-11d38B2B-00105A13D8B9} aufweist, können wir die Variable IID_IMyInterface folgendermaßen definieren: DEFINE_GUID(IID_IMyInterface 0xd9542ed2, 0x705e, 0x11d3, 0x8b, 0x2b, 0x0, 0x10, 0x5a, 0x13, 0xd8, 0xb9);

Hiervon ausgehend verwenden wir IID_IMyInterface für die GUID der Schnittstelle IMyInterface.

1.4.4

COM+ und Attribute

Zusammen mit Windows 2000 stellt Microsoft jetzt COM+ zur Verfügung. Hierbei handelt es sich um eine Weiterentwicklung von COM. Ich hätte als Namen eher »COM 2000« gewählt, um zu signalisieren, dass es sich hierbei um eine neue Version von COM handelt, analog zu »Windows 2000«.

Sandini Bib 1.4

Das Komponentenobjektmodell

35

COM+-Objekte haben deklarative Attribute. Diese beschreiben, grob gesagt, den Kontext, in dem die Exemplare laufen sollen, z.B. innerhalb oder außerhalb des Prozesses, im Rahmen welcher Transaktion oder gleichzeitig mit welchem Vorgang. CoCreateInstance überprüft das Attribut der Komponente und richtet den geeigneten Kontext ein. Wenn das Objektexemplar prozessintern laufen soll, lädt COM die DLL und fordert die Instanziierung an. Andernfalls erzeugt COM einen gesonderten Prozess und lädt die DLL dort. Das Gleiche gilt für Transaktionen, Sicherheit, Gleichzeitigkeit und weitere Attribute. COM+ ist eine neue und nützliche Technologie, deren Beschreibung in einem gesonderten Buch erfolgen muss. Es wäre zu empfehlen, dass Sie sich ein solches Buch besorgen und sich damit über COM+ informieren, denn diese Technologie wird erhebliche Auswirkungen auf die Software-Entwicklung unter Windows haben. Auf OLE DB wirkt sich COM+ nicht so sehr aus, weil OLE DB-Objekte den COM+-Kontext nicht berücksichtigen. Die Aufgabe eines OLE DB-Objekts besteht im Abrufen von Daten und es kann diesen Vorgang entweder innerhalb oder außerhalb eines Prozesses, synchron oder von einer eingereihten Komponente aus durchführen. Eine wichtige Ausnahme von dieser Regel ist der Transaktionskontext, da ein OLE DB-Datenobjekt eng einer Transaktion zugeordnet werden kann. Näheres hierzu erfahren Sie in Kapitel 12, Transaktionen.

1.4.5

Die Vorteile von COM

Wir haben die Probleme angesprochen, mit denen die Programmierer beim Entwickeln von Komponenten konfrontiert werden. Im Folgenden erfahren Sie, wie diese von COM gelöst werden:

Leistung Ein Funktionsaufruf an einer Schnittstelle für eine prozessinterne COM-Komponente stellt nichts anderes dar als einen indirekten Funktionsaufruf über zwei Zeiger. Mit anderen Worten entspricht ein COM-Aufruf einem virtuellen Methodenaufruf in C++. Er ist geringfügig langsamer als ein direkter Funktionsaufruf und der Compiler kann keine Inline-Generierung dafür ausführen. In den meisten Fällen ist die Geschwindigkeit jedoch akzeptabel. Für eine prozessexterne COM-Komponente funktioniert ein COM-Funktionsaufruf erheblich langsamer. Die Geschwindigkeit hängt auch davon ab, ob die Komponente auf derselben Maschine ausgeführt wird oder nicht.

Sandini Bib 36

1

Windows DNA und COM

Aktualisierungen Schnittstellen sind feststehende Elemente, was bedeutet, dass sie nach ihrer Veröffentlichung nie mehr geändert werden sollten. Dies mag den Eindruck eines Nachteils erwecken, stellt aber eine beachtliche Stärke von Schnittstellen dar. Bei einer »sanften« Komponentenaktualisierung braucht der Client der betreffenden Komponente nicht mitaktualisiert werden. Bei der Aktualisierung einer Komponente fügen Sie in der Regel neue Funktionen hinzu oder verbessern die Funktionalität. Zur Verbesserung der vorhandenen Merkmale ohne Änderung der Schnittstelle bietet es sich an, eine neue Implementierung der Schnittstelle zur Verfügung zu stellen (siehe Abbildung 1.9). In diesem Fall läuft die Interaktion zwischen beliebigen Clients und der neuen Version der Komponente genauso ab, wie dies mit der alten Version der Fall war. Wenn Sie jedoch Funktionen hinzufügen, müssen Sie vermutlich Unterstützung für mindestens eine zusätzliche Schnittstelle definieren. Dies ist der Fall, wenn die neue Funktion mit keiner vorhandenen Funktion in Beziehung steht (siehe Abbildung 1.10). IA

IA Alte Implementierung

Neue Implementierung

Alte Komponente

Neue Komponente

Abbildung 1.9: Alte und neue Komponente, Schnittstelle unverändert

IA Implementierung von A

IA Implementierung von A Alte Komponente

IB Implementierung von B Neue Komponente

Abbildung 1.10: Die neue Komponente unterstützt die zusätzliche Schnittstelle IB

Sandini Bib 1.4

Das Komponentenobjektmodell

IA

37

IA Implementierung von A

IA

Alte Komponente

Neue Implementierung von A

Neue Komponente

Abbildung 1.11: Die neue Komponente unterstützt die aktualisierte Schnittstelle

Manchmal ist die alte Schnittstelle, nennen wir sie in diesem Beispiel IA, für die vorliegende Aufgabe nicht mehr geeignet und muss durch eine neue Version IA′ ersetzt werden. Der COM-Ansatz besteht darin, eine neue Komponente zur Verfügung zu stellen, die sowohl IA als auch IA′ unterstützt. Ein »alter« Client würde in diesem Fall auf IA zugreifen, ohne dass ihm die neue Schnittstellenversion bekannt ist. Ein »neuer« Client würde dann IA′ verwenden (siehe Abbildung 1.11).

Sprachenunabhängigkeit In COM besteht eine Schnittstelle lediglich aus einem Array von Funktionen. Daher kann jede Sprache, die Funktionszeiger unterstützt, ein COM-Objekt problemlos implementieren. Zu diesen Sprachen gehören C++, Visual Basic und Java. Für COM bietet sich vor allem C++ an, denn diese Sprache bietet vollständige Unterstützung für Zeiger, virtuelle Funktionstabellen und Strukturen. Andererseits eignen sich Visual Basic und Java hervorragend für eine Untermenge von COM-Objekten, nämlich für diejenigen, die duale Schnittstellen unterstützen.

Transparenz des Ausführungsortes Ein COM-Server kann prozessintern oder prozessextern auf derselben oder auf einer anderen Maschine implementiert werden. COM weiß in jedem Fall, wo sich die Komponenten befinden und wie sie zu erzeugen sind. Außerdem führt COM die damit zusammenhängenden »Bastelarbeiten« durch. Aus der Sicht des Programmierers ist praktisch kein Unterschied zu erkennen.

Wiederverwendung von Komponenten Das Konzept der Wiederverwendung ist in COM in mehreren Formen existent. Am deutlichsten ist in dieser Hinsicht ein Zeiger auf die Schnittstelle eines COM-Objektexemplars. Dies entspricht in etwa der Verwendung eines Zeigers auf ein C++-Objektexemplar. Die zweite Methode, die Definition einer Teile/Ganzes-Beziehung (Aggregation), ist subtiler.

Sandini Bib 38

1

1.4.6

Windows DNA und COM

Teile/Ganzes-Beziehung bei COM

C++ unterstützt das Vererbungskonzept, bei dem die abgeleitete Klasse die Funktionalität der Basisklasse vererbt bekommt, d.h. übernimmt. Optional kann die abgeleitete Klasse die vererbten Methoden neu definieren und sie kann Funktionalität von mehreren Basisklassen vererbt bekommen. COM unterstützt das Vererbungskonzept nicht, sondern bietet einen Mechanismus, der einige Ähnlichkeiten zur Vererbung aufweist: die Teile/Ganzes-Beziehung (Aggregation). Die Teile/Ganzes-Beziehung stellt eine der erweiterten COM-Techniken dar und viele COM-Programmierer können darauf verzichten. Da sich OLE DB jedoch in erheblichem Maße auf dieses Konzept stützt, sollten Sie es auf jeden Fall von A bis Z verstehen. Bei der Teile/Ganzes-Beziehung in COM handelt es sich um die Beziehung zwischen zwei Exemplaren, von denen jedes eine bestimmte Aufgabe hat. Das eine ist das innere Objekt (Exemplar), das andere das äußere Objekt. Und so funktioniert die Teile/Ganzes-Beziehung: Zunächst führt ein Client eine Instanziierung eines äußeren Objektexemplars durch und fordert eine bestimmte Schnittstelle an (siehe Abbildung 1.12). Anschließend erzeugt dieses äußere Exemplar ein inneres Exemplar (siehe Abbildung 1.13) und übergibt sich selbst als äußeres Argument von CoCreateInstance. WINOLEAPI CoCreateInstance(REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID FAR* ppv);

Im Aufruf von CoCreateInstance gilt Folgendes: 왘 rdlcid ist die CLSID für das innere Objekt 왘 pUnkOuter ist die IUnknown-Schnittstelle für das äußere Exemplar 왘 riid entspricht der Schnittstelle IID_IUnknown. Das äußere Objekt fordert die IUnknown-Schnittstelle des inneren Exemplars an. Beachten Sie, dass das innere Objekt nicht AddRef für das äußere Objekt aufrufen darf, weil es als Bestandteil des Objekts angesehen wird. Die Sache wird an dieser Stelle für das innere Objekt ein wenig kompliziert, denn es muss sich als Exemplar des äußeren Objekts mit der Zugriffsklasse private verhalten, und es muss genauso aussehen wie das gleiche Exemplar eines Clients des äußeren Objekts. Daher führt das äußere Objekt etwas durch, was ansonsten verboten ist: Es implementiert zwei IUnknown-Schnittstellen.

Sandini Bib 1.4

Das Komponentenobjektmodell

Schnittstelle???

39

Äußeres Objekt

Abbildung 1.12: Teile/Ganzes-Beziehung, Schritt 1

IUnknown

Äußeres Objekt

Inneres objekt ???

Abbildung 1.13: Das äußere Exemplar erstellt ein inneres Objekt

Delegierende IUnknownSchnittstelle

???

Äußeres Objekt

IUnknown

Inneres Objekt

Interne IUnknownSchnittstelle Abbildung 1.14: Das innere Exemplar unterstützt zwei IUnknown-Schnittstellen

Eine IUnknown-Schnittstelle steht nur dem äußeren Exemplar zur Verfügung und ermöglicht es diesem, intern QueryInterface, AddRef und Release aufzurufen. Wir nennen diese Schnittstelle interne IUnknown-Schnittstelle. Die andere IUnknown-Schnittstelle delegiert einfach alle ihre Aufrufe an die andere IUnknown-Schnittstelle, daher nennen wir sie delegierende Schnittstelle. Bei seiner Erzeugung liefert das innere Objekt seine interne IUnknown-Schnittstelle zurück, weil es weiß, dass es zu einer Teile/GanzesBeziehung gehört. Die interne und die delegierende IUnknown-Schnittstelle sind in Abbildung 1.14 dargestellt. Von jetzt an enthält das äußere Exemplar eine Referenz auf die interne IUnknownSchnittstelle des inneren Objekts und das innere Exemplar enthält eine Referenz auf die IUnknown-Schnittstelle des äußeren Objekts. Angenommen, das äußere Exemplar unterstützt eine Schnittstelle, die wir IOuter nennen, und das innere Exemplar unterstützt eine Schnittstelle mit dem Namen IInner.

Sandini Bib 40

1

Windows DNA und COM

Die Implementierung der Methode QueryInterface für die äußere IUnknown-Schnittstelle lautet dann folgendermaßen: // Pseudocode QueryInterface(REFIID riid, LPVOID * ppvObj) { if (riid == IID_IOuter) // Implementierung der äußeren Schnittstelle zurückliefern else inner->QueryInterface(riid, ppvObj); // delegierende IUnknown-Schnittstelle zurückliefern }

Die Implementierung von QueryInterface für die innere IUnknown-Schnittstelle lautet folgendermaßen: // Pseudocode QueryInterface(REFIID riid, LPVOID * ppvObj) { if (riid == IID_IInner) // Implementierung der inneren Schnittstelle zurückliefern else if (riid == IUnknown) // die delegierende IUnknown-Schnittstelle zurückliefern }

Das äußere Exemplar kann durch folgenden Aufruf eine Referenz auf die delegierende IUnknown-Schnittstelle abrufen: inner->QueryInterface(IID_IUnknown, ..);

Die Implementierung von QueryInterface für die delegierende IUnknown-Schnittstelle lautet folgendermaßen: // Pseudocode QueryInterface(REFIID riid, LPVOID * ppvObj) { if (riid ++ IID_IUnknown) // Zeiger auf delegierende IUnknown-Schnittstelle // zurückliefern else return outer->QueryInterface (riid, ppvObj); }

Nach der Erzeugung des inneren Exemplars muss das äußere Objekt die angeforderte Schnittstelle zurückliefern. Wenn die Schnittstelle IUnknown angefordert wird, liefert das äußere Exemplar die delegierende Schnittstelle des inneren Objekts zurück. Dies führt dazu, dass die IUnknown-Schnittstelle des äußeren Exemplars nur für das innere Exemplar und die interne IUnknown-Schnittstelle des inneren Exemplars nur für das äußere Exemplar sichtbar ist (siehe Abbildung 1.15).

Sandini Bib 1.4

Das Komponentenobjektmodell

41

Wenn der Client eine Schnittstellenanforderung nach IOuter absetzt, leitet die delegierende IUnknown-Schnittstelle den Aufruf an die äußere IUnknown-Schnittstelle weiter, die IOuter zurückliefert. Wenn der Client eine Schnittstellenanforderung nach IInner absetzt, leitet die delegierende IUnknown-Schnittstelle den Aufruf an die äußere IUnknown-Schnittstelle weiter. Da das äußere Objekt diese Schnittstellen nicht unterstützt, leitet sie den Aufruf an die interne IUnknown-Schnittstelle weiter, die dann IInner zurückliefert (siehe Abbildung 1.16). Angenommen, der Client verfügt über eine Schnittstelle zu IOuter und setzt dann eine Schnittstellenanforderung nach IInner ab. In diesem Fall geschieht Folgendes: Zunächst einmal wird mit IOuter::QueryInterface vermutlich die gleiche Art von Schnittstelle implementiert, wie es die äußere Schnittstelle IUnknown::QueryInterface darstellt. Da IInner nicht vom äußeren Exemplar implementiert wird, übergibt sie anschließend die Anforderung an die interne IUnknown-Schnittstelle, die dann ihrerseits IInner zurückliefert.

Delegierende IUnknownSchnittstelle Client

IOuter

Äußeres Objekt

IInner

IUnknown

Inneres Objekt

Interne IUnknownSchnittstelle Abbildung 1.15: Die delegierende IUnknown-Schnittstelle erscheint für den Client als IUnknown-Schnittstelle der Teile/Ganzes-Beziehung

Delegierende IUnknownSchnittstelle Client 1

IOuter 2

3

Äußeres IUnknown Objekt

2

IInner

Inneres Objekt

Interne IUnknownSchnittstelle

Abbildung 1.16: QueryInterface ausgehend von IUnknown

Sandini Bib 42

1

Windows DNA und COM

Wenn der Client jetzt über eine Schnittstelle zu IInner verfügt und einen Schnittstellenaufruf nach IOuter absetzt, entsteht folgende Situation: Die Implementierung von IInner::QueryInterface entspricht der delegierenden IUnknown-Schnittstelle. Die IUnknown-Schnittstelle leitet die Anforderungen an die äußere IUnknown-Schnittstelle weiter, die ihrerseits die IOuter-Schnittstelle zurückliefert. Abbildung 1.17 zeigt einen geschlossenen Kreis zwischen IOuter und IInner. Um diesen Ablauf deutlich zu machen, weist die Abbildung je einen Pfeil von IInner und IOuter zu den Implementierungen des QueryInterface-Ereignisses für diese beiden Schnittstellen auf, auch wenn an diesen beiden Stellen kein wirklicher Wechsel auf eine andere Schnittstelle stattfindet. 4

IInner

IOuter

Delegierende IUnknownSchnittstelle

1

Client

6

3

Äußeres IUnknown Objekt

5

2

Inneres Objekt

Interne IUnknownSchnittstelle

Abbildung 1.17: QueryInterface zwischen IInner und IOuter

IOuter

IUnknown

Äußeres Objekt

IInner

Inneres Objekt

Abbildung 1.18: Teile/Ganzes-Beziehung aus Sicht des Clients

Was bleibt als Erkenntnis übrig? Aus der Perspektive des Clients sind drei Schnittstellen vorhanden (IUnknown, IOuter und IInner), die scheinbar auf dasselbe Objekt verweisen (Abbildung 1.18).

Sandini Bib 1.5

ATL zusammen mit COM-Objekten verwenden

43

1.5 ATL zusammen mit COM-Objekten verwenden Das direkte Arbeiten mit COM-Objekten ist fehlerträchtig. Bei jeder Verwendung eines COM-Objekts müssen Sie AddRef aufrufen und den Vorgang anschließend mit einem Aufruf von Release abschließen. Wenn Sie vergessen, AddRef zum richtigen Zeitpunkt aufzurufen, wird das COM-Objekt wahrscheinlich vorzeitig freigegeben, was anschließend zu einer Zugriffsverletzung führt. Vergessen Sie den Aufruf von Release, bleibt das COM-Objekt für immer bestehen. ATL (ActiveX Template Library), die Template-Bibliothek für ActiveX, stellt die Klasse CComPtr zur Verfügung, die den Umgang mit COM-Objekten erleichtert. CComPtr ist folgendermaßen definiert: Template class CComPtr { /// Funktionen T* p } ;

CComPtr ist eine einfache, aber leistungsfähige Klasse zur Implementierung eines Zei-

gers. In den meisten Fällen kann ein Zeiger für eine unspezifische Schnittstelle durch einen COM-Zeiger ersetzt werden. Ein COM-Zeiger implementiert das Konzept der intelligenten Zeiger für COMObjekte. Mit anderen Worten übernimmt er den Aufruf von AddRef und Release für den Programmierer. Zudem bietet er einige COM-spezifische Funktionen. Die folgenden Codebeispiele zeigen, wie mit CComPtr einfache Aufgaben ausgeführt werden können. CComPtr integriert CoCreateInstance, so dass die Erzeugung eines COM-Exemplars direkt aus einem CComPtr-Zeiger nicht schwer ist. CComPtr spMyInterface; hr = spMyInterface.CoCreateInstance(CLSID_MyClass /*optionale Parameter*/);

Wenn Sie einen Standard-Schnittstellenzeiger erhalten, können Sie ihn einem COMSchnittstellenzeiger zuweisen. CComPtr übernimmt dann den Aufruf von AddRef. IMyInterface* other; CComPtr spMyInterface = other; SpMyInterface = other;

Wenn Sie einen CComPtr-Zeiger einem NULL-Zeiger zuweisen, erledigt dieser automatisch den Aufruf von Release. Alternativ dazu können Sie Release direkt aufrufen. { CComPtr spMyInterface; /// spMyInterface verwenden

Sandini Bib 44

1

Windows DNA und COM

spMyInterface = NULL; // Methode 1 spMyInterface.Release() //Methode 2 }

CComPtr kann für sich selbst die Typenumwandlung in einen Schnittstellenzeiger vor-

nehmen. Daraus ergibt sich, dass – sofern eine Methode einen Schnittstellenzeiger akzeptiert – auch ein CComPtr-Zeiger übergeben werden kann, vorausgesetzt, er verweist auf eine gültige Schnittstelle. HRESULT DoSomething(IMyInterface * param); // CComPtr spMyInterface; DoSomething(spMyInterface);

Funktionen akzeptieren bisweilen auch einen Zeiger auf einen Schnittstellenzeiger. Ein Zeiger kann nur dann an einen CComPtr-Zeiger übergeben werden, wenn der CComPtrZeiger auf einen NULL-Zeiger verweist. HRESULT c(IMyInterface** param); CComPtr spMyInterface; GetInterfacePointer(&spMyInterface); GetInterfacePointer(&spMyInterface);///kann einen Fehler ///verursachen!

CComPtr erleichtert das Abfragen der COM-Schnittstelle. Sie brauchen nur QueryInterface aufzurufen; CComPtr leitet den Schnittstellenbezeichner entsprechend ab. CComPtr spInterface1; CComPtr spInterface2; hr = spInterface1.CoCreateInstance(…); hr = spInterface1.QueryInterface(&spInterface2); //oder hr = spInterface1.QueryInterface(IID_IInterface2, reinterpret_cast(&spInterface2));

Der Aufruf einer Methode einer COM-Schnittstelle ist mit dem Aufruf einer Methode für einen Schnittstellenzeiger vergleichbar. CComPtr spMyInterface; // Schnittstellenzeiger abrufen hr = spMyInterface->CallMethod(argument1, argument2, …);

1.6 Kapselung von COM-Komponenten Inzwischen müssten Sie sich über die Leistungsfähigkeit von Komponenten, die mehrere Schnittstellen unterstützen, im Klaren sein. Wenn Sie mit COM-Objekten arbeiten, müssen Sie eine Sprache auswählen. In diesem Buch wird dies C++ sein, da es sich bei den OLE DB Consumer Templates um eine C++-Bibliothek handelt. Es gibt drei Möglichkeiten zur Verwendung eines COM-Objekts von C++ aus. Das erste Verfahren

Sandini Bib 1.6

Kapselung von COM-Komponenten

45

besteht im Aufruf von Funktionen für eine unspezifische Schnittstelle. Die zweite Technik stützt sich auf CComPtr und wurde im vorherigen Abschnitt beschrieben. In diesem Abschnitt stelle ich Ihnen das dritte Verfahren vor: Kapselung der COMKomponente in einer C++-Klasse. Ein Problem liegt darin, dass eine COM-Komponente mehrere Schnittstellen unterstützt, während eine C++-Klasse nur über eine Schnittstelle verfügt. Angenommen, IA hat die beiden Funktionen a1() und a2(), IB die Funktion b() und IC die Funktion c(). Die C++-Klasse müsste über eine Schnittstelle wie die Folgende verfügen: Class CMyClass { HRESULT Create(); HRESULT a1(); HRESULT a2(); HRESULT b(); HRESULT c(); } ;

Die Implementierung der Klasse CMyClass kann auf zweierlei Weisen erfolgen: Das erste Verfahren besteht in der Verwendung eines Zeigers für jede der Schnittstellen (siehe Abbildung 1.19). Dies bedeutet, dass die Klasse die drei folgenden Klassenelemente (Class Members) aufweisen würde: CComPtr m_spIA; CComPtr m_spIB; CComPtr m_spIC;

Die Methode Create sähe dann etwa so aus: IA C++ Klasse mit Methoden von IA, IB, und IC

IB IC

COMKomponente

Abbildung 1.19: Kapselung von COM-Objekten, 1.Verfahren // Pseudocode! HRESULT Create(..) { m_spIA.CoCreateInstance(CLSIDComponent); m_spIA.QueryInterface(&m_spIB); m_spIA.QueryInterface(&m_spIC); }

Sandini Bib 46

1

Windows DNA und COM

Die Implementierung der anderen Methode würde dann folgendermaßen lauten: // Pseudocode, ATLASSERT wäre erforderlich HRESULT ia1() { return m_spIA->a1(); } HRESULT ia2() { return m_spIA->a2(); } HRESULT ib() { return m_spIB->b(); } HRESULT ic() { return m_spIC->c(); }

Das zweite Verfahren zur Implementierung der C++-Klasse besteht darin, einen Schnittstellenzeiger auf lediglich eine Schnittstelle, beispielsweise IA, zu verwenden und die anderen Schnittstellen nach Bedarf abzufragen. Konkret weist die Klasse nur ein Klassenelement auf: CComPtr m_spIA. Die Methode Create ändert sich dann folgendermaßen: MM HRESULT Create(..) { return m_spIA.CoCreateInstance(CLSIDComponent); }

Für a1() und a2()gilt dieselbe Implementierung wie zuvor. Dagegen ist für b() und c() die folgende Anpassung erforderlich: // Pseudocode HRESULT b() { CComPtr spB HRESULT hr = m_spA.QueryInterface(&spB) if (FAILED(hr)) return hr; return spb->b(); }

Die Implementierung für c()wäre ähnlich (siehe Abbildung 1.20). Beide Verfahren haben Vor- und Nachteile. Das erste Verfahren ist speicherintensiver und ruft ggf. Schnittstellenzeiger auf Schnittstellen ab, die nicht benötigt werden. Andererseits sind die Methodenaufrufe schneller,

Sandini Bib 1.7

Zusammenfassung

47

weil keine Notwendigkeit zum Aufruf von QueryInterface besteht. Das zweite Verfahren verwendet nur ein Klassenelement und fordert nur benötigte Schnittstellen an. Allerdings wird QueryInterface für zahlreiche Methodenaufrufe benötigt. Der mit den OLE DB Consumer Templates verfolgte Ansatz liegt zwischen den beiden hier vorgestellten Verfahren. Grundsätzlich wird bei den OLE DB Consumer Templates immer nur ein Schnittstellenzeiger verwendet. Wenn jedoch die zusätzliche Schnittstelle vermutlich häufig benötigt wird, arbeitet die Klasse mit einem zusätzlichen Schnittstellenzeiger. IA C++-Klasse mit Methoden von IA, IB, und IC

IB IC

COM Komponente

Abbildung 1.20: Kapselung von COM-Objekten, 2.Verfahren

1.7 Zusammenfassung In diesem Kapitel wurden Windows DNA und COM im Überblick vorgestellt. Das nächste Kapitel behandelt den Datenaspekt und insbesondere OLE DB.

Sandini Bib

Sandini Bib

2 Universeller Datenzugriff In diesem Kapitel werde ich Ihnen eine der Komponenten von Windows DNA, nämlich den universellen Datenzugriff (Universal Data Access, UDA), vorstellen und dabei den Schwerpunkt auf OLE DB legen, eine der Komponenten von UDA. Zudem erhalten Sie hier eine erste Einführung in die Consumer Templates von OLE DB. In den vergangenen Jahren hat sich die Welt der Daten grundlegend verändert. Unter anderem sind Datenbankverwaltungssysteme (Database Management Systems, DBMS) inzwischen allgemein verbreitet. Zwar gibt es solche Systeme schon seit einigen Jahren, aber erst seit kurzem ist ihre Entwicklung nicht mehr so schwierig und kostenaufwändig. Genauso wie es möglich ist, im heimischen Schlafzimmer eine Website zu gestalten, können Sie nun auch eine Datenbank für Datenmengen im Terabyte-Bereich selbst erstellen. Microsoft stellt sogar unentgeltlich eine Datenbank-Engine für Windows zur Verfügung: die Microsoft Data Engine (MSDE). Eine andere Veränderung betrifft die erheblich angewachsene Menge an Daten und Datenbanktypen. Früher enthielt eine typische Datenbank lediglich Text und Zahlen. Jetzt dient sie auch zum Speichern von Multimedia-Inhalten, speziellen Datentypen usw. Zugleich gibt es inzwischen spezifische Datenspeicher für die einzelnen Datentypen: SQL-Datenbanken für relationale Daten, Mail-Server für E-Mail-Nachrichten, Verzeichnisdienste für Unternehmensdaten, OLAP-Server für mehrdimensionale Daten usw. Die dritte und faszinierendste Veränderung liegt im Aufstieg des Internets. In einem verschwindend kurzen Zeitraum ist es zu einer der Hauptquellen für die Beschaffung von Daten geworden, die uns als Grundlage für alltägliche Vorgänge dienen, z.B. online einkaufen oder Informationen im WWW suchen. Manchmal ist es wirklich erheblich einfacher, Informationen im Internet zu finden als in einer unternehmensweiten Datenbank. Hinsichtlich der Server bedeutet dies, dass sie für den Zugriff einer großen Anzahl potenzieller Benutzer ausgelegt sein müssen.

Sandini Bib 50

2

Universeller Datenzugriff

2.1 Universeller Daten-Server Als Antwort auf diese Veränderungen implementieren mehrere Datenbankanbieter eine Strategie mit »universellem Daten-Server«. Einfach ausgedrückt unterstützt ein universeller Daten-Server zahlreiche Datentypen, ermöglicht eine Skalierung auf eine sehr große Anzahl von Benutzern und wird mit einem hohen Datenaufkommen fertig. Die Idealvorstellung geht dahin, dass Unternehmen alle ihre Daten auf einem Server zentral verwalten können. Abbildung 2.1 zeigt die Architektur mit universellem Daten-Server. Dieses Konzept weist eine Reihe von Vorteilen auf: 왘 Geringere Verwaltungskosten aufgrund der zentralisierten Struktur, u.a. zentrales Sicherheitsmanagement, zentrale Hardware, zentrale Datensicherung usw. 왘 Das Vorhandensein eines einzigen Datenspeichers ermöglicht Optimierungen, die bei Verwendung mehrerer Datenspeicher nur schwer zu erreichen sind. 왘 Der Server kann einheitliche Modelle für Dienste (z.B. Sicherheit oder Transaktionen) zur Verfügung stellen. Es überrascht daher nicht, dass sich Anbieter mit Schwerpunkt auf Datenbankverwaltung für diese Strategie aussprechen, aber der universelle Daten-Server ist aus mehreren Gründen nicht ganz unproblematisch. Einerseits zwingt dieser Ansatz den Anwender dazu, die Daten aus den traditionellen Systemen in eine neue Datenbank zu überführen, was bei kritischen Daten ein offensichtliches Risiko darstellt. Andererseits bedeutet dies, dass die bewährten Prozesse zur Verarbeitung der Daten neu programmiert werden müssen, damit sie in der neuen Datenbank auf die Daten zugreifen können. Dieser Aspekt stellt in der Regel das größte Problem bei der Konvertierung dar und macht deutlich, warum so viele Unternehmen nach wie vor mit alten Systemen und Formaten arbeiten. Wenn das Datenbankverwaltungssystem von Grund auf neu erstellt wird, fällt dieses Konvertierungsproblem weg. In diesem Fall muss jedoch zunächst einmal eine Datenbank gefunden werden, die entsprechende Unterstützung für die benötigten Datentypen und den gewünschten Datenzugriff bietet. Dies mag manchmal ganz einfach sein, in anderen Fällen stößt man hierbei jedoch auf erhebliche Schwierigkeiten. Aber auch wenn diese Hürden überwunden sind, ist der Ansatz mit Verwendung eines universellen Daten-Servers nicht frei von Problemen. Zunächst einmal bedeutet die zentralisierte Struktur auch zentrale Verwaltung. Dies mag zwar der Traum eines jeden Systemverwalters sein, steht aber oftmals im Gegensatz zur jeweiligen Unternehmenskultur. Ein Unternehmen setzt sich normalerweise aus mehreren autonomen Unternehmensbereichen zusammen, die in der Regel selbst über das Format ihrer Daten und den Zugriff darauf bestimmen wollen.

Sandini Bib 2.2

Näheres zum universellen Datenzugriff (UDA)

Anwendung 1

51

Anwendung 2

Universeller Datenserver Relationale Daten

E-Mail-Daten

Andere Daten

Abbildung 2.1: Architektur mit universellem Daten-Server

Zudem entsteht dadurch eine zu große Abhängigkeit vom Datenbankanbieter, was natürlich ganz im Interesse des Anbieters liegt, aber für Ihr Unternehmen ein ernsthaftes Risiko darstellt, da Software-Unternehmen nicht unfehlbar sind.

2.2 Näheres zum universellen Datenzugriff (UDA) Microsoft bietet mit dem universellen Datenzugriff (UDA) eine Alternative (siehe Abbildung 2.2). Hierbei liegt der Schwerpunkt nicht auf der Einheitlichkeit der Daten, d.h. der Speicherung an einem einzigen Ort, sondern auf der Einheitlichkeit des Datenzugriffs. Dies bedeutet, dass mit ein und derselben Schnittstelle auf verschiedene Datenspeicher zugegriffen werden kann, unabhängig davon, wo die Daten herstammen. Wichtig ist, dass es sich bei UDA eher um einen Standard als um ein Produkt handelt. Ohne den zugrunde liegenden Datenspeicher können Sie mit UDA nicht viel anfangen. Verstehen Sie UDA daher auch nicht als Konkurrenten für Ihr traditionelles Datenbankverwaltungssystem, sondern als Ergänzung, die Ihnen Lösungen zur Überwindung der Unzulänglichkeiten bietet, die der Ansatz mit einem universellen DatenServer mit sich bringt. UDA trennt die Systementwicklung von der darunter liegenden Datenbank. Dies bedeutet, dass sich Entwickler und Anwender nicht »auf Gedeih und Verderb« einem bestimmten Datenbankanbieter verschreiben müssen. Außerdem brauchen hierbei die Daten nicht unbedingt auf einen neuen Server übertragen zu werden, sondern Sie können diese im alten Datenspeicher belassen und über moderne Schnittstellen darauf zugreifen. Mit UDA kann der Zugriff auf mehrere Datenquellen annähernd so reibungslos erfolgen wie auf eine einzige Datenquelle. Dies erweist sich vor allem dann als wichtig, wenn Sie Daten-Server von mehreren Anbietern im Einsatz haben. Zudem sind Szenarien möglich, bei denen die Daten zwar auf mehrere Einheiten eines Unternehmens verteilt sind, aber trotzdem der Eindruck entsteht, sie wären an einer Stelle zusammengefasst.

Sandini Bib 52

2

Anwendung 1

Universeller Datenzugriff

Anwendung 2

Universeller Datenzugriff Server für relationale Daten

Server für E-MailDaten

Server für andere Daten

Abbildung 2.2: Architektur mit universellem Datenzugriff

Die Verteilung der Daten auf mehrere Server kann Nachteile mit sich bringen. Da sich die Daten nicht an einer Stelle befinden, ist es komplizierter, sie zu verwalten. Wenn beispielsweise jeder Server mit einem eigenen Sicherheitssystem arbeitet, ist die Verwaltung der einzelnen Zugriffsstrategien schwieriger. Bei der Lösung dieser Probleme können sich jedoch Verzeichnisdienste als sehr hilfreich erweisen. Der Einsatz verschiedener physischer Server kann auch den Datenverkehr im Netzwerk erhöhen und sich daher negativ auf die Systemleistung auswirken. Zudem erschwert die Durchführung Server-übergreifende Optimierungen. Es ist wichtig, sich klar zu machen, dass Sie die Entscheidung für UDA nicht dazu zwingt, die Strategie mit einem universellen Daten-Server aufzugeben. Die beiden Konzepte sind durchaus kooperationsfähig. Der universelle Datenzugriff kann Ihnen bei der Migration von einem Anbieter auf einen anderen helfen und ist flexibler. Beispielsweise können Sie den Hauptteil Ihrer Daten auf einem universellen Daten-Server speichern und den universellen Datenzugriff nutzen um in gleicher Weise auch auf die übrigen Daten zuzugreifen.

2.3 UDA-Komponenten Die folgenden drei Komponenten bilden den universellen Datenzugriff: 왘 ODBC 왘 OLE DB 왘 ADO Kurz zusammengefasst lassen sich diese drei Akronyme folgendermaßen erklären: ODBC ist eine traditionelle Datenzugriffs-API für SQL-Daten, OLE DB stellt eine COM-gestützte Gruppe von Schnittstellen zum Zugriff auf beliebige Daten dar, und

Sandini Bib 2.3

UDA-Komponenten

53

ADO ist eine höhere Schicht für Datenobjekte, die auf OLE DB aufsetzt. In den folgenden Abschnitten werden diese drei Technologien näher erläutert.

2.3.1

ODBC (Open DataBase Connectivity)

Genauso wie Windows DNA als Architektur ausgehend von DDE und OLE entwickelt wurde, liegen die Anfangsgründe für UDA historisch gesehen in der Open DataBase Connectivity.

ODBC und der Datenzugriff unter Windows Als der SQL-Standard noch in den Kinderschuhen steckte, begann die Verbreitung zahlreicher Datenzugriffsschnittstellen. Zu dieser Zeit setzte Microsoft auf eine anbieterunabhängige Anwendungsprogrammierschnittstelle (API) mit dem Namen Open DataBase Connectivity (ODBC). Hierbei handelte es sich um eine SQL-gestützte API, die naturgemäß für die SQL-Programmierung eine hohe Attraktivität besaß. Da diese API zu einem Standard wurde, entstanden auf dieser Basis zahlreiche ODBC-Provider bzw. -Treiber sowie viele Tools. Im Laufe der Zeit erweiterte Microsoft die Palette der von ODBC unterstützten Funktionen, so dass ODBC heute als Standard einen festen Platz in der Welt der Datenbanken hat.

Anwendung 1

Anwendung 2

ODBC Treiber-Manager Oracle ODBC-Treiber

SQL-Server ODBC-Treiber

Anderer ODBC-Treiber

Oracle-Server

SQL-Server

Anderer Server

Abbildung 2.3: ODBC-Architektur

Architektur ODBC weist eine Reihe von Standardfunktionen auf, die von einem ODBC-Treiber in einer DLL implementiert werden. Zudem verfügt diese Technologie über einen Treiber-Manager, d.h. eine Komponente zur Verwaltung der verschiedenen auf einer Maschine installierten ODBC-Treiber. Wenn eine Client-Anwendung auf einen Datenbank-Server zugreifen will, fordert sie zunächst den entsprechenden Treiber beim

Sandini Bib 54

2

Universeller Datenzugriff

ODBC-Treiber an. Die Client-Anwendung ruft die ODBC-Funktionen über den geladenen Treiber auf. Abbildung 2.3 zeigt die ODBC-Architektur im Überblick. Programmierer klagen immer wieder darüber, dass ODBC zu langsam ist. Da ODBC jedoch nur einen Standard darstellt, hängt die Leistung immer vom zugrunde liegenden Treiber ab. In vielen Fällen ist die Leistung des ODBC-Treibers mit der der nativen API vergleichbar.

2.3.2

Nachteile von ODBC

ODBC unterstützt nur SQL-Daten. Heutzutage ist jedoch ein Großteil der Daten nicht in relationalen Datenbanken gespeichert, sondern auf OLAP- oder Mail-Servern. Da es zudem ODBC schon vor COM gab, stand dessen modulare Architektur noch nicht zur Verfügung. Dies bedeutet, dass der Treiber die gesamte ODBC-Funktionalität implementieren muss. Ferner muss viel binärer Code dupliziert werden, was das System aufbläht und seine Verwaltung erschwert. Beispielsweise sind für jeden ODBCTreiber ein SQL-Parser und eine Cursor-Engine erforderlich. Was spricht dagegen, diese Elemente im Betriebssystem zu verankern, anstatt sie in jedem ODBC-Treiber zu duplizieren?

2.3.3

OLE DB

Es erleichtert das Verständnis von OLE DB, wenn man diese Technologie als eine neue Form von ODBC auf der Basis von COM betrachtet. Mit OLE DB geht auch ein neues Vokabular einher. So wird ein Treiber zu einem Provider (»Anbieter«) und ein Client zu einem Consumer (»Nutzer«). OLE DB unterscheidet sich von ODBC in dreierlei Hinsicht: 왘 OLE DB unterstützt mehrere Provider-Typen, nicht nur Provider für SQL-Daten. 왘 OLE DB basiert auf COM. 왘 OLE DB stellt eine neue Architektur zur Verfügung. In den folgenden Abschnitten werden diese Unterschiede im Einzelnen erläutert.

Typen von OLE DB-Providern Es gibt die folgenden drei Kategorien von OLE DB-Daten-Providern: 왘 Tabellen-Provider 왘 Mehrdimensionale Provider 왘 Hierarchische Provider

Sandini Bib 2.3

UDA-Komponenten

55

Tabellen-Provider Diese Provider bestehen aus einer Reihe von Tabellen. In der Regel weisen diese Tabellen Beziehungen untereinander auf. In diesem Fall spricht man von relationalen Providern. Beispiele hierfür sind Microsoft Access, SQL Server und Oracle. Nicht alle Tabellen-Provider unterstützen SQL, aber umgekehrt gehören alle SQL-Provider zur Gruppe der Tabellen-Provider. Mit anderen Worten ist es möglich, einen Tabellen-Provider zu erstellen, der mit einem anderen Verfahren als SQL auf Tabellen und Sichten zugreift, beispielsweise über Direktzugriff oder eine benutzerdefinierte Sprache. Abbildung 2.4 zeigt eine Tabelle eines Tabellen-Providers. Da ODBC auf SQL-Provider ausgerichtet ist, erstreckt sich der Gültigkeitsbereich dieser Technologie nur auf einen Teil der Tabellen-Provider. Die von ODBC nicht abgedeckten Provider-Typen werden in den nächsten beiden Abschnitten behandelt. Spalte

Tabelle

Spalte 1

Spalte 2

Wert

Wert

Wert

Wert

Wert

Wert

Wert

Wert

Wert

Wert

Wert

Wert

Wert

Wert

Wert

Wert

Zeile

Abbildung 2.4: Tabelle eines Tabellen-Providers

Dimension 2 Dimension 1

3 on nsi e Dim

Abbildung 2.5: Würfel eines mehrdimensionalen Providers

Sandini Bib 56

2

Universeller Datenzugriff

Mehrdimensionale Provider Eine relationale Tabelle kann man sich als zweidimensionales Rechteck aus Zeilen und Spalten vorstellen. Mehrdimensionale Provider (oder OLAP-Provider) führen in das allgemeinere Konzept »Würfel« ein. Im Gegensatz zu einer relationalen Tabelle kann ein Würfel mehr als zwei Dimensionen aufweisen. Tatsächlich sind die meisten Würfel drei- oder vierdimensional. Abbildung 2.5 zeigt einen Würfel eines mehrdimensionalen Providers. Mit Version 2.0 von OLE DB wurde die Unterstützung für mehrdimensionale Provider eingeführt. Der Zugriff darauf wird in Kapitel 15 beschrieben.

Hierarchische Provider Werfen Sie einen Blick auf Ihre persönlichen Daten, und stellen Sie sich selbst die Frage, ob alle in relationalen Tabellen gespeichert oder in Form von Dateien und E-Mails auf Ihrer Festplatte verstreut sind. Die Antwort lautet wahrscheinlich: sowohl als auch. Relationale Datenbanken eignen sich zwar hervorragend für klar strukturierte Daten, für andere Datentypen sind sie aber unpraktisch. Hierarchische Provider, die es seit Version 2.5 von OLE DB gibt, sind mit einem Dateisystem vergleichbar und weisen ordner- oder dateiähnliche Elemente auf. Sie eignen sich besonders gut für Dateisysteme (lokale oder Internet-Dateisysteme), können aber auch für E-Mail-Nachrichten oder Kontaktadressen verwendet werden. Abbildung 2.6 zeigt ein Beispiel. Hierarchische Provider werden in Kapitel 16 ausführlich behandelt.

Zeile Kind-Zeile 1 Enkel 1 Kind-Zeile 2

...

Enkel 2

... ...

... ...

... Abbildung 2.6: Zeilen eines hierarchischen Providers

OLE DB-Typen Die einzelnen OLE DB-Typen sind in Abbildung 2.7 dargestellt. Die Basis von OLE DB bildet der Typ Datenquelle (Data Source). Die Datenquelle bietet zahlreiche Möglichkeiten zur Angabe, wo sich die Daten befinden.

Sandini Bib 2.3

UDA-Komponenten

Transaktion

Fehler

57

Datenquelle

Sicht

Sitzung

Zeilenmenge

Befehl

Zeile

Datenmenge

Datenstrom

OLE DB 1.0

OLE DB 2.0

OLE DB 1.5

OLE DB 2.5

Binder

Abbildung 2.7: Die OLE DB-Typen

Der Typ Sitzung (Session) entspricht der eigentlichen Verbindung zum Datenspeicher. Die Sitzung kann optional auch im Rahmen einer Transaktion (Transaction) ausgeführt werden. Der Typ Zeilenmenge (Rowset) stellt ein zentrales Konzept dar. Es überrascht nicht, dass dieser Typ für eine Menge aus Zeilen mit Spalten steht. Eine Zeilenmenge kann direkt über eine Sitzung oder durch Ausführung eines Befehls (Command) geöffnet werden. Der Typ Sicht (View) stellt eine Ansicht einer Zeilenmenge dar. Sichten sind ein praktisches Instrument für Client-Operationen zum Sortieren und Filtern von Daten. Das Konzept der Zeilenmengen eignet sich zwar gut für tabellenförmige Daten, aber für mehrdimensionale Provider wird auch der Typ Datenmenge (Dataset) benötigt. Kurz gesagt handelt es sich bei einer Datenmenge um eine mehrdimensionale Zeilenmenge. Der Typ Zeile (Row) steht für eine Zeile bei hierarchischen Providern. Es würde sich zwar anbieten, auch bei nicht hierarchischen Providern ein Zeilenobjekt zu verwenden, aber dies ist nicht der Fall. Der Typ Bindungsobjekt (Binder) bietet eine Möglichkeit, eine Zeile eines URLs zu öffnen. Der Typ Datenstrom (Stream) definiert eine Möglichkeit, einen Datenstrom aus Zeilen oder BLOBs (siehe Kapitel 11) aus- und in Zeilenmengen einzulesen. Schließlich können alle OLE DB-Typen Fehler (Error) erzeugen.

Sandini Bib 58

2

Universeller Datenzugriff

OLE DB und COM Leistung Wie auch bei ODBC ist es nicht sinnvoll, die Leistung von OLE DB für sich selbst genommen zu betrachten. Wichtig ist hingegen die Leistung der einzelnen Provider. In Kapitel 1 wurde die Leistung eines COM-Objekts erläutert. OLE DB-Provider werden als DLL implementiert, und OLE DB-Komponenten werden prozessintern aktiviert. Die Ausführung einer Methode für ein OLE DB-Objekt läuft so schnell ab wie der Aufruf einer virtuellen Funktion für ein C++-Objekt. OLE DB schreibt nicht zwingend vor, dass OLE DB-Provider COM für ihre Implementierung verwenden, und viele tun dies auch nicht. Die einzige Forderung besteht darin, dass OLE DB-Objekte eine Reihe von Schnittstellen bereitstellen, was sich in der Praxis in einer Leistungsbegrenzung wie bei virtuellen Funktionsaufrufen widerspiegelt. Da OLE DB neuer ist als ODBC, stehen für OLE DB-Provider noch weniger Optimierungen zur Verfügung als für ihre ODBC-Pendants. Dies wird sich jedoch im Laufe der Zeit ändern.

Flexibilität bei Verwendung mehrerer Schnittstellen In Kapitel 1 habe ich ausgeführt, dass ein COM-Objekt in der Lage ist, mehr als eine Schnittstelle zu unterstützen. Dies gilt genauso für OLE DB-Objekte, die in der Regel sogar viele Schnittstellen unterstützen. Für jeden Objekttyp sind in OLE DB eine Reihe von Schnittstellen definiert, sowohl obligatorische als auch optionale. Ein OLE DBBefehl beispielsweise muss immer die obligatorische Schnittstelle ICommand unterstützen, die über die Methode Execute verfügt. Wenn für einen Befehl zusätzlich noch Parameter definiert sind, unterstützt dieser auch die optionale Schnittstelle ICommandWithParameters. Ein Provider kann auch eigene, proprietäre Schnittstellen definieren und unterstützen und dem Consumer auf diese Weise Zugriff auf proprietäre Funktionen ermöglichen. Sobald der Consumer davon ausgeht, dass das Objekt Provider-spezifische Schnittstellen unterstützt, ist der Code natürlich nicht mehr portierbar.

Eine neue Architektur Einer der Nachteile von ODBC liegt darin, dass diese Technologie das Schreiben hochwertiger Treiber erschwert. OLE DB kann hier durch die Bereitstellung wiederverwendbarer Dienste zur Durchführung verschiedener Aufgaben, die für zahlreiche Provider gleich sind, ein wenig Abhilfe schaffen. Beispielsweise bietet OLE DB einen Cursor-Dienst zur Umwandlung einer einfachen, unidirektionalen Zeilenmenge in eine Zeilenmenge, die alle Möglichkeiten der Anzeige und Bearbeitung bietet. Die Verwendung von OLE DB-Diensten ist in zweierlei Hinsicht vorteilhaft. Erstens sind diese Dienste bereits implementiert und getestet, so dass sich die gewünschten

Sandini Bib 2.3

UDA-Komponenten

59

Provider schneller und zuverlässiger erstellen lassen. Zweitens stehen verbesserte Versionen eines Dienstes den Entwicklern immer automatisch zur Verfügung. OLE DB bietet zwei Arten von Diensten: Dienstkomponenten und Dienst-Provider. Der Unterschied zwischen diesen beiden Elementen ist nicht besonders klar gefasst, daher werde ich Ihnen meine eigene Definition nennen. Eine Dienstkomponente implementiert bestimmte Verbesserungen für eine gegebene OLE DB-Komponente. Beispielsweise implementiert der Cursor-Dienst zusätzliche Funktionalität für ein ZeilenmengenObjekt. Ein Dienst-Provider wirkt sich dagegen auf einen ganzen Provider aus. Er kann selbst als OLE DB-Provider betrachtet werden, hinter dem ein anderer OLE DB-Provider steht.

Dienstkomponenten Wie kann OLE DB Dienstkomponenten zur Verfügung stellen? Die Antwort liegt in einer Teile/Ganzes-Beziehung, die zwei Komponenten so miteinander kombiniert, dass sie aus der Sicht des Clients wie eine einzige Komponente erscheinen. Bei einer Teile/Ganzes-Beziehung ist der OLE DB-Dienst das äußere und das Objekt des Providers das innere Objekt. Nehmen wir beispielsweise an, die Datenquellenkomponente benötigt im Rahmen der Skalierung für eine große Anzahl von Benutzern ein Ressourcen-Pooling. Anstatt jeden Provider seinen eigenen Mechanismus dafür implementieren zu lassen, implementiert OLE DB den Mechanismus lediglich ein einziges Mal und stellt ihn mit dem Betriebssystem zur Verfügung. Auf diese Weise hat der Consumer die Wahl zwischen einer Einzeldatenquelle und einer Datenquelle mit Pooling in einer Teile/Ganzes-Beziehung (siehe Abbildung 2.8). OLE DB beinhaltet die drei folgenden Dienstkomponenten: 왘 Ressourcen-Pooling (Resource Pooling) dient zur Skalierung von Ressourcen für eine große Anzahl von Benutzern. 왘 Transaktionseinbindung (Transaction Enlistment) dient zur Integration von Transaktionen in eine COM+-Transaktion oder einen Microsoft-Transaktions-Server. 왘 Client-Cursor (Client Cursor) bietet umfassende Funktionalität für eine Zeilenmenge, einschließlich bidirektionaler Cursor, Suchvorgänge, Sichten usw.

Data Shape-Dienst Eine der Herausforderungen bei der Entwicklung von Datenbankanwendungen besteht in der Handhabung von Beziehungen. Normalerweise muss der Entwickler mit Schlüsseln, Mehrfachabfragen und Zeilenmengen umgehen, was zwar nicht grundsätzlich schwierig ist, aber stellenweise viel Wiederholungsarbeit beinhaltet. In OLE DB sind diese Routineaufgaben in einer Komponente, dem Data Shape-Dienst, zusammenge-

Sandini Bib 60

2

Universeller Datenzugriff

fasst, der mit jedem regulären OLE DB-Provider funktioniert. Die Aufgabe des Data Shape-Dienstes besteht darin, eine bestimmte Sprache, die so genannte Data Shape-Sprache syntaktisch zu analysieren (Parsing) und Abfragen an den zugrunde liegenden OLE DB-Provider zu generieren. Daraufhin liefert der Provider eine Reihe von Zeilenmengen zurück, die anschließend vom Data Shape-Dienst und dem Provider in hierarchischer Form präsentiert werden (siehe Abbildung 2.9).

Datenquelle mit Ressourcen-Pooling DatenquellenDienstkomponente (implementiert das Ressourcen-Pooling)

Datenquelle (kein RessourcenPooling)

Abbildung 2.8: Teile/Ganzes-Beziehung zwischen einer Datenquelle und einer Datenquellen-Dienstkomponente

Consumer Data Shape-Sprache

Analysieren

Hierarchie

Data ShapeDienst

SQLBefehl

Erstellen

Zeilenmenge

OLE DB-Provider Abbildung 2.9: Data Shape-Architektur

Der Consumer sieht den zugrunde liegenden OLE DB-Provider nicht. Aus seiner Sicht leistet der Data Shape-Dienst die ganze Arbeit, während in Wirklichkeit einen Großteil davon der OLE DB-Provider erledigt.

OLE DB-Provider Zu den Datenzugriffskomponenten von Microsoft (Microsoft Data Access Components, MDAC) gehören eine Reihe von OLE DB-Providern. Die bekanntesten darunter sind natürlich Microsoft Access, Microsoft SQL Server und Oracle. Zudem gibt es noch nicht mit SQL arbeitende Datenspeicher wie die Microsoft-Verzeichnisdienste (Directory Services), die Microsoft-Indizierungsdienste (Indexing Services) und Microsoft

Sandini Bib 2.3

UDA-Komponenten

61

OLAP. Am bedeutendsten ist der OLE DB-Provider für ODBC, der die Kluft zwischen OLE DB und ODBC überbrückt, d.h. den Zugriff auf alle (auch zukünftige) ODBCDatenspeicher über OLE DB ermöglicht. Abbildung 2.10 zeigt die verschiedenen OLE DB-Provider im Überblick.

Consumer

OLE DB

OLE DB

SQL Server

Oracle

OLE DB Verzeichnisdienste

OLE DB

OLE DB

OLAP

ODBC

ODBCDatenspeicher 1

ODBCDatenspeicher 2

SQL Server

Abbildung 2.10: OLE DB-Provider

2.3.4

Vergleichende Betrachtung von ODBC und OLE DB

Ist OLE DB eine Technologie zur Ablösung von ODBC? Die Antwort lautet ja und nein. Dies hat folgende Gründe: 왘 ODBC ist ein fester Bestandteil von UDA. Microsoft wird daher ODBC so lange unterstützen und erweitern, wie noch Bedarf am Markt vorhanden ist. 왘 OLE DB verfügt über einen ODBC-Provider. Daher können alle ODBC-Treiber entweder direkt oder über OLE DB verwendet werden. 왘 ODBC unterstützt nur SQL-Daten, während sich OLE DB für alle Arten von Daten eignet. 왘 Die Grundlage für OLE DB ist COM, während ODBC auf einer unspezifischen API basiert. Die Entsprechungen zwischen ODBC und OLE DB sind in Tabelle 2.1 aufgeführt.

2.3.5

OLE DB-Versionen

Es gibt den beliebten Spruch, der besagt, dass die Produkte von Microsoft erst ab Version 3 brauchbar sind. Dies trifft auf OLE DB nicht zu, da hier die mit Windows 2000 ausgelieferte Version 2.5 bereits sehr weit ausgereift und mit einer umfangreichen Funktionalität ausgestattet ist.

Sandini Bib 62

2

Universeller Datenzugriff

Es ist schwierig, die einzelnen Versionen im Überblick darzustellen, da sie einen bestimmten Freigabezeitpunkt und keinen einheitlichen Funktionsumfang widerspiegeln. Außerdem bieten neue OLE DB-Releases Verbesserungen, die schwer in eine Kategorie einzuordnen sind. Nichtsdestoweniger werde ich versuchen, Ihnen mit der folgenden Liste eine grobe Übersicht über die Hauptversionen von OLE DB zu geben. 왘 Version 1.0 enthält die Hauptschnittstellen zum Zugriff auf relationale Datenbanken. Die gebotene Funktionalität reicht für die meisten Anwendungen aus. 왘 Mit Version 1.5 wurden die folgenden beiden Konzepte eingeführt: Kapitelmarkierungen und Sichten. Kapitelmarkierungen dienen zur Identifikation einer Untermenge der gesamten Zeilen aus einer Zeilenmenge. Sichten ermöglichen dem Consumer die Durchführung von Operationen wie Sortieren und Filtern von Zeilenmengen. 왘 Mit Version 2.0 wurde die Unterstützung für mehrdimensionale Datenspeicher eingeführt. 왘 Mit Version 2.5 wurde die Unterstützung für hierarchische Datenspeicher eingeführt. ODBC

OLE DB

ODBC-Treiber

OLE DB-Provider

ODBC-Client

OLE DB-Consumer

C-Funktionen

COM-Schnittstellen

ausschließlich SQL-Daten

Verschiedene Datenspeicher

Monolithische Treiber

Datenbankkomponenten

Tabelle 2.1: Entsprechungen zwischen ODBC und OLE DB

Zum Zeitpunkt, als ich dieses Buch schrieb, befand sich Microsoft in der Vorbereitungsphase für OLE DB 2.6 und SQL Server 2000. OLE DB 2.6 war seinerzeit noch nicht über die Beta-Version hinausgekommen und wird daher hier nicht behandelt. Mit dieser Version wird native Unterstützung für XML eingeführt, einschließlich dem Abruf von Daten als XML-Datenstrom und nicht als herkömmliche Zeilenmenge und der Definition von Abfragen mit XML oder XPath anstatt mit SQL. Auf der Website zu diesem Buch werde ich Ihnen zeigen, wie Sie die entsprechenden Operationen mit OLE DB 2.6 durchführen können. Zum Glück ist dieser Teil von OLE DB nicht sehr umfangreich und unproblematisch in der Handhabung.

Sandini Bib 2.3

UDA-Komponenten

2.3.6

63

ADO (ActiveX Data Object)

OLE DB ist zwar eine exzellente Technologie, aber auch nicht ganz frei von Nachteilen. Beispielsweise ist sie für Anfänger oder Gelegenheitsprogrammierer zu kompliziert. Zudem arbeitet sie mit Techniken auf unterer Ebene, wie benutzerdefinierten Schnittstellen, Zeigern und expliziter Speicherverwaltung. Sprachen wie Visual Basic und Java, die diese Elemente nicht unterstützen, können nicht mit OLE DB kommunizieren. Diese Nachteile könnten zahlreiche Entwickler vergraulen. Um dies zu verhindern, stellt Microsoft eine Bibliothek mit dem Namen ADO (ActiveX Data Object) zur Verfügung. Bei ADO handelt es sich um eine COM-Bibliothek, die auf OLE DB aufsetzt und nur Objekte der oberen Ebene darstellt. Da diese Bibliothek automationsgestützt ist, verwendet sie duale Schnittstellen. Dies bedeutet, dass sowohl Compiler- als auch Interpreter-Sprachen darauf zugreifen können, was ADO für Entwickler attraktiv macht, die mit Visual Basic, Java und Skriptsprachen wie VBScript und JavaScript arbeiten.

Architektur ADO ähnelt früheren Microsoft-Technologien wie DAO (Data Access Objects) und RDO (Remote Data Objects). Dieser Abschnitt enthält eine kurze Übersicht über die wichtigsten Objekte von ADO. 왘 Verbindungsobjekte (Connection) 왘 Befehlsobjekte (Command) 왘 Datensatzmengen-Objekte (Recordset) 왘 Fehlerobjekte (Error) 왘 Parameterobjekte (Parameter) 왘 Feldobjekte (Field) Abbildung 2.11 zeigt die Architektur von ADO. Das Verbindungsobjekt stellt eine Sitzung unter Verwendung einer Datenquelle dar und bildet in der Regel das Stammobjekt. Mit diesem Objekt können Sie den Speicherort der Datenquelle angeben, Verbindungsoptionen individuell anpassen und Transaktionen durchführen. Nachdem Sie das Verbindungsobjekt geöffnet haben, können Sie darauf aufbauend weitere Objekte erzeugen. Normalerweise erzeugen Sie für die Verbindung ein Befehlsobjekt. Dieses Objekt stellt einen Befehl oder eine Abfrage dar, der bzw. die mit der Datenquelle durchzuführen ist. Sie können den Befehlstext angeben und den Befehl dann ausführen lassen. Welche Syntax für den Befehlstext verwendet wird, hängt von der zugrunde liegenden Datenquelle ab. In der Regel wird dies SQL sein, aber es sind auch andere Sprachen möglich.

Sandini Bib 64

2

Verbindung

Universeller Datenzugriff

Fehler

Befehl

Parameter

Datensatzmenge

Feld

Abbildung 2.11: Vereinfachte Darstellung der ADO-Architektur

Der Befehl kann eine Reihe von Parameterobjekten enthalten. Wenn die zugrunde liegende Datenquelle Befehle mit Parametern unterstützt, können Sie hier so viele Parameter angeben, wie für den Befehl erforderlich sind. Andernfalls werden die Parameter nicht berücksichtigt. Das Datensatzmengen-Objekt stellt eine Reihe von Datensätzen dar. Ein solches Objekt wird der Ausführung eines Befehls zurückgeliefert, wenn die zugrunde liegende Datenquelle Befehle unterstützt. Eine Datensatzmenge kann auch durch Angabe eines Tabellennamens geöffnet werden. Sie können auf die Daten in einer Datensatzmenge über eine Menge von Feldobjekten zugreifen, von denen jedes eine Spalte darstellt. Ein Datensatzmengen-Objekt in ADO bietet reichhaltige Möglichkeiten der Bearbeitung. Selbstverständlich ermöglicht es Operationen zum Holen von Daten und zum Blättern durch diese Daten (Fetch- und Scroll-Operationen). Zudem verfügt es über hoch entwickelte Optionen zur Angabe der Cursorposition (Client oder Server) und stellt eine umfassende Funktionalität wie z.B. Filterfunktionen zur Verfügung. Das Fehlerobjekt enthält Fehlerinformationen. Zum Feld- und zum Parameterobjekt gibt es keine entsprechenden OLE DB-Objekte.

Wann bietet sich die Verwendung von ADO an? Es erübrigt sich anzumerken, dass ADO als »Einfachversion« von OLE DB nicht so leistungsfähig ist wie der große Bruder OLE DB. In einigen Bereichen ist ADO bedeutend langsamer und bietet keine vergleichbare Funktionalität. Für viele Anwendungen kann diese Bibliothek jedoch absolut ausreichend sein, besonders wenn es sich um kleinere Projekte handelt. In welchen Bereichen ist ADO schwächer? Zunächst einmal handelt es sich bei ADO um eine Schicht, die auf OLE DB aufsetzt, daher bewirken die Aufrufe eine doppelte Indirektion. Diese Tatsache ist in den meisten Fällen jedoch vernachlässigbar. Der hauptsächliche Nachteil betrifft den Datenaustausch zwischen dem COM- und dem C++-Objekt. Bei OLE DB schreibt das COM-Objekt die Daten direkt in den C++-Spei-

Sandini Bib 2.4

OLE DB Templates

65

cher, mit ADO muss der Programmierer auf eine Reihe von Feldern zugreifen, was den ganzen Vorgang sehr viel langsamer macht, da genauso viele Aufrufe benötigt werden, wie Spalten vorhanden sind. Zudem enthalten Felder Varianten, die zusätzlichen Aufwand mit sich bringen. ADO-Objekte weisen noch einen weiteren Nachteil auf: Sie unterstützen nur eine einzige Schnittstelle, so dass ein Zugriff auf spezialisierte Schnittstellen schwierig ist. Die Wahl zwischen ADO und OLE DB fällt nicht immer leicht, da beide Technologien hervorragende Eigenschaften haben. Wenn Sie sich für eine der beiden entscheiden müssen, hilft es, sich die folgenden Fragen zu stellen: 왘 Welche Sprache wird verwendet? 왘 Was für Programmierer sind am Projekt beteiligt? 왘 Wie umfangreich ist das Projekt und wie schnell muss es fertig sein? 왘 Welche Leistungsanforderungen gelten für das Projekt? Wenn beispielsweise ein Team aus Visual Basic-Entwicklern ein kleines Projekt in kurzer Zeit auf die Beine stellen muss, ist ADO sicherlich eine gute Wahl. Handelt es sich aber darum, mit einem Team aus hoch qualifizierten C++-Programmierern ein Datenbankprodukt auf höchstem Leistungsniveau abzuliefern, kommt nur OLE DB in Frage. Wenn Sie sich absolut nicht entscheiden können, besteht die Möglichkeit, ADO und OLE DB auch gemeinsam in einem Projekt einzusetzen. Hierfür gibt es die »ADO-OLE DB-Brücke«. Damit ist es beispielsweise möglich, eine OLE DB-Zeilenmenge in eine ADO-Zeilenmenge umzuwandeln und umgekehrt.

2.4 OLE DB Templates Das Programmieren von OLE DB-Providern und -Consumern ist keine leichte Aufgabe. Microsoft stellt dafür die OLE DB Templates zur Verfügung. Bei diesen Templates handelt es sich um eine C++-Bibliothek für OLE DB, die eine erhebliche Erleichterung für den Programmierer darstellt. Die OLE DB Templates bestehen aus zwei gesonderten Unterbibliotheken, den OLE DB Provider Templates zum Programmieren von OLE DB-Provider-Objekten und den OLE DB Consumer Templates zum Schreiben von OLE DB-Consumer-Objekten. Die OLE DB Consumer Templates sind das eigentliche Thema dieses Buches. Im ersten Kapitel haben Sie erfahren, warum der COM-Ansatz gegenüber dem herkömmlichen Ansatz mit einer C++-Bibliothek so viel besser ist. Nun werden Sie sich fragen, warum dann hier wieder eine C++-Bibliothek verwendet wird. OLE DB bietet zwar hervorragende Möglichkeiten zur Steuerung von Objekten, aber es kann schwie-

Sandini Bib 66

2

Universeller Datenzugriff

rig sein, diese Steuerung in der richtigen Weise wahrzunehmen. Die OLE DB Consumer Templates erleichtern Ihnen die Programmierung von Consumer-Code. Wie der Name bereits andeutet, greifen die OLE DB Consumer Templates auf die C++Template zurück, und diese wird von COM nicht unterstützt. Auf den ersten Blick kann dies Verwirrung stiften, aber sobald Sie gewohnt sind, mit diesen Templates zu arbeiten, werden Sie nicht mehr ohne sie programmieren wollen. Außerdem verwenden COM und OLE DB COM-Typen wie BSTR. Mit den OLE DB Consumer Templates ist eine komfortable Zuordnung dieser Typen zu den üblichen C++-Typen wie char* oder WCHAR* möglich. Und schließlich unterstützen die OLE DB Consumer Templates Sie auch bei der Verwaltung des C++- und des COM-Speichers. Überzeugender als langatmige Ausführungen sind meistens jedoch Beispiele. Ich werde Ihnen ein einfaches Beispiel vorstellen, in dem einmal OLE DB direkt und einmal die OLE DB Consumer Templates verwendet werden. In diesem Szenario soll mit Hilfe eines SQL-Befehls eine Tabelle mit dem Namen »testtable« geöffnet werden. Diese Tabelle enthält nur eine Spalte namens »a« mit dem Datentyp integer. Die Tabelle befindet sich auf dem Server SERVERNAME in einer Datenbank mit dem Namen databasename. Nach dem Öffnen der Tabelle sollen die Zeilen einzeln nacheinander abgerufen werden.

2.4.1

Verwendung von OLE DB direkt

Die Klasse Ctesttable enthält den Wert. Sie ist folgendermaßen definiert: class Ctesttable { public: LONG m_a; } ;

Der folgende, sehr lange Code zeigt, wie die einzelnen Operationen mit OLE DB direkt durchgeführt werden. Halten Sie sich nicht damit auf, ihn vollständig durchzulesen. Er soll nur zeigen, wie mühsam eine solche Programmierung ist. HRESULT PerformActionWithRawOLEDB() { HRESULT hr = S_OK; USES_CONVERSION; DBPROPSET dbinit; DBPROP Props [5]; dbinit.cProperties = 5; dbinit.rgProperties = Props; dbinit.guidPropertySet = DBPROPSET_DBINIT; for (int iii = 0; iii < 5; iii ++) {

Sandini Bib 2.4

OLE DB Templates Props[iii].colid = DB_NULLID; Props[iii].dwOptions = DBPROPOPTIONS_REQUIRED; } Props[0].dwPropertyID = DBPROP_AUTH_INTEGRATED; Props[0].vValue.vt = VT_BSTR; Props[0].vValue.bstrVal = SysAllocString(OLESTR("SSPI")); Props[1].dwPropertyID = DBPROP_INIT_CATALOG; Props[1].vValue.vt = VT_BSTR; Props[1].vValue.bstrVal = SysAllocString(OLESTR("databasename")); Props[2].dwPropertyID = DBPROP_INIT_DATASOURCE; Props[2].vValue.vt = VT_BSTR; Props[2].vValue.bstrVal = SysAllocString(OLESTR("SERVERNAME")); Props[3].dwPropertyID = DBPROP_INIT_LCID; Props[3].vValue.vt = VT_I4; Props[3].vValue.iVal = 1033; Props[4].dwPropertyID = DBPROP_INIT_PROMPT; Props[4].vValue.vt = VT_I4; Props[4].vValue.iVal = 4; CComPtr m_spInit; { CLSID clsid; hr = CLSIDFromProgID(T2COLE("SQLOLEDB.1"), &clsid); if (FAILED(hr)) return hr; m_spInit.Release(); hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IDBInitialize, (void**)&m_spInit); if (FAILED(hr)) return hr; CComPtr spProperties; HRESULT hr; hr = m_spInit->QueryInterface(IID_IDBProperties, (void**)&spProperties); if (FAILED(hr)) return hr; hr = spProperties->SetProperties(1, &dbinit); if (FAILED(hr)) return hr; hr = m_spInit->Initialize(); } if (FAILED(hr)) return hr;

67

Sandini Bib 68

2 CComPtr m_spOpenRowset; { CComPtr spSession; // Herstellung der Verbindung zur Datenbank überprüfen ATLASSERT(m_spInit != NULL); hr = m_spInit->QueryInterface(IID_IDBCreateSession, (void**)&spSession); if (FAILED(hr)) return hr; hr = spSession->CreateSession(NULL, IID_IOpenRowset, (IUnknown**)&m_spOpenRowset); } if (FAILED(hr)) return hr; Ctesttable test; CComPtr m_spCommand; HACCESSOR HAccessor; CComPtr Rowset; { // Überprüfen, ob die Sitzung gültig ist ATLASSERT(m_spOpenRowset != NULL); CComPtr spCreateCommand; HRESULT hr = m_spOpenRowset->QueryInterface( IID_IDBCreateCommand, (void**)&spCreateCommand); if (FAILED(hr)) return hr; hr = spCreateCommand->CreateCommand(NULL, IID_ICommand, (IUnknown**)&m_spCommand); if (SUCCEEDED(hr)) { CComPtr spCommandText; hr = m_spCommand->QueryInterface(&spCommandText); if (SUCCEEDED(hr)) hr = spCommandText->SetCommandText(DBGUID_DEFAULT, T2COLE("b aus testtable auswählen")); } if (FAILED(hr)) return hr; { HRESULT DBPARAMS

hr; *pParams;

Universeller Datenzugriff

Sandini Bib 2.4

OLE DB Templates pParams = NULL; hr = m_spCommand->Execute(NULL, IID_IRowset, NULL, NULL, (IUnknown**)&Rowset); if (FAILED(hr)) return hr; CComPtr spAccessor; hr = Rowset->QueryInterface(&spAccessor); if (SUCCEEDED(hr)) { DBBINDING* pBindings = NULL; ULONG nColumns; nColumns = 1; ATLTRY(pBindings = new DBBINDING[nColumns]); if (pBindings == NULL) return E_OUTOFMEMORY; ZeroMemory(pBindings, sizeof(DBBINDING)); pBindings[0].cbMaxLen = sizeof(int); pBindings[0].dwPart = DBPART_VALUE; pBindings[0].obValue = (int)(&test.m_a) – (int) (&test); pBindings[0].wType = DBTYPE_I4; pBindings[0].iOrdinal = 1; if (FAILED(hr)) return hr; hr = spAccessor->CreateAccessor(DBACCESSOR_ROWDATA, nColumns, pBindings, sizeof(CtesttableAccessor), &HAccessor, NULL); delete [] pBindings; } } } if (FAILED(hr)) return hr; { hr = Rowset->RestartPosition(NULL); if (FAILED(hr)) return hr; } HROW m_hRow = NULL; while (hr == S_OK) { ULONG ulRowsFetched = 0; if (m_hRow != NULL) {

69

Sandini Bib 70

2

Universeller Datenzugriff

hr = Rowset->ReleaseRows(1, &m_hRow, NULL, NULL, NULL); m_hRow = NULL; } HROW* phRow = &m_hRow; hr = Rowset->GetNextRows(NULL, 0, 1, &ulRowsFetched, &phRow); if (hr != S_OK) return hr; hr = Rowset->GetData(m_hRow, HAccessor, &test); if (FAILED(hr)) { ATLTRACE2(atlTraceDBClient, 0, _T( "GetData failed – HRESULT = 0x%X\ n"),hr); if (m_hRow != NULL) { hr = Rowset->ReleaseRows(1, &m_hRow, NULL, NULL, NULL); m_hRow = NULL; } } } return hr; }

2.4.2

Verwendung der OLE DB Consumer Templates

Wie Sie gerade gesehen haben, ist die grundsätzlich einfache Aufgabe, eine Tabelle zu öffnen, in der Implementierung sehr aufwändig und mühsam. Die OLE DB Consumer Templates vereinfachen diesen Vorgang erheblich. Wir definieren zunächst eine Zugriffsobjekt-Klasse (Accessor-Klasse), die für die Bindung zwischen der Datenbank und dem C++-Objekt zuständig ist: class CtesttableAccessor: public Ctesttable { BEGIN_COLUMN_MAP(CtesttableAccessor) COLUMN_ENTRY(1, m_a) END_COLUMN_MAP() DEFINE_COMMAND(CtesttableAccessor, _T(" \ SELECT \ a \ FROM dbo.testtable")) void ClearRecord() { memset(this, 0, sizeof(*this)); } } ;

Sandini Bib 2.5

Erweiterungen der OLE DB Consumer Templates

71

Der folgende Code zeigt dasselbe Szenario bei Verwendung der OLE DB Consumer Templates. Der Code ist kürzer und einfacher zu lesen. HRESULT PerformActionsWithOLEDBConsumerTemplates() { HRESULT hr = S_OK; CDataSource db; CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_AUTH_INTEGRATED, OLESTR("SSPI")); dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR("databasename")); dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("SERVERNAME")); dbinit.AddProperty(DBPROP_INIT_LCID, (long)1033); dbinit.AddProperty(DBPROP_INIT_PROMPT, (short)4); hr = db.Open(_T("SQLOLEDB.1"), &dbinit); if (FAILED(hr)) return hr; CSession Session; Session.Open(db); if (FAILED(hr)) return hr; CCommand test; hr = test.Open(Session); if (FAILED(hr)) return hr; hr = test.MoveFirst(); while (hr == S_OK) { hr = test.MoveNext(); } return hr; }

2.5 Erweiterungen der OLE DB Consumer Templates Die weiteren Kapitel dieses Buches werden zeigen, dass das Arbeiten mit OLE DB Consumer Templates wirklich Spaß macht. Dennoch habe ich dabei auch festgestellt, dass sie nicht alle denkbaren Fälle abdecken und auch nicht für alle Anforderungen geeignet sind. Aus diesen Gründen begann ich Erweiterungen zu den OLE DB Consumer Templates zu entwickeln, und im Laufe der Zeit konnte ich damit immer mehr der vorhandenen Mängel beseitigen. Ich habe die Erweiterungen der OLE DB Consumer Templates als Bibliothek (OLE DB Extensions) unentgeltlich auf die Website zu diesem Buch gestellt. So weit wie möglich werde ich in diesem Buch mit den Standard-Templates arbeiten, und nur dann, wenn ein bestimmter Aspekt damit nicht berücksichtigt werden kann, werde ich die Erweiterungen heranziehen.

Sandini Bib 72

2

Universeller Datenzugriff

Die OLE DB-Erweiterungen dienen folgenden Zwecken: 1. Sie füllen die von den OLE DB Consumer Templates nicht abgedeckten Lücken. 2. Sie zeigen alternative Methoden auf. 3. Sie stellen in gewissem Rahmen wieder verwendbaren Code bereit. Achten Sie beim Schreiben einer Anwendung genau darauf, welche Bibliothek Sie verwenden. Microsoft unterstützt die OLE DB-Erweiterungen ganz eindeutig nicht. Wenn Microsoft neue Versionen der Consumer Templates herausgibt, versuche ich, die Erweiterungen so zu aktualisieren, dass sie so weit wie möglich auf die OLE DB Consumer Templates zurückgreifen. Mein Ziel ist es nicht, die Arbeit von Microsoft zu duplizieren oder eine Konkurrenzversion zu erstellen.

2.6 Zusammenfassung Microsoft bietet mit dem universellen Datenzugriff und OLE DB als Grundpfeiler eine flexible und effiziente Datenzugriffsstrategie. Wenn Sie eine C++-Anwendung entwickeln wollen, die optimale Leistungsfähigkeit bietet, sollten Sie mit OLE DB und nicht mit ADO arbeiten. Die Arbeit mit OLE DB ist zwar mühsam, kann aber durch Verwendung der OLE DB Consumer Templates erheblich vereinfacht werden. In den folgenden Kapiteln werden wir uns in aller Ausführlichkeit mit dieser Bibliothek beschäftigen.

Sandini Bib

3 Der OLE DB-Assistent Mit den OLE DB Consumer Templates arbeitet es sich zwar sehr viel einfacher als mit unspezifischen OLE DB-Schnittstellen, doch auf den ersten Blick können sie den interessierten Programmierer durchaus auch abschrecken. Um hier Abhilfe zu schaffen, steht im Microsoft Visual Studio ein Wizard, oder auf Deutsch Assistent, zur Verfügung, mit dem Sie aus einer Datenbanktabelle oder einer Stored Procedure OLE DB Template-Klassen generieren lassen können. Der Assistent vermittelt einen guten ersten Eindruck von den OLE DB Template-Klassen und eignet sich, wie andere derartige Assistenten, hervorragend zur schnellen Erzeugung von Code. Der generierte Code muss anschließend allerdings auch gepflegt werden, daher ist es wichtig, dass Sie verstehen, was dahinter steht. Dieses Kapitel bietet Ihnen Folgendes: 왘 Sie lernen, wie Sie mit dem Assistenten arbeiten. 왘 Sie lernen, was hinter dem generierten Code steht. 왘 Sie lernen, wie Sie den generierten Code anwenden können. 왘 Sie erfahren, welche Verbesserungen Sie am generierten Code vornehmen können. 왘 Sie erhalten eine kurze Einführung in die OLE DB Consumer Templates.

3.1 Wie nützlich ist der Assistent? Einige wenige Mausklicks genügen und der Assistent generiert für Sie in großem Umfang fehlerfreien Code. Das Arbeiten mit diesem Code kann jedoch lästig sein. Außerdem ist nicht immer klar, welche Teile davon geändert werden können und welche nicht. Anders als andere Assistenten (beispielsweise normale MFC-Assistenten) funktioniert der OLE DB-Assistent nur in eine Richtung, d.h. es steht kein Mechanismus zur Verfügung, mit dem Sie an eine bestimmte Stelle im Generierungsprozess zurückspringen können, um ggf. erforderliche Änderungen vorzunehmen. Wenn der Code vom Assistenten einmal generiert wurde, führt kein Weg zurück.

Sandini Bib 74

3

Der OLE DB-Assistent

Dieser Ansatz hat zwei Vorteile. Erstens braucht kein assistentenspezifischer Code eingefügt zu werden. Der Assistent für die Microsoft Foundation Classes (MFC) muss beispielsweise Codeabschnitte wie "//{ { AFX_MSG_MAP" einfügen um den von ihm generierten Code vom Code des Programmierers abzugrenzen. Anders der OLE DBAssistent: Er generiert »sauberen« Code. Der zweite Vorteil liegt darin, dass auf diese Weise keine Veranlassung besteht, den Code mit dem Assistenten »kompatibel« zu halten. Der Code kann ausgehend von seinem ursprünglichen Zustand ungehindert weiterentwickelt werden. Mit dem OLE DB-Assistenten können Sie nicht für alle OLE DB Consumer Templates den gewünschten Code generieren, sondern er deckt nur die Hauptszenarien ab: Öffnen einer Tabelle, SQL-Abfragen oder Stored Procedures. Die Programmierung für mehrdimensionale oder hierarchische Provider bzw. für komplexe SQL-Abfragen ist mit dem Assistenten nicht möglich.

3.2 Verwendung des Assistenten Der OLE DB Template-Assistent kann nur für ATL-Projekte (ActiveX Template Library) und bestimmte MFC-Projekte (ausführbare Dateien, Steuerelemente und reguläre DLLs) eingesetzt werden. Wenn ein Projekt den OLE DB-Assistenten nicht unterstützt, wird die in Abb. 3.1 gezeigte Warnmeldung eingeblendet. Da OLE DB Templates zur ATL-Bibliothek gehören, bietet es sich an, sie in einem ATL-Projekt zu verwenden.

3.2.1

ATL-Projekt erstellen

Gehen Sie zum Erstellen eines ATL-Projekts folgendermaßen vor (siehe Abbildung 3.2 und 3.3). 1. Wählen Sie im Hauptmenü nacheinander DATEI und NEU... Das Dialogfenster NEU wird eingeblendet. 2. Wählen Sie in der Registerkarte PROJEKTE den spezifischen Assistenten über den Eintrag ATL-COM-ANWENDUNGS-ASSISTENT aus und geben Sie in das Eingabefeld PROJEKTNAME den Namen für Ihr Projekt ein. Klicken Sie anschließend auf OK. 3. Wählen Sie einen Server-Typ. Sie können als Server für OLE DB Consumer Templates eine DLL-Datei oder eine EXE-Datei (Programmdatei oder Dienst) wählen.

Sandini Bib 3.2

Verwendung des Assistenten

Abbildung 3.1: Warnmeldung bei Projekten, die den Assistenten nicht unterstützen

Abbildung 3.2: Dialogfenster Neu

Abbildung 3.3: Der ATL-COM-Anwendungs-Assistent – Schritt 1

75

Sandini Bib 76

3

Der OLE DB-Assistent

Abbildung 3.4: Der ATL-Objekt-Assistent

Abbildung 3.5: Der spezielle Assistent für die OLE DB Consumer Templates

3.2.2

Den Assistenten aufrufen

Wählen Sie im Hauptmenü von Visual Studio im Menü EINFÜGEN den Befehl NEUES ATL-OBJEKT, wenn Sie mit OLE DB Consumer Templates arbeiten wollen. Im Dialogfenster ATL-OBJEKT-ASSISTENT werden die verfügbaren ATL-Assistenten angezeigt. Wählen Sie im Feld KATEGORIE den Eintrag DATENZUGRIFF und im Feld OBJEKTE das Symbol NUTZER. Klicken Sie anschließend auf WEITER (siehe Abbildung 3.4). Der Assistent für die OLE DB Consumer Templates wird geöffnet (siehe Abbildung 3.5), in dem Sie Folgendes auswählen können: 왘 Über die Schaltfläche DATENQUELLE WÄHLEN... können Sie die Datenquelle und die Tabelle auswählen, die geöffnet werden sollen.

Sandini Bib 3.2

Verwendung des Assistenten

77

왘 Im Gruppenfeld TYP können Sie das Tabellen- oder das Befehlsverfahren zum Abrufen der Daten wählen (Optionen TABELLE oder BEFEHL). 왘 Unter UNTERSTÜTZUNG können Sie festlegen, für welche Datenaktualisierungsoperationen Unterstützung implementiert werden soll (Optionen ÄNDERN, EINFÜGEN und LÖSCHEN).

Datenquelle auswählen Der erste Schritt beim Auswählen der Datenquelle besteht darin, im Dialogfenster DATENLINKEIGENSCHAFTEN die gewünschte OLE DB-Datenquelle zu wählen (siehe Abbildung 3.6). Dieses Dialogfenster enthält vier Seiten (Registerkarten) zur Festlegung der Eigenschaften für die Verknüpfung mit der Datenquelle: PROVIDER, VERBINDUNG, WEITERE und ALLE. Auf der ersten Registerkarte können Sie den Typ des OLE DB-Providers auswählen. Für unsere Zwecke wählen wir den Eintrag MICROSOFT OLE DB PROVIDER FOR SQL SERVER aus, der native Unterstützung für MS SQL Server bietet. Durch Klicken auf die Schaltfläche WEITER oder auf das Register VERBINDUNG gelangen Sie auf die nächste Seite des Dialogfensters.

Abbildung 3.6: Seite 1 des Dialogfensters zum Auswählen der Datenquelle

Sandini Bib 78

3

Der OLE DB-Assistent

Abbildung 3.7: Seite 2 des Dialogfensters zum Auswählen der Datenquelle

Auf der zweiten Eigenschaftenseite (siehe Abbildung 3.7) können Sie die am häufigsten verwendeten Eigenschaften festlegen. Diese sind Provider-spezifisch, d.h. sie hängen davon ab, welchen Provider-Typ Sie im ersten Schritt gewählt haben. Auf der dritten Eigenschaftenseite (siehe Abbildung 3.8) können Sie noch weitere Eigenschaften wählen. Hier empfiehlt es sich in der Regel, die Standardeinstellungen zu übernehmen. Die vierte und letzte Eigenschaftenseite (siehe Abbildung 3.9) bietet Ihnen die Möglichkeit, die bisher gewählten Eigenschaften zu prüfen und ggf. noch zu ändern.

Tabelle auswählen Der nächste Schritt nach dem Festlegen der Einstellungen für die gewünschte Datenquelle besteht in der Auswahl der Tabelle. In unserem Beispiel wählen wir die Tabelle Mytable (siehe Abbildung 3.10). Die Standardeinstellung ist, dass der Assistent den Namen der Tabelle sowohl für die Kurzbezeichnung als auch für den Dateinamen verwendet (siehe Abbildung 3.11).

Sandini Bib 3.2

Verwendung des Assistenten

Abbildung 3.8: Seite 3 des Dialogfensters zum Auswählen der Datenquelle

Abbildung 3.9: Seite 4 des Dialogfensters zum Auswählen der Datenquelle

79

Sandini Bib 80

3

Der OLE DB-Assistent

Abbildung 3.10: Dialogfenster zum Auswählen der Tabelle

Abbildung 3.11: Der Assistent für die OLE DB Consumer Templates

3.3 Generierter Code Beim Arbeiten mit dem Assistenten werden dreierlei Arten von Code generiert: Die erste Art ist statischer Code, der assistentenunabhängig und immer gleich ist. Die zweite Art von Code stammt aus der Datenbank und ist assistentenabhängig, aber der Programmierer hat nur indirekt Einfluss darauf. Die dritte Art hängt direkt davon ab, was der Benutzer in den Assistenten eingegeben hat, ist also in vollem Maße benutzerdefiniert. Der im folgenden Abschnitt aufgeführte Code ist so ausgezeichnet, dass er widerspiegelt, in welchem Maße der Benutzer darauf Einfluss hat. Die Formatierung des Codes ist so gewählt, dass sie das Seitenformat dieses Buches einhält. In Wirklichkeit erscheint der Code in einem anderen Format, aber die Semantik bleibt die Gleiche.

Sandini Bib 3.3

Generierter Code

81

Dies ist sich wiederholender Code, der sich nicht ändert Dieser Code ist aus der Datenbank abgeleitet. Der Benutzer hat einen gewissen Einfluss darauf. Dieser Code kommt direkt aus dem Assistenten und hängt vollständig von den Eingaben des Benutzers ab.

Wenn Sie zum ersten Mal ein OLE DB-Objekt mit dem Assistenten einfügen, bindet dieser die OLE DB Include-Datei in die übergeordnete include-Datei mit dem Namen stdafx.h ein: #include

Dies ist ein einmaliger Vorgang, der nicht wiederholt zu werden braucht.

3.3.1

Tabelle öffnen

Wenn Sie die Tabelle MyTable wählen, generiert der Assistent Code mit Hilfe der Klasse CMyTable und unter Verwendung des von uns gewählten Dateinamens MyTable.H. Der Code besteht aus folgenden beiden Klassen: CMyTableAccessor, die zur Implementierung zu verwenden ist, und CMyTable, die für die Clients bestimmt ist. Die Zuständigkeiten dieser Klassen sind genau abgegrenzt: CMyTableAccessor definiert die Klassenelemente und die Bindung mit der zugrunde liegenden Tabelle und CMyTable öffnet die Verbindung zur Datenbank und die Zeilenmenge in der Tabelle. CMyTableAccessor definiert zunächst pro Tabellenspalte ein Klassenelement. In unserem Fall weist der Code zwei Klassenelemente auf: public: LONG m_Column1; TCHAR m_Column2[11];

Im nächsten Schritt muss in der Klasse angegeben werden, welches Klassenelement welcher Spalte zugeordnet ist. In OLE DB werden die Spalten durch ihre Nummer ausgehend von 1 identifiziert. So erhält die erste Spalte die Nummer 1, die zweite die Nummer 2 usw. Die Zugriffsobjekt-Klasse (Accessor-Klasse) definiert die Bindung folgendermaßen: BEGIN_COLUMN_MAP(CMyTableAccessor) COLUMN_ENTRY(1, m_Column1) COLUMN_ENTRY(2, m_Column2) END_COLUMN_MAP()

Abschließend initialisiert eine fertig vorgegebene Funktion die Klassenelemente. void ClearRecord() { memset(this, 0, sizeof(*this)); }

Sandini Bib 82

3

Der OLE DB-Assistent

CMyTableAccessor CAccessor

CRowset

CTable CMyTable Abbildung 3.12: Vererbung zwischen CMyTable und CMyTableAccessor

Da wir im Assistenten die Tabellenoption gewählt haben, erbt CMyTable von CTable: class CMyTable : public CTable

Es stellt sich heraus, dass CTable von der Klasse CAccessor erbt und die Klasse CAccessor ihrerseits von ihrem Template-Parameter CMyTableAccessor erbt. Dies ergibt eine Vererbungsfolge von CMyTableAccessor bis hin zu CMyTable. CMyTable erbt zusätzlich von der Klasse CRowset (siehe Abbildung 3.12). Die Klasse CMyTable weist die drei folgenden Methoden auf: Open, OpenDataSource, und OpenRowset. Open ist eine einfache Methode, die versucht eine Verbindung zur Datenbank herzustellen. Ist die Verbindung hergestellt, versucht sie die Zeilenmenge zu öffnen: HRESULT Open() { HRESULT hr; hr = OpenDataSource(); if (FAILED(hr)) return hr; return OpenRowset(); }

OpenDataSource versucht ebenfalls eine Verbindung zur Datenbank herzustellen. Für diese Methode übernimmt der Assistent Informationen aus dem Dialogfenster DATENLINKEIGENSCHAFTEN und generiert die Eigenschaften sowie die ProgID: HRESULT OpenDataSource() { HRESULT hr; CDataSource db; CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_AUTH_INTEGRATED,OLESTR("SSPI")); dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR("wizard"));

Sandini Bib 3.3

Generierter Code

83

dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("SERVER")); dbinit.AddProperty(DBPROP_INIT_LCID, (long)1033); dbinit.AddProperty(DBPROP_INIT_PROMPT, (short)4); hr = db.Open(_T("SQLOLEDB.1"), &dbinit); if (FAILED(hr)) return hr; return m_session.Open(db); }

OpenRowset leitet den Aufruf einfach an die entsprechende Methode von CTable weiter: HRESULT OpenRowset() { return CTable::Open( m_session, _T("dbo.MyTable")); }

Der vollständige Code für CMyTable und CMyTableAccessor lautet folgendermaßen: // MyTable.H : Deklaration der Klasse CMyTable #ifndef __MYTABLE_H_ #define __MYTABLE_H_ class CMyTableAccessor { public: LONG m_Column1; TCHAR m_Column2[11]; BEGIN_COLUMN_MAP(CMyTableAccessor) COLUMN_ENTRY(1, m_Column1) COLUMN_ENTRY(2, m_Column2) END_COLUMN_MAP()

// Es empfiehlt sich, diese Funktion aufzurufen, wenn Sie einen // Datensatz einfügen und alle Felder initialisieren möchten, // ohne alle Felder explizit zu setzen. void ClearRecord() { memset(this, 0, sizeof(*this)); } } ; class CMyTable : public CTable { public: HRESULT Open() {

Sandini Bib 84

3 HRESULT

Der OLE DB-Assistent

hr;

hr = OpenDataSource(); if (FAILED(hr)) return hr; return OpenRowset(); } HRESULT OpenDataSource() { HRESULT hr; CDataSource db; CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_AUTH_INTEGRATED,OLESTR("SSPI")); dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR("wizard")); dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("SERVER")); dbinit.AddProperty(DBPROP_INIT_LCID, (long)1033); dbinit.AddProperty(DBPROP_INIT_PROMPT, (short)4); hr = db.Open(_T("SQLOLEDB.1"), &dbinit); if (FAILED(hr)) return hr; return m_session.Open(db); } HRESULT OpenRowset() { return CTable::Open( m_session, _T("dbo.MyTable")); } CSession m_session; } ; #endif // __MYTABLE_H_

3.3.2

Befehl öffnen

Wenn Sie im Assistenten im Bereich TYP anstelle des Optionsfelds TABELLE das Optionsfeld BEFEHL wählen, wird der Code über die Klasse CCommand generiert. Das Wählen dieser Option bringt zwei Unterschiede mit sich: Erstens muss der Assistent den Text der Abfrage in CMyTableAccessor angeben: DEFINE_COMMAND(CMyTableAccessor2, _T(" \ SELECT \ Column1, \ Column2 \ FROM dbo.MyTable"))

Sandini Bib 3.3

Generierter Code

85

Zweitens erbt CMyTable von CCommand anstelle von CTable: class CMyTable2 : public CCommand

3.3.3

Unterstützung für Änderungen hinzufügen

Wenn Sie eine der Optionen unter UNTERSTÜTZUNG angeklickt haben, fügt der Assistent die folgenden Zeilen in die Methode OpenRowset ein: CDBPropSet propset(DBPROPSET_ROWSET); propset.AddProperty(DBPROP_IRowsetChange, true); propset.AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE);

Die erste Eigenschaft, DBPROP_IRowsetChange, definiert die Unterstützung für Änderungen an der Zeilenmenge. Mit der zweiten Eigenschaft, DBPROP_UPDATABILITY, wird die Art der benötigten Unterstützung angegeben: In diesem speziellen Fall werden DBPROPVAL_UP_CHANGE, DBPROPVAL_UP_INSERT und DBPROPVAL_UP_DELETE angegeben, da Unterstützung zum Ändern, Einfügen und Löschen benötigt wird. Das folgende Beispiel zeigt einen Befehl mit Unterstützung für Einfüge-, Lösch- und Änderungsoperationen (Insert, Delete, Change): // MyTable2.H : Deklaration der Klasse CMyTable2 #ifndef __MYTABLE2_H_ #define __MYTABLE2_H_ class CMyTableAccessor2 { public: LONG m_Column1; TCHAR m_Column2[11]; BEGIN_COLUMN_MAP(CMyTableAccessor2) COLUMN_ENTRY(1, m_Column1) COLUMN_ENTRY(2, m_Column2) END_COLUMN_MAP() DEFINE_COMMAND(CMyTableAccessor2, _T(" \ SELECT \ Column1, \ Column2 \ FROM dbo.MyTable")) // Es empfiehlt sich, diese Funktion aufzurufen, wenn Sie einen // Datensatz einfügen und alle Felder initialisieren möchten, // ohne alle Felder explizit zu setzen.

Sandini Bib 86

3 void ClearRecord() { memset(this, 0, sizeof(*this)); } } ; class CMyTable2 : public CCommand { public: HRESULT Open() { HRESULT hr; hr = OpenDataSource(); if (FAILED(hr)) return hr; return OpenRowset(); } HRESULT OpenDataSource() { HRESULT hr; CDataSource db; CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_AUTH_INTEGRATED,OLESTR("SSPI")); dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR("wizard")); dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("SERVER")); dbinit.AddProperty(DBPROP_INIT_LCID, (long)1033); dbinit.AddProperty(DBPROP_INIT_PROMPT, (short)4); hr = db.Open(_T("SQLOLEDB.1"), &dbinit); if (FAILED(hr)) return hr; return m_session.Open(db); } HRESULT OpenRowset() { // Eigenschaften zum Öffnen setzen CDBPropSet propset(DBPROPSET_ROWSET); propset.AddProperty(DBPROP_IRowsetChange, true); propset.AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE); return CCommand::Open( m_session, NULL, &propset); } CSession

m_session;

Der OLE DB-Assistent

Sandini Bib 3.3

Generierter Code

87

} ; #endif // __MYTABLE2_H_

3.3.4

Stored Procedures

Eine Stored Procedure hat große Ähnlichkeit mit einem normalen SQL-Befehl. Der signifikante Unterschied besteht darin, dass Stored Procedures Ausgabeparameter bzw. Rückgabewerte liefern. Im Abschnitt PARAM_MAP können sowohl Eingabe- als auch Ausgangsparameter angegeben werden. Vor jedem Parameter gibt der Assistent den Parametertyp an. Im folgenden Beispiel weist die Stored Procedure einen Parameter (Parameter1) und einen Rückgabewert auf. // dboStoredProcedureName1.H : Deklaration der Klasse CdboStoredProcedureName1 #ifndef __DBOSTOREDPROCEDURENAME1_H_ #define __DBOSTOREDPROCEDURENAME1_H_ class CdboStoredProcedureName1Accessor { public: LONG m_RETURNVALUE; LONG m_Parameter1; LONG m_colColumn1; TCHAR m_colColumn2[11]; BEGIN_PARAM_MAP(CdboStoredProcedureName1Accessor) SET_PARAM_TYPE(DBPARAMIO_OUTPUT) COLUMN_ENTRY(1, m_RETURNVALUE) SET_PARAM_TYPE(DBPARAMIO_INPUT) COLUMN_ENTRY(2, m_Parameter1) END_PARAM_MAP() BEGIN_COLUMN_MAP(CdboStoredProcedureName1Accessor) COLUMN_ENTRY(1, m_colColumn1) COLUMN_ENTRY(2, m_colColumn2) END_COLUMN_MAP() DEFINE_COMMAND(CdboStoredProcedureName1Accessor, _T("{ dbo.StoredProcedureName;1 (?) }"))

? = CALL

// Es empfiehlt sich, diese Funktion aufzurufen, wenn Sie einen // Datensatz einfügen und alle Felder initialisieren möchten, // ohne alle Felder explizit zu setzen. void ClearRecord() { memset(this, 0, sizeof(*this)); } } ;

Sandini Bib 88

3

Der OLE DB-Assistent

class CdboStoredProcedureName1 : public CCommand { public: HRESULT Open() { HRESULT hr; hr = OpenDataSource(); if (FAILED(hr)) return hr; return OpenRowset(); } HRESULT OpenDataSource() { HRESULT hr; CDataSource db; CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_AUTH_INTEGRATED,OLESTR("SSPI")); dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR("wizard")); dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("SERVER")); dbinit.AddProperty(DBPROP_INIT_LCID, (long)1033); dbinit.AddProperty(DBPROP_INIT_PROMPT, (short)4); hr = db.Open(_T("SQLOLEDB.1"), &dbinit); if (FAILED(hr)) return hr; return m_session.Open(db); } HRESULT OpenRowset() { return CCommand::Open(m_session); } CSession m_session; } ; #endif // __DBOSTOREDPROCEDURENAME1_H_

3.3.5

Verbesserungen am generierten Code

Wie bereits erwähnt, bietet der Assistent eine gute Einführung in die OLE DB Consumer Templates, er stellt jedoch keine perfekte Lösung dar. In diesem Abschnitt werden eine Reihe von Verbesserungen beschrieben, die Sie an dem generierten Code vornehmen können.

Sandini Bib 3.4

Der MFC-Assistent

89

Häufig werden Sie auf mehrere Tabellen derselben Datenquelle zugreifen müssen. Hierbei erweist sich der Assistent als nachteilig, weil er für jede Tabelle, auf die zugegriffen wird, den Datenquellencode dupliziert. Wenn Sie folglich die Datenquelle verlagern, müssen Sie den Code an mehreren Stellen entsprechend aktualisieren. Besser wäre es, den Code für die Datenquelle zentral an einer Stelle zu haben und diesen vererben zu lassen. Ein weiteres Problem hinsichtlich des Datenquellencodes besteht darin, dass die Datenquelle ohne Dienstkomponenten geöffnet wird (in Kapitel 6 wird die Bedeutung von Dienstkomponenten näher erläutert). Im Allgemeinen empfiehlt es sich, eine Datenquelle mit Hilfe von Dienstkomponenten zu öffnen. Hierzu müssen Sie die Zeile hr = db.Open("SQLOLEDB.1", &dbinit);

durch hr = db.OpenWithServiceComponents("SQLOLEDB.1", &dbinit);

ersetzen. Im weiteren Verlauf dieses Buches werden Sie sehen, daß die OLE DB Consumer Templates weitaus mehr Möglichkeiten bieten, als vom Assistenten wahrgenommen werden. Der Assistent ist ein interessantes Werkzeug für die Generierung der Spaltenbindungen und der Datenquelleneigenschaften, aber es gibt keinen Grund dafür, Ihren Code nicht weiterzuentwickeln und individuell anzupassen.

3.4 Der MFC-Assistent Microsoft stellt einen Assistenten für MFC-Projekte zur Verfügung. Da dieser prinzipiell genauso aufgebaut ist wie der OLE DB-Assistent, braucht hier nicht speziell beschrieben zu werden, wie man mit ihm arbeitet. Wenn Sie jedoch von einem zum anderen wechseln, werden Sie einige Unterschiede feststellen. Erstens öffnet der MFCAssistent die Datenquelle mit Hilfe von Dienstkomponenten. Zweitens springt er nach dem Abrufen der Zeilenmenge automatisch in die erste Zeile. Abgesehen von diesen beiden Detailunterschieden ist der jeweils generierte Code sehr ähnlich.

3.5 Die Klassen der OLE DB Consumer Templates im Überblick Die vom Assistenten generierten Klassen stellen einen guten Einstieg in die Klassen der OLE DB Consumer Templates dar. In diesem Abschnitt wird der Code in einer typischen Ausführungsreihenfolge erläutert. Bevor Sie weiterlesen, empfiehlt es sich, dass Sie Anhang A (erweiterte C++-Techniken) durcharbeiten um sich mit diesen Techniken vertraut zu machen.

Sandini Bib 90

3

Der OLE DB-Assistent

Open ist die erste vom Assistenten generierte Methode. Diese Methode dient zur Ausführung der beiden Aktionen OpenDataSource und OpenRowset. OpenDataSource verwendet drei Klassen. Die erste Klasse, CDataSource, kapselt ein OLE DB-Datenquellenobjekt. Dieses Objekt gibt an, wo sich der Datenspeicher befindet. In Kapitel 6 werden die Datenquellen ausführlicher behandelt.

Die zweite Klasse, CDBPropSet, legt die Eigenschaften der Datenquelle fest. Diese Klasse weist ein Set von OLE DB-Eigenschaften auf. In OLE DB gibt es für viele Objekte Eigenschaften-Sets. Das Arbeiten mit Eigenschaften-Sets wird in Kapitel 5 erläutert. Die dritte Klasse, CSession, kapselt das OLE DB-Sitzungsobjekt, das sich zwischen dem Datenquellenobjekt und den anderen OLE DB-Objekten befindet. Der Hauptzweck von Sitzungen besteht darin, Unterstützung für Transaktionen zur Verfügung zu stellen. In Kapitel 12 erfahren Sie Näheres zu dieser Klasse. OpenRowset verwendet CDBPropSet genauso wie OpenDataSource. Die Implementierung von OpenRowset ist jedoch ein wenig komplexer, da sie vererbte Methoden verwendet, was uns dazu zwingt, einen Blick auf die Gesamtarchitektur zu werfen.

3.5.1

Architektur der Zugriffsobjekt-Klassen für Tabellen, Befehle und Zeilenmengen

Der Kern der OLE DB Consumer Templates besteht aus einer Gruppe von Klassen, die auf subtile Weise miteinander verwoben sind. Diese Architektur ist für einen Neuling zwar einigermaßen kompliziert, sobald er jedoch damit vertraut ist, wird er ihre außergewöhnliche Leistungsfähigkeit und Flexibilität sehr schätzen. Die den Kern bildenden Klassen werden nie isoliert, sondern immer in Kombination miteinander verwendet. Für die Festlegung der Datenmanipulation sollten Sie sich folgende vier Fragen stellen: 왘 Befehl oder Tabelle? 왘 Wie viele Zeilenmengen? 왘 Welche Art der Navigation? 왘 Welche Art der Bindung? In den folgenden Abschnitten werden diese Fragen einzeln behandelt.

Befehl oder Tabelle? Sie haben im Assistenten schon gesehen, dass mit OLE DB entweder Tabellen geöffnet oder Befehle ausgeführt werden können. Das Öffnen einer Tabelle stellt die einfachste Möglichkeit zum Abrufen von Daten dar. Eine OLE DB-Tabelle entspricht einer voll-

Sandini Bib 3.5

Die Klassen der OLE DB Consumer Templates im Überblick

91

ständigen Tabelle einer relationalen Datenbank. Verwenden Sie zum Öffnen von Tabellen die Klasse CTable. Ein Befehl ist ein Objekt mit einer Ausführungsmethode. In der Regel enthält er Text, wobei es sich meistens um einen SQL-Befehl handelt. Verwenden Sie zum Manipulieren von Daten mit Befehlen die Klasse CCommand.

Wie viele Zeilenmengen? Einige Befehle liefern keine Zeilenmengen zurück. Beispielsweise fügt der Befehl INSERT INTO XXX VALUES... Zeilen in eine Tabelle ein, führt aber sonst keine weitere Aktion durch. Andere Befehle, wie SELECT * FROM XXX WHERE..., liefern eine Zeilenmenge mit den ausgewählten Zeilen zurück. Wenn keine Zeile ausgewählt werden konnte, liefert der Befehl eine leere Zeilenmenge zurück, in anderen Fällen mehrere Zeilenmengen. Beispielsweise liefert SELECT * FROM XXX ; SELECT * FROM YYY pro SELECT-Anweisung je eine Zeilenmenge zurück, in diesem Fall insgesamt zwei Zeilenmengen. Auf die Frage nach der Anzahl der Zeilenmengen gibt es bei OLE DB zwei mögliche Antworten: »eine oder weniger« bzw. »zwei oder mehr«. Sobald mindestens zwei Zeilenmengen zurückgeliefert werden, ist ein Mechanismus zum Springen von einer Zeilenmenge zur nächsten erforderlich. Bei nur einer Zeilenmenge fällt dies weg, weil keine Iteration erforderlich ist. Verwenden Sie die Klasse CMultipleResults, wenn Sie Ergebnisse mit mehreren Zeilenmengen unterstützen wollen, andernfalls die Klasse CNoMultipleResults. Beachten Sie, dass eine Tabelle immer nur genau eine Zeilenmenge zurückliefert und daher Mehrfachergebnisse nicht unterstützt.

Welche Art der Navigation? Sie müssen festlegen, ob die Bewegung zwischen den Zeilen einer zurückgelieferten Zeilenmenge sequenziell oder durch Springen zwischen Zeilen erfolgen soll. Wenn sich der Cursor in einer bestimmten Zeile befindet, sollen möglicherweise Daten mit dem zugrunde liegenden Datenspeicher ausgetauscht werden. Diese Funktionen werden durch die drei Zeilenmengen-Klassen für Zeilenmengen ausgeführt, die in den OLE DB Consumer Templates zur Verfügung stehen: CRowset, CBulkRowset und CArrayRowset. Diese drei Klassen bieten im Wesentlichen die gleiche Funktionalität mit geringen Abweichungen. Wenn der Befehl keine Zeilenmenge abrufen soll, besteht kein Bedarf an Unterstützung für Zeilenmengen. Verwenden Sie in diesem Fall die Klasse CNoRowset, die keine zeilgenmengenbezogene Funktionalität aufweist. Eine Verwendung von CNoRowset zusammen mit CMultipleResults ergibt daher ganz offensichtlich keinen Sinn.

Sandini Bib 92

3

Der OLE DB-Assistent

Welche Art der Bindung? Beim Austauschen von Daten müssen Sie die Beziehung zwischen den verschiedenen Spalten und dem C++-Speicher angeben. Eine Zeilenmenge enthält beispielsweise die Spalten A und B, während ein entsprechendes Objekt die Klassenelemente m_a und m_b aufweisen könnte. Die Zugriffsobjekt-Klassen (Accessor-Klassen) definieren die Bindung zwischen dem C++-Speicher und der zugrunde liegenden Zeilenmenge. In den OLE DB Consumer Templates gibt es die folgenden vier Zugriffsobjekt-Klassen: 왘 CAccessor 왘 CDynamicAccessor 왘 CDynamicParameterAccessor 왘 CManualAccessor

Jede dieser Klassen stellt einen anderen Bindungsmechanismus zur Verfügung. In Kapitel 9 werden die Zugriffsobjekt-Klassen ausführlich behandelt. Wenn keine Zeilenmenge abgerufen werden soll, ist kein Zugriffsobjekt (Accessor) notwendig. Hierfür steht die Klasse CNoAccessor zur Verfügung, deren Name diese Funktion andeutet. Verwenden Sie folglich in solchen Fällen eine Kombination aus den Klassen CNoRowset und CNoAccessor.

3.5.2

Klassen kombinieren

Sobald Sie wissen, welche Klassen Sie benötigen, können Sie diese mit Hilfe eines Template-Parameters kombinieren. Für Tabellen sieht das so aus: CTable

Für Befehle lautet die Zeile folgendermaßen: CCommand < AccessorClass, RowsetClass, NoMultipleResultsClass>

Es gibt eine Reihe möglicher Kombinationen. Im Folgenden sind einige aufgeführt: CTable; CCommand; CCommand;

Viele andere Kombinationen sind unsinnig und können nicht kompiliert werden. Beispielsweise ist es nicht möglich, CNoAccessor und CRowset zu kombinieren.

3.6 CAccessorRowset Auf der unspezifischen Ebene von OLE DB stellt eine Zeilenmenge (Rowset) lediglich eine Reihe aus Zeilen und Spalten dar. Wie bereits erwähnt, grenzen die OLE DB Con-

Sandini Bib 3.6

CAccessorRowset

93

sumer Templates den Bindungsteil vom Navigationsteil einer Zeilenmenge ab. Die Bindung bezieht sich auf die Spalten, die Navigation auf die Zeilen. Beim Zugriff auf ein OLE DB-Zeilenmengen-Objekt müssen Sie beides festlegen, wie in Abbildung 3.13 gezeigt. Die Klasse CAccessorRowset stellt eine Kombination aus einer Zeilenmenge und einem Zugriffsobjekt dar. Hierbei handelt es sich um die zentrale Klasse in den OLE DB Templates, daher ist das Verständnis dieser Klasse notwendig, um die OLE DB Consumer Templates als Ganzes zu verstehen. Gelegenheitsprogrammierer brauchen nicht direkt mit dieser Klasse zu arbeiten. CAccessorRowset ist ein gutes Beispiel für eine Klasse, die von ihren Template-Parametern erbt. Ihr hauptsächlicher Zweck besteht darin, die Zugriffsobjekt-Klasse (AccessorKlasse) und die Zeilenmengen-Klasse (Rowset-Klasse) miteinander zu kombinieren. Sie ist in Abbildung 3.14 dargestellt. Ihre Deklaration lautet folgendermaßen: template class CAccessorRowset : public TAccessor, public TRowset

Zugriffsobjekt-Klasse

Zeilenmengen-Klasse

Spalte 1

Spalte 2

Spalte 3

Zeile 1 Zeile 2 Zeile 3 Zeile 4 Zeile 5 Zeile 6 Zeilenmengen-Objekt

Abbildung 3.13: Zugriffsobjekt-Klasse (Accessor-Klasse) und Zeilenmengen-Klasse (Rowset-Klasse)

Zugriffsobjekt

Zeilenmenge

CAccessorRowset

Abbildung 3.14: Vererbung von CAccessorRowset

Sandini Bib 94

3

Der OLE DB-Assistent

Die Zugriffsobjekt-Klasse und die Zeilenmengen-Klasse sind nicht symmetrisch. Die Zeilenmengen-Klasse (Rowset-Klasse) benötigt nämlich in der Regel Zugriff auf die Zugriffsobjekt-Klasse (Accessor-Klasse), aber die Zugriffsobjekt-Klasse braucht die Zeilenmengen-Klasse nicht zu kennen. Der Konstruktor von CAccessorRowset stellt die Bindung zwischen der ZeilenmengenKlasse und der Zugriffsobjekt-Klasse her: CAccessorRowset() { // Für die Zeilenmenge einen Zeiger auf das Zugriffsobjekt // setzen SetAccessor(this); }

SetAccessor ist eine Methode, die vom Argument der Zeilenmengen-Template erbt. Sie

ermöglicht es, einen Zeiger auf das Zugriffsobjekt (Accessor) zu setzen. Bei einer leeren Klasse wie CNoRowset bewirkt diese Methode nichts. In allen anderen Fällen setzt die Zeilenmengen-Klasse damit ihr Klassenelement m_pAccessor: void SetAccessor(CAccessorBase* pAccessor) { m_pAccessor = pAccessor; }

Das Ergebnis dieses Mechanismus ist in Abbildung 3.15 dargestellt.

TAccessorKlassenelemente TRowsetKlassenelemente m_spRowset

OLE DB ZeilenmengenObjekt

m_pAccessor

Abbildung 3.15: Die Zeilenmengen-Klasse (Rowset-Klasse) zeigt auf die Zugriffsobjekt-Klasse (Accessor-Klasse)

3.6.1

CAccessorRowset schließen

Das Schließen von CAccessorRowset bedeutet, dass die Zugriffsmethoden- (Accessor) und die Zeilenmengen-Klasse (Rowset) geschlossen werden. void Close() { if (GetInterface() != NULL) {

Sandini Bib 3.7

Zusammenfassung

95

ReleaseAccessors(GetInterface()); TAccessor::Close(); TRowset::Close(); } }

Die Methode GetInterface erbt vom jeweiligen TRowset-Typ und liefert den Schnittstellenzeiger auf das OLE DB-Zeilenmengen-Objekt zurück. Wenn der Zeiger NULL ist, sind keine Zeilenmenge und kein Zugriffsobjekt vorhanden. In allen anderen Fällen werden die folgenden drei Aktionen durchgeführt: 왘 ReleaseAccessors(GetInterface()) wird von TAccessor vererbt. TAccessor benötigt einen Zeiger auf das Zeilenmengen-Objekt um Zugriffsobjekt-Handles freizugeben. Da die Klasse TAccessor keine Referenz auf das COM-Objekt enthält, akzeptiert sie das Zeilenmengen-Objekt von TRowset als Argument. 왘 TAccessor::Close();gibt interne Strukturen von TAccessor frei. 왘 TRowset::Close();gibt den Schnittstellenzeiger auf das Zeilenmengen-Objekt frei. Bei der Auflösung ruft CAccessorRowset die Funktion Close auf: ~CAccessorRowset() { Close(); }

3.7 Zusammenfassung In diesem Kapitel habe ich den Assistenten vorgestellt und die OLE DB Consumer Templates kurz im Überblick dargestellt. In den nächsten Kapiteln werden diese Themen detaillierter behandelt. Kapitel 4 behandelt den Umgang mit Fehlern und ihre Interpretation, Kapitel 5 die OLE DB-Eigenschaften. Kapitel 6 bietet eine Untersuchung von Datenquellen und in den Kapiteln 7 bis 10 werden die Konzepte Tabelle, Befehl, Zeilenmenge und Zugriffsobjekt erläutert. Die Kapitel 11 bis 16 schließlich stellen erweiterte Konzepte für bestimmte Provider vor.

Sandini Bib

Sandini Bib

4 Fehler und Fehlermechanismen Die Programmierung von Datenbanken unterscheidet sich ein wenig von anderen Programmieraufgaben, weil hier jegliche graphische Benutzeroberfläche fehlt. Im Allgemeinen ist es nicht möglich, graphische Elemente, wie z.B. Meldungsfelder, für Rückmeldungen bei auftretenden Fehlern zu programmieren. Als Datenbankentwickler sind Sie dafür verantwortlich, dem Benutzer oder Systemadministrator möglichst viele hilfreiche Fehlerinformationen zur Verfügung zu stellen. Daher müssen Sie verstehen, wie der Fehlermechanismus von OLE DB funktioniert. In den Anfangszeiten der C-Programmierung war es üblich, dass Funktionen einen Statuscode wie den Folgenden zurücklieferten: ERROR_CODE MyFunction (..);

Seit C++ gibt es Ausnahmen (Exceptions), die eine weitaus elegantere Lösung darstellen und den Programmierern eine effektivere Fehlerbehandlung ermöglichen. Ausnahmen haben jedoch den Nachteil, dass sie nur für den lokalen Prozess und den betreffenden Rechner gelten. Beispielsweise ist es nicht möglich, eine C++-Ausnahme in einem bestimmten Prozess auszulösen und sie in einem anderen Prozess abzufangen. Dies funktioniert in einer Einplatz- und Einprozessumgebung hervorragend, stellt jedoch in einer verteilten Umgebung ein Problem dar und steht vollständig im Gegensatz zur allgemeinen COM-Philosophie. COM verfügt über zwei Mechanismen zur Fehlerbehandlung. Einerseits liefern praktisch alle COM-Methoden einen Fehlercode mit dem Namen HRESULT zurück, der dem alten Fehlercode von C-Funktionen ähnelt. Nach dem Auftreten eines Fehlers ist es dann möglich, anhand eines COM-Objekts, das Informationen zu früheren Fehler enthält, mehr über den neuen Fehler zu erfahren.

4.1 HRESULT Bei COM liefern praktisch alle Methoden den Fehlercode HRESULT zurück. Dabei handelt es sich einfach um eine 32-Bit-Struktur, die alle Informationen über den Erfolg oder das Scheitern einer Methodenausführung enthält. HRESULT ist folgendermaßen definiert: typedef LONG HRESULT;

Sandini Bib 98

4

Fehler und Fehlermechanismen

Ein HRESULT-Fehlercode enthält Fehlerinformationen in drei Teilen: Der Severity-Teil gibt an, ob die Methode erfolgreich ausgeführt werden konnte oder nicht. Wenn dieser Teil gesetzt ist, trat bei der Ausführung ein Fehler auf, wenn nicht, konnte die Methode erfolgreich ausgeführt werden. Der Facility-Teil identifiziert die Gruppe, zu der der Code gehört. In den meisten Fällen lautet die Angabe hier FACILITY_ITF, was darauf hinweist, dass die Bedeutung des Codes von der zugrunde liegenden Schnittstelle abhängt. Der Code-Teil gibt den eigentlichen Fehlercode wieder, d.h. er beschreibt, welcher Fehler aufgetreten ist. Sie können auf die einzelnen Teile von HRESULT über die folgenden Makros zugreifen: 왘 HRESULT_SEVERITY (HRESULT hr) 왘 HRESULT_FACILITY (HRESULT hr) 왘 HRESULT_CODE (HRESULT hr)

Über die folgenden Makros können Sie ermitteln, ob ein HRESULT-Code eine erfolgreiche oder eine gescheiterte Methodenausführung bezeichnet: 왘 FAILED (HRESULT hr) 왘 SUCCEEDED (HRESULT hr)

In der Praxis gibt es hauptsächlich drei Fälle. Im ersten Fall ist FAILED (hr) wahr oder SUCCEEDED (hr) falsch. Dies bedeutet, dass die Methode nicht erfolgreich ausgeführt werden konnte. Wenn der Methode beispielsweise der Zugriff verweigert wurde, wird der Fehlercode DB_SEC_E_PERMISSIONDENIED zurückgeliefert. Im zweiten Fall ist FAILED (hr) falsch oder SUCCEEDED (hr) wahr, aber es wird nicht S_OK zurückgeliefert. Das bedeutet, dass die Methode erfolgreich ausgeführt werden konnte, aber einige Warnungen, z.B. DB_S_ROWLIMIT EXCEEDED, generiert wurden. Im dritten Fall wird S_OK zurückgeliefert, was bedeutet, dass die Methode erfolgreich und ohne Warnungen ausgeführt werden konnte.

4.1.1

Interpretation von HRESULT

Das OLE DB Software Development Kit (SDK) enthält ausführliche Informationen über die HRESULT-Codes, die von den einzelnen OLE DB-Methoden zurückgeliefert werden können, und erläutert deren exakte Bedeutungen. Wenn eine Methode einen Fehler zurückliefert, besteht die präziseste Möglichkeit zur Analyse der Fehlerbedingung darin, über eine Ablaufverfolgung festzustellen, welche Methode aufgerufen wurde und zu welcher Schnittstelle sie gehört. Mit diesen Informationen versehen können Sie in der Dokumentation zum OLE DB-SDK Näheres zum betreffenden Fehler nachlesen. Das mit der Dokumentation zu Visual Studio gelieferte Fehlernachschlageprogramm erleichtert es ebenfalls, die Bedeutung eines HRESULT-Codes zu ermitteln. Eine weitere Alternative stellt die API-Funktion FormatMessage dar, die die Fehlerbeschreibung eines

Sandini Bib 4.1

HRESULT

99

bestimmten HRESULT-Codes für ein bestimmtes Modul heraussucht. Wenn der HRESULTCode einer COM-Standardfehlerbedingung entspricht, findet die Funktion automatisch das betreffende Modul im System. Wenn der HRESULT-Code jedoch eine OLE DBFehlerbedingung kennzeichnet, müssen Sie das OLE DB-Fehlermodul MSDAERR.DLL explizit übergeben. Weitere Informationen zu FormatMessage finden Sie im SDK von Microsoft. Das folgende Beispiel zeigt, wie die Beschreibung eines HRESULT-Codes ermittelt werden kann. HRESULT TraceHresult (HRESULT hr) { LPCTSTR lpMsgBuf; HMODULE hModule = LoadLibrary ("C:\ \ Program Files\ \ Common Files\ \ SYSTEM\ \ ole db\ \ MSDAERR.DLL"); if (FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_FROM_SYSTEM, hModule, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL)) { OutputDebugString(lpMsgBuf); LocalFree((HLOCAL)lpMsgBuf); } FreeLibrary(hModule); return S_OK; }

Beispielsweise generiert der Code TraceHresult(DB_E_CANCELED); TraceHresult(E_NOINTERFACE);

die folgende Ausgabe: The change was canceled during notification; no columns are changed No such interface supported

4.1.2

Bedeutung von HRESULT in der Praxis

Beim Programmieren einer COM-Komponente rufen Sie eine Reihe von Methoden auf, von denen jede einen HRESULT-Code zurückliefert. Der Code könnte dann insgesamt so aussehen: HRESULT CMyComponent::MyMethod() { HRESULT hr = S_OK; hr = Object1->Method1(..); if (FAILED(hr)) return(hr); hr = Object2->Method2(..); if (FAILED(hr))

Sandini Bib 100

4

Fehler und Fehlermechanismen

return(hr); hr = Object2->Method2(..); if (FAILED(hr)) return(hr); return hr; }

Wie Sie sehen, neigt diese Art der Programmierung zu zahlreichen Wiederholungen. Um das zu vermeiden, steht Ihnen das Makro CHECK_RESULT zur Verfügung. Dieses ist folgendermaßen definiert: #define CHECK_RESULT(_expr_) \ { HRESULT _hr_ = _expr_; if (FAILED(_hr_)) return _hr_;}

Bei Verwendung dieses Makros schrumpft der Code folgendermaßen: HRESULT CMyComponent::MyMethod() { CHECK_RESULT (Object1->Method1(..)) CHECK_RESULT (Object2->Method2(..)) return Object3->Method3(..); }

Es wird deutlich, dass diese Art der Programmierung kompakteren Code erzeugt. Andererseits ist das Debugging bei diesem Code ein wenig schwieriger.

4.2 Fehlerinfo- und Fehlerdatensatz-Objekte Wenn der HRESULT-Code keine ausreichenden Informationen liefert, können Sie zusätzliche Details über die Fehlerinfo-Objekte erhalten.

4.2.1

Ablaufverfolgung für Fehler

Das Abrufen von Fehlerinfo-Objekten ist etwas kompliziert. Am einfachsten ist es, hierfür eine Funktion der OLE DB Consumer Templates zu verwenden. In diesem Fall werden die Fehlerinformationen im Debug-Fenster von Visual Studio angezeigt. Die Funktion zur Ablaufverfolgung (Trace) ist folgendermaßen deklariert: inline void AtlTraceErrorRecords(HRESULT hrErr = S_OK)

Verwenden Sie diese Funktion nach dem Auftreten eines Fehlers. Beispiel: HRESULT CMyComponent::MyMethod() { HRESULT hr = Object1->Method1(..)); if (FAILED(hr)) AtlTraceErrorRecords (hr); returned }

Sandini Bib 4.2

Fehlerinfo- und Fehlerdatensatz-Objekte

101

Die Implementierung der Funktion AtlTraceErrorRecords hängt von den Optionen ab, die für die Kompilierung festgelegt wurden. Im Modus ohne Debugging bewirkt sie nichts. Visual C++-Entwickler kommen mit HRESULT-Codes sicher hervorragend zurecht, doch z.B. Visual Basic-Entwickler können nichts damit anfangen. Für sie stellt Microsoft einen Automationsmechanismus für Fehlerinformationen zur Verfügung, der es den Programmierern ermöglicht, Fehlerinformationen festzulegen und abzurufen, die jeweils für einen ganzen Thread gelten. Bei diesen Informationen handelt es sich schlicht um ein COM-Objekt, das die Schnittstelle IErrorInfo unterstützt. Die folgenden Schritte zeigen, was geschieht, wenn ein Client eine Methode aufruft, die einen Fehler verursacht: 1. Der Client ruft eine Methode für das Server-Objekt auf. 2. Das Server-Objekt erkennt, dass es die angeforderte Aktion nicht ausführen kann. 3. Das Server-Objekt erzeugt ein COM-Objekt, das die Schnittstelle IErrorInfo unterstützt, und ruft die API-Funktion SetErrorInfo auf. 4. Da das Server-Objekt nicht in der Lage ist, die angeforderte Aktion auszuführen, liefert es einen Fehlercode zurück. 5. Wenn das Client-Objekt den Rückgabecode empfängt, stellt es über HRESULT fest, dass ein Fehler aufgetreten ist. Anschließend ruft es die API-Funktion GetErrorInfo auf und ruft das vom Server-Objekt erzeugte Fehlerobjekt ab.

(1) Methode aufrufen (2) Fehler tritt auf

Fehler (4) HRESULT zurückliefern

(3) Fehlerobjekt erzeugen und SetErrorInfo aufrufen

(5) Aufrufen GetErrorInfo

(6) Fehlerinformation analysieren Client

Abbildung 4.1: Fehlerinfo-Mechanismus

Server

Sandini Bib 102

4

Fehler und Fehlermechanismen

6. Der Client ruft Methoden für das Fehlerobjekt auf um weitere Einzelheiten zum Fehler zu erhalten, und gibt das Objekt anschließend frei. Dieser Vorgang ist in Abbildung 4.1 gezeigt. An dieser Stelle sind einige wichtige Bemerkungen angebracht. Erstens gibt es hierbei nur ein Fehlerobjekt pro Thread. Wenn daher vor dem Aufruf von SetErrorInfo bereits ein Fehlerobjekt vorhanden war, geht dieses verloren. Zweitens wird durch den Aufruf von GetErrorInfo das globale Fehlerinfo-Objekt auf NULL zurückgesetzt. Wenn Sie es aufrufen, müssen Sie dafür sorgen, dass das zurückgelieferte Objekt beibehalten wird, da es keine Möglichkeit gibt, es wieder zurückzuholen. Wenn Sie nämlich GetErrorInfo erneut aufrufen, wird der Wert NULL zurückgeliefert.

4.2.2

IErrorInfo

Mit GetDescription können Sie einen den Fehler beschreibenden Text abrufen. Wenn Sie beispielsweise versuchen, eine nicht vorhandene Tabelle zu öffnen, könnte die folgende Beschreibung ausgegeben werden: "Ungültiger Objektname 'ungültige_tabelle'."

GetSource liefert einen String zurück, der die fehlerauslösende Klasse darstellt. Alternativ hätte eine CLSID zurückgeliefert werden können. GetGUID ermittelt die Schnittstelle, die den Fehler definiert. Ein besserer Name für diese Methode wäre GetIID gewesen. GetHelpFile und GetHelpContext stellen den Namen der Hilfedatei und den Hilfekontext zur Verfügung. IErrorInfo wurde definiert, bevor es HTML gab, und das

zeigt sich hier. Heutzutage wäre ein URL angemessener. Im Folgenden ist eine typische Anwendung von IErrorInfo gezeigt. // es ist ein Fehler aufgetreten { CComPtr ErrorInfo; GetErrorInfo (0, &ErrorInfo); BSTR bstrDescription; BSTR bstrSource; ErrorInfo->GetDescription (&bstrDescription); ErrorInfo->GetSource(&bstrSource); // Freigabe der Strings ist notwendig }

4.2.3

IErrorRecords

Bis jetzt war in dem Fehlermechanismus noch nichts Spezifisches von OLE DB zu erkennen. Bei jedem beliebigen Typ von COM-Anwendung wäre alles genauso abgelaufen. Von jetzt an wird alles Weitere OLE DB-spezifisch sein.

Sandini Bib 4.2

Fehlerinfo- und Fehlerdatensatz-Objekte

103

Der Fehlermechanismus ist vor allem dadurch eingeschränkt, dass pro Thread jeweils nur eine Fehlerinformation möglich ist. Bei einer einfachen Visual Basic-Anwendung mag sich dies nicht nachteilig auswirken, aber für jedes höher entwickelte Projekt bedeutet diese Tatsache eine echte Behinderung. OLE DB behebt dieses Problem mit der zusätzlichen Schnittstelle IErrorRecords. Mit IErrorRecords werden zwei Verbesserungen realisiert. Einerseits enthält diese Schnittstelle mehrere Fehlerdatensätze und nicht nur einen. Jeder Datensatz ist einfach ein COM-Objekt, das die Schnittstelle IErrorInfo unterstützt. Andererseits enthält jeder Fehlerdatensatz sowohl grundlegende als auch benutzerdefinierte Fehlerinformationen. Am häufigsten werden die grundlegenden Informationen angefordert. Bei den benutzerdefinierten Informationen handelt es sich um ein COM-Objekt, das Fehlerinformationen zurückliefert, die nicht zur herkömmlichen Struktur der Fehlerinformationen passen. IErrorRecords weist folgende sechs Methoden auf:

왘 GetRecordCount ruft die Anzahl der enthaltenen Datensätze ab. 왘 GetErrorInfo ruft das Fehlerinfo-Objekt für einen bestimmten Datensatzindex auf (der Datensatzindex fängt bei 0 an). 왘 GetBasicErrorInfo ruft die grundlegenden Fehlerinformationen für einen bestimmen Datensatzindex ab. 왘 GetCustomErrorObject ruft das benutzerdefinierte Fehlerobjekt für einen bestimmten Datensatzindex auf. 왘 AddErrorRecord fügt einen neuen Fehlerdatensatz hinzu. Diese Methode wird vor allem von Providern verwendet. 왘 GetErrorParameters liefert die fehlerverursachenden Parameter zurück. GetBasicErrorInfo()

Grundlegende Fehlerinfos

GetErrorInfo() IErrorInfo QueryInterface()

IErrorInfo

Fehlerinfo für Datensatz

IErrorRecords

GetCustomErrorObject()

BasisFehlerobjekt

::GetErrorInfo()

Benutzerdefiniertes Fehlerinfo-Objekt für Datensatz Benutzerdefinierte Schnittstelle

Abbildung 4.2: Fehlerinfo- und Fehlerdatensatz-Objekte

Sandini Bib 104

4

Fehler und Fehlermechanismen

In Abbildung 4.2 ist der Unterschied zwischen Fehlerinfo-Objekten, FehlerdatensatzObjekten und benutzerdefinierten Fehlerinformationen im Überblick dargestellt.

4.2.4

Die OLE DB-Klassen

Die OLE DB Consumer Templates stellen mit CDBErrorInfo nur eine Klasse für Fehler zur Verfügung. Durch den Aufruf von GetErrorRecords wird jeweils ein Exemplar von CDBErrorInfo initialisiert: HRESULT GetErrorRecords(ULONG* pcRecords)

wobei pcRecords die Anzahl der Datensätze empfängt. Verwirrend dabei ist, dass CDBErrorInfo die Anzahl der Datensätze intern nicht speichert. Der Benutzer der Klasse muss dies wissen. Bei einem besseren Design der Klasse würde die Anzahl der Datensätze als Klassenelement von CDBErrorInfo gespeichert. CDBErrorInfo enthält jeweils einen Zeiger auf die Schnittstelle IErrorInfo und auf die Schnittstelle IErrorRecords: // Implementierung CComPtr CComPtr

m_spErrorInfo; m_spErrorRecords;

GetErrorRecords versucht zunächst den Zeiger auf IErrorInfo abzurufen. Wenn dieser Versuch erfolgreich ist, versucht diese Methode anschließend den Zeiger auf IErrorRecords abzurufen, sonst wird der Inhalt von pcRecords auf 1 gesetzt. Der Zeiger auf die Schnittstelle IErrorInfo wird als einziger Fehlerdatensatz betrachtet. Andernfalls speichert spErrorRecords den Schnittstellenzeiger auf IErrorRecords und pcRecords

empfängt die tatsächliche Datensatznummer. Wenn Sie CDBErrorInfo erfolgreich öffnen, besteht der nächste Schritt darin, alle darin enthaltenen Fehlerinformationen durch folgenden Aufruf abzurufen: HRESULT GetAllErrorInfo(ULONG ulRecordNum, LCID lcid, BSTR* PbstrDescription, BSTR* pbstrSource = NULL, GUID* pguid=NULL, DWORD* pdwHelpContext = NULL,BSTR* pbstrHelpFile = NULL)const

GetAllErrorInfo weist zwei verschiedene Verhaltensmuster auf, je nachdem, ob das Fehlerinfo-Objekt die Schnittstelle IErrorRecords unterstützt oder nicht.

Wenn das Objekt IErrorRecords nicht unterstützt (siehe Abbildung 4.3), verwendet das Objekt m_spErrorInfo als Fehlerdatensatz und berücksichtigt weder ulRecordNum noch lcid. GetAllErrorInfo ruft dann die verschiedenen Methoden für m_spErrorInfo auf. Wenn das Fehlerobjekt IErrorRecords unterstützt, ruft es zunächst das FehlerinfoObjekt von IErrorRecords ab und ruft dann die verschiedenen Methoden dafür auf. Abbildung 4.4 veranschaulicht diesen Fall.

Sandini Bib 4.2

Fehlerinfo- und Fehlerdatensatz-Objekte

105

Keine IErrorRecords-

Schnittstelle

CDBErrorInfo IErrorInfo m_spErrorInfo m_spErrorRecords (NULL)

Methoden aufrufen

Abbildung 4.3: Erster Fall: IErrorRecords wird nicht unterstützt

Methoden aufrufen

CDBErrorInfo IErrorInfo IErrorRecords

m_spErrorInfo m_spErrorRecords IErrorRecords

GetErrorInfo(ulRecordNum,...)

Abbildung 4.4: Zweiter Fall: IErrorRecords wird unterstützt

4.2.5

Die grundlegenden Fehlerinformationen abrufen

Jeder Fehlerdatensatz enthält grundlegende Fehlerinformationen, die von der Struktur ERRORINFO dargestellt werden. typedef struct tagERRORINFO { HRESULT hrError; DWORD dwMinor; CLSID clsid; IID iid; DISPID dispid; } ERRORINFO;

Der Name ERRORINFO ist einigermaßen irreführend, weil die dargestellte Struktur nicht mit der Schnittstelle IErrorInfo zusammenhängt. Betrachten Sie sie vielmehr als zusätzliche Information zum Fehlerinfo-Objekt. ERRORINFO hätte auch als COM-Objekt konstruiert werden können, das eine gedachte Schnittstelle vom Typ IBasicErrorInfo unterstützt. Eine Struktur vereinfacht allerdings die Programmierung. 왘 hrError ist der HRESULT-Code für den Fehler. Es kann hilfreich sein, die verschiedenen HRESULT-Codes für die einzelnen Datensätze zu verfolgen. Der erste Datensatz enthält den HRESULT-Code für den ursprünglichen Fehler. Der letzte Datensatz enthält den an den Client zurückgelieferten HRESULT-Code. Zwischen diesen beiden Datensätzen kann der Provider hrError ändern.

Sandini Bib 106

4

Fehler und Fehlermechanismen

왘 clsid und iid sind die Klassen- bzw. Schnittstellen-ID des Exemplars, das die Fehlerbedingung erzeugt hat. 왘 dwMinor und dispid enthalten optionale Provider-spezifische Codes. In vielen Fällen sind beide gleich 0. Consumer können die grundlegenden Fehlerinformationen durch Aufruf der Methode GetBasicErrorInfo abrufen. HRESULT GetBasicErrorInfo(ULONG ulRecordNum, ERRORINFO* pErrorInfo) const

ulRecordNum stellt den Datensatzindex dar und pErrorInfo empfängt die grundlegen-

den Fehlerinformationen.

4.2.6

Benutzerdefiniertes Fehlerobjekt abrufen

Wenn das Fehlerinfo-Objekt und die grundlegenden Fehlerinformationen nicht ausreichen, können Sie ein benutzerdefiniertes Fehlerobjekt verwenden. Hierbei handelt es sich um ein COM-Objekt, das eine Provider-spezifische Schnittstelle unterstützt. Dieses können die Consumer durch Aufruf der Methode GetCustomErrorObject abrufen. HRESULT GetCustomErrorObject ( ULONG ulRecordNum, REFIID riid, IUnknown ** ppObject);

wobei 왘 ulRecordNum der Datensatzzähler ist 왘 riid der Schnittstellenbezeichner der angeforderten Schnittstelle ist 왘 ppObject den Schnittstellenzeiger empfängt Eine der benutzerdefinierten Schnittstellen ist ISQLErrorInfo, eine Schnittstelle für SQL-Provider. Sie weist die folgende Funktion auf: HRESULT GetSQLInfo ( BSTR * pbstrSQLState, LONG * plNativeError);

pbstrSQLState empfängt einen über den ANSI SQL-Standard definierten String aus fünf Zeichen, der den Fehler beschreibt. plNativeError empfängt Provider-spezifischen Fehlercode.

Ablaufverfolgung für benutzerdefinierte Fehlerinformationen Die Funktion AtlTraceErrorRecords eignet sich hervorragend zur Ablaufverfolgung für Fehler, sie deckt aber die am häufigsten verwendete benutzerdefinierte Fehlerinforma-

Sandini Bib 4.2

Fehlerinfo- und Fehlerdatensatz-Objekte

107

tionsschnittstelle ISQLErrorInfo nicht ab. Aus diesem Grund enthalten die Erweiterungen der OLE DB Customer Templates (OLE DB Extensions) die Funktion AtlTraceErrorRecordsEx, die die Funktionalität von AtlTraceErrorRecords bietet und zusätzlich versucht, die Schnittstelle ISQLErrorInfo abzurufen und ihre Informationen zu verfolgen. Sie wird folgendermaßen implementiert: inline void AtlTraceErrorRecordsEx(HRESULT hrErr = S_OK) { CDBErrorInfo ErrorInfo; ULONG cRecords; HRESULT hr; ULONG i; CComBSTR bstrDesc, bstrHelpFile, bstrSource; GUID guid; DWORD dwHelpContext; WCHAR wszGuid[40]; USES_CONVERSION; // Wenn der Benutzer einen HRESULT-Code übergeben hat, eine // Ablaufverfolgung dafür durchführen if (hrErr != S_OK) ATLTRACE2(atlTraceDBClient, 0, _T("OLE DB-Fehlerdatensatzauszug für hr = 0x%x\ n"), hrErr); LCID lcLocale = GetSystemDefaultLCID(); hr = ErrorInfo.GetErrorRecords(&cRecords); if (FAILED(hr) && ErrorInfo.m_spErrorInfo == NULL) { ATLTRACE2(atlTraceDBClient, 0, _T("Keine OLE DB-Fehlerinformationen vorhanden: hr = 0x%x\ n"), hr); } else { for (i = 0; i < cRecords; i++) { hr = ErrorInfo.GetAllErrorInfo(i, lcLocale, &bstrDesc, &bstrSource, &guid, &dwHelpContext, &bstrHelpFile); if (FAILED(hr)) { ATLTRACE2(atlTraceDBClient, 0, _T("Fehler bei Abruf von OLE DB-Fehlerdatensatzauszug: hr = 0x%x\ n"), hr); return; } StringFromGUID2(guid, wszGuid, sizeof(wszGuid) / sizeof(WCHAR)); LPOLESTR IidSymbol; SymbolStringFromIID(guid, &IidSymbol); ATLTRACE2(atlTraceDBClient, 0,

Sandini Bib 108

4

Fehler und Fehlermechanismen

_T("Zeile #: %4d Quelle: \ "%s\ " Beschreibung: 11\ "%s\ " ➥Hilfedatei: \ "%s\ " Hilfekontext: %4d GUID: %s %s\ n"), i, OLE2T(bstrSource), OLE2T(bstrDesc), OLE2T(bstrHelpFile), dwHelpContext,OLE2T(IidSymbol), OLE2T(wszGuid)); CoTaskMemFree(IidSymbol); bstrSource.Empty(); bstrDesc.Empty(); bstrHelpFile.Empty(); if (ErrorInfo.m_spErrorRecords) { ERRORINFO BasicErrorInfo; hr = ErrorInfo.GetBasicErrorInfo(i,&BasicErrorInfo); if (FAILED(hr)) ATLTRACE2(atlTraceDBClient, 0, _T("Keine grundlegenden Fehlerinformationen\ n")); else TraceBasicErrorInfo(&BasicErrorInfo); CComPtr spSQLErrorInfo; hr = ErrorInfo.GetCustomErrorObject(i, IID_ISQLErrorInfo, (IUnknown **)&spSQLErrorInfo); if (FAILED(hr) || !spSQLErrorInfo) ATLTRACE2(atlTraceDBClient, 0, _T("Keine SQL-Fehlerinformationen\n")); else { CComBSTR strSQLState; LONG NativeError; spSQLErrorInfo->GetSQLInfo(&strSQLState, &NativeError); ATLTRACE2(atlTraceDBClient, 0,_T("SQL-Fehlerinformationen: ➥SQLState:%n , NativeError: %i\ n"), W2T(strSQLState), NativeError); } } } ATLTRACE2(atlTraceDBClient, 0, _T("Ende des OLE DB-Fehlerdatensatzauszugs\ n")); } }

Das folgende Beispiel zeigt die Ausgabe von AtlTraceErrorRecordsEx. ATL: Row #: 0 Source: "Microsoft OLE DB Provider for SQL Server" Description: "Ungültiger Objektname 'invalid_table'." Hilfedatei: "(null)" Hilfekontext: 0 GUID: IID_ ICommand { 0C733A63-2A1C-11CE-ADE5-00AA0044773D} ATL: Basic error info: hrError: 0x80040e37, dwMinor: 208, clsid: CLSID_SQLOLEDB { 0C7FF16C-38E3-11D0-97AB-00C04FC2AD98} , iid: IID_ICommand { 0C733A63-2A1C-11CEADE5-00AA0044773D} , dispid: 0 ATL: SQL error information: SQLState: , NativeError: 208

Sandini Bib 4.2

Fehlerinfo- und Fehlerdatensatz-Objekte

4.2.7

109

Unterstützung für ErrorInfo

Eine der Schwierigkeiten mit dem COM-Fehlermechanismus besteht darin, dass jedes beliebige Objekt einen Fehler erzeugen kann, aber nicht alle Objekte tatsächlich Fehler erzeugen. Wenn Sie beispielsweise eine Methode aufrufen und ein HRESULT-Code für einen Fehler zurückgeliefert wird, sollten Sie dann versuchen, die zugehörigen Fehlerinformationen abzurufen? Wenn Sie diese Informationen erhalten, sind diese dann die Fehlerinformationen für den soeben aufgetretenen Fehler, oder beziehen sie sich auf einen früheren Fehler? Die Schnittstelle ISupportErrorInfo liefert eine eindeutige Antwort auf beide Fragen. Die OLE DB-Objekte, die Fehler erzeugen könnten, sollten die Schnittstelle ISupportErrorInfo unterstützen. Diese Schnittstelle weist nur eine Methode auf: HRESULT InterfaceSupportsErrorInfo (REFIID riid);

wobei riid den Bezeichner für die Schnittstelle darstellt, die möglicherweise Fehler verursacht. Wenn InterfaceSupportsErrorInfo den Code S_OK zurückliefert, erzeugen alle Schnittstellenmethoden Fehlerinformationen, sofern der betreffende Methodenaufruf scheitert. Wenn im Gegensatz dazu InterfaceSupportsErrorInfo den Code S_FALSE zurückliefert, wird bei einem Methodenaufruf für diese Schnittstelle nie ein neues Fehlerinformationsobjekt erzeugt. Vergegenwärtigen Sie sich, dass S_FALSE hier als Erfolgscode anzusehen ist (SUCCEEDED(S_FALSE) ist wahr). Er bedeutet aber nicht, dass das Objekt »erfolgreich« Fehler erzeugt. Wenn beispielsweise ein Objekt zwei Schnittstellen IA und IB unterstützt und ein Client eine Methode für IA aufruft, kann es sein, dass das Objekt einen Fehler erzeugt. Wenn der Client jedoch eine Methode für IB aufruft, wird nie ein Fehlerobjekt erzeugt. Daher müsste SupportErrorInfo(IID_IA) den Code S_OK zurückliefern, während SupportErrorInfo(IID_IB) den Code S_FALSE zurückliefern müsste. Diese Informationen sind statisch, was bedeutet, dass SupportErrorInfo für die Lebensdauer des Objekts das gleiche Verhalten zeigt. Sie können diese Informationen durch Aufruf der Funktion SupportErrorInfo aus den Erweiterungen der OLE DB Consumer Templates abrufen: template HRESULT SupportErrorInfo(Q* p)

Sie können hierzu Folgendes programmieren: ICommand* pCommand; //…Befehl abrufen HRESULT hr = SupportErrorInfo(pCommand);

Sandini Bib 110

4

4.2.8

Fehler und Fehlermechanismen

Das Puzzle zusammenfügen

Wenn ein Client eine Methode für ein bestimmtes Objekt aufruft, muss er den Rückgabecode überprüfen: HRESULT hr = SomeObject->SomeMethod(); if (FAILED(hr)) {

Wenn hr ein Fehlercode ist, besteht der erste Schritt darin zu überprüfen, ob das Objekt ein Fehlerobjekt erzeugt hat: if (SupportErrorInfo(SomeObject) == S_OK) {

Der Client kann dann über die Klasse CDBErrorInfo weitere Informationen erhalten. Zunächst muss er die Anzahl der Fehlerdatensätze abrufen: CDBErrorInfo ErrorInfo; ULONG cRecords; ErrorInfo. GetErrorRecords(&cRecords);

Wenn Fehlerdatensätze vorhanden sind, kann der Client die Liste iterativ durchgehen um die Informationen, die in den Fehlerobjekten für die einzelnen Datensätze enthalten sind, nacheinander abzurufen: for (i = 0; i < cRecords; i++) { hr = ErrorInfo.GetAllErrorInfo(i, lcLocale, &bstrDesc, &bstrSource, &guid, &dwHelpContext, &bstrHelpFile);

Die grundlegenden Fehlerinformationen: ERRORINFO BasicErrorInfo; hr = ErrorInfo.GetBasicErrorInfo(i, &BasicErrorInfo);

Oder ein benutzerdefiniertes Fehlerobjekt: CComPtr spSQLErrorInfo; hr = ErrorInfo.GetCustomErrorObject(i, IID_ISQLErrorInfo, (IUnknown **)&spSQLErrorInfo); } } }

4.3 Zusammenfassung In diesem Kapitel wurden die verschiedenen Möglichkeiten zum Abrufen von Fehlerinformationen vorgestellt. Während HRESULT-Codes nur wenige Hinweise auf den auf-

Sandini Bib 4.3

Zusammenfassung

111

getretenen Fehler geben können, bietet OLE DB hier mehr. Der Aufruf von AtlTraceErrorRecords stellt die einfachste Möglichkeit des Zugriffs auf diese Informationen dar. Im nächsten Kapitel werden die OLE DB-Eigenschaften vorgestellt. Diese bieten den Ausgangspunkt für eine Beschäftigung mit den Kernklassen.

Sandini Bib

Sandini Bib

5 OLE DB-Eigenschaften Die meisten OLE DB-Objekte unterstützen OLE DB-Eigenschaften. Diese unterscheiden sich von anderen COM-Eigenschaften, so dass es sich empfiehlt, vor den Objekten selbst zunächst einmal die Eigenschaften unter die Lupe zu nehmen. Der Mechanismus zur Verwendung der Eigenschaften ist für alle OLE DB-Objekte gleich. In diesem Kapitel werden die in OLE DB verfügbaren Eigenschaften vor allem unter den folgenden Gesichtspunkten behandelt: 왘 Was ist das Besondere an OLE DB-Eigenschaften? 왘 Die Architektur von Eigenschaften. 왘 Wie können Eigenschaften gesetzt, abgerufen und aufgezählt werden?

5.1 Der Eigenschaftsbegriff Eine Eigenschaft ist als Merkmal definiert, das zu einem Objekt gehört und diesem eigen ist. Diese Definition ist sehr weit gefasst und ermöglicht eine flexible Implementierung. In C++ kann eine Eigenschaft beispielsweise entweder als Elementvariable oder als Paar von Elementfunktionen implementiert werden. Das folgende Codebeispiel zeigt eine Klasse mit zwei Eigenschaften. class CMyClass { type Property1; void SetProperty2(type value); type GetProperty2() const; } ;

Bei COM unterstützen Schnittstellen nur Funktionen und keine Elementvariablen. Im folgenden Beispiel unterstützt die COM-Schnittstelle zwei Eigenschaften. Die Eigenschaft Property1 ermöglicht Lese-/Schreibzugriff, während Property2 nur Lesezugriff gestattet: Interface IMyInterface : IDispatch { [propget,id(1), HRESULT Property1([out, retval] short *pVal);

Sandini Bib 114

5

OLE DB-Eigenschaften

[propput,id(1), HRESULT Property1([in] short newVal); [propget,id(2), HRESULT Property2([out, retval] short *pVal); } ;

Dies ist der herkömmliche COM-Ansatz, der vor allem beim Arbeiten mit Visual BasicTools Verwendung findet. Er ist leicht verständlich und erleichtert die Programmierung. Ein Nachteil besteht jedoch darin, dass hierbei nicht mehrere Eigenschaften in einem Funktionsaufruf gesetzt werden können. Das Setzen mehrerer Eigenschaften in einem Funktionsaufruf bietet einige Vorteile. Einerseits wird damit die Anzahl von COM-Funktionsaufrufen verringert, was sich zwar bei einer Anwendung, die nur auf einem Rechner läuft, kaum auswirkt, aber für ein Szenario, in dem sich der Server auf einer anderen Maschine befindet, einen wesentlichen Unterschied ausmachen kann. Andererseits können einfache Schnittstellen verwendet werden. Wenn zum Abrufen und Setzen jeder einzelnen Eigenschaft eine eigene Methode benötigt würde, wäre die Anzahl der Methoden doppelt so groß wie die der Eigenschaften. Da viele Eigenschaften vorhanden sind, könnten die vielen Methoden kaum noch verwaltet werden. Zudem rufen einige Objekte bei ihrer Erstellung jeweils eine ganze Reihe von Eigenschaften auf, so dass das Konzept der Eigenschaften-Sets notwendig wird.

5.1.1

Eigenschaften und Eigenschaften-Sets

Ein Eigenschaften-Set enthält eine Reihe von Eigenschaften. Innerhalb des Eigenschaften-Sets ist jede Eigenschaft durch einen eindeutigen Eigenschaftsbezeichner (Eigenschaften-ID) gekennzeichnet. Der Datentyp für den Bezeichner, DBPROPID, ist folgendermaßen definiert: typedef DWORD DBPROPID;

Der Datentyp einer Eigenschaft kann unterschiedlich sein. Beispielsweise ist ein Benutzername eine Eigenschaft vom Typ string, während eine Zeitsperre (Timeout) zum Typ integer gehört. Daher ist jeder Wert einer Eigenschaft in einer Struktur vom Typ VARIANT enthalten.

5.1.2

GUIDs von Eigenschaften-Sets

Eines der mit OLE DB verfolgten Ziele besteht darin, einem Datenbank-Provider die Definition eigener Zusatzfunktionen, einschließlich Eigenschaften, zu ermöglichen. Die Entwickler bei Microsoft geben nicht vor, alle möglichen Eigenschaften von OLE DB-Objekten zu kennen, so dass die einzelnen Provider benutzerdefinierte Eigenschaften deklarieren und folglich auch eigene Bezeichner erstellen müssen. Hierbei zeigt sich ein Problem: Wie kann sichergestellt werden, dass alle Eigenschaften-IDs eindeutig sind? Wenn alle Eigenschaften-IDs von ein und demselben Anbieter definiert würden, könnte man sich eine zentrale Datenbank mit allen Eigenschaften-IDs vorstellen.

Sandini Bib 5.1

Der Eigenschaftsbegriff

115

Für die einzelnen Datenbank-Provider gibt es jedoch keine verlässliche Möglichkeit, über die Arbeitsergebnisse der anderen Provider immer auf dem Laufenden zu sein. GUID

Beschreibung

DBPROPSET_COLUMN

Zeilenmengen-Eigenschaften für Spalten

DBPROPSET_DATASOURCE

Datenquelleneigenschaften

DBPROPSET_DATASOURCEINFO

Eigenschaften für Datenquelleninformationen

DBPROPSET_DBINIT

Eigenschaften für die Datenquelleninitialisierung

DBPROPSET_INDEX

Indexeigenschaften

DBPROPSET_ROWSET

Zeilenmengen-Eigenschaften

DBPROPSET_SESSION

Sitzungseigenschaften

DBPROPSET_TABLE

Tabelleneigenschaften

DBPROPSET_TRUSTEE

Trustee-Eigenschaften

DBPROPSET_VIEW

Eigenschaften für Sichten

Tabelle 5.1: In OLE DB definierte GUIDs für Eigenschaften-Sets

Wie bei ähnlichen Fällen bieten GUIDs eine Lösung. Für jedes Eigenschaften-Set gibt es eine eindeutige GUID. Die Eigenschaften-IDs sind nur innerhalb des Eigenschaften-Sets eindeutig. Mit anderen Worten erfüllen die GUIDs dieselbe Funktion wie die Namensbereiche in C++. In OLE DB sind eine Reihe von GUIDs für Eigenschaften-Sets definiert. Die Eigenschaften-Sets sind jeweils für ein bestimmtes OLE DB-Objekt in einer bestimmten Operation definiert und sie enthalten nur in OLE DB definierte Eigenschaften. Provider-spezifische Eigenschaften sind immer Bestandteil eines separaten Eigenschaften-Sets. Tabelle 5.1 zeigt die in OLE DB definierten Eigenschaften-Sets im Überblick.

5.1.3

Eigenschaftengruppen

Jede Eigenschaft gilt für ein bestimmtes Objekt oder Konzept. Beispielsweise kann sich eine Eigenschaft auf das Zeilenmengen-Objekt beziehen, während eine andere Eigenschaft zum Initialisieren einer Datenquelle verwendet wird. Das Eigenschaften-Set beinhaltet zwar nützliche Implementierungsinformationen, dient aber nicht zur Ermittlung, auf welches Objekt oder Konzept sich die betreffende Eigenschaft bezieht. Die GUID eines Eigenschaften-Sets gibt nämlich nicht eindeutig an, wofür die Eigenschaft gilt, weil für eine bestimmte Operation mehrere Eigenschaften-Sets verwendet werden können. An dieser Stelle kommen Eigenschaftengruppen ins Spiel. Jede Eigenschaft gehört auch zu genau einer Eigenschaftengruppe. Auch wenn der Name etwas Ähnliches wie ein

Sandini Bib 116

5

OLE DB-Eigenschaften

Eigenschaften-Set suggeriert, haben die beiden Konzepte nichts miteinander zu tun. Eine Eigenschaftengruppe stellt einfach das OLE DB-Objekt oder -Konzept dar, auf das sich die Eigenschaft bezieht. Wenn beispielsweise eine Eigenschaft zur Eigenschaftengruppe DBPROPFLAGS_DATASOURCE gehört, bezieht sie sich auf das Datenquellenobjekt, ob sie aber zu dem Eigenschaften-Set gehört, das durch DBPROPSET_DATASOURCE gekennzeichnet ist, oder zu einem spezifischen Eigenschaften-Set eines anderen Providers, ist hiermit nicht angegeben. Gruppe

Beschreibung

DBPROPFLAGS_COLUMN

Bezieht sich auf eine Spalte

DBPROPFLAGS_DATASOURCE

Bezieht sich auf ein Datenquellenobjekt

DBPROPFLAGS_DATASOURCECREATE

Bezieht sich auf die Erzeugung einer Datenquelle

DBPROPFLAGS_DATASOURCEINFO

Liefert Informationen über Datenquellen

DBPROPFLAGS_DBINIT

Dient zur Initialisierung einer Datenquelle

DBPROPFLAGS_INDEX

Bezieht sich auf einen Index

DBPROPFLAGS_ROWSET

Bezieht sich auf eine Zeilenmenge

DBPROPFLAGS_SESSION

Bezieht sich auf eine Sitzung

DBPROPFLAGS_TABLE

Bezieht sich auf eine Tabelle

DBPROPFLAGS_VIEW

Bezieht sich auf eine Sicht

Tabelle 5.2: Eigenschaftengruppen

Die von OLE DB definierten Eigenschaftengruppe sind in Tabelle 5.2 aufgeführt. Beachten Sie, dass eine Eigenschaftengruppe nicht zwingend einem OLE DB-Objekt entspricht. Beispielsweise gibt es eine Eigenschaftengruppe für Spalten, aber in OLE DB ist kein Spaltenobjekt definiert. Außerdem entspricht die Eigenschaftengruppe DBPROPFLAGS_DBINIT nur der Initialisierung einer Datenquelle. Dies bedeutet, dass diese die Eigenschaften dieser Gruppe zur Initialisierung einer Datenquelle verwendet werden, aber nach Abschluss dieses Vorgangs nicht mehr gültig sind.

5.1.4

Eigenschaftsbezogene Operationen

Es gibt die vier folgenden Operationen für Eigenschaften: 왘 Eigenschaften setzen 왘 Eigenschaften abrufen 왘 Eigenschaften aufzählen 왘 Eigenschafteninformationen abrufen Diese Operationen werden in den folgenden Abschnitten näher erläutert.

Sandini Bib 5.2

Eigenschaften setzen

117

5.2 Eigenschaften setzen Ein Eigenschaften-Set enthält Eigenschaften mit jeweils der gleichen GUID. Beim Setzen von Eigenschaften kann es vorkommen, dass Eigenschaften aus verschiedenen Eigenschaften-Sets benötigt werden, d.h. Sie brauchen ein Array aus Eigenschaften-Sets. OLE DB-Objekte, die das Setzen von Eigenschaften unterstützen, weisen eine Methode wie die Folgende auf: HRESULT Method(DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1)

wobei pPropSet ein Array aus Eigenschaften-Sets und nPropertySets die Anzahl der darin enthaltenen Eigenschaften-Sets bezeichnet.

5.2.1

DBPROPSET

Die Struktur DBRPROPSET stellt genau ein Eigenschaften-Set dar. Sie ist folgendermaßen definiert: typedef struct tagDBPROPSET { DBPROP * rgProperties; ULONG cProperties; GUID guidPropertySet; } DBPROPSET;

wobei 왘 rgProperties ein Zeiger auf ein Array aus DBPROP-Strukturen ist 왘 cProperties die Anzahl von DBPROP-Strukturen in rgProperties ist 왘 guidPropertySet die allen Eigenschaften gemeinsame GUID ist Abbildung 5.1 auf der nächsten Seite zeigt ein Array aus Eigenschaften-Sets aus DBPROP-Strukturen.

5.2.2

DBPROP

Jede DBPROP-Struktur stellt eine Eigenschaft in einem Eigenschaften-Set dar: typedef struct tagDBPROP { DBPROPID dwPropertyID; DBPROPOPTIONS dwOptions; DBPROPSTATUS dwStatus; DBID colid; VARIANT vValue; } DBPROP;

Sandini Bib 118

5

DBPROPSET 2

dwPropertyID

1

dwOptions

GUID1

dwStatus

Zeiger

colid

1

vValue

DBPROP

DBPROPSET-Array

DBPROPSET 1

Zeiger

OLE DB-Eigenschaften

GUID2

Zeiger DBPROPSET 3

dwPropertyID

2 dwStatus colid

DBPROP

dwOptions GUID3

vValue dwPropertyID1 DBPROP 1

... dwPropertyID1

...

DBPROP 2

Abbildung 5.1: Array aus DBPROPSET-Strukturen

Die wichtigsten Felder der Struktur sind die Eigenschaften-ID (dwPropertyID) und der Wert (vValue). Da die Variante dem Typ VARIANT angehört, kann es notwendig sein, sie nach der Verwendung freizugeben. Dafür ist immer der Consumer zuständig. Die anderen Felder der Struktur sind weniger wichtig, wir werden jeweils im Bedarfsfall auf sie zurückkommen. Die Methoden zum Setzen von Eigenschaften weisen in der Regel Standardargumente auf. Wenn Sie kein Argument angeben, wird kein Eigenschaften-Set übergeben. Wenn Sie nur das Argument pPropSet angeben, geht die Methode davon aus, dass das Array aus Eigenschaften-Sets nur ein Element enthält.

5.2.3

Verwendung unspezifischer Strukturen

Die übliche Methode zum Festlegen von OLE DB-Eigenschaften besteht aus den folgenden sechs Schritten: 1. Die Eigenschaften-IDs und die GUIDs der Eigenschaften-Sets ermitteln, zu denen die zu setzenden Eigenschaften gehören. 2. Die Eigenschaften mit gleicher GUID innerhalb von Eigenschaften-Sets neu gruppieren. Zu diesem Zeitpunkt wissen Sie, wie viele Eigenschaften-Sets Sie brauchen und wie viele Eigenschaften die einzelnen Eigenschaften-Sets enthalten.

Sandini Bib 5.2

Eigenschaften setzen

119

3. Ein Array aus Eigenschaften-Sets (PROPSET) zuweisen. Setzen Sie für jedes Eigenschaften-Set eine GUID. 4. Für jedes Eigenschaften-Set ein Array aus DBPROP-Strukturen zuweisen und die entsprechenden Felder definieren. 5. Das Array aus Eigenschaften-Sets an eine der Methoden SetProperty oder Open des Objekts übergeben. 6. Alle Varianten und alle Strukturen freigeben. Selbst wenn Sie nur eine Eigenschaft setzen, müssen Sie ein Eigenschaften-Set-Array zuweisen. Das folgende Beispiel zeigt, wie direkt über die OLE DB-Strukturen zwei Eigenschaften aus einem Eigenschaften-Set gesetzt werden. DBPROPSET PropSet; DBPROP Props [2]; PropSet.guidPropertySet = DBPROPSET_DBINIT; PropSet.cProperties = 2; PropSet.rgProperties = Props; Props[0].colid = DB_NULLID; Props[0].dwOptions = DBPROPOPTIONS_REQUIRED; Props[0].dwPropertyID = DBPROP_AUTH_INTEGRATED; Props[0].dwStatus = DBPROPSTATUS_OK; Props[0].vValue.vt = VT_BSTR; Props[0].vValue.bstrVal = SysAllocString(OLESTR("SSPI")); Props[1].colid = DB_NULLID; Props[1].dwOptions = DBPROPOPTIONS_REQUIRED; Props[1].dwPropertyID = DBPROP_AUTH_INTEGRATED; Props[1].dwStatus = DBPROPSTATUS_OK; Props[1].vValue.vt = VT_BOOL; Props[1].vValue.boolVal = VARIANT_FALSE; CDataSource DataSource; HRESULT hr = DataSource.Open(_T("SQLOLEDB.1"), &PropSet); VariantClear(&Props[0].vValue); VariantClear(&Props[1].vValue);

Wie bereits erwähnt, ist der Client bzw. Consumer dafür zuständig, durch den Aufruf von VariantClear alle Werte freizugeben. In diesem Beispiel ist die Freigabe der Strukturen nicht erforderlich, weil sie bei der Zuweisung auf den Stack gelegt wurden.

5.2.4

CDBPropSet

Die Verwaltung der Strukturen DBPROPSET und DBPROP kann schwierig und fehlerträchtig sein. Aus diesem Grund stellen die OLE DB Consumer Templates Klassen zur Ver-

Sandini Bib 120

5

OLE DB-Eigenschaften

fügung, die den Umgang mit Eigenschaften-Sets erleichtern. Die Klasse CDBPropSet ist eine Kapselung von DBPROPSET. Sie ist folgendermaßen definiert: class CDBPropSet : public tagDBPROPSET

CDBPropSet fügt keine Klassenelemente, sondern nur Methoden hinzu. Dies bedeutet, dass Sie CDBPropSet überall anstelle von DBPROPSET verwenden können.

Die Klasse CDBPropSet bietet mehrere Verbesserungen. Erstens passt sie das Array aus Eigenschaften dynamisch an, so dass Sie dies nicht statisch durchführen müssen, zweitens setzt sie automatisch den Variantentyp des Wertes, drittens, und das ist am wichtigsten, gibt sie alle Varianten automatisch frei. Beim Arbeiten mit einem Exemplar von CDBPropSet müssen Sie zunächst die GUID des Eigenschaften-Sets setzen. Sie können hierzu entweder den Konstruktor oder die Methode SetGUID verwenden. Interessant ist die Methode AddProperty, die eine Eigenschaft aus zwei Argumenten hinzufügt: bool AddProperty( DWORD dwPropertyID, type Value );

wobei dwPropertyID die Eigenschaften-ID der hinzuzufügenden Eigenschaft ist und Value für den Wert der Eigenschaft steht. AddProperty akzeptiert für Value zahlreiche Datentypen. Bei Bedarf wird dieser Wert in einen VARIANT-kompatiblen Wert konvertiert und der Variantentyp wird dann automatisch gesetzt. In Tabelle 5.3 sind die Typen aufgeführt, die von der Methode AddProperty und ihren Varianten akzeptiert werden. AddProperty liefert bei erfolgreicher Ausführung den Wert True, andernfalls den Wert False zurück. Beispielsweise kann die Ausführung scheitern, wenn nicht genügend Speicher vorhanden ist, oder wenn Sie einen NULL-String übergeben haben. CDBPropSet erleichtert die Aufgabe des Programmierers erheblich. Eine Neuprogrammierung des vorherigen Beispiels mit CDBPropSet sieht folgendermaßen aus: CDBPropSet MyDBPropset(DBPROPSET_DBINIT); MyDBPropset.AddProperty(DBPROP_INIT_DATASOURCE, "NAME"); MyDBPropset.AddProperty(DBPROP_INIT_MODE, (long)0);

C++-Typ

Variantentyp

VARIANT

Derselbe Typ wie in der Variante

LPCTSTR

VT_BSTR

LPCWSTR

VT_BSTR

bool

VT_BOOL

Tabelle 5.3: C++-Typen und Variantentypen

Sandini Bib 5.2

Eigenschaften setzen

C++-Typ

121

Variantentyp

BYTE

VT_UI1

short

VT_I2

long

VT_I4

float

VT_R4

double

VT_R8

CY (Währung)

VT_CY

Tabelle 5.3: C++-Typen und Variantentypen (Fortsetzung)

Wenn die Eigenschaften zu mehreren Eigenschaften-Sets gehören, müssen Sie ein Array aus CDBPropSet-Strukturen verwenden und die GUIDs manuell setzen. Das folgende Codebeispiel zeigt die Verwendung eines Arrays aus zwei Eigenschaften-Sets, wobei zwei Eigenschaften in einem Set enthalten sind und eine Eigenschaft im anderen. CDBPropSet PropSets [2]; PropSets[0].SetGUID(DBPROPSET_DBINIT); PropSets[0].AddProperty(DBPROP_AUTH_INTEGRATED, OLESTR("SSPI")); PropSets[0].AddProperty(DBPROP_AUTH_PERSIST_SENSITIVE_AUTHINFO, false); PropSets[1].SetGUID(DBPROPSET_DBINIT); PropSets[1].AddProperty(SSPROP_INIT_AUTOTRANSLATE, true); CDataSource DataSource; HRESULT hr = DataSource.Open(_T(“SQLOLEDB.1”), PropSets, 2);

Wenn sich die Eigenschaften in ein und demselben Eigenschaften-Set befinden, brauchen Sie kein Array aus CDBPropSet-Strukturen zu definieren, sondern können einfach die Adresse von CDBPropSet übergeben. In der Regel ist die Vorgabe für die Anzahl von Eigenschaften-Sets gleich eins. Daher brauchen Sie bei nur einem Eigenschaften-Set die Anzahl nicht zu übergeben. Im folgenden Beispiel ist lediglich ein Eigenschaften-Set mit zwei Eigenschaften vorhanden. CDBPropSet PropSet (DBPROPSET_DBINIT); PropSet.AddProperty(DBPROP_AUTH_INTEGRATED, OLESTR(“SSPI”)); PropSet.AddProperty(DBPROP_AUTH_PERSIST_SENSITIVE_AUTHINFO, false); CDataSource DataSource; HRESULT hr = DataSource.Open(_T("SQLOLEDB.1"), &PropSet);

CDBPropSet weist zwei Nachteile auf. Erstens wird jedes Mal, wenn Sie eine Eigenschaft hinzufügen, das gesamte Eigenschaften-Array neu zugewiesen. Jede AddPropertyMethode ruft Add auf, wobei es sich um eine Implementierungsmethode handelt. Im

Folgenden werden wir uns diese Methode einmal ansehen:

Sandini Bib 122

5

OLE DB-Eigenschaften

bool Add() { rgProperties = (DBPROP*)CoTaskMemRealloc(rgProperties, (cProperties + 1) * sizeof(DBPROP)); if (rgProperties != NULL) { rgProperties[cProperties].dwOptions = DBPROPOPTIONS_REQUIRED; rgProperties[cProperties].colid = DB_NULLID; rgProperties[cProperties].vValue.vt = VT_EMPTY; return true; } else return false; }

Der zweite Nachteil besteht darin, dass bei CDBPropSet die Eigenschaftsoption immer auf »required« (erforderlich) gesetzt wird und die Spalten-ID immer DB_NULLID lautet. Wenn Sie optionale Eigenschaften und Spalteneigenschaften verwenden wollen, brauchen Sie eine weitere Klasse. Dieses Thema wird weiter unten noch behandelt.

5.2.5

Eine Eigenschaft setzen

Wenn Sie nur eine einzige Eigenschaft setzen wollen, kann das Hantieren mit einem Eigenschaften-Set ziemlich mühsam werden. Eine Alternative bietet der Aufruf der Methode SetProperty: template HRESULT SetProperty(const GUID& guid, DBPROPID propid, T Value)

wobei guid und propid die Eigenschaft kennzeichnen und Value den Wert der Eigenschaft enthält. SetProperty ist folgendermaßen definiert: template HRESULT SetProperty(const GUID& guid, DBPROPID propid, T Value) { CDBPropSet PropSet (guid); PropSet.AddProperty(propid, Value); return SetProperties(&PropSet); }

Die für die Werte möglichen Datentypen sind die Gleichen wie für CDBPropSet::AddProperty. Im folgenden Beispiel wird die Eigenschaft CURRENTCATALOG im Eigenschaften-Set für die Datenquelle auf CatalogName gesetzt.

Sandini Bib 5.2

Eigenschaften setzen

123

hr = DataSource.SetProperty(DBPROPSET_DATASOURCE, DBPROP_CURRENTCATALOG, "CatalogName");

Das Arbeiten mit SetProperty ist einfach. Allerdings bietet diese Methode keine Möglichkeit zur Fehlerdiagnose.

5.2.6

Erforderliche und optionale Eigenschaften

Sie können angeben, ob eine zu setzende Eigenschaft optional oder erforderlich ist. Die Eigenschaftsoption weist den Typ DBPROPOPTIONS auf und kann einen der Werte DBPROPOPTIONS_REQUIRED (erforderlich) und DBPROPOPTIONS_OPTIONAL (optional) annehmen. Die Eigenschaftsoption ist immer dann wichtig, wenn eine Methode nicht in der Lage ist, die Eigenschaft zu setzen. Wenn die Eigenschaft erforderlich ist, scheitert die Methode und liefert einen Fehler wie z.B. DB_E_ERRORSOCCURRED zurück. Sie setzt dann keine weitere Eigenschaft. Wenn die Eigenschaft dagegen optional ist, liefert die Methode einen Erfolgscode wie z.B. DB_S_ERRORSOCCURRED zurück und ignoriert die Eigenschaft. Wenn das Eigenschaften-Set noch weitere Eigenschaften enthält, versucht die Methode dann, diese zu setzen. Die Standardeinstellung für CDBPropSet ist, dass alle Eigenschaften als erforderlich definiert werden. Es gibt jedoch zwei Möglichkeiten, Eigenschaften als optional zu definieren. Die eine Möglichkeit besteht darin, mit den unspezifischen OLE DB-Strukturen zu arbeiten. Sie können dwOptions auf DBPROPOPTIONS_OPTIONAL statt auf DBPROPOPTIONS_REQUIRED setzen. Ansonsten ist der Code identisch. Als zweite Möglichkeit steht Ihnen die Klasse CDBPropSetEx aus den OLE DB-Erweiterungen (OLE DB Extensions) zur Verfügung. CDBPropSetEx erbt von CDBPropSet. Die beiden Klassen sind völlig gleich, mit Ausnahme ihrer AddProperty-Methoden. Die Methode AddProperty von CDBPropSetEx ähnelt zwar der von CDBPropSet, aber sie akzeptiert zwei zusätzliche Argumente, nämlich die Eigenschaftsoption und die Eigenschaftsspalte (Eigenschaftsspalten werden im nächsten Abschnitt behandelt). bool AddProperty(DWORD dwPropertyID, type Value, DBPROPOPTIONS dwOptions = DBPROPOPTIONS_REQUIRED, DBID colid= DB_NULLID)

Da die Standardparameter der Implementierung von CDBPropSet entsprechen, können Sie CDBPropSet ohne jegliche Kompatibilitätsprobleme durch CDBPropSetEx ersetzen. Das folgende Beispiel zeigt, wie eine optionale Eigenschaft gesetzt wird: CDBPropSetEx PropSet (DBPROPSET_DBINIT); PropSet.AddProperty(DBPROP_AUTH_PERSIST_SENSITIVE_AUTHINFO, False, DBPROPOPTIONS_OPTIONAL);

Sandini Bib 124

5.2.7

5

OLE DB-Eigenschaften

Spaltenbezeichner

In OLE DB sind Objekte mit Spalten definiert, wie z.B. Tabellen oder Indizes, aber keine Spaltenobjekte. Wenn Sie eine Eigenschaft setzen wollen, die sich auf nur eine Spalte bezieht, müssen Sie die betreffende Spalte angeben. Dies führt dazu, dass alle OLE DB-Eigenschaften einen Spaltenbezeichner des Typs DBID enthalten (in der Struktur DBPROP ist dies colid). In den meisten Fällen hat der Spaltenbezeichner keine Bedeutung und ist auf DB_NULLID zu setzen. DBPropSet setzt den Spaltenbezeichner automatisch auf DB_NULLID. Die Provider können verschiedene Möglichkeiten zur Kennzeichnung einer Spalte nutzen: Angabe des Namens, Verwendung einer GUID, einer Ganzzahl oder einer Kombination aus beiden. Die Struktur DBID enthält alle diese Bezeichner sowie einen Schalter zur Angabe, welcher Bezeichner verwendet werden soll. DBID ist folgendermaßen definiert: typedef struct tagDBID { union { GUID guid; GUID *pguid; /* Empty union arm */ } uGuid; DBKIND eKind; union { LPOLESTR pwszName; ULONG ulPropid; /* Empty union arm */ } uName; } DBID;

eKind ist der Schalter.

Das folgende Beispiel zeigt, wie DBID auf einen bestimmten Namen gesetzt werden kann: dbid.eKind = DBKIND_NAME; dbid.uName.pwszName = T2W(aName);

Es gibt zwei Möglichkeiten, den Spaltenbezeichner einer Eigenschaft zu setzen: Verwendung der unspezifischen OLE DB-Strukturen und der Klasse CDBPropSetEx: PropSet.AddProperty(DBPROP_COL_NULLABLE, False, DBPROPOPTIONS_REQUIRED, dbid);

Sandini Bib 5.2

Eigenschaften setzen

5.2.8

125

Benutzerdefinierte Eigenschaften-Klassen

Es ist nicht immer leicht, zu wissen und sich daran zu erinnern, welche EigenschaftenID welcher Eigenschaften-GUID entspricht. Die OLE DB-Erweiterungen bieten pro Eigenschaften-Set von OLE DB eine benutzerdefinierte Klasse. Diese Klassen stellen Methoden in der folgenden Form zur Verfügung: bool AddXXX(type Value) { return AddProperty(DBPROP_XXX, Value); }

wobei XXX für eine Eigenschaft und type für den Typ steht, dem die Eigenschaft XXX angehört. Beispielsweise kann der Code im Abschnitt CDBPropSet durch folgenden Code ersetzt werden: CInitializationPropSet PropSet; PropSet.AddIntegrated(_T("SSPI")); PropSet.AddPersistSensitiveAuthInfo(false); CDataSource DataSource; HRESULT hr = DataSource.Open (_T("SQLOLEDB.1"), &PropSet);

Tabelle 5.4 enthält eine Übersicht über die GUIDs der von OLE DB definierten Eigenschaften-Sets und die entsprechenden OLE DB-Erweiterungsklassen. GUID

OLE DB-Erweiterungsklasse

DBPROPSET_COLUMN

CColumnPropSet

DBPROPSET_DATASOURCE

CDataSourcePropSet

DBPROPSET_DBINIT

CInitializationPropSet

DBPROPSET_INDEX

CIndexPropSet

DBPROPSET_ROWSET

CRowsetPropSet

DBPROPSET_SESSION

CSessionPropSet

DBPROPSET_TABLE

CTablePropSet

DBPROPSET_VIEW

CViewPropSet

Tabelle 5.4: GUIDs von Eigenschaften-Sets und benutzerdefinierte Klassen

5.2.9

Eigenschaftenstatus prüfen

dwStatus gibt den Eigenschaftenstatus an, d.h. ob eine Eigenschaft ordnungsgemäß gesetzt wurde und ob sie dem Typ DBPROPSTATUS angehört. Wenn alles korrekt programmiert wurde, lautet der Status DBPROPSTATUS_OK. Eine andere Statusangabe gibt an,

dass ein Fehler aufgetreten ist. Tabelle 5.5 zeigt die möglichen Werte für DBPROPSTATUS.

Sandini Bib 126

5

OLE DB-Eigenschaften

Statusangabe

Beschreibung

DBPROPSTATUS_OK

Die Eigenschaft wurde ordnungsgemäß gesetzt.

DBPROPSTATUS_BADCOLUMN

Die Spalte war ungültig.

DBPROPSTATUS_BADOPTION

Die Option war ungültig.

DBPROPSTATUS_BADVALUE

Der Wert oder der Typ war ungültig.

DBPROPSTATUS_CONFLICTING

Der Wert hat einen Konflikt mit einer anderen Eigenschaft verursacht.

DBPROPSTATUS_NOTSET

Die Eigenschaft war optional und wurde nicht gesetzt.

DBPROPSTATUS_NOTSUPPORTED

Das Objekt unterstützt diese Eigenschaft nicht.

DBPROPSTATUS_NOTSETTABLE

Die Eigenschaft kann nur gelesen werden.

DBPROPSTATUS_NOTALLSETTABLE

Die Eigenschaft galt für alle Spalten, konnte jedoch auf keine davon angewendet werden.

Tabelle 5.5: Status von Eigenschaften

Ablaufverfolgung für Eigenschaften Durch Aufruf der Funktion AtlTracePropertySets aus den OLE DB-Erweiterungen können Sie den Eigenschaftenstatus schnell prüfen. Diese Funktion verfolgt Fehler, die in einem Array aus Eigenschaften-Sets auftreten. Wenn kein Fehler auftritt, bleibt sie inaktiv. Die Deklaration von AtlTracePropertySets lautet folgendermaßen: inline HRESULT AtlTracePropertySets (DBPROPSET* PropertySets, ULONG PropertySetsCount = 1)

wobei PropertySets ein Array aus Eigenschaften-Sets und PropertySetsCount die Anzahl der in PropertySets enthaltenen Eigenschaften-Sets bezeichnet. Das folgende Beispiel zeigt ein Eigenschaften-Set mit zwei Fehlern: 왘 Der Typ des Kennwortes ist ein boolescher Wert, es müsste aber ein String sein. 왘 INVALID_PROPERTY ist eine ungültige Eigenschaften-ID. CDBPropSet Propset[1]; Propset[0].SetGUID(DBPROPSET_DBINIT); Propset[0].AddProperty(DBPROP_AUTH_USERID, "Hallo"); Propset[0].AddProperty(DBPROP_AUTH_PASSWORD, false); Propset[0].AddProperty(INVALID_PROPERTY, (short)4); CDataSource DataSource; HRESULT hr = DataSource.Open(_T("SQLOLEDB.1"), Propset, 1); if (FAILED(hr)) AtlTracePropertySets(Propset, 1);

Sandini Bib 5.3

Eigenschaften abrufen

127

Dieses Beispiel liefert die folgende Ausgabe von AtlTracePropertySets: ATL: (Bad ATL: (Not

Bad Property in Property Set # 0, Property # 1, PropertyID = 9 , Status = 2 value) Bad Property in Property Set # 0, Property # 2, PropertyID = 255 , Status = 1 Supported)

5.3 Eigenschaften abrufen Der erste Schritt beim Abrufen von Eigenschaften besteht darin, die gewünschten Eigenschaften anzugeben. Objekte, die Eigenschaften abrufen, weisen eine Methode wie die Folgende auf: HRESULT STDMETHODCALLTYPE GetProperties( ULONG cPropertyIDSets, DBPROPIDSET *rgPropertyIDSets, ULONG *pcPropertySets, DBPROPSET **prgPropertySets);

wobei rgPropertyIDSets ein DBPROPIDSET-Array bezeichnet, das die IDs der abzurufenden Eigenschaften enthält (cPropertyIDSets ist die Anzahl der in rgPropertyIDSets enthaltenen Elemente). Der Provider weist anschließend ein Array aus Eigenschaften-Sets zu, füllt dieses auf und liefert es in prgPropertySets zurück, so dass pcPropertySets die Anzahl der in prgPropertySets enthaltenen Elemente angibt.

5.3.1

DBPROPIDSET

DBPROPIDSET stellt ein Set aus Strukturen vom Typ DBPROPID dar und ist folgendermaßen

definiert: typedef struct tagDBPROPIDSET { DBPROPID * rgPropertyIDs; ULONG cPropertyIDs GUID guidPropertySet; } DBPROPIDSET;

wobei 왘 rgPropertyIDs ein Zeiger auf ein Array aus DBPROPID-Strukturen ist. 왘 cPropertyIDs die Anzahl von DBPROPID-Strukturen in rgPropertyIDs angibt. 왘 guidPropertySet die GUID ist, die für alle Eigenschaften des Sets gilt. Abbildung 5.2 zeigt ein Beispiel mit zwei Sets von Eigenschaften-IDs (DBPROPIDSET): Das erste Set gibt drei Eigenschaften (Eigenschaften-IDs 1, 2 und 3) an, das zweite nur eine. Dafür weist der Provider ein Array aus DBPROPSET-Strukturen zu, das dem übergebenen Array aus DBPROPRIDSET-Strukturen ähnelt: Es weist die gleiche Anzahl von Sets mit jeweils denselben Eigenschaften auf. Die vom Provider zugewiesenen Strukturen sind in Grau dargestellt.

Sandini Bib 128

5

OLE DB-Eigenschaften

Das Abrufen von OLE DB-Eigenschaften erfolgt in folgenden sechs Schritten: 1. Die Eigenschaften-IDs und die GUID der Eigenschaften-Sets für die gewünschten Eigenschaften ermitteln. 2. Die Eigenschaften mit gleicher GUID innerhalb von Sets aus Eigenschaften-IDs neu gruppieren. Zu diesem Zeitpunkt wissen Sie, wie viele Eigenschaften-Sets Sie brauchen und wie viele Eigenschaften die einzelnen Eigenschaften-Sets enthalten. 3. Ein Array aus Sets aus Eigenschaften-IDs (property ID) zuweisen. Setzen Sie für jedes Eigenschaften-Set die GUID und das Array aus Eigenschaften-IDs. 4. Das Array aus Eigenschaften-ID-Sets (property ID) an die Methode GetProperties des betreffenden Objekts übergeben. 5. Das zurückgelieferte Array aus Eigenschaften-Sets verwenden. 6. Abschließend das Eigenschaften-Set freigeben.

DBPROPSET 1 rgPropertyIDSets

Zeiger

Eigenschaften-ID 1

3

Eigenschaften-ID 2

GUID1

Eigenschaften-ID 3

Zeiger DBPROPIDSET 2 cPropertyIDSets = 2

Eigenschaften-ID 4

1 GUID2

Eigenschaften-ID 1 Wert1

Zeiger

...

3

Eigenschaften-ID 2

GUID1

Wert 2

Zeiger

...

1

Eigenschaften-ID 3

GUID2

Wert 3

DBPROPSET 1

DBPROPSET 2

... Eigenschaften-ID 1 Wert 1

... Abbildung 5.2: Eigenschaftenstrukturen abrufen

Sandini Bib 5.3

Eigenschaften abrufen

129

Das folgende Beispiel veranschaulicht das Abrufen von Eigenschaften: DBPROPIDSET PropIDSet; PropIDSet.guidPropertySet = DBPROPSET_DATASOURCEINFO; // GUID des Sets von Eigenschaften-IDs setzen PropIDSet.cPropertyIDs = 2; DBPROPID PropertyIDs [2]; PropIDSet.rgPropertyIDs = PropertyIDs; // Array aus Eigenschaften-IDs definieren PropertyIDs[0] = DBPROP_ACTIVESESSIONS; PropertyIDs[1] = DBPROP_ABORTPRESERVE; ULONG PropSetCount; DBPROPSET * PropSets; hr = DataSource.GetProperties(1, &PropIDSet, &PropSetCount, &PropSets); // Die Eigenschaften-Sets (PropSets) verwenden // Die PropSets müssen freigegeben werden, siehe nächsten Abschnitt

5.3.2

CDBPropIDSet

Auch wenn das Arbeiten mit der Struktur DBPROPIDSET nicht besonders schwierig ist, stellen die OLE DB Consumer Templates dennoch als Erleichterung die Klasse CDBPropIDSet zur Verfügung. Diese ist folgendermaßen definiert: class CDBPropIDSet : public tagDBPROPIDSET

CDBPropIDSet fügt keine neuen Klassenelemente hinzu und kann überall dort verwendet werden, wo DBPROPIDSET benötigt wird. Diese Klasse weist die beiden folgenden Methoden auf:

왘 Einen Konstruktor, der die GUID des Eigenschaften-Sets als Argument verwendet. 왘 Die Methode AddPropertyID, die die Größe des PROPID-Arrays automatisch anpasst und den Zähler für PROPID aktualisiert. Der folgende Code zeigt die Verwendung von CDBPropIDSet: CDBPropIDSet MyDBPropIDSet(DBPROPSET_DBINIT); MyDBPropIDSet.AddPropertyID(DBPROP_ACTIVESESSIONS); MyDBPropIDSet.AddPropertyID(DBPROP_ABORTPRESERVE);

5.3.3

Speicherverwaltung

Der OLE DB-Provider weist das Array aus Eigenschaften-Sets zu, der Consumer ist dann für die Freigabe zuständig. Diese Freigabe umfasst Folgendes: 왘 Alle Varianten der einzelnen Eigenschaften freigeben. 왘 Alle Eigenschaften-Sets freigeben. 왘 Das Array aus Eigenschaften-Sets freigeben.

Sandini Bib 130

5

OLE DB-Eigenschaften

Wenn Sie mit unspezifischen OLE DB-Strukturen arbeiten, können Sie diesen Vorgang manuell durchführen, indem Sie folgenden Code programmieren: for (ULONG i = 0; i < PropSetCount; i++) { for (ULONG j; j < PropSets[i].cProperties; j++) { VariantClear(&(PropSets[i].rgProperties[j].vValue)); } CoTaskMemFree(PropSets[i].rgProperties); } CoTaskMemFree(PropSets);

Sie können hierzu auch die Funktion FreePropertySets aus den OLE DB-Erweiterungen verwenden. Der Destruktor von CDBPropSet sorgt bereits für die Freigabe der Varianten und gibt sein Array aus Eigenschaften frei. FreePropertySets bedient sich des Destruktors um dann das Array freizugeben. inline HRESULT FreePropertySets(DBPROPSET * Sets, ULONG SetCount = 1) { CDBPropSet * Sets2 = (CDBPropSet *)Sets; for(ULONG i = 0; i < SetCount; i++) { Sets2[i].~CDBPropSet(); } CoTaskMemFree(Sets); return S_OK; }

Der Code zum Abrufen von Eigenschaften ändert sich dann folgendermaßen: CDBPropIDSet PropIDSet(DBPROPSET_DATASOURCEINFO); PropIDSet.AddPropertyID(DBPROP_ACTIVESESSIONS); PropIDSet.AddPropertyID(DBPROP_ABORTPRESERVE); ULONG PropSetCount; CDBPropSet * PropSets; hr = DataSource.GetProperties(1, &PropIDSet, &PropSetCount, reinterpret_cast(&PropSets)); FreePropertySets(PropSets, PropSetCount);

5.3.4

Eine Eigenschaft abrufen

Das Hantieren mit den verschiedenen Eigenschaften-Sets kann sehr mühevoll werden, vor allem dann, wenn Sie nur eines benötigen. Für diesen Fall gibt es die Funktion GetProperty. HRESULT GetProperty(const GUID& guid, DBPROPID propid, VARIANT* pVariant) const

Sandini Bib 5.4

Eigenschaften aufzählen

131

wobei 왘 guid die GUID des Eigenschaften-Sets bezeichnet. 왘 propid die Eigenschaften-ID der Eigenschaft bezeichnet. 왘 pVariant den Wert der Eigenschaft empfängt. Der folgende Code zeigt die Verwendung von GetProperty. CComVariant variant; hr = DataSource.GetProperty(DBPROPSET_DATASOURCEINFO, DBPROP_ACTIVESESSIONS, &variant);

Beachten Sie, dass Sie die Variante nicht explizit freigeben müssen, wenn Sie eine Klasse wie CComVariant verwenden. Bei Verwendung einer Struktur vom Typ VARIANT ist dies allerdings erforderlich.

5.4 Eigenschaften aufzählen Im vorherigen Abschnitt haben wir untersucht, wie Eigenschaften aus Sets von Eigenschaften-IDs abgerufen werden können, d.h. die Eigenschaften, deren IDs bereits bekannt sind. Es kommt jedoch auch vor, dass alle Eigenschaften eines bestimmten Eigenschaften-Sets abgerufen werden müssen. OLE DB ermöglicht dies für alle Eigenschaften, die von einem bestimmten Objekt unterstützt werden. Der Mechanismus ähnelt dem Abrufen von Eigenschaften, allerdings verwenden Sie hier anstelle der normalen Eigenschaften-GUIDs so genannte Aufzählungs-GUIDs. Wenn eine DBPROPIDSET-Struktur eine Aufzählungs-GUID enthält, werden alle Eigenschaften zurückgeliefert. Der Provider beachtet dabei die übergebenen EigenschaftenIDs überhaupt nicht. Im Gegensatz zu einer »regulären« Eigenschaften-GUID macht es die Aufzählungs-GUID möglich, dass das zurückgelieferte Eigenschaften-Set mehr Eigenschaften enthält, als im Set der Eigenschaften-IDs angegeben wurde. Tabelle 5.6 enthält die in OLE DB definierten Aufzählungs-GUIDs. Trotz der Namensgleichheit sind Aufzählungs-GUIDs keine GUIDs von Eigenschaften-Sets, sondern sie beziehen sich auf Eigenschaftengruppen. Beispielsweise sind unter DBPROPSET_DBINITALL alle Eigenschaften zur Initialisierung von Datenquellen gruppiert. Zwar stammen viele dieser Eigenschaften vermutlich aus der Eigenschaftengruppe DBPROPSET_DBINIT, aber andere können auch Bestandteil eines Provider-spezifischen Eigenschaften-Sets zur Initialisierung von Datenquellen sein. Grundsätzlich kann gesagt werden, dass eine Aufzählungs-GUID mehrere Eigenschaften-Sets hervorbringen kann.

Sandini Bib 132

5

OLE DB-Eigenschaften

Symbolischer Wert

Beschreibung

DBPROPSET_COLUMNALL

Liefert alle Spalteneigenschaften zurück.

DBPROPSET_CONSTRAINTALL

Liefert alle Eigenschaften von Zwangsbedingungen zurück.

DBPROPSET_DATASOURCEALL

Liefert alle Datenquelleneigenschaften zurück.

DBPROPSET_DATASOURCEINFOALL

Liefert alle Eigenschaften für Datenquelleninformationen zurück.

DBPROPSET_DBINITALL

Liefert alle Eigenschaften für die Initialisierung von Datenquellen zurück.

DBPROPSET_INDEXALL

Liefert alle Indexeigenschaften zurück.

DBPROPSET_ROWSETALL

Liefert alle Zeilenmengen-Eigenschaften zurück.

DBPROPSET_SESSIONALL

Liefert alle Sitzungseigenschaften zurück.

DBPROPSET_TABLEALL

Liefert alle Tabelleneigenschaften zurück.

DBPROPSET_TRUSTEEALL

Liefert alle Trustee-Eigenschaften zurück.

DBPROPSET_VIEWALL

Liefert alle Eigenschaften zu Sichten zurück.

Tabelle 5.6: Aufzählungs-GUIDs

Das folgende Beispiel zeigt das Abrufen aller Eigenschaften aus der Eigenschaftengruppe für die Datenquelleninformationen. CDBPropIDSet PropIDSet(DBPROPSET_DATASOURCEINFOALL); ULONG PropSetCount; CDBPropSet * PropSets; hr = DataSource.GetProperties(1, &PropIDSet, &PropSetCount, reinterpret_cast(&PropSets)); FreePropertySets(PropSets, PropSetCount);

Wie bei den regulären Eigenschaften muss das Array für CDBPropSet nach seiner Verwendung mit FreePropertySets freigegeben werden. Da der Provider spezifische Eigenschaften-Sets mit einbeziehen kann, ist nicht garantiert, dass die Anzahl der Eigenschaften-Sets vom Typ CDBPropSets gleich eins ist.

5.4.1

Fehler aufzählen

Wenn beim Setzen von Eigenschaften ein Fehler zurückgeliefert wird, besteht die Vorgehensweise normalerweise darin, den Fehlerstatus zu prüfen. OLE DB bietet hierzu eine Alternative mit dem Eigenschaften-Set DBPROPSET_PROPERTIESINERROR. Hierbei handelt es sich eigentlich nicht um ein reguläres Eigenschaften-Set, da es keine entsprechende Eigenschaft gibt, die zu diesem Set gehört. Statt dessen nimmt dieses Set alle Eigenschaften auf, die einen Fehler verursacht haben. Beachten Sie, dass Sie diese GUID direkt nach dem Auftreten eines Fehlers verwenden müssen, da andere Methoden-

Sandini Bib 5.5

Eigenschafteninformationen abrufen

133

aufrufe ansonsten zuvorkommen und das zugrunde liegende Objekt veranlassen können, die fehlerhaften Eigenschaften zu löschen. Das folgende Beispiel zeigt, wie solche Eigenschaften abgerufen werden: CDBPropIDSet PropIDSet(DBPROPSET_PROPERTIESINERROR); ULONG PropSetCount; CDBPropSet * PropSets; hr = DataSource.GetProperties(1, &PropIDSet, &PropSetCount, reinterpret_cast(&PropSets)); FreePropertySets(PropSets, PropSetCount);

5.5 Eigenschafteninformationen abrufen Die Eigenschafteninformationen umfassen alle Informationen über eine bestimmte Eigenschaft. Diese Informationen sind statisch, was bedeutet, dass sie nicht vom Wert der Eigenschaft oder des Objekts abhängen. Sie beziehen sich auf den Provider und nicht auf das Objekt selbst. Die Eigenschafteninformationen stellen gewissermaßen alle Metadaten zu einer Eigenschaft dar (Beschreibung, Typ usw.). Beispielsweise lautet die Textbeschreibung der Zeilenmengen-Eigenschaft DBPROP_ABORTPRESERVE »Preserve on Abort« (bei Abbruch beibehalten) und der zugehörige Datentyp ist boolesch. Diese beiden Informationselemente hängen nicht davon ab, welche Zeilenmenge zugrunde liegt oder ob nach einem Abbruch die Zeilen der Zeilenmenge tatsächlich beibehalten werden. Folglich rufen Sie auf diese Weise nicht die Eigenschafteninformationen für das Objekt ab, zu dem die Eigenschaft gehört, sondern diejenigen für die Datenquelle, die das Objekt erzeugt hat. Wenn Sie z.B. die Eigenschafteninformationen von DBPROP_ ABORTPRESERVE für eine bestimmte Zeilenmenge benötigen, müssen Sie zunächst auf die Datenquelle zugreifen, die die Zeilenmenge erzeugt hat, und anschließend die Eigenschafteninformationen für die Zeilenmenge abrufen. (In Kapitel 6 finden Sie weitere Informationen über Datenquellen). Die Klasse CDataSource unterstützt Eigenschafteninformationen nicht explizit. Die OLE DB-Erweiterungen enthalten jedoch mit CDataSourceEx eine erweiterte Version dieser Klasse, die diese Anforderung erfüllt. Die Programmierung für das Abrufen von Eigenschafteninformationen ähnelt der zum Abrufen von Eigenschaftswerten: HRESULT GetPropertyInfo(ULONG cPropertyIDSets, const DBPROPIDSET rgPropertyIDSets[], ULONG * pcPropertyInfoSets, DBPROPINFOSET ** prgPropertyInfoSets, OLECHAR ** ppDescBuffer)

wobei rgPropertyIDSets ein Array aus Eigenschaften-ID-Sets bezeichnet (die Anzahl der von cPropertyIDSets dargestellten Elemente). Daraufhin weist das Datenquellen-

Sandini Bib 134

5

OLE DB-Eigenschaften

objekt ein Array aus Eigenschafteninformationen-Sets zu, füllt dieses auf und liefert es in prgPropertyInfoSets zurück (pcPropertyInfoSets liefert so die Anzahl der in prgPropertyInfoSets enthaltenen Elemente). Etwas knifflig ist die Verwendung des Arguments ppDescBuffer. Hierbei handelt es sich um einen Puffer, der die beschreibenden Strings für die Eigenschafteninformationen enthält. Sie können dieses Argument zwar nicht direkt verwenden, müssen es aber freigeben, sobald der Vorgang zum Abrufen der Eigenschafteninformationen abgeschlossen ist. Vor einer Beschreibung der Vorgehensweise hierzu empfiehlt es sich, die einzelnen Strukturen näher zu beleuchten.

5.5.1

DBPROPINFOSET

Entsprechend dem Modell, das auch für die anderen Set-Strukturen gilt, stellt DBPROPINFOSET ein Set aus DBPROPINFO-Strukturen dar. typedef struct tagDBPROPINFOSET { DBPROPINFO * rgPropertyInfos; ULONG cPropertyInfos; GUID guidPropertySet; } DBPROPINFOSET;

5.5.2

DBPROPINFO

Die Struktur DBPROPINFO enthält die Informationen über eine Eigenschaft. Sie ist folgendermaßen definiert: typedef struct tagDBPROPINFO { LPOLESTR pwszDescription; DBPROPID dwPropertyID; DBPROPFLAGS dwFlags; VARTYPE vtType; VARIANT vValues; } DBPROPINFO;

Die Eigenschaftsbeschreibung (Datentyp LPOLESTR) ist ein String zur Beschreibung der Eigenschaft. Er dient hauptsächlich zur eindeutigen Beschreibung von VerbindungsStrings. Diese Strings können zum Erstellen von Benutzerschnittstellen hilfreich sein, sie sind allerdings nicht lokalisiert. Der Eigenschaftstyp (Datentyp VARTYPE) definiert den Typ, zu dem die Eigenschaft gehört. Die Eigenschaften-Flags in OLE DB geben Folgendes an: 왘 Die Eigenschaftengruppe, zu der die Eigenschaft gehört (weitere Informationen zu Eigenschaftengruppen finden Sie im Abschnitt über Eigenschaftengruppen).

Sandini Bib 5.5

Eigenschafteninformationen abrufen

135

왘 Die Zugriffsart (Lesen/Schreiben). 왘 Ob die Eigenschaft erforderlich ist oder nicht. Zur Angabe der Zugriffsart gibt es die beiden folgenden Flag-Werte: 왘 DBPROPFLAGS_READ: OLE DB-Methoden können diese Eigenschaft lesen. 왘 DBPROPFLAGS_WRITE: OLE DB-Methoden können diese Eigenschaft schreiben. Auf einige Eigenschaften ist nur Lesezugriff möglich, aber in den meisten Fällen gilt, dass eine Eigenschaft, die geschrieben werden kann, auch Lesezugriff gestattet. Daher enthält das Eigenschaften-Flag entweder den Wert DBPROPFLAGS_READ oder den Wert DBPROPFLAGS_READ | DBPROPFLAGS_WRITE. Ferner kann es einen der folgenden Flag-Werte aufweisen: 왘 DBPROPFLAGS_REQUIRED: Die Eigenschaft ist erforderlich. 왘 DBPROPFLAGS_NOTSUPPORTED: Der Provider unterstützt diese Eigenschaft nicht. 왘 DBPROPFLAGS_COLUMNOK: Die Eigenschaft gilt auch für Spalten. vValues ist eine Variante, die eine Liste möglicher Werte für die zugehörige Eigenschaft enthält. Sie gilt nur für Eigenschaften mit einer begrenzten Anzahl möglicher Werte und ist in der Regel leer.

Das folgende Beispiel zeigt den Code zum Abrufen der Eigenschafteninformationen für zwei Eigenschaften: DBPROPIDSET PropIDSet; PropIDSet.guidPropertySet = DBPROPSET_DATASOURCEINFO; DBPROPID PropIDs [2]; PropIDSet.cPropertyIDs = 2; PropIDSet.rgPropertyIDs = PropIDs; PropIDs[0] = DBPROP_ACTIVESESSIONS; PropIDs[1] = DBPROP_ACTIVESESSIONS; ULONG PropertyInfoSetsCount; DBPROPINFOSET * PropertyInfoSets; OLECHAR * Strings; hr = DataSource.GetPropertyInfo(1, &PropIDSet, &PropertyInfoSetsCount, &PropertyInfoSets, &Strings); for (ULONG i = 0; i < PropertyInfoSetsCount; i++) { for (ULONG j; j < PropertyInfoSets[i].cPropertyInfos; j++) { VariantClear(&(PropertyInfoSets[i].rgPropertyInfos[j].vValues)); } CoTaskMemFree(PropertyInfoSets[i].rgPropertyInfos);

Sandini Bib 136

5

OLE DB-Eigenschaften

} CoTaskMemFree(PropertyInfoSets); CoTaskMemFree(Strings);

5.5.3

CDBPropertyInfoSet

Der direkte Umgang mit DBPROPINFOSET kann schwierig sein, weil Sie hierbei den Speicher manuell verwalten müssen. Die OLE DB-Erweiterungen bieten eine Kapselung von DBPROPINFOSET: Die Klasse CDBPropertyInfoSet erfüllt im Wesentlichen die gleiche Funktion für DBPROPINFOSET wie die Klasse CDBPropSet für DBPROPSET. Das folgende Beispiel zeigt den Code zum Abrufen der Eigenschafteninformationen für zwei Eigenschaften: CDBPropIDSet PropIDSet(DBPROPSET_DATASOURCEINFO); PropIDSet.AddPropertyID(DBPROP_ACTIVESESSIONS); PropIDSet.AddPropertyID(DBPROP_DATASOURCENAME); ULONG PropertyInfoSetsCount; CDBPropertyInfoSet * PropertyInfoSets; CAutoMemory Strings; hr = DataSource.GetPropertyInfo(1, &PropIDSet, &PropertyInfoSetsCount, (DBPROPINFOSET ** )&PropertyInfoSets, &Strings); ATLTRACE(PropertyInfoSets[0].rgPropertyInfos[0].pwszDescription); ATLTRACE("\ n"); ATLTRACE(PropertyInfoSets[0].rgPropertyInfos[1].pwszDescription); FreePropertyInfoSets(PropertyInfoSets, PropertyInfoSetsCount);

5.6 Zusammenfassung In diesem Kapitel wurde der Mechanismus zum Arbeiten mit OLE DB-Eigenschaften vorgestellt. Sie erfuhren insbesondere, wie Eigenschaften abgerufen, gesetzt und aufgezählt sowie Eigenschafteninformationen abgerufen werden können. Jetzt sind wir so weit, dass wir uns den einzelnen OLE DB-Objekten zuwenden können.

Sandini Bib

6 Datenquellen und Sitzungen Beim Erstellen einer OLE DB-Anwendung besteht der erste Schritt darin, die Daten anzugeben, mit denen Sie arbeiten wollen. Dies bedeutet, dass Sie wissen müssen, wo diese Daten gespeichert sind. Das scheint auf den ersten Blick eine Selbstverständlichkeit zu sein, ist aber nicht immer einfach. Es gibt in der Tat zahlreiche Möglichkeiten anzugeben, wo sich der Datenspeicher befindet und wie auf ihn zugegriffen werden soll. Der Datenspeicher kann sich beispielsweise in einer Datei in einer bestimmten SQL Server- oder Oracle-Server-Datenbank, in einer MS Access-Datenbank oder im Internet befinden. 왘 Befindet er sich in einer SQL Server-Datenbank, müssen Sie zumindest einen Server-Namen und eine Authentifizierungsmethode angeben. 왘 Befindet er sich in einer Access-Datenbank, müssen Sie einen Dateinamen, einen Benutzernamen, ein Kennwort und einen Modus zum Öffnen der Datenbank angeben. 왘 Befindet er sich im Internet, müssen Sie einen URL angeben. Die Speicherorte für die Daten können so verschiedenartig sein, dass dafür ein eigenes Objekt vorgesehen ist: Der OLE DB-Objekttyp Datenquelle (Data Source), der in den OLE DB Consumer Templates in der Klasse CDataSource gekapselt ist. Das Datenquellenobjekt alleine bewirkt nicht viel. Es benötigt ein Objekt vom Typ Sitzung (Session) um andere OLE DB-Objekte zu öffnen, deren Hauptzweck darin besteht, eine Verbindung zur Datenquelle herzustellen und Transaktionen zu unterstützen. Das Sitzungsobjekt wird von der Klasse CSession gekapselt. In diesem Kapitel werden wir das Datenquellenobjekt ausführlich behandeln. Dies umfasst insbesondere folgende Themen: 왘 Öffnen einer Datenquelle über die Programmierung. 왘 Verwendung von Verbindungs-Strings. 왘 Verwendung der Dialogfenster zur Angabe von Datenquellen. 왘 Aufzählung der Datenquellentypen. 왘 Die verschiedenen Dienstkomponenten. 왘 Die Grundlagen einer Sitzung

Sandini Bib 138

6

Datenquellen und Sitzungen

6.1 Geschichtlicher Rückblick In diesem Abschnitt werfen wir einen Blick zurück auf die Datenquellen aus der Zeit vor OLE DB.

6.1.1

Proprietäre Datenbanken

Bei Verwendung einer proprietären Datenbank wird eine Funktion wie die Folgende aufgerufen: HCONNECTION API Connect(char* ServerName, char*UserName, char* Password, UINT Mode)

Dieser Ansatz weist zwei Nachteile auf: Erstens ist eine proprietäre Datenbank nicht portierbar und funktioniert nur mit einem Typ von Datenbank-Server, und zweitens müssen Sie den Namen des physischen Servers, den Benutzernamen und das Kennwort übergeben. Eine Lösung besteht darin, diese Informationen fest zu codieren. Die Anwendung wird dann jedes Mal neu kompiliert, wenn Sie den Server verlagern oder die Anmeldeinformationen ändern. Alternativ dazu können diese Informationen außerhalb der Anwendung gespeichert oder vom Benutzer jedes Mal über einen Dialog angefordert werden. Die Daten-Provider stellen jedoch in der Regel keine systemeigene Unterstützung hierfür zur Verfügung. Eine dritte Lösung besteht darin, das Problem durch eine Indirektion zu umgehen.

6.1.2

ODBC

ODBC (Open DataBase Connectivity) stellt diese Indirektion über den ODBC-Datenquellen-Administrator zur Verfügung, der die zum Zugriff auf die Daten erforderlichen Informationen als Liste von Datenquellen speichert. Die Aufgabe des Programmierers beschränkt sich dabei auf eine Zeile wie die Folgende: Connect (DSNName);

wobei DSN für den Namen der Datenquelle steht. Dies bedeutet, dass es möglich ist, die Informationen zu einer Datenquelle über den Data Source Administrator zu ändern, ohne auf Anwendungsebene eingreifen zu müssen. Ferner kann die Datenquelle über das Dialogfenster ODBC-DATENQUELLEN-ADMINISTRATOR verwaltet werden. Abbildung 6.1 zeigt dieses Dialogfenster, das Sie durch Anklicken des Symbols ODBC in der Systemsteuerung öffnen können. Der Vorteil dieses Ansatzes besteht darin, dass die Datenquellen auf Systemebene und nicht auf Anwendungsebene angegeben werden. Auf diese Weise kann die Datenquelle außerhalb der Anwendung konfiguriert werden und die Anwendung braucht beim Wechsel der Datenquelle nicht neu kompiliert zu werden.

Sandini Bib 6.2

Datenquelle über die Programmierung öffnen

139

Abbildung 6.1: Dialogfenster ODBC-Datenquellen-Administrator

Die OLE DB-Datenquelle ist mit allen Vorzügen der ODBC-Datenquelle ausgestattet und bietet demgegenüber noch einige Verbesserungen. Erstens basiert das Datenquellenobjekt von OLE DB auf COM und ist daher ein COM-Objekt, zweitens kann die OLE DB-Datenquelle in einen String umgewandelt und in dieser Form in einer Datei gespeichert werden, so dass die Übertragung ihres Inhalts einfacher ist, und drittens können bei Verwendung von OLE DB die ODBC-Datenquellen weiterverwendet werden, da sie von OLE DB-Datenquellen referenziert werden können.

6.2 Datenquelle über die Programmierung öffnen Das Öffnen einer Datenquelle besteht darin, das COM-Objekt für die Datenquelle mit einer bestimmten CLSID zu erzeugen und die Datenquelleneigenschaften zu setzen. Es stehen zwar verschiedene Open-Methoden für Datenquellen zur Verfügung, aber schließlich werden immer eine CLSID und eine Reihe von Eigenschaften zugewiesen. In diesem Abschnitt werden die einzelnen Methoden beschrieben.

6.2.1

ProgID und CLSID

Im Abschnitt über COM haben wir gesehen, dass ein Klassenbezeichner (CLSID) ein aus 16 Byte bestehender eindeutiger Bezeichner für eine COM-Klasse ist. CLSID_MSDASQL ist beispielsweise die CLSID des OLE DB-Providers für ODBC. Eine COM-Klasse kann aber auch über eine ProgID angegeben werden. Hierbei handelt es sich um eine für den Benutzer leichter verständliche Zeichenfolge, die einer CLSID zugeordnet ist. »MSDASQL«

Sandini Bib 140

6

Datenquellen und Sitzungen

ist beispielsweise die ProgID des OLE DB-Providers für ODBC. Während die CLSID mit Sicherheit immer eindeutig ist, ist dies bei der ProgID keinesfalls gewährleistet. Zum Konvertieren einer CLSID in eine ProgID programmieren Sie folgenden Aufruf: HRESULT CLSIDFromProgID (LPCOLESTR lpszProgID, LPCLSID pclsid );

Zum Konvertieren einer ProgID in eine CLSID programmieren Sie folgenden Aufruf: WINOLEAPI ProgIDFromCLSID( REFCLSID clsid ,//CLSID, für die die ProgID angefordert wird LPOLESTR * lplpszProgID //Adresse der Ausgabevariablen, die // einen Zeiger auf die angeforderte ProgID-Zeichenfolge // empfängt. );

Die Klasse CDataSource erleichtert die Aufgabe des Programmierers, da sie sowohl eine ProgID als auch eine CLSID akzeptiert. Wenn Sie für diese Klasse eine ProgID an eine Open-Methode übergeben, wird diese automatisch in eine CLSID konvertiert und es wird diejenige Open-Methode aufgerufen, die CLSIDs akzeptiert.

6.2.2

Datenquelle mit Benutzername und Kennwort öffnen

Wenn Sie bisher mit einer anderen Datenbank-API gearbeitet haben, sind Sie es sicherlich gewöhnt, eine Datenquelle über Eigenschaften für die vier folgenden Angaben zu öffnen: 왘 Name 왘 Benutzername 왘 Kennwort 왘 Initialisierungsmodus Die OLE DB Consumer Templates bieten hierzu zwei Funktionen: HRESULT Open(const CLSID& clsid, LPCTSTR pName, LPCTSTR pUserName = NULL, LPCTSTR pPassword = NULL, long nInitMode = 0) HRESULT Open(LPCTSTR szProgID, LPCTSTR pName, LPCTSTR pUserName = NULL, LPCTSTR pPassword = NULL, long nInitMode = 0)

pName ist der Name des Servers. Die genaue Bedeutung ist vom Provider abhängig. Bei einem ODBC-Provider steht pName für den Namen der ODBC-Datenquelle, der im Dia-

logfenster ODBC-DATENQUELLEN-ADMINISTRATOR aufgeführt ist. Bei einer AccessDatenbank wird mit pName der Name der Datenbankdatei angegeben.

Sandini Bib 6.2

Datenquelle über die Programmierung öffnen

141

Den Benutzernamen und das Kennwort brauche ich nicht weiter zu erläutern. Wenn Sie hier NULL (Standardwert) übergeben, wird die Eigenschaft nicht gesetzt. Die Eigenschaft für den Initialisierungsmodus nInitMode kann als Wert eine Kombination aus 0 und weiteren Zahlen/Ziffern aufweisen, wie in Tabelle 6.1 gezeigt. Wenn Sie für den Modus den Standardwert 0 übergeben, wird die Eigenschaft nInitMode nicht gesetzt. Die Initialisierung ist vor allem für Arbeitsplatzdatenbanken wie Microsoft Access wichtig. Andere Datenbanktypen berücksichtigen diese Eigenschaft in der Regel nicht. Das folgende Beispiel zeigt, wie eine Datenquelle durch Angabe von Name, Benutzername und Kennwort geöffnet wird: CDataSource DataSource; DataSource.Open("MSDASQL", Name, UserName, Password, Mode); //oder DataSource.Open(CLSID_MSDASQL, Name, UserName, Password, Mode);

Name (Server-Name), UserName (Benutzername) Password (Kennwort) und Mode (Initialisierungsmodus) sind vier Eigenschaften der Datenquelle. Das Öffnen einer Datenquelle mit diesen Eigenschaften bedeutet nichts anderes als eine Prüfung der Datenquelle durch die Methode Open und das Setzen dieser vier Eigenschaften. Es sind aber noch zahlreiche andere Eigenschaften im Spiel. Schauen Sie sich hierzu das nächste Codebeispiel an, das diesen Umstand für CDataSource::OpenWithNameUserPassword zeigt.

Symbolischer Wert

Wert

Bedeutung

DB_MODE_READ

0x01

Nur-Lese-Zugriff

DB_MODE_WRITE

0x02

Nur-Schreib-Zugriff

DB_MODE_READWRITE

0x03

Schreib-/Lese-Zugriff (DB_MODE_READ | DB_MODE_WRITE)

DB_MODE_SHARE_DENY_READ

0x04

Die Datenquelle kann von anderen Benutzern nicht im Nur-Lese-Modus geöffnet werden.

DB_MODE_SHARE_DENY_WRITE

0x08

Die Datenquelle kann von anderen Benutzern nicht im Schreib-Modus geöffnet werden.

DB_MODE_SHARE_EXCLUSIVE

0x0c

Die Datenquelle kann von anderen Benutzern nicht im Schreib-/Lese-Modus geöffnet werden (DB_MODE_SHARE_DENY_READ | DB_MODE_SHARE_DENY_WRITE).

DB_MODE_SHARE_DENY_NONE

0x10

Anderen Benutzern kann weder der Lese- noch der Schreibzugriff verweigert werden.

Tabelle 6.1: Initialisierungsmodi

Sandini Bib 142

6

Datenquellen und Sitzungen

HRESULT OpenWithNameUserPassword(LPCTSTR pName, LPCTSTR pUserName, LPCTSTR pPassword, long nInitMode = 0) { ATLASSERT(m_spInit != NULL); CComPtr spProperties; HRESULT hr; hr = m_spInit->QueryInterface(IID_IDBProperties, (void**)&spProperties); if (FAILED(hr)) return hr; // Verbindungseigenschaften setzen CDBPropSet propSet(DBPROPSET_DBINIT); // Datenbankname, Benutzername und Kennwort hinzufügen if (pName != NULL) propSet.AddProperty(DBPROP_INIT_DATASOURCE, pName); if (pUserName != NULL) propSet.AddProperty(DBPROP_AUTH_USERID, pUserName); if (pPassword != NULL) propSet.AddProperty(DBPROP_AUTH_PASSWORD, pPassword); if (nInitMode) propSet.AddProperty(DBPROP_INIT_MODE, nInitMode); hr = spProperties->SetProperties(1, &propSet); if (FAILED(hr)) return hr; // Provider initialisieren return m_spInit->Initialize(); }

dsedit Auf der Website zu diesem Buch gibt es ein Online-Beispiel mit dem Namen »dsedit«. Es enthält den Code zum Öffnen und Bearbeiten einer Datenquelle. Um dieses Beispiel auszuprobieren, wählen Sie zunächst das Menüelement DATA SOURCE und anschließend OPEN USER PASSWORD. Abbildung 6.2 zeigt das daraufhin angezeigte Dialogfenster. Wählen Sie im Listenfeld oben den gewünschten Provider aus, geben Sie unter NAME den Server-Namen ein, unter USER NAME den Benutzernamen, unter PASSWORD das Kennwort und unter MODE den Initialisierungsmodus.

Sandini Bib 6.2

Datenquelle über die Programmierung öffnen

6.2.3

143

Datenquelle über ein Eigenschaften-Set öffnen

Das Datenquellenobjekt verfügt über Eigenschaften für die Initialisierung, die Datenquelle und die Datenquelleninformationen. Wie sich leicht erraten lässt, werden die Eigenschaften für die Initialisierung zum Öffnen der Datenquelle benötigt. Die Eigenschaften für die Datenquelle und die Datenquelleninformationen werden erst wichtig, wenn die Datenquelle bereits geöffnet ist. Wenden wir uns zunächst den Initialisierungseigenschaften zu (Tabelle 6.2).

Abbildung 6.2: Eine Datenquelle durch Angabe von Name, Benutzername und Kennwort öffnen

Symbolischer Wert

Bedeutung

DBPROP_INIT_DATASOURCE

Name der Datenquelle

DBPROP_AUTH_USERID

Benutzername

DBPROP_AUTH_PASSWORD

Kennwort

DBPROP_INIT_MODE

Öffnungsmodus

Tabelle 6.2: Grundlegende Initialisierungseigenschaften

Mit den beiden folgenden Funktionen können Sie eine Datenquelle über ein Eigenschaften-Set öffnen: HRESULT Open(const CLSID& clsid, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1) HRESULT Open(LPCTSTR szProgID, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1)

Sandini Bib 144

6

Datenquellen und Sitzungen

Die in Tabelle 6.2 aufgeführten Eigenschaften wurden bereits im vorigen Abschnitt behandelt. In diesem Abschnitt werden wir uns einigen weiteren häufig verwendeten Eigenschaften zuwenden. Die einzelnen Provider unterstützen jeweils nicht alle Eigenschaften, sondern nur diejenigen, die relevant für sie sind. Bei einem Datenspeicher vom Typ SQL Server oder anderen »ernsthaften« Datenspeichern müssen Sie sowohl den Namen der Maschine angeben, auf der die Datenbank gespeichert ist, als auch den Namen der betreffenden Datenbank. DBPROP_INIT_ DATASOURCE steht für den Namen der Maschine, während DBPROP_INIT_CATALOG die SQL-Datenbank bezeichnet. Der folgende Beispielcode öffnet die SQL-Database »database« auf dem Server »SERVER«. CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_AUTH_USERID, OLESTR("username")); dbinit.AddProperty(DBPROP_AUTH_PASSWORD, OLESTR("password")); dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR("database")); dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("SERVER")); CDataSource MyDataSource MyDataSource.Open (_T("SQLOLEDB.1"), &dbinit);

Zusätzlich stellt OLE DB die in Tabelle 6.3 aufgeführten Eigenschaften zur Verfügung. Symbolischer Wert

Beschreibung

DBPROP_AUTH_CACHE_AUTHINFO

Authentifizierung zwischenspeichern

DBPROP_AUTH_ENCRYPT_PASSWORD

Kennwort verschlüsseln

DBPROP_AUTH_MASK_PASSWORD

Kennwort maskieren

DBPROP_AUTH_PERSIST_ENCRYPTED

Verschlüsselung persistent machen

DBPROP_AUTH_PERSIST_SENSITIVE_AUTHINFO

Sicherheitsinformationen persistent machen

DBPROP_INIT_IMPERSONATION_LEVEL

Identitätswechselebene (Impersonation Level)

DBPROP_INIT_LCID

Bezeichner für die Länderkennung (Locale)

DBPROP_INIT_TIMEOUT

Zeitüberschreitung beim Herstellen einer Verbindung (Connect Timeout)

DBPROP_INIT_ASYNCH

Asynchrone Verarbeitung

Tabelle 6.3: Weitere Initialisierungseigenschaften

6.2.4

Datenquelle mit unvollständigen Informationen öffnen

In manchen Fällen ist das übergebene Eigenschaften-Set bewusst unvollständig. Sie können beispielsweise einen Benutzernamen übergeben, aber absichtlich das Kennwort oder den Namen des Servers auslassen. Sie können dann anfordern, dass der Provider beim Öffnen der Datenquelle ein Dialogfenster zur Angabe der fehlenden

Sandini Bib 6.2

Datenquelle über die Programmierung öffnen

145

Informationen durch den Benutzer einblendet. Dieses Verhalten kann über die Eigenschaft DBPROP_INIT_PROMPT angegeben werden. Die Eigenschaft kann die in Tabelle 6.4 aufgeführten Werte annehmen. Sie können auch das übergeordnete Element des vom Provider ggf. eingeblendeten Dialogfensters über die Eigenschaft DBPROP_INIT_HWND angeben. DBPROP_INIT_HWND ist keine erforderliche Eigenschaft. Wenn Sie diese Eigenschaft nicht setzen, gibt es kein übergeordnetes Element für dieses Dialogfenster. Wert

Bedeutung

DBPROMPT_PROMPT

Der Provider blendet immer ein Dialogfenster ein, auch wenn keine Informationen fehlen. Der Benutzer kann alle vorhandenen Eigenschaften ändern.

DBPROMPT_COMPLETE

Der Provider blendet nur dann ein Dialogfenster ein, wenn Informationen fehlen; Der Benutzer kann alle Eigenschaften ändern.

DBPROMPT_COMPLETEREQUIRED

Der Provider blendet nur dann ein Dialogfenster ein, wenn Informationen fehlen. Der Benutzer kann nur die fehlenden Eigenschaften bearbeiten.

DBPROMPT_NOPROMPT

Der Provider blendet kein Dialogfenster ein, selbst wenn Informationen fehlen. Ist dies der Fall, kann die Datenquelle nicht erfolgreich geöffnet werden.

Tabelle 6.4: Konstanten für die Eigenschaft DBPROP_INIT_PROMPT

6.2.5

Sicherheit

Die OLE DB-Sicherheit umfasst eine Authentifizierungs- und eine Schutzfunktion. Über die Authentifizierungsfunktion können Sie steuern, welche Benutzer Zugriff auf die Daten haben sollen. Die Schutzfunktion dient dazu, die Kommunikation zwischen dem Client und dem Datenspeicher abzusichern.

Authentifizierung Die einfachste Möglichkeit zur Authentifizierung eines Benutzers bietet die Verwendung eines Datenpaars (Benutzername und Kennwort). Dieses Verfahren weist jedoch die beiden folgenden Nachteile auf: 왘 Wenn Sie den Benutzernamen und das Kennwort im Quellcode fest codieren, müssen Sie den Quellcode schützen. Außerdem müssen Sie den Benutzernamen und das Kennwort zum Zeitpunkt der Kompilierung kennen. 왘 Wenn Sie den Benutzernamen und das Kennwort im Quellcode nicht fest codieren, müssen diese von anderer Seite bereitgestellt werden. Entweder müssen Sie vom Benutzer über ein Dialogfenster eingegeben oder irgendwo auf der Festplatte

Sandini Bib 146

6

Datenquellen und Sitzungen

gespeichert werden. Im einen Fall kann es für den Benutzer lästig werden, jedes Mal beim Öffnen einer Datenquelle sein Kennwort eingeben zu müssen, im anderen Fall muss ein auf der Festplatte gespeichertes Kennwort geschützt werden. Somit sind beide Fälle nicht ideal. Die Lösung besteht darin, generell ohne Benutzernamen und Kennwort zu arbeiten und auf den in Windows NT/2000 integrierten Sicherheitsmechanismus zurückzugreifen. Wenn sich ein Benutzer auf einem Rechner unter Windows NT/2000 anmeldet, muss er einen Anmeldenamen und ein Kennwort eingeben. Wenn diese Angaben gültig sind, kann er eine Sitzung in dem entsprechenden Betriebssystem starten. Der Dienst, über den die Windows-Sitzung gewährt oder verweigert wird, ist ein so genannter SSP-Dienst (Security Service Provider) – NTLM SSP und Kerberos sind zwei Beispiele für einen solchen Sicherheitsdienst. Die Schnittstelle, über die die einzelnen SSPDienste abgefragt werden können, wird SSPI genannt. Der Benutzer wird beim Start der Windows NT/2000-Sitzung authentifiziert. Wenn der OLE DB-Provider SSPI auch unterstützt, kann er zur Authentifizierung des Benutzers mit dem Client entsprechend kommunizieren. Alle hierbei erforderlichen Vorgänge laufen ohne Austausch eines Kennwortes ab. Über die Eigenschaft DBPROP_AUTH_INTEGRATED wird der Name der SSPI angegeben. Wenn Sie sie verwenden, berücksichtigt der Provider die anderen Authentifizierungseigenschaften nicht. Der Wert für diese Eigenschaft muss dann »SSPI« lauten. Der folgende Code zeigt, wie Sie die integrierte Sicherheitseigenschaft nutzen können: CDataSource db; CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_AUTH_INTEGRATED, "SSPI"); dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR("database")); dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("SERVER")); hr = db.Open(_T("SQLOLEDB.1"), &dbinit);

Dieser Mechanismus bietet mehrere Vorteile, aber er wird nicht von allen Providern unterstützt. Informieren Sie sich entsprechend. SQL Server unterstützt die integrierte Sicherheit.

Schutz und Verschlüsselung Sie können die Schutz- und Verschlüsselungsebene zwischen Client und Datenspeicher festlegen. Beachten Sie, dass es sich hierbei um einen Schutz auf COM-Ebene handelt, die Provider jedoch nicht in jedem Fall über diese Ebene mit dem Datenspeicher kommunizieren. Die Eigenschaft DBPROP_INITPROTECTION_LEVEL beschreibt die COMSchutzebene für die Kommunikation, die nicht über RPC (und nicht über COM) erfolgt. Für diese Eigenschaft kann einer der in Tabelle 6.5 aufgeführten Werte gesetzt werden.

Sandini Bib 6.3

Datenverknüpfungen und Initialisierungs-Strings

147

Symbolischer Wert

Beschreibung

DB_PROT_LEVEL_NONE

Keine Schutzebene

DB_PROT_LEVEL_CONNECT

Der aufrufende Client wird zum Zeitpunkt der Verbindungsherstellung authentifiziert.

DB_PROT_LEVEL_CALL

Der aufrufende Client wird bei jeder Anforderung authentifiziert.

DB_PROT_LEVEL_PKT

Alle vom Client kommenden Netzwerk-Datenpakete werden authentifiziert.

DB_PROT_LEVEL_PKT_INTEGRITY

Alle vom Client kommenden Netzwerk-Datenpakete werden authentifiziert, und es wird geprüft, ob sie während der Übertragung geändert wurden.

DB_PROT_LEVEL_PKT_PRIVACY

Wie DB_PROT_LEVEL_PKT_INTEGRITY. Zusätzlich werden noch alle Pakete verschlüsselt.

Tabelle 6.5: Konstanten für die Schutzebene

6.3 Datenverknüpfungen und Initialisierungs-Strings Im vorherigen Abschnitt haben wir gesehen, dass eine Datenquelle über eine CLSID und ein Eigenschaften-Set definiert wird. Was ist aber zu tun um diese Angaben so zu speichern, dass sie später wieder verwendet werden können? Natürlich können Sie eine eigene Serialisierungsmethode programmieren. OLE DB stellt jedoch eine bessere Möglichkeit zur Verfügung: Umwandlung der Datenquelle in einen UNICODE-String, den wir Initialisierungs-String nennen. Das UNICODE-Format zeichnet sich gegenüber dem ANSI-Format dadurch aus, dass es in jeder Sprache bearbeitet werden kann. In gewissem Sinne ähnelt ein Initialisierungs-String einem URL. Dieser Ansatz ist deshalb so gut, weil er inzwischen zum Standard geworden ist – andere Anwendungen verwenden dasselbe Speicherformat – und weil der Benutzer ihn versteht und bearbeiten kann. Eine Datenverknüpfung (Data Link) ist eine Datei, die einen Initialisierungs-String enthält. Die zugehörige Dateierweiterung lautet .UDL. Im Grunde ist eine Dateiverknüpfung eine Art Verweis in Dateiform, nämlich eine Datei, die anstelle der eigentlichen Informationen eine Angabe enthält, wo sich die Informationen befinden. Einen allgemein gültigen Standard zum Erstellen einer Dateiverknüpfung gibt es in den einzelnen Versionen von Windows und OLE DB nicht. Manchmal genügt es, im Windows Explorer durch Klicken mit der rechten Maustaste das Kontextmenü aufzurufen und die entsprechende Option zu wählen, in anderen Versionen steht diese Möglichkeit nicht zur Verfügung. Das sicherste Verfahren besteht darin, eine leere Datei zu erstellen und mit einer .UDL-Erweiterung zu versehen.

Sandini Bib 148

6

Datenquellen und Sitzungen

Wenn Sie auf die Datei doppelklicken, wird das Dialogfenster DATENLINKEIGENSCHAFTEN geöffnet, in dem Sie die Eigenschaften der Datenverknüpfung bearbeiten können. Abbildung 6.3 zeigt dieses Dialogfenster. Nachdem Sie das Dialogfenster DATENLINKEIGENSCHAFTEN geschlossen haben, können Sie sich die Datei in einem Texteditor (z.B. Notepad) ansehen. Der Inhalt sieht in etwa so aus: [oledb] ; Der gesamte Text nach dieser Zeile ist ein OLE DB-Initialisierungs-String Provider=Microsoft.Jet.OLEDB.3.51;Persist Security Info=False; Data Source=C:\ path\ file.mdb

Beachten Sie, dass einige Versionen von Notepad das UNICODE-Format nicht unterstützen. Daher müssen Sie zum Bearbeiten einer Datenverknüpfung einen mit UNICODE kompatiblen Editor verwenden.

Abbildung 6.3: Dialogfenster Datenlinkeigenschaften

Rufen Sie die Methode OpenFromInitializationString auf um eine Datenquelle über einen Initialisierungs-String zu öffnen:

Sandini Bib 6.3

Datenverknüpfungen und Initialisierungs-Strings

149

HRESULT OpenFromInitializationString (SLPCOLESTR szInitializationString)

Bevor Sie OpenFromInitializationString aufrufen können, müssen Sie die Datenquelle schließen, wenn sie zuvor bereits geöffnet war. Hierin unterscheidet sich diese Methode von den anderen Open-Methoden. Um den Initialisierungs-String einer bereits geöffneten Datenquelle abzurufen, setzen Sie folgenden Aufruf ab: HRESULT GetInitializationString(BSTR* pInitializationString, bool bIncludePassword=false)

wobei bIncludePassword angibt, ob das Kennwort in den Initialisierungs-String aufgenommen werden soll. Denken Sie daran, dass dies in der Voreinstellung nicht erfolgt. Um die Datenquelle über eine Datenverknüpfung zu öffnen, rufen Sie OpenFromFileName auf: HRESULT OpenFromFileName(LPCOLESTR szFileName)

Im folgenden Beispiel sind die drei soeben beschriebenen Operationen zusammengefasst. CDataSource DataSource; DataSource.OpenFromInitializationString(MyInitString); BSTR bstrInitString; DataSource.GetInitializationString (&bstrInitString); DataSource.OpenFromFileName(MyFileName);

Die Eigenschaft DBPROP_INIT_PROVIDERSTRING gibt den Initialisierungs-String an und ist für Provider-spezifische Eigenschaften vorgesehen.

Abbildung 6.4: Haupt-Dialogfenster des Online-Beispiels dsedit

dsedit Im Online-Beispiel dsedit wird der Verbindungs-String systematisch im Bearbeitungsfeld des Dialogfensters angezeigt (siehe Abbildung 6.4). Wenn keine Datenquelle vorhanden ist, lautet der Text »No data source«.

Sandini Bib 150

6

Datenquellen und Sitzungen

Wenn Sie in dsedit eine Datenquelle über einen Initialisierungs-String öffnen wollen, müssen Sie diesen im Bearbeitungsfeld eingeben und auf die Schaltfläche OPEN FROM INITIALIZATION STRING klicken. Der Initialisierungs-String wird hier angezeigt, sobald Sie eine Datenquelle mit einer beliebigen Methode öffnen.

6.3.1

Verbindungs-String speichern

Es gibt eine Reihe von Möglichkeiten zum Speichern des Verbindungs-Strings. Der erste Gedanke, den String fest zu codieren, ist die schlechteste Lösung. In diesem Fall müsste der Code bei jeder Änderung des Verbindungs-Strings neu kompiliert werden. Die zweite Lösung besteht darin, den Verbindungs-String in einer Datenverknüpfungsdatei zu speichern. Dies bietet mehr Flexibilität, weil Sie den String ohne Neukompilierung des Codes ändern können. Allerdings hat auch diese Lösung Nachteile. Erstens kann es sein, dass der Vorgang durch das Lesen der Datenverknüpfungsdatei zu langsam ist, wenn die Datenquelle häufig geöffnet werden muss. Zweitens müssen Sie nach wie vor die Dateien und ihren Speicherort verwalten. Dieses Verfahren ist zwar machbar, empfiehlt sich aber nicht. Bei der dritten Lösung wird der Verbindungs-String in der Registrierung gespeichert. Dies bedeutet, dass das Risiko eines versehentlichen Löschens durch einen Benutzer sehr viel geringer ist. Wenn Sie mit der Registrierung arbeiten, müssen Sie im ersten Schritt einen Pfad für den Schlüssel wählen. Dann müssen Sie sicherstellen, dass jede Komponente auf den richtigen Registrierungswert zugreift und dass kein Pfadkonflikt besteht. Ferner muss dokumentiert werden, wo die einzelnen Komponenten ihren Verbindungs-String speichern. Sie können dies vermeiden, wenn Sie einen Konstruktions-String für COM+-Objekte verwenden, auf den alle Exemplare einer bestimmten COM+-Klasse zugreifen können. Der COM+-Katalog verwaltet den Objektkonstruktions-String, so dass Sie sich nicht um einen Pfad kümmern müssen. Außerdem bietet er eine Benutzerschnittstelle zum Ändern des Konstruktions-Strings. Dies bedeutet, dass der Systemadministrator den Speicherort der Datenquelle im COM+-Katalog ändern kann. Zum Zeitpunkt seiner Erzeugung liest das Exemplar den Konstruktions-String und verwendet ihn zum Öffnen der Datenquelle. Auf der Website zu diesem Buch wird dieses Thema ausführlicher erläutert.

6.4 Datenquelle interaktiv öffnen In den vorherigen Abschnitten haben wir uns damit befasst, wie eine Datenquelle über die Programmierung geöffnet werden kann. In diesem Fall bestimmt der Entwickler, wie diese Vorgänge ablaufen. Alternativ können Sie aber auch dem Benutzer gestatten, die Datenquelle über ein Dialogfenster zu öffnen.

Sandini Bib 6.4

Datenquelle interaktiv öffnen

151

Für OLE DB sind die beiden Dialogfenster DATENLINKEIGENSCHAFTEN und DATENVERKNÜPFUNGSDATEI AUSWÄHLEN vorgesehen.

6.4.1

Das Dialogfenster DATENLINKEIGENSCHAFTEN

Über das Dialogfenster DATENLINKEIGENSCHAFTEN können Sie die CLSID interaktiv abrufen und das Eigenschaften-Set erstellen (siehe Abbildung 6.5). Auf der ersten Registerkarte wählen Sie den Provider und damit die CLSID aus. Auf der zweiten und der dritten Registerkarte wählen Sie Eigenschaften für das Eigenschaften-Set. Die vierte Registerkarte zeigt dann alle gewählten Eigenschaften im Überblick. Rufen Sie folgende Funktion auf, wenn die Datenquelle über das Dialogfenster DATENLINKEIGENSCHAFTEN geöffnet werden soll: HRESULT Open(HWND hWnd = GetActiveWindow(), DBPROMPTOPTIONS dwPromptOptions= DBPROMPTOPTIONS_WIZARDSHEET)

Wenn der Benutzer auf ABBRECHEN klickt, liefert die Methode den Wert DB_E_CANCELED.

Abbildung 6.5: Dialogfenster Datenlinkeigenschaften

Sandini Bib 152

6

Datenquellen und Sitzungen

Abbildung 6.6: Dialogfenster Datenverknüpfungsdatei auswählen

6.4.2

Das Dialogfenster Datenverknüpfungsdatei auswählen

Im Dialogfenster DATENVERKNÜPFUNGSDATEI AUSWÄHLEN kann der Benutzer eine Datenverknüpfung (.udl) wählen und die Datenquelle über den darin enthaltenen Verbindungs-String öffnen. Abbildung 6.6 veranschaulicht diesen Vorgang. Wenn eine Datenquelle über dieses Dialogfenster geöffnet werden soll, müssen Sie folgende Funktion aufrufen: HRESULT OpenWithPromptFileName(HWND hWnd = GetActiveWindow(), DBPROMPTOPTIONS dwPromptOptions = DBPROMPTOPTIONS_NONE, LPCOLESTR szInitialDirectory = NULL)

dsedit Im Online-Beispiel dsedit müssen Sie zunächst DATA SOURCE und anschließend OPEN FROM DIALOG wählen.

6.5 Dienstkomponenten und Ressourcen-Pooling Bisher bestand das Erzeugen des Datenquellenobjekts darin, die Methode CoCreateInstance mit der geeigneten CLSID aufzurufen. In manchen Fällen reicht jedoch die von der Datenquelle zur Verfügung gestellte Funktionalität nicht aus und Sie müssen eine Datenquelle in einer Teile/Ganzes-Beziehung zusammen mit einer Dienstkomponente verwenden. OLE DB stellt die drei folgenden Dienstkomponenten zur Verfügung:

Sandini Bib 6.5

Dienstkomponenten und Ressourcen-Pooling

153

왘 Ressourcen-Pooling (Resource Pooling) ist eine Dienstkomponente für das Datenquellenobjekt. 왘 Transaktionseinbindung (Transaction Enlistment) ist eine Dienstkomponente für das Sitzungsobjekt. 왘 Client-Cursor (Client Cursor) ist eine Dienstkomponente für das ZeilenmengenObjekt. Beachten Sie, dass Transaktionseinbindungen Ressourcen-Pooling erfordern. Wenn eine Datenquelle mit Dienstkomponenten geöffnet werden soll, müssen Sie eine der folgenden Funktionen aufrufen: HRESULT OpenWithServiceComponents(const CLSID& clsid, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1) HRESULT OpenWithServiceComponents(LPCTSTR szProgID, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1)

Die Eigenschaft DBPROP_INIT_OLEDBSERVICES legt die zu verwendenden Dienstkomponenten fest. Sie stellt eine Kombination aus den in Tabelle 6.6 aufgeführten Werten dar. Wenn Sie alle Dienstkomponenten verwenden wollen, müssen Sie die Eigenschaften auf den Wert DBPROPVAL_OS_ENABLEALL setzen. Wenn Sie die Eigenschaft DBPROP_INIT_OLEDBSERVICES nicht setzen, verwendet OLE DB zur Laufzeit eine Standardkombination der Dienstkomponenten. Dieser Standardwert ist im Registrierungswert OLEDB_SERVICES unterhalb des Registrierungsschlüssels des CLSIDProviders gespeichert. Abbildung 6.7 zeigt, welche Dienste der Provider für SQL Server in der Voreinstellung verwendet. In diesem Fall sind dies alle verfügbaren Dienste. Symbolischer Wert

Bedeutung

DBPROPVAL_OS_RESOURCEPOOLING

Verwendung des Ressourcen-Poolings

DBPROPVAL_OS_TXNENLISTMENT

Verwendung der Transaktionseinbindung

DBPROPVAL_OS_CLIENTCURSOR

Verwendung des Client-Cursors

DBPROPVAL_OS_ENABLEALL

Verwendung aller Dienstkomponenten

Tabelle 6.6: Konstanten für Dienstkomponenten

Abbildung 6.7: Registrierungswert des Providers für SQL Server

Sandini Bib 154

6

Datenquellen und Sitzungen

Die beiden Methoden vom Typ OpenWithServiceComponents ähneln der bisher behandelten, »normalen« Open-Methode in hohem Maße. Allerdings ist die Implementierung unterschiedlich, denn mit OpenWithServiceComponents erfolgt kein direkter Aufruf von CoCreateInstance, sondern es wird zunächst ein Objekt erzeugt, das die Schnittstelle IDataInitialize unterstützt und dafür die Methode CreateDBInstance aufruft: CComPtr spDataInit; HRESULT hr; hr = CoCreateInstance(CLSID_MSDAINITIALIZE, NULL, CLSCTX_INPROC_SERVER, IID_IDataInitialize, (void**)&spDataInit); if (FAILED(hr)) return hr; m_spInit.Release(); hr = spDataInit->CreateDBInstance(clsid, NULL, CLSCTX_INPROC_SERVER, NULL, IID_IDBInitialize, (IUnknown**)&m_spInit); if (FAILED(hr)) return hr; // ..weiterer Code

Die Methode CreateDBInstance hat die gleiche Funktion wie CoCreateInstance, mit der Ausnahme, dass sie zunächst einen Komponenten-Manager erzeugt und die Datenquellenobjekte in einer Teile/Ganzes-Beziehung kombiniert. Abbildung 6.8 zeigt den Unterschied zwischen Open und OpenWithServiceComponents. Dies ist nur eine ungefähre Darstellung, denn die tatsächliche Implementierung von CreateDBInstance wurde von Microsoft nicht dokumentiert. Sie kann daher von der hier gezeigten Implementierung abweichen, vom Prinzip her gibt es jedoch keinen Unterschied. CoCreateInstance

Datenquelle Open CoCreateInstance

CoCreateInstance

init

CoCreateInstance

Dienstkomponente

Datenquelle

OpenWithServiceComponents

Abbildung 6.8: Die Methoden Open und OpenWithServiceComponents im Vergleich

Die Datenquellen-Dienstkomponente verwaltet auch die Erzeugung der SitzungsDienstkomponente. Wenn keine andere Dienstkomponente angegeben ist, fängt die Datenquellen-Dienstkomponente die Erzeugung des Sitzungsobjekts ab und leitet die-

Sandini Bib 6.5

Dienstkomponenten und Ressourcen-Pooling

155

ses einfach an die Datenquelle weiter. Wenn Sie jedoch Transaktionseinbindung angegeben haben, erzeugt die Datenquellen-Dienstkomponente zunächst eine SitzungsDienstkomponente. Anschließend leitet sie die Sitzungserzeugung an die Datenquelle weiter und übergibt die Sitzungs-Dienstkomponente als äußeres Objekt. Daraus ergibt sich eine Teile/Ganzes-Beziehung zwischen der Sitzung und der Sitzungs-Dienstkomponente. In Abbildung 6.9 ist dieser Mechanismus zusammengefasst, der auch für die Zeilenmengen-Dienstkomponente und die Client-Cursor-Dienstkomponente für Zeilenmengen gilt. Grundsätzlich lässt sich sagen, dass Sie (1) angeben, welche Dienstkomponente beim Erzeugen der Datenquelle verwendet werden soll, und (2) sich nach der Erzeugung der Datenquelle nicht mehr darum zu kümmern brauchen. Im Allgemeinen ist es immer besser, mit Dienstkomponenten zu arbeiten. Dass für die Methode Open keine Dienstkomponenten zur Verfügung stehen, ist ein einigermaßen unglücklicher Umstand, denn viele Programmierer verwenden sie und wissen gar nicht, dass es auch eine Methode zum Öffnen von Datenquellen mit Dienstkomponenten gibt. Die Tatsache, dass der Assistent den Code mit Open öffnet, ist hier auch nicht sehr hilfreich. Ich persönlich würde es vorziehen, wenn die Methode Open Dienstkomponenten hätte und es zusätzlich eine Version dieser Methode gäbe, z.B. OpenWithNoServiceComponent, die explizit ohne Dienstkomponenten arbeitet. Dies würde für Klarheit bei den Programmierern sorgen. Die folgenden Open-Methoden verwenden Dienstkomponenten: 왘 OpenFromFileName 왘 OpenFromInitializationString 왘 Open(HWND hWnd = GetActiveWindow(),... 왘 OpenWithPromptFileName

Dienstkomponente

Datenquelle

(1) Dienstkomponente

Dienstkomponente

(2) Sitzung

Zeilenmenge

Abbildung 6.9: Dienstkomponenten für Datenquellen, Sitzungen und Zeilenmengen

Sandini Bib 156

6.5.1

6

Datenquellen und Sitzungen

Ressourcen-Pooling

Bei der Verwendung von Ressourcen-Pooling werden die nicht verwendeten Ressourcen nicht systematisch freigegeben, sondern zwischengespeichert. Es handelt sich hierbei um transiente Objekte, deren Erzeugung aufwändig ist, z.B. Datenbankverbindungen, Netzwerkverbindungen und große Speicherblöcke. In OLE DB sind die wichtigsten Ressourcen natürlich Datenbankverbindungen und Sitzungen. Das Ressourcen-Pooling für Datenquellen (siehe Abbildung 6.10) funktioniert folgendermaßen: 1. Der Client fordert ein Datenquellenobjekt vom Provider an. Der Dienst für das Ressourcen-Pooling fängt die Anforderung ab und liefert ein Proxy-Datenquellenobjekt als »Stellvertreter« für das eigentliche Datenquellenobjekt zurück. Aus der Sicht des Clients verhält sich das Proxy-Objekt wie ein reguläres Datenquellenobjekt. Zu diesem Zeitpunkt ist jedoch kein »echtes« Datenquellenobjekt vorhanden. 2. Wenn der Client das Proxy-Datenquellenobjekt initialisiert, sucht der Dienst für das Ressourcen-Pooling im Ressourcen-Pool nach dem entsprechenden Objekt. Dieses ist eine Datenquelle, deren Eigenschaften mit den angeforderten übereinstimmen, die also beispielsweise den gleichen Server-Namen und die gleiche Benutzer-ID aufweist. Wenn kein solches Objekt im Ressourcen-Pool vorhanden ist, wird ein neues »echtes« Datenquellenobjekt erzeugt. Gibt es jedoch ein solches Objekt bereits, wird es vom Proxy-Datenquellenobjekt verwendet, und die Datenquelle wird aus dem Pool entfernt. 3. Bei der Freigabe des Proxy-Datenquellenobjekts wird die »echte« Datenquelle nicht gelöscht, sondern zurück in den Ressourcen-Pool gestellt. 4. Der Ressourcen-Pool prüft regelmäßig die zwischengespeicherten Objekte. Objekte, die für einen bestimmten Zeitraum nicht verwendet wurden, werden freigegeben.

ProxyDatenquelle Datenquellenverbindung

Datenquellenverbindung Datenquellenverbindung Datenquellenverbindung

Abbildung 6.10: Ressourcen-Pooling

Sandini Bib 6.6

Enumeratoren

157

Ressourcen-Pooling ist ein sehr effektiver Mechanismus, wenn Datenquellen, deren Erzeugung aufwändig ist, häufig erzeugt und gelöscht werden müssen. Wenn er aktiviert ist, genießen Sie sogar den Vorteil, dass Sie Datenquellenobjekte erzeugen und löschen können, ohne sich Gedanken über eventuelle Leistungseinbußen machen zu müssen, da der Datenquellen-Pool die Vorgänge entsprechend steuert.

6.5.2

Datenquelle schließen

Mit der folgenden Methode wird die Datenquelle geschlossen: void Close()

Beachten Sie, dass Sie die Datenquelle nicht explizit zu schließen brauchen. Sobald das Datenquellenobjekt den Gültigkeitsbereich verlassen hat, wird es durch den intelligenten Zeiger automatisch freigegeben. Zudem brauchen Sie die Datenquelle vor einem erneuten Öffnen nicht explizit zu schließen, da dies von den meisten Open-Methoden automatisch mit erledigt wird.

6.6 Enumeratoren In den vorherigen Abschnitten haben wir uns mit dem Öffnen von bekanntermaßen vorhandenen Datenquellen beschäftigt. Wie können Sie jedoch am Anfang feststellen, welche Provider auf einer Maschine installiert sind? An dieser Stelle kommen Enumeratoren ins Spiel. Sie dienen zum Aufzählen von Objekten. Der Ausgangspunkt für eine Aufzählung ist der Stamm-Enumerator, der mit MDAC geliefert wird. Dieser erstellt eine Aufzählung der einzelnen auf der Maschine installierten Provider, so dass die Datenquelle für den angeforderten Provider geöffnet werden kann. Ferner zählt er Provider-Enumeratoren auf, die ihrerseits zur Aufzählung von Provider-spezifischen Ressourcen dienen. Beispielsweise zählt der ODBC-Provider-Enumerator die auf der Maschine installierten ODBC-Datenquellen auf. Abbildung 6.11 zeigt einen Stamm-Enumerator mit zwei Providern und einem Provider-Enumerator, der seinerseits zwei weitere Elemente aufzählt. Enumeratoren werden durch die beiden folgenden Klassen dargestellt: class CEnumeratorAccessor class CEnumerator : public CAccessorRowset

Sie sehen, dass ein Enumerator vom Typ her auch eine Zeilenmenge ist. Machen Sie sich jedoch keine Sorgen, zum Umgang mit Enumeratoren brauchen Sie zum jetzigen Zeitpunkt nur minimale Kenntnisse über Zeilenmengen. Diese werden Sie in Kapitel 8 erweitern können.

Sandini Bib 158

6

Datenquellen und Sitzungen

Stamm-Enumerator

Provider

ProviderEnumerator

Provider

Abbildung 6.11: Enumeratoren und Datenquellen

In Abbildung 6.12 ist die Vererbung der Klasse CEnumerator im Überblick dargestellt. Als Implementierungsklasse für CEnumerator steht CEnumeratorAccessor zur Verfügung, daher brauchen Sie CEnumerator nicht direkt zu verwenden. CEnumeratorAccessor weist die folgenden fünf Attribute auf: 왘 WCHAR m_szName[129]: Name der Datenquelle bzw. des Enumerators. 왘 WCHAR m_szParseName[129]: String, der an IParseDisplayName übergeben wird, um eine »Unterschrift« der Datenquelle bzw. des Enumerators abzurufen. Hierbei handelt es sich um eine Darstellung der CLSID als Zeichenfolge. 왘 WCHAR m_szDescription[129]: Beschreibung der Datenquelle bzw. des Enumerators. 왘 USHORT m_nType: Typ der Datenquelle bzw. des Enumerators. Dieses Attribut kann nur einen der in Tabelle 6.7 aufgeführten Werte annehmen. 왘 VARIANT_BOOL m_bIsParent: Variable, die, wenn es sich bei der Zeile um einen Enumerator handelt, angibt, ob der Enumerator ein übergeordneter Enumerator ist. Anders ausgedrückt, wenn m_bIsParent den Wert TRUE aufweist, gehören die Attribute zum Enumerator selbst. Ist der Wert FALSE, gehören die Attribute zum aktuellen untergeordneten Objekt (Kindobjekt). Der Wert für dieses Attribut lautet in der Regel FALSE, was bedeutet, dass der Enumerator seine untergeordneten Objekte aufzählt und selbst nicht in der Aufzählung enthalten ist.

CEnumeratorAccessor

CEnumerator

Abbildung 6.12: Vererbung von CEnumerator

CRowset

Sandini Bib 6.6

Enumeratoren

159

Symbolischer Wert

Wert

Bedeutung

DBSOURCETYPE_DATASOURCE

1

Das Objekt ist eine Datenquelle.

DBSOURCETYPE_ENUMERATOR

2

Das Objekt ist ein Enumerator.

DBSOURCETYPE_DATASOURCE_MDP

3

Das Objekt ist eine mehrdimensionale Datenquelle.

Tabelle 6.7: Konstanten für Datenquellen bzw. Enumeratoren

6.6.1

Der Stamm-Enumerator

Wie bereits erwähnt, ist der Stamm-Enumerator der Ausgangspunkt einer Aufzählung. Um einen Stamm-Enumerator abzurufen, setzen Sie einen Aufruf der Methode Open ab: HRESULT Open(const CLSID* pClsid = &CLSID_OLEDB_ENUMERATOR)

Als Standardparameter fungiert die CLSID des Stamm-Enumerators. Programmieren Sie beispielsweise das Öffnen des Stamm-Enumerators folgendermaßen: CEnumerator RootEnumerator RootEnumerator.Open();

Bewegen Sie dann den Cursor durch die einzelnen Zeilen des Enumerators.

6.6.2

Cursorbewegung im Enumerator

Durch Verwendung von Zeilenmengen können Listen mit Objekten effektiv durchlaufen werden. Für solche Operationen sind jedoch nur die beiden folgenden Methoden wesentlich: 왘 MoveFirst: Der Cursor wird auf das erste untergeordnete Objekt gesetzt. 왘 MoveNext: Der Cursor wird auf das nächste untergeordnete Objekt gesetzt. MoveNext liefert jedes Mal, wenn eine neue Zeile erfolgreich abgerufen werden kann, den Wert S_OK zurück. Wenn keine weiteren Zeilen vorhanden sind, liefert die Methode den Wert DB_S_ENDOFROWSET zurück. Denken Sie daran, dass dieser Wert als »Erfolgsmeldung« anzusehen ist, d.h. SUCCEEDED(DB_S_ENDOFROWSET) ist dann auf TRUE gesetzt.

Eine Aufzählung von untergeordneten Objekten sieht folglich so aus: CEnumerator Enumerator; // initialisierter Enumerator HRESULT hr = Enumerator.MoveFirst(); while (hr == S_OK) { // Operationen mit dem Enumerator; hr = Enumerator.MoveNext(); }

Sandini Bib 160

6

Datenquellen und Sitzungen

Ersetzen Sie die Schleifenbedingung nicht durch die Klausel while (SUCCEEDED(hr)), da sonst eine unendliche Schleife entsteht.

6.6.3

Datenquellen und Enumeratoren

Einer der Hauptzwecke eines Enumerators ist es, Datenquellen zu öffnen. Da ein Enumerator jedoch nur die CLSID eines Providers aufzählt, ist es zusätzlich notwendig, ein Eigenschaften-Set anzugeben. Die Klasse CDataSource bietet eine Open-Methode, die einen Enumerator und ein Eigenschaften-Set akzeptiert: HRESULT Open(const CEnumerator& enumerator, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1)

Intern ruft diese Methode die OLE DB-Datenquelle aus den Enumerator-Informationen ab und setzt die Eigenschaften. Alternativ können ein Name, ein Benutzername, ein Kennwort und ein Modus übergeben werden, die dann schließlich in ein Eigenschaften-Set konvertiert werden: HRESULT Open(const CEnumerator& enumerator, LPCTSTR pName, LPCTSTR pUserName = NULL, LPCTSTR pPassword = NULL, long nInitMode = 0)

Der folgende Beispielcode zeigt, wie Datenquellen mit Hilfe von Enumeratoren geöffnet werden können: CEnumerator RootEnumerator RootEnumerator.Open(); CDataSource DataSource1; CDataSource DataSource2; // Eigenschaften-Set abrufen // Die Datenquelle über den ersten aufgezählten Provider öffnen RootEnumerator.MoveFirst(); DataSource1.Open(RootEnumerator, &PropSet); // Die Datenquelle über den zweiten aufgezählten Provider öffnen RootEnumerator.MoveNext(); DataSource2.Open(RootEnumerator, &PropSet);

6.6.4

Provider-Enumeratoren

Sie können einen Provider-Enumerator mit seinem übergeordneten Enumerator als Parameter in der Methode Open öffnen. HRESULT Open(const CEnumerator& enumerator) CEnumerator RootEnumerator; CEnumerator ProviderEnumerator; // übergeordneten Enumerator öffnen ProviderEnumerator.Open(RootEnumerator);

Sandini Bib 6.7

Erweiterungen für Datenquellen

161

dsedit Wählen Sie im Online-Beispiel dsedit nacheinander BROWSE und ENUMERATOR. Abbildung 6.13 zeigt das Dialogfenster für Enumeratoren. Beachten Sie, dass für den Stamm-Enumerator ein Provider-Enumerator mit dem Namen MSDASQL vorhanden ist (MSDASQL ist ein anderer Name für ODBC), der die einzelnen ODBC-Datenquellentypen aufzählt.

Abbildung 6.13: Dialogfenster für Enumeratoren

6.7 Erweiterungen für Datenquellen Wir haben uns bisher mit der Klasse CDataSource beschäftigt. In den OLE DB-Erweiterungen (OLE DB Extensions) finden Sie auch zu dieser Klasse eine Erweiterung, nämlich die Klasse CDataSourceEx.

6.7.1

Verbindungs-String in einer Datei speichern

Mit CDataSource können Sie zwar eine Datenquelle über eine .udl-Datei öffnen, die einen Verbindungs-String enthält, das Speichern des Verbindungs-Strings einer Datenquelle in einer Datei gestattet diese Klasse jedoch nicht. CDataSourceEx schafft hier mit der Methode WriteStringToStorage Abhilfe: HRESULT WriteStringToStorage (LPCOLESTR pwszFileName, DWORD dwCreationDisposition = CREATE_NEW, bool bIncludePassword=false)

Die Parameter bedürfen keiner Erläuterungen.

Sandini Bib 162

6.7.2

6

Datenquellen und Sitzungen

Datenquelle bearbeiten

Über die Klasse CDataSourceEx können Sie eine Datenquelle interaktiv über das Dialogfenster DATENLINKEIGENSCHAFTEN bearbeiten: HRESULT Edit(HWND hWnd = GetActiveWindow(), DBPROMPTOPTIONS dwPromptOptions = DBPROMPTOPTIONS_WIZARDSHEET, LPCOLESTR pwszszzProviderFilter = NULL)

Diese Methode ähnelt zwar der Methode Open, weist aber die folgenden Unterschiede auf: 왘 Die Datenquelle muss geöffnet sein, bevor Sie Edit aufrufen können. 왘 Das Dialogfenster wird mit dem Datenquellentyp und den Eigenschaften der Datenquelle initialisiert. 왘 Wenn Sie auf ABBRECHEN klicken, liefert die Methode nicht den Wert S_OK. Die Datenquelle ist jedoch nach wie vor geöffnet.

dsedit Wählen Sie im Online-Beispiel nacheinander EDIT und EDIT NO BROWSE. Daraufhin wird das in Abbildung 6.14 gezeigte Dialogfenster eingeblendet.

Abbildung 6.14: Dialogfenster Datenlinkeigenschaften zum Bearbeiten einer Datenquelle

Sandini Bib 6.8

Sitzungen

6.7.3

163

Die CLSID abrufen

Eine Datenquelle wird über eine CLSID und ein Eigenschaften-Set bestimmt. Mit den OLE DB Consumer Templates können Sie zwar die Eigenschaften abrufen, nicht aber die CLSID. Die Klasse CDataSourceEx ermöglicht Ihnen das Abrufen der CLSID über die folgende Methode: HRESULT STDMETHODCALLTYPE GetClassID(CLSID *pClassID) { CComPtr spPersist; // Überprüfen, ob die Verbindung hergestellt ist ATLASSERT(m_spInit != NULL); HRESULT hr = m_spInit->QueryInterface(&spPersist); if (FAILED(hr)) return hr; hr = spPersist->GetClassID(pClassID); return hr; }

6.8 Sitzungen Datenquellen erzeugen keine Befehls- und keine Zeilenmengen-Objekte. Es wird ein »Vermittlungsobjekt« zwischen diesen und anderen OLE DB-Objekten, d.h. ein Sitzungsobjekt (Session) benötigt. Die Datenquelle gibt nur an, wie auf die Daten zugegriffen werden kann. Demgegenüber stellt die Sitzung eine tatsächliche Verbindung zum Datenspeicher dar. Beachten Sie jedoch, dass Datenquellen Sitzungen vorzeitig erzeugen und zwischenspeichern können. Das Sitzungsobjekt unterstützt Transaktionen. Selbst wenn Sie keine Transaktionen verwenden wollen, müssen Sie ein Sitzungsobjekt erzeugen. Datenquellen können mehrere Sitzungen erzeugen, es empfiehlt sich aber, je Datenquelle nur eine zu erzeugen, da eventuelle Optimierungen nur für die erste Sitzung gelten. Die Klasse CSession in den OLE DB Consumer Templates kapselt die OLE DBSitzung. Ein typischer Code für einen OLE DB-Consumer sieht folgendermaßen aus: CDataSource DataSource; DataSource.Open (…); CSession Session; Session.Open(DataSource); //Über die Sitzung weitere OLE DB-Objekte erzeugen.

In Kapitel 12 werden Sitzungen und Transaktionen ausführlicher behandelt.

Sandini Bib 164

6

Datenquellen und Sitzungen

6.9 Zusammenfassung In diesem Kapitel haben Sie erfahren, was eine Datenquelle ist und warum dieses Objekt so wichtig ist. Wir haben außerdem die verschiedenen Möglichkeiten zum Öffnen einer Datenquelle untersucht. Als wichtigen Punkt sollten Sie im Gedächtnis behalten, dass es an der Datenquelle liegt, ob Dienstkomponenten verwendet werden oder nicht. Dieser Umstand ist beim Arbeiten mit Zeilenmengen und Transaktionen wichtig. Abschließend haben wir uns kurz mit Sitzungen beschäftigt. Jetzt ist es an der Zeit, die anderen OLE DB-Objekte unter die Lupe zu nehmen.

Sandini Bib

7 Tabellen und Befehle OLE DB stellt zwei Konzepte zum Zugriff auf Daten zur Verfügung: Tabellen und Befehle. Tabellen bieten das einfachere Verfahren. Sie entsprechen den Tabellen im zugrunde liegenden Datenspeicher. Wenn Sie eine Tabelle öffnen, rufen Sie alle darin enthaltenen Zeilen und Spalten in der im Datenspeicher definierten Reihenfolge ab. Befehle sind etwas komplexer. Einfach ausgedrückt ist ein Befehl ein Objekt, das mit einer Execute-Methode ausgeführt werden kann. Die auszuführende Aktion kann über einen Befehlstext oder über einen anderen unterstützten Mechanismus definiert werden. In der Praxis unterstützen Befehle jedoch Befehlstexte, die in den meisten Fällen SQL-Anweisungen entsprechen. Optional können Befehle vorbereitet werden und Parameter aufweisen. Im Abschnitt über Befehle wird dies näher erläutert.

7.1 Soll eine Tabelle oder ein Befehl verwendet werden? Beim Zugriff auf Daten ist als Erstes zu entscheiden, ob Tabellen oder Befehle verwendet werden sollen. Eine Tabelle hat folgende Vorteile: 왘 Sie ist einfach zu verwenden. Bei Tabellen gibt es keinen SQL-Text, sondern nur einen Tabellennamen, daher sind Syntaxfehler ausgeschlossen. Zudem ist die Implementierungsklasse für Tabellen, CTable, sehr kurz und einfach aufgebaut. Daher ist auch das Debugging sehr einfach. 왘 Das Öffnen einer Tabelle erfolgt meistens auch schneller als die Ausführung eines Befehls, da kein Befehlstext benötigt wird. Ferner brauchen keine OLE DB-Objekte erzeugt zu werden. Dieser scheinbare Leistungsvorteil kann jedoch auch irreführend sein, weil die OLE DB-Provider beim Öffnen einer Tabelle unbemerkt im Hintergrund einen Befehl ausführen. Wenn Sie beispielsweise die Tabelle X öffnen, führt der Provider etwa den Befehl SELECT * FROM X aus. Mit anderen Worten hängt der Leistungsaspekt vom zugrunde liegenden Provider ab. Eine Tabelle hat auch folgende Nachteile: 왘 Es gibt keine Möglichkeit zur Angabe der Spaltenreihenfolge. Die Spalten erscheinen in der Reihenfolge, die im zugrunde liegenden Datenspeicher definiert ist, was

Sandini Bib 166

7

Tabellen und Befehle

Überraschungen hervorrufen kann. Beispiel: Ein Programmierer schreibt eine Komponente ausgehend von einer Tabelle mit zwei Spalten und während des Tests scheint alles in Ordnung zu sein. Nach der Bereitstellung kann es jedoch sein, dass die Tabelle mit vertauschter Reihenfolge der Spalten neu erstellt wird. Aus der Datenbankperspektive ist dieser Vorgang ohne weiteres gültig, aber die Komponente wird dadurch nicht mehr verwendbar, da sie nichts von den vertauschten Spalten weiß. 왘 Eine Tabelle stellt alle Zeilen des zugrunde liegenden Datenspeichers dar, so dass die Zeilen in der dort gespeicherten Reihenfolge erscheinen. Bei einer OLE DBTabelle gibt es jedoch keine Möglichkeit, beim Öffnen bestimmte Zeilen auszuschließen oder die Zeilen in eine andere Reihenfolge zu stellen. Allerdings stellt sie das Sichtobjekt (View) zur Verfügung, über das eine offene Zeilenmenge gefiltert und sortiert werden kann. Sichten werden in Kapitel 13 ausführlicher behandelt. Es sieht so aus, als ob die Nachteile überwiegen. Welchen Grund sollte es also geben, zunächst einmal eine Tabelle zu verwenden? Es gibt sogar drei mögliche Gründe: 왘 Sie haben die Steuerung über den Datenspeicher in Ihren Händen und können mit einer Zugriffsobjekt-Klasse (Accessor-Klasse) arbeiten, die über Spaltennamen auf Zeilenwerte zugreifen kann. Sichten geben Ihnen zudem die Möglichkeit, erweiterte Operationen durchzuführen. 왘 Sie wollen keinen zu großen Aufwand treiben und Tabellen genügen für Ihre Zwecke. 왘 Der Provider unterstützt keine Befehle und Sie haben daher keine andere Wahl. Wenn keiner dieser Gründe zutrifft, sollten Sie Befehle verwenden, da diese eine höhere Entwicklungsstufe als Tabellen darstellen: 왘 Sie sind in der Lage, komplexe Befehlstexte wie z.B. SQL-Anweisungen zu unterstützen. 왘 Sie können in vorbereitetem oder unvorbereitetem Zustand verwendet werden. 왘 Sie sind in der Lage, Parameter zu unterstützen. 왘 Sie sind in der Lage, Mehrfachergebnisse zu unterstützen.

7.2 Tabellen Die Klasse CTable kapselt Tabellen. Sie ist folgendermaßen definiert: template class CTable : public CAccessorRowset

Sandini Bib 7.2

Tabellen

167

Die erste Aufgabe besteht darin, die Zugriffsobjekt-Klasse (TAccessor) und die Zeilenmengen-Klasse (TRowset) auszuwählen. Per Voreinstellung ist CRowset die Zeilenmengen-Klasse, die sich für Objekte vom Typ Zeilenmenge (Rowset) empfiehlt. Obwohl TAccessor die Klasse CNoAccessor als Standardparameter verwendet, ist es nicht sinnvoll, diesen Standardparameter zu verwenden, weil eine Tabelle immer ein Zugriffsobjekt benötigt. Außerdem wird bei Verwendung aller Standardparameter wie im Code CTable test;

der folgende Kompilierungsfehler generiert: c:\ program files\ microsoft visual studio\ vc98\ atl\ include\ atldbcli.h(2818) : error C2664: 'SetAccessor' : cannot convert parameter 1 from 'class ATL::CAccessorRowset *const ' to 'class ATL::CAccessorBase *'

Grundsätzlich kann gesagt werden, dass der Template-Parameter TAccessor nicht optional ist, der Template-Parameter TRowset dagegen schon. Die OLE DB Consumer Templates bieten mehrere Auswahlmöglichkeiten für das Zugriffsobjekt und die Zeilenmenge. Mit diesen werden wir uns in den nächsten Kapiteln beschäftigen. Beispielsweise können Sie die Klasse CTable folgendermaßen verwenden: CTable CTable CTable CTable

7.2.1

Table1; Table2; Table3; Table4;

Tabelle öffnen

Die Klasse CTable verfügt lediglich über die beiden folgenden Methoden, die einander sehr ähnlich sind: HRESULT Open(const CSession& session, LPCTSTR szTableName, DBPROPSET* pPropSet = NULL)

und HRESULT Open(const CSession& session, DBID& dbid, DBPROPSET* pPropSet = NULL)

Bei der ersten Methode wird die Tabelle über ihren Namen identifiziert, bei der zweiten über eine DBID. In Kapitel 5 ist die Struktur einer DBID ausführlich beschrieben. Für die erste Open-Methode gibt es die folgenden drei Argumente: 왘 session steht für die Sitzung, in der die Tabelle geöffnet werden soll. Selbstverständlich muss diese bereits geöffnet sein und ihr Klassenelement m_spOpenRowset darf nicht auf NULL gesetzt sein.

Sandini Bib 168

7

Tabellen und Befehle

왘 szTableName steht für den Namen der zu öffnenden Tabelle. 왘 pPropSet steht für ein optionales Eigenschaften-Set für die zu öffnende Zeilenmenge. Für die Tabelle selbst ist keine Eigenschaft vorhanden. Die Eigenschaften beziehen sich auf das Zeilenmengen-Objekt. (Die Eigenschaften von Zeilenmengen werden in Kapitel 8 behandelt.) Die erste Open-Methode bewirkt nicht sehr viel. Sie konvertiert lediglich den Tabellennamen in eine DBID und ruft die zweite Open-Methode auf: HRESULT Open(const CSession& session, LPCTSTR szTableName, DBPROPSET* pPropSet = NULL) { USES_CONVERSION; DBID idTable; idTable.eKind = DBKIND_NAME; idTable.uName.pwszName = (LPOLESTR)T2COLE(szTableName); return Open(session, idTable, pPropSet); }

Für das eigentliche Öffnen der Tabelle ist die zweite Open-Methode zuständig: HRESULT Open(const CSession& session, DBID& dbid, DBPROPSET* pPropSet = NULL) {

Zunächst ruft sie den Schnittstellenzeiger auf das Zeilenmengen-Objekt ab. Die hierfür in der Klasse TRowset definierte Methode lautet GetInterfacePtr. Diese liefert die Adresse des Schnittstellenzeigers zurück. Der Zeiger wird dann durch das Klassenelement m_spOpenRowset->OpenRowset geöffnet. // Überprüfung, ob die Sitzung gültig ist ATLASSERT(session.m_spOpenRowset != NULL); HRESULT hr; hr = session.m_spOpenRowset->OpenRowset(NULL, &dbid, NULL, GetIID(), (pPropSet) ? 1 : 0, pPropSet, (IUnknown**)GetInterfacePtr());

Wenn dieser Vorgang erfolgreich durchgeführt werden konnte, ruft die zweite OpenMethode die Methode SetupOptionalRowsetInterfaces auf, die von der Klasse TRowset erbt. Von dieser erhält sie möglicherweise zusätzliche Schnittstellenzeiger auf das Zeilenmengen-Objekt. if (SUCCEEDED(hr)) { SetupOptionalRowsetInterfaces();

Sandini Bib 7.3

Befehle

169

Anschließend überprüft sie _OutputColumnsClass::HasOutputColumns(), die von der Zugriffsobjekt-Klasse erbt. Es gibt keinen Grund dafür, dass HasOutputColumns nicht auf TRUE gesetzt sein sollte, da mit einer Tabelle nicht viel anzufangen ist, wenn ihre Zeilenmenge nicht abgerufen werden kann. Bind erbt von CAccessorRowset und ermöglicht es der Zugriffsobjekt-Klasse, ihr eigenes Handle zu erstellen. // Wenn Ausgabespalten vorhanden sind, dann binden if (_OutputColumnsClass::HasOutputColumns()) hr = Bind(); } return hr; }

Wenn die Tabelle geöffnet ist, können Sie die Methode aufrufen, die von der Zeilenmenge oder dem Zugriffsobjekt vererbt wurde, um den Cursor zwischen den Zeilen zu bewegen und die Spaltenwerte abzurufen.

7.3 Befehle Die OLE DB Consumer Templates enthalten die beiden Befehlsklassen CCommand und CCommandBase. CCommandBase ist eine Basisklasse zur Implementierung, die nicht direkt verwendet wird. CCommand ist die tatsächlich zu verwendende Klasse. Theoretisch können beide Klassen zu einer kombiniert werden, aber das Vorhandensein von zwei Klassen verbessert die Lesbarkeit des Codes. Im Gegensatz zu Tabellen gibt es für Befehle einen eigenen OLE DB-Typ. In Abbildung 7.1 sind die OLE DB-Schnittstellen für Befehle im Überblick dargestellt. Die obligatorischen Schnittstellen sind fett gedruckt, die optionalen Schnittstellen in normaler Schrift. ICommand ist eine zentrale Schnittstelle, denn sie enthält die Funktion Execute. ICommandText und ICommandProperties dienen zum Setzen des Befehlstextes bzw. der Befehlseigenschaften. IAccessor und ICommandWithParameters stehen für Parameter zur Verfügung. ICommandPrepare schließlich unterstützt die Vorbereitung des Befehls. CCommandBase enthält über das Klassenelement m_spCommand eine Referenz auf das OLE

DB-Befehlsexemplar: CComPtr

m_spCommand;

Zusätzlich implementiert diese Klasse einige Basismethoden (siehe Tabelle 7.1). Über das Klassenelement m_hParameterAccessor enthält sie auch eine Referenz auf ein ParameterZugriffsobjekt. Weitere Informationen hierzu finden Sie im Abschnitt über Parameter.

Sandini Bib 170

7

Tabellen und Befehle

IAccessor IColumnsInfo ICommand ICommandProperties

Befehlsobjekt

ICommandText IConvertType IColumnsRowset ICommandPrepare ICommandWithParameters ICommandPersist ISupportErrorInfo

Abbildung 7.1: Schnittstellen für Befehle

7.3.1

CCommand

CCommand ist die zu verwendende Klasse. Sie ist wie folgt deklariert: template class CCommand : public CAccessorRowset, public CCommandBase, public TMultiple

Die ersten beiden Template-Parameter ähneln denen für CTable. Der dritte TemplateParameter, TMultiple, gibt an, dass Mehrfachergebnisse unterstützt werden. Weitere Informationen hierzu finden Sie im Abschnitt über Befehle mit mehreren ParameterSets. Name der Methode

Beschreibung

CreateCommand

Erzeugt ein OLE DB-Befehlsobjekt.

Create

Erzeugt ein OLE DB-Befehlsobjekt und definiert dessen Befehlstext.

ReleaseCommand

Gibt das OLE DB-Befehlsobjekt frei.

Prepare

Bereitet den Befehl vor.

Unprepare

Hebt die Vorbereitung des Befehls auf.

GetParameterInfo/

Ruft die Parameterinformationen ab bzw. setzt diese. Wird nirgendwo sonst verwendet.

SetParameterInfo

Tabelle 7.1: Methoden für CCommandBase

Sandini Bib 7.3

Befehle

171

Im Gegensatz zu Tabellen liefern Befehle nicht in jedem Fall eine Zeilenmenge zurück. Einfache Befehle liefern überhaupt kein Ergebnis zurück. Beispielsweise ist eine Befehlsanweisung wie UPDATE MyTable SET Column1 = 123

ein solcher einfacher Befehl. Er benötigt weder ein Zugriffsobjekt noch eine Zeilenmenge. Der vorgegebene Template-Parameter für TRowset lautet CRowset.CCommand und ist daher mit dem Parameter CCommand gleichwertig, der keine gültige Kombination aus Zeilenmenge und Zugriffsobjekt darstellt. Sie müssen die Template-Parameter explizit angeben. Der folgende Code zeigt ein Beispiel für einen einfachen Befehl: CCommand Command; hr = Command.Open(Session, UPDATE MYTABLE SET COLUMN1=123);

oder typedef CCommand CSimpleCommand; CSimpleCommand Command2; hr = Command2.Open(Session, UPDATE MYTABLE SET COLUMN1=123);

Wenn der Befehl eine Zeilenmenge zurückliefert, können Sie die Template-Parameter wie bei CTable verwenden.

7.3.2

Befehl öffnen

Das Öffnen eines Befehls besteht aus folgenden sechs Schritten: 1. Erzeugen des OLE DB-Befehlsexemplars. 2. Definition des Befehlstexts. 3. Binden der Parameter, sofern erforderlich. 4. Setzen der Befehlseigenschaften. 5. Ausführen des Befehls. 6. Bei Vorliegen eines Ergebnisses die entsprechende Zeilenmenge abrufen und binden. Die Klasse CCommand verfügt über zwei Open-Methoden. Die erste ist wie folgt deklariert: HRESULT Open(const CSession& session, LPCTSTR szCommand = NULL, DBPROPSET *pPropSet = NULL, LONG* pRowsAffected = NULL, REFGUID guidCommand = DBGUID_DEFAULT, bool bBind = true)

Sandini Bib 172

7

Tabellen und Befehle

wobei 왘 session für die Sitzung steht, zu der der Befehl gehört. 왘 szCommand für den ggf. vorhandenen Befehlstext steht. 왘 pPropSet für das Eigenschaften-Set des Befehls und der Zeilenmenge steht. 왘 pRowsAffected die Anzahl der von dem Befehl betroffenen Zeilen empfängt. 왘 guidCommand die Sprache für den Befehlstext angibt. In der Voreinstellung ist dies DBGUID_DEFAULT, die Standardsprache des Providers. 왘 bBind angibt, ob nach der Ausführung des Befehls ein Bindevorgang erfolgen soll. Wenn dieser Parameter auf TRUE gesetzt ist (Regelfall) und das ererbte Zugriffsobjekt das Binden unterstützt, ruft der Befehl die Methode Bind() auf. Die zweite Open-Methode ist folgendermaßen deklariert: HRESULT Open(DBPROPSET *pPropSet = NULL, LONG* pRowsAffected = NULL, bool bBind = true)

Die Parameter haben die gleiche Bedeutung wie bei der ersten Open-Methode. Die beiden Open-Methoden sind zwar gleich benannt, weisen jedoch deutliche Unterschiede auf. Die erste Methode erzeugt das COM-Objekt, definiert den Befehlstext und ruft die zweite Methode auf. Die zweite Methode bindet die Parameter, setzt die Eigenschaften, führt den Befehl aus und bindet ggf. die zurückgelieferte Zeilenmenge. Die erste Open-Methode könnte somit auch CreateAndExecute heißen, die zweite dagegen Execute.

Open(1)

Create

CreateCommand

Befehlstext definieren

Open(2)

Parameter binden

Execute

OLE DB-Befehl erstellen

Befehlstext definieren

ggf. vorhandene Parameter binden

Eigenschaften setzen

ICommand::Execute

Bind

Abbildung 7.2: Einen Befehl öffnen

Bei Bedarf Ausgabespalten binden

Sandini Bib 7.3

Befehle

173

Abbildung 7.2 veranschaulicht die einzelnen Schritte beim Öffnen eines Befehls. Die weißen Kästchen stehen für Code in der Klasse CCommand, die grauen entstehen durch Vererbung.

Analyse der ersten Open-Methode Die erste Open-Methode führt drei Aktionen durch: 왘 Sie ruft den Befehlstext ab. 왘 Sie erzeugt das zugrunde liegende OLE DB-Befehlsobjekt. 왘 Sie ruft die zweite Open-Methode auf. // Befehl für die Sitzung erzeugen und ausführen HRESULT Open(const CSession& session, LPCTSTR szCommand = NULL, DBPROPSET *pPropSet = NULL, LONG* pRowsAffected = NULL, REFGUID guidCommand = DBGUID_DEFAULT, bool bBind = true) { HRESULT hr; if (szCommand == NULL) { hr = _CommandClass::GetDefaultCommand(&szCommand); if (FAILED(hr)) return hr; } hr = Create(session, szCommand, guidCommand); if (FAILED(hr)) return hr; return Open(pPropSet, pRowsAffected, bBind); }

Wie zuvor bereits erwähnt, erbt die Methode Create von CCommandBase. Sie erzeugt das zugrunde liegende COM-Objekt und definiert den zugehörigen Befehlstext.

Woher stammt der Befehlstext? Wenn der Client den Befehlstext angibt, wird dieser in der vorliegenden Form an die Methode Create übergeben. Andernfalls versucht der Befehl ihn von _CommandClass:: GetDefaultCommand abzurufen. Die globale Definition des Typs _CommandClass lautet wie folgt: typedef _CNoCommand

CommandClass;

Aus dem Namen geht bereits hervor, dass die Klasse _CNoCommand keinen Befehlstext enthält. Sie ist wie folgt definiert: class _CNoCommand { public:

Sandini Bib 174

7

Tabellen und Befehle

static HRESULT GetDefaultCommand(LPCTSTR* /*ppszCommand*/) { return S_OK; } } ;

Die typedef-Deklaration für _CommandClass kann mit einer lokal für diese Klasse geltenden typedef-Deklaration überschrieben werden: #define DEFINE_COMMAND(x, szCommand) \ typedef x _CommandClass; \ static HRESULT GetDefaultCommand(LPCTSTR* ppszCommand) \ { \ *ppszCommand = szCommand; \ return S_OK; \ }

Im Allgemeinen verwendet die Zugriffsobjekt-Klasse dieses Makro für die Definition des Befehlstextes.

Befehlssprachendialekte Während die ODBC-Treiber nur SQL-Befehle unterstützen, verstehen die OLE DBProvider Befehlstext in mehreren Dialekten, die jeweils durch eine eindeutige GUID gekennzeichnet sind. Die Consumer können über den Parameter guidCommand einen Dialekt angeben. Tabelle 7.2 zeigt die von OLE DB definierten GUIDs. Provider, die andere Dialekte unterstützen, definieren ihre eigenen GUIDs. Die interessanteste GUID ist DBGUID_DEFAULT. Wenn sie als Dialekt-GUID übergeben wird, weisen Sie den Provider damit an, den Befehlstext mit seinem Standarddialekt zu interpretieren. Wenn der Provider nur SQL unterstützt, bedeutet DBGUID_DEFAULT das Gleiche wie DBGUID_SQL. Analog dazu gilt, dass DBGUID_DEFAULT und DBGUID_MDX gleichbedeutend sind, wenn der Provider nur MDX unterstützt. Mit anderen Worten ist DBGUID_DEFAULT bei Providern, die nur einen Dialekt unterstützen, unproblematisch. Bei Providern, die mehrere Dialekte unterstützen, kann die Verwendung von DBGUID_DEFAULT unliebsame Überraschungen hervorrufen. Wenn ein Provider beispielsweise sowohl SQL als auch MDX unterstützt, müssen Sie wissen, welcher Dialekt die Standardeinstellung ist. In solchen Fällen ist es immer sicherer, den Dialekt explizit anzugeben.

Sandini Bib 7.3

Befehle

175

Dialekt-GUID

Beschreibung

DBGUID_SQL

Der Befehl verwendet den SQL-Dialekt zum Interpretieren des Befehlstextes.

DBGUID_MDX

Der Befehl verwendet den MDX-Dialekt zum Interpretieren des Befehlstextes (siehe Kapitel 15).

MDGUID_MDX

Gleichbedeutend mit DBGUID_MDX.

DBGUID_DEFAULT

Der Befehl verwendet den Standarddialekt zum Interpretieren des Befehlstextes.

Tabelle 7.2: GUIDs für Dialekte

In der Voreinstellung verwendet die Open-Methode DBGUID_DEFAULT als Dialekt-GUID: HRESULT Open(const CSession& session, LPCTSTR szCommand = NULL, DBPROPSET *pPropSet = NULL, LONG* pRowsAffected = NULL, REFGUID guidCommand = DBGUID_DEFAULT, bool bBind = true)

Bisher wurde in allen Beispielen der Standarddialekt verwendet. Sie können jedoch den Dialekt wie folgt auch explizit angeben: CCommand MyCommand; hr = MyCommand.Open(Session, CommandText, NULL, NULL, DBGUID_MDX);

Analyse der zweiten Open-Methode Die zweite Open-Methode führt nacheinander die drei folgenden Aufgaben durch: 1. Sie bindet Parameter, sofern vorhanden. 2. Sie führt den Befehl aus und ruft ein ggf. vorhandenes Ergebnis (Zeilenmenge) ab. 3. Sie bindet bei Bedarf die Spalten der Zeilenmengen. Sie ist folgendermaßen implementiert: // Wird verwendet, wenn zuvor der Befehl erzeugt wurde HRESULT Open(DBPROPSET *pPropSet = NULL, LONG* pRowsAffected = NULL, bool bBind = true) { HRESULT hr; DBPARAMS params; DBPARAMS *pParams; // Die ggf. vorhandenen Parameter binden if (_ParamClass::HasParameters()) { hr = BindParameters(&m_hParameterAccessor, m_spCommand, ¶ms.pData); if (FAILED(hr)) return hr;

Sandini Bib 176

7

Tabellen und Befehle

// Die Struktur DBPARAMS einrichten params.cParamSets = 1; params.hAccessor = m_hParameterAccessor; pParams = ¶ms; } else pParams = NULL; hr = Execute(GetInterfacePtr(), pParams, pPropSet, pRowsAffected); if (FAILED(hr)) return hr; // Nur binden, wenn dies angefordert wurde und Ausgabespalten // vorhanden sind if (bBind && _OutputColumnsClass::HasOutputColumns()) return Bind(); else return hr; }

Dieser Code mag zunächst verwirrend aussehen, da er zwei Typen verwendet: _ParamClass und _OutputColumnsClass. Dabei gibt _ParamClass an, in welchem Maße Parameter unterstützt werden. Die globale Definition von _ParamClass lautet in der Voreinstellung wie folgt: typedef _CNoParameters

ParamClass;

_CNoParameters gibt an, dass keine Parameter unterstützt werden. class _CNoParameters { public: static bool HasParameters() { return false; } static HRESULT _GetParamEntries(ULONG*, DBBINDING*, BYTE* pBuffer = NULL) { pBuffer; return E_FAIL; } } ;

Im Abschnitt über Parameter erfahren Sie, wie Sie Unterstützung für Parameter hinzufügen können. _OutputColumnsClass gibt an, in welchem Maße Ausgabespalten unterstützt werden. Die globale Definition von _OutputColumnsClass lautet in der Voreinstellung folgendermaßen: typedef _CNoOutputColumns

OutputColumnsClass;

Sandini Bib 7.3

Befehle

177

_CNoOutputColumns gibt an, dass keine Ausgabespalten unterstützt werden. class _CNoOutputColumns { public: static bool HasOutputColumns() { return false; } static ULONG _GetNumAccessors() { return 0; } static HRESULT _GetBindEntries(ULONG*, DBBINDING*, ULONG, bool*, BYTE* pBuffer = NULL) { pBuffer; return E_FAIL; } } ;

Zugriffsobjekt-Elemente mit Ausgabespalten definieren den Typ _OutputColumnsClass neu.

7.3.3

Befehl schließen

Ein Exemplar von CCommand stellt eine Kombination aus den beiden OLE DB-Objekten Befehl und Zeilenmenge dar. Daher gibt es dafür zwei Methoden zum Schließen: 왘 ReleaseCommand gibt nur das Befehlsobjekt frei. 왘 Close schließt das Zeilenmengen-Objekt, nicht aber das Befehlsobjekt. Es erbt von CAccessorRowset.

Befehlsobjekt freigeben Der Destruktor von CCommandBase schließt das Befehlsobjekt: ~CCommandBase() { ReleaseCommand(); }

Da CCommand von CCommandBase erbt, schließt zum Zeitpunkt der Objektlöschung ein beliebiges Exemplar dieser Klasse das OLE DB-Befehlsobjekt. Zusätzlich ruft CreateCommand die Methode ReleaseCommand auf, so dass diese Methode im Endeffekt auch aus der ersten Open-Methode heraus aufgerufen wird, d.h. es besteht für den Programmierer keine Veranlassung, ReleaseCommand explizit aufzurufen.

Sandini Bib 178

7

Tabellen und Befehle

Die Methode ReleaseCommand führt zwei Aktionen durch. Erstens gibt sie das Parameter-Zugriffsobjekt frei: void ReleaseCommand() { // Bei Bedarf das Parameter-Zugriffsobjekt freigeben, // bevor der Befehl freigegeben wird if (m_hParameterAccessor != NULL) { CComPtr spAccessor; HRESULT hr = m_spCommand->QueryInterface(&spAccessor); if (SUCCEEDED(hr)) { spAccessor->ReleaseAccessor(m_hParameterAccessor,NULL); m_hParameterAccessor = NULL; } }

Zweitens gibt sie den Schnittstellenzeiger auf das OLE DB-Befehlsobjekt frei: m_spCommand.Release(); }

Abbildung 7.3 zeigt das Verhalten des Destruktors von CCommand.

~CCommand

~CAcessorRowset

Close

TAccessor::Close

TRowset::Close ~CCommandBase

Gibt Spaltenzugriffsobjekt frei Gibt ggf. Zeilenmengenobjekt frei

ReleaseCommand

m_spCommand Release

Gibt Parameterzugriffsobjekt frei

Gibt ggf. ~ReleaseAccessor Befehlsobjekt frei Abbildung 7.3: Destruktor von CCommand

Die Open- und Close-Methoden im Überblick In Tabelle 7.3 sind die einzelnen Open- und Close-Methoden einander gegenübergestellt.

Sandini Bib 7.3

Befehle

179

Open

Close

CreateCommand

ReleaseCommand

Create

ReleaseCommand

BindParameters

ReleaseAccessor in ReleaseCommand

Open(const CSession&

Close und ReleaseCommand

session, . . .) Open(DBPROPSET

Close

*pPropSet, . . .)

Tabelle 7.3: Die Open- und Close-Methoden

7.3.4

Befehl vorbereiten

Aus der Sicht des Providers besteht das Öffnen eines OLE DB-Befehls aus folgenden drei Schritten: 1. Der Befehl ruft den Befehlstext ab. Zu diesem Zeitpunkt kann der Befehlstext syntaktische oder semantische Fehler aufweisen.

Leerer Befehl (1) Befehlstext definieren Befehlstext

(2) Vorbereiten Ausführungsplan (3) Ausführen Ergebnis des Befehls Abbildung 7.4: Die einzelnen Phasen der Befehlsausführung

2. Der Befehl führt ein Parsing für den Befehlstext durch, wobei Syntax und Semantik geprüft werden. Werden hier keine Fehler gefunden, erstellt der Befehl einen Ausführungsplan, der alle Informationen enthält, die zur optimalen Ausführung des

Sandini Bib 180

7

Tabellen und Befehle

Befehls erforderlich sind, beispielsweise die zu verwendenden Indizes. Dies ist die Phase der Befehlsvorbereitung. 3. Der Befehl wird ausgeführt und liefert schließlich ein Ergebnis. Die einzelnen Phasen der Befehlsausführung sind in Abbildung 7.4 schematisch dargestellt. Der Ausführungsplan kann ziemlich komplex werden. OLE DB gewährt Ihnen keinen Zugriff auf den Ausführungsplan, sondern lässt Sie nur steuern, wann er erstellt und wann er gelöscht wird. Das mit SQL Server ab Version 7.0 gelieferte Hilfsprogramm SQL Server Query Analyzer macht Ihnen das Wesen eines Ausführungsplans transparent. Abbildung 7.5 zeigt einen Ausführungsplan in SQL Server Query Analyzer.

Abbildung 7.5: Ausführungsplan in SQL Server Query Analyzer

Für jeden der oben aufgeführten Schritte gibt es eine entsprechende Methode aus der Klasse CCommand: 왘 Create erzeugt das OLE DB-Befehlsobjekt und führt Schritt 1 durch. 왘 Prepare führt Schritt 2 durch. 왘 Execute führt Schritt 3 durch. Wenn Prepare nicht vor Execute aufgerufen wird, nimmt der Provider die Vorbereitung des Befehls vor seiner Ausführung vor. Anders ausgedrückt kann Schritt 2 auch implizit erfolgen. Wenn wir das Diagramm für die Open-Methode aus Abbildung 7.2 nochmals betrachten, erhalten wir nun Abbildung 7.6, in der die Vorbereitung implizit enthalten ist. Wenn jedoch Prepare explizit aufgerufen wird, ist die Vorbereitung, wie in Abbildung 7.7 gezeigt, ein explizit durchgeführter Schritt.

Sandini Bib 7.3

Befehle

Open(1)

181

Create

CreateCommand

Leerer Befehl Befehlstext definieren Befehlstext Open(2)

Parameter binden

Execute

Ausführungsplan

Ergebnisse der Ausführung Bind

Abbildung 7.6: Ausführung eines Befehls ohne Vorbereitung

In der zuletzt genannten Abbildung wird der Befehl nur einmal ausgeführt. Grundsätzlich ist jedoch vorgesehen, dass der Befehl nach einem Aufruf von Prepare viele Male ausgeführt werden kann. Auf diese Weise fällt der Aufwand für die Vorbereitung nur einmal an und jede darauf folgende Ausführung läuft schneller ab als eine unvorbereitete Ausführung. Das nächste Beispiel zeigt die einzelnen Schritte zur Vorbereitung eines Befehls sowie zu seiner mehrmaligen Ausführung. CCommand Command; Command.Create (MySession, CommandText); Command.Prepare (); Command.Open (); Command.Open ();

Wenn der Befehl eine Zeilenmenge zurückliefert, muss vor jeder erneuten Ausführung des Befehls Close aufgerufen werden, damit die Zeilenmenge freigegeben wird: CCommand Command; Command.Create (MySession, CommandText); Command.Prepare (); Command.Open (); Command.Close (); Command.Open ();

Sandini Bib 182

7

Create

CreateCommand

Befehlstext definieren

OLE DB-Befehl erstellen

Befehlstext definieren

Befehl vorbereiten

Prepare

Open(2)

Tabellen und Befehle

Parameter binden

Execute

Bind

ggf. Parameter binden

Befehl ausfüheren

ggf. Ausgabespalten binden

Abbildung 7.7: Ausführung eines Befehls mit Vorbereitung

7.3.5

Befehle mit Parametern

Es ist möglich, aber nicht unbedingt erforderlich, dass Provider Befehle mit Parametern unterstützen. Die Schnittstelle ICommandWithParameters ist optional. Provider wie SQL Server akzeptieren auch folgende Art von Befehlstext: SELECT * FROM MyTable where column = ?

In diesem Fall wird der tatsächliche Wert des Parameters dynamisch aus dem Consumer-Code erzeugt. Die Definition eines Parameter-Zugriffsobjekts (siehe Kapitel 9) stellt eine einfache Möglichkeit zur Verwendung von Befehlen mit Parametern dar. Eine Zugriffsobjekt-Klasse für den vorherigen Befehl könnte beispielsweise folgendermaßen definiert sein: CMyAccessor { public: int m_Column; BEGIN_PARAM_MAP(CMyAccessor) COLUMN_ENTRY(1, m_Column) END_PARAM_MAP(CMyAccessor) } ;

Befehle mit Parametern lassen sich problemlos vorbereiten und eignen sich sehr gut für die Ausführung von Stored Procedures.

Sandini Bib 7.3

Befehle

183

Das Makro BEGIN_PARAM_MAP ist zwar einfach in der Verwendung, möglicherweise aber nicht auf Anhieb zu verstehen. Es ist wie folgt definiert: #define BEGIN_PARAM_MAP(x) \ public: \ typedef x _classtype; \ typedef x _ParamClass; \ static bool HasParameters() { return true; } \ static HRESULT _GetParamEntries(ULONG* pColumns, \ DBBINDING *pBinding, BYTE* pBuffer = NULL) \ { \ ATLASSERT(pColumns != NULL); \ DBPARAMIO eParamIO = DBPARAMIO_INPUT; \ int nColumns = 0; \ pBuffer;

Die Hauptanweisung lautet wie folgt: typedef x _ParamClass; \

Die globale Deklaration von _ParamClass wird durch eine lokale Typendeklaration (typedef) überschrieben. Da x die aktuelle Klasse ist, übernimmt _ParamClass den Typ der aktuellen Klasse. Der restliche Code definiert die Implementierung der erforderlichen Methoden HasParameters und GetParamEntries.

7.3.6

Befehle mit Mehrfachergebnis

Bisher haben alle behandelten Befehle immer keine oder genau eine Zeilenmenge zurückgeliefert. Der folgende Code zeigt, dass jedoch auch mehr als nur eine Zeilenmenge zurückgeliefert werden kann. Die erste Zeilenmenge entspricht Column1 = 1, die zweite Column1 = 2. SELECT * FROM TableName WHERE Column1 = 1 ;SELECT * FROM TableName WHERE Column1 = 2

Mehrfachergebnisse Wenn ein Befehl mehrere Zeilenmengen zurückliefert, geht OLE DB die Liste der Zeilenmengen iterierend durch. Wenn Sie den Befehl ausführen, zeigt der TRowset-Schnittstellenzeiger für Zeilenmengen auf die erste vom Befehl zurückgelieferte Zeilenmenge. Zum Abrufen einer zweiten Zeilenmenge rufen Sie die Methode GetNextResult auf. Der Zeiger verweist dann auf die zweite Zeilenmenge usw. (siehe Abbildung 7.8). Der erste Schritt besteht darin, die Klasse CMultipleResults als Template-Parameter für TMultiple zu verwenden. Beispielsweise können Sie eine Variable wie die Folgende deklarieren: CCommand Command;

Sandini Bib 184

7

Tabellen und Befehle

OLE DB-ZeilenmengenObjekt 1 Klassenelemente von

GetNextResults

OLE DB-ZeilenmengenObjekt 2

TAccessor

Klassenelemente von TRowset

OLE DB-ZeilenmengenObjekt 3

m_spMultipleResults (CMultipleResults)

Mehrfachergebnisobjekt m_spCommand

IMultipleResults

OLE DB-Befehlsobjekt ICommand Abbildung 7.8: Mehrfachergebnis

Zwischen einem Befehl mit Mehrfachergebnis und einem Befehl mit einem einzigen Ergebnis bestehen keine großen Unterschiede. Nachdem der Befehl geöffnet wurde, verweist der Zeiger jeweils auf die erste Zeilenmenge. Durch den Aufruf von GetNextResult wird die aktuelle Zeilenmenge freigegeben und die nächste aufgerufen. Wenn keine weiteren Zeilenmengen vorhanden sind, liefert die Methode GetNextResult den Wert DB_S_NORESULT zurück. Denken Sie daran, dass DB_S_NORESULT ein »Erfolgscode« ist. Das folgende Beispiel zeigt die Verwendung eines Mehrfachergebnisses. hr = Command.Open(Session, _T("SELECT * FROM TableName WHERE \ Column1 = 1 ; SELECT * FROM TableName WHERE Column1 = 2")); // Cursor zur ersten Zeilenmenge bewegen Command.MoveFirst(); Command.MoveNext(); // die nächste Zeilenmenge abrufen hr = Command.GetNextResult(NULL); // eine beliebige Aktion durchführen // durch die übrigen Zeilenmengen iterieren while (Command.GetNextResult(NULL) == S_OK) { // eine beliebige Aktion durchführen }

Dieser Code enthält jedoch einen Schönheitsfehler: Wenn Sie Close aufrufen, wird zwar das Zeilenmengen-Objekt freigegeben, nicht jedoch das MehrfachergebnisObjekt. Zudem verwendet die über IMultipleResults** GetMultiplePtrAddress() { return &m_spMultipleResults.p; }

Sandini Bib 7.3

Befehle

185

definierte Methode GetMultiplePtrAddress das Klassenelement &m_spMultipleResults.p anstelle von &m_spMultipleResults. Der Unterschied besteht darin, dass die Methode in diesem Fall auch dann gültig ist, wenn m_spMultipleResults nicht NULL ist. Dies führt dazu, dass keine Warnung erscheint, wenn ein Schnittstellenzeiger für ein neues Mehrfachergebnis-Objekt abgerufen wird, bevor das vorherige freigegeben wurde. Bei Ausführung des folgenden Codes reicht beispielsweise der Speicher nicht aus: CCommand Command; hr = Command.Open(Session, _T("SELECT * FROM TableName WHERE \ Column1 = 1 ; SELECT * FROM TableName WHERE Column1 = 2")); Command.Close(); Command.Open();

Um dies zu vermeiden, müssen Sie den Schnittstellenzeiger auf das MehrfachergebnisObjekt explizit freigeben, bevor der Befehl erneut geöffnet wird: sor, CRowset, CMultipleResults> Command; hr = Command.Open(Session, _T("SELECT * FROM TableName WHERE \ Column1 = 1 ; SELECT * FROM TableName WHERE Column1 = 2")); Command.Close(); Command.m_spMultipleResults.Release(); Command.Open();

Das Mehrfachergebnis-Objekt im Detail In der Voreinstellung werden Mehrfachergebnis-Objekte nicht unterstützt. Die zugehörige Klasse ist CNoMultipleResults. Sie ist folgendermaßen definiert: class CNoMultipleResults { public: bool UseMultipleResults() { return false; } IMultipleResults** GetMultiplePtrAddress() { return NULL; } IMultipleResults* GetMultiplePtr() { return NULL; } } ;

Im Gegensatz dazu ist die Klasse CMultipleResults zur Unterstützung von Mehrfachergebnis-Objekten folgendermaßen definiert: class CMultipleResults { public: bool UseMultipleResults() { return true; } IMultipleResults** GetMultiplePtrAddress() { return &m_spMultipleResults.p; } IMultipleResults* GetMultiplePtr() { return m_spMultipleResults; } CComPtr m_spMultipleResults; } ;

Sandini Bib 186

7

Tabellen und Befehle

Wenn Sie CMultipleResults anstelle der Standardklasse CNoMultipleResults als Template-Parameter verwenden, hat dies mehrere Änderungen zur Folge. Erstens liefert UseMultipleResults den Wert True anstelle von False wie bei CNoMultipleResults. Zweitens erbt der Befehl das Klassenelement spMultipleResults, das einen Schnittstellenzeiger auf ein COM-Objekt für Mehrfachergebnisse darstellt. Dieses Objekt verwaltet die Liste der zurückgelieferten Zeilenmengen. Innerhalb der Execute-Methode überprüft der Befehl, ob mehrere Parameter unterstützt werden. Ist dies der Fall, versucht sie, das Klassenelement m_spMultipleResults zu setzen und die erste Zeilenmenge abzurufen. Andernfalls ruft sie die Zeilenmenge direkt ab: if (UseMultipleResults()) { hr = m_spCommand->Execute(NULL, IID_IMultipleResults,pParams, pAffected, (IUnknown**)GetMultiplePtrAddress()); if (SUCCEEDED(hr)) { hr = GetNextResult(pAffected, false); } else { // Wenn IMultipleResults nicht abgerufen werden kann, dann // versuchen, nur IRowset abzurufen hr = m_spCommand->Execute(NULL, IID_IRowset, pParams, pAffected, (IUnknown**)GetInterfacePtr()); } } else { hr = m_spCommand->Execute(NULL, GetIID(), pParams, pAffected, (IUnknown**)ppRowset); if (SUCCEEDED(hr)) SetupOptionalRowsetInterfaces();

7.4 Erweiterungen für Befehle Die OLE DB-Erweiterungen (OLE DB Extensions) enthalten eine Erweiterung für CCommand, nämlich die Klasse CCommandEx, die die beiden folgenden Arten zusätzlicher Unterstützung bietet: 왘 Unterstützung für mehrere Parameter-Zugriffsobjekte pro Klasse. 왘 Unterstützung für mehrere Parameter-Sets. Diese Erweiterungen werden in den folgenden beiden Abschnitten erläutert.

Sandini Bib 7.4

Erweiterungen für Befehle

7.4.1

187

Befehle mit mehreren Parameter-Zugriffsobjekten

Bisher haben wir meistens ein Parameter-Zugriffsobjekt (Parameter Accessor) pro Klasse verwendet. Manchmal ist dies jedoch nicht sehr praktisch. Sehen wir uns das folgende Beispiel einer »Kundentabelle« (Customer Table) mit mindestens zwei Spalten an: 왘 Id ist ein interner Datenbankbezeichner. 왘 name steht für den Namen des Kunden. Manchmal müssen die Kundeninformationen über Id abgerufen werden, in anderen Fällen über name. Die erste Lösung besteht darin, eine Zugriffsobjekt-Klasse pro Zugriff zu erzeugen. In diesem Beispiel beginnen wir mit einer Klasse, die nur die zugehörigen Klassenelemente enthält: class CustomersClassMembers { public: int m_Id; WCHAR m_Name [33]; } ;

Die erste Zugriffsobjekt-Klasse ruft die Kundendaten über ihre Id ab: class CCustomerAccessorById: public CustomersClassMembers { public: BEGIN_COLUMN_MAP(CCustomerAccessorById) COLUMN_ENTRY(1, m_Id) COLUMN_ENTRY(2, m_Name) END_COLUMN_MAP() BEGIN_PARAM_MAP(CCustomerAccessorById) COLUMN_ENTRY(1, m_Id) END_PARAM_MAP() DEFINE_COMMAND(CCustomerAccessorById, _T(" \ SELECT \ Id, \ Name \ FROM Customer WHERE Id = ?")) } ;

DEFINE_COMMAND definiert einen Befehl mit Id als Parameter (WHERE Id = ?). BEGIN_PARAM_ MAP definiert eine Parameterzuordnung mit m_Id als eindeutigem Parameter.

Sandini Bib 188

7

Tabellen und Befehle

Wir können somit eine Klasse mit dem Namen CCustomerById definieren: class CCustomerById : public CCommand >

Analog dazu greift eine zweite Zugriffsobjekt-Klasse über name auf die Kundendaten zu: class CCustomerAccessorByName: public CustomersClassMembers { public: BEGIN_COLUMN_MAP(CCustomerAccessorByName) COLUMN_ENTRY(1, m_Id) COLUMN_ENTRY(2, m_Name) END_COLUMN_MAP() BEGIN_PARAM_MAP(CCustomerAccessorByName) COLUMN_ENTRY(1, m_Name) END_PARAM_MAP() DEFINE_COMMAND(CCustomerAccessorByName, _T(" \ SELECT \ Id, \ Name \ FROM Customer WHERE Name = ?")) } ;

Folglich können wir eine Klasse mit dem Namen CCustomerByName definieren. Der Vorteil dieser Methode liegt in der leichten Lesbarkeit, was auch das Debugging einfach macht. Allerdings sind hierbei zur Durchführung einer relativ einfachen Aufgabe viele Klassen erforderlich. Es gibt zahlreiche andere Möglichkeiten um die Verwendung mehrerer ZugriffsobjektKlassen zu vermeiden. Beispielsweise kann anstelle eines statischen ein dynamisches Zugriffsobjekt verwendet werden (das Konzept der dynamischen Zugriffsobjekte ist in Kapitel 9 erläutert). Die OLE DB-Erweiterungen bieten eine andere Lösung: Gruppieren des Parameter-Zugriffsobjekts und des Befehlstextes in lokalen Klassen, die für dieses Beispiel CAccessbyIdAccess und CAccessbyNameAccess heißen: class CCustomerMultiAccessor { public: int m_Id; WCHAR m_Name [33]; BEGIN_COLUMN_MAP(CCustomerMultiAccessor) COLUMN_ENTRY(1, m_Id) COLUMN_ENTRY(2, m_Name) END_COLUMN_MAP()

Sandini Bib 7.4

Erweiterungen für Befehle

189

class CAccessbyIdAccess: public CAccessorBase { public: BEGIN_PARAM_MAP_EX(CAccessbyIdAccess, CCustomerMultiAccessor) COLUMN_ENTRY(1, m_Id) END_PARAM_MAP() DEFINE_COMMAND(CAccessbyIdAccess, _T(" \ SELECT \ Id, \ Name \ FROM Customer WHERE Id = ?")) } ; CAccessbyIdAccess AccessbyId; class CAccessbyNameAccess: public CAccessorBase { public: BEGIN_PARAM_MAP_EX(CAccessbyNameAccess, CCustomerMultiAccessor) COLUMN_ENTRY(1, m_Name) END_PARAM_MAP() DEFINE_COMMAND(CAccessbyNameAccess, _T(" \ SELECT \ Id, \ Name \ FROM Customer WHERE Name = ?")) } ; CAccessbyNameAccess AccessbyName; } ;

Die lokale Klasse definiert das Parameter-Zugriffsobjekt und den Befehlstext, weist aber selbst keine Klassenelemente auf. Der Trick besteht darin, auf das Klassenelement der ihr übergeordneten Klasse zuzugreifen. Das Makro BEGIN_PARAM_MAP_EX akzeptiert zwei Parameter. Der erste gibt den Typ der lokalen Klasse an, der zweite den Typ der übergeordneten Klasse: #define BEGIN_PARAM_MAP_EX(internal, external) \ public: \ typedef external _classtype; \ typedef internal _ParamClass; \ static bool HasParameters() { return true; } \ static HRESULT _GetParamEntries(ULONG* pColumns, \ DBBINDING *pBinding, BYTE* pBuffer = NULL) \ { \ ATLASSERT(pColumns != NULL); \ DBPARAMIO eParamIO = DBPARAMIO_INPUT; \ int nColumns = 0; \ pBuffer;

Sandini Bib 190

7

Tabellen und Befehle

Das explizite Definieren der lokalen Klassen ist allerdings immer noch recht mühsam. In den OLE DB-Erweiterungen finden Sie zwei Makros zur Automatisierung dieses Prozesses: 왘 BEGIN_ACCESS startet die Deklaration der lokalen Klasse. 왘 END_ACCESS beendet die Deklaration und deklariert die Zugriffsfunktion. #define BEGIN_ACCESS(_accessname_) \ class C##_accessname_##Access : public CAccessorBase \ { \ public: \ typedef C##_accessname_##Access _self_; #define END_ACCESS(_accessname_) \ } ; \ C##_accessname_##Access _accessname_;

Die Klasse ist dann folgendermaßen definiert: class CCustomerMultiAccessor2 { public: int m_Id; WCHAR m_Name [33]; BEGIN_COLUMN_MAP(CCustomerMultiAccessor2) COLUMN_ENTRY(1, m_Id) COLUMN_ENTRY(2, m_Name) END_COLUMN_MAP() BEGIN_ACCESS(AccessbyId) BEGIN_PARAM_MAP_EX(_self_, CCustomerMultiAccessor2) COLUMN_ENTRY(1, m_Id) END_PARAM_MAP() DEFINE_COMMAND(_self_, _T(" \ SELECT Id, Name FROM Customer WHERE Id = ?")) END_ACCESS(AccessbyId) BEGIN_ACCESS(AccessbyName) BEGIN_PARAM_MAP_EX(_self_, CCustomerMultiAccessor2) COLUMN_ENTRY(1, m_Name) END_PARAM_MAP() DEFINE_COMMAND(_self_, _T(" \ SELECT Id, Name FROM Customer WHERE Name = ?")) END_ACCESS(AccessbyName) } ;

CCommandEx verfügt über zwei OpenEx-Methoden, die die gleiche Funktion haben wie die Open-Methoden in der Klasse CCommand. Sie akzeptieren jedoch auch den Typ der Klasse,

die den Parameter enthält. Beim Öffnen des Befehls kann der Typ des zu verwenden-

Sandini Bib 7.4

Erweiterungen für Befehle

191

den Zugriffsobjekts angegeben werden. Zum Zugriff auf die Kundendaten über Id können Sie beispielsweise folgende Zeilen schreiben: CCommandEx , CRowset> Command; hr = Command.OpenEx(Session,Command.AccessbyId);

7.4.2

Befehle mit mehreren Parameter-Sets

Bisher wurden die Befehle jeweils mit einem einzigen Parameter-Set ausgeführt. OLE DB sieht jedoch die Verwendung mehrerer Parameter-Sets bereits vor. Die OLE DBStruktur zur Beschreibung der Parameter-Sets ist nämlich wie folgt definiert: struct DBPARAMS { void * pData; ULONG cParamSets; HACCESSOR hAccessor; } ;

In dieser Struktur steht cParamSets für die Anzahl von Parameter-Sets. Die OLE DB Consumer Templates setzen dieses Feld immer auf 1, aber es kann auch auf einen höheren Wert gesetzt werden. In diesem Fall führt der Datenspeicher den Befehl je einmal pro Parameter-Set aus. Wenn Sie beispielsweise eine Reihe von Zeilen einfügen wollen, definieren Sie normalerweise eine Zugriffsobjekt-Klasse wie die Folgende: class CMyAccessor { public: TCHAR m_a[11]; LONG m_b; BEGIN_PARAM_MAP(CMyAccessor) COLUMN_ENTRY(1, m_a) COLUMN_ENTRY(2, m_b) END_PARAM_MAP() DEFINE_COMMAND(CMyAccessor, _T(" \ INSERT INTO table1 \ (a, b) \ VALUES (?, ?)")) } ;

Dies ist der nächste Schritt: CCommand Command; // m_a und m_b setzen hr = Command.Open (..);

Wir könnten diesen Vorgang für das Einfügen mehrerer Spalten wiederholen. Beachten Sie jedoch, dass dieses Verfahren für jede einzufügende Zeile wieder ganz von vorne durchzuführen ist.

Sandini Bib 192

7

Tabellen und Befehle

Die Alternative besteht in der Definition zweier Klassen. Die erste Klasse definiert die Parameter: class CMyAccessor { public: TCHAR m_a[11]; LONG m_b; BEGIN_PARAM_MAP(CMyAccessor) COLUMN_ENTRY(1, m_a) COLUMN_ENTRY(2, m_b) END_PARAM_MAP() DEFINE_COMMAND(CMyAccessor, _T(" \ INSERT INTO table1 \ (a, b) \ VALUES (?, ?)")) } ;

Die zweite Klasse enthält ein Array aus Parametern: class CMyAccessor { public: CParameters parameters [100]; PARAMETER_SET_COUNT(Cdbotable1Accessor, 100) } ;

Das Makro PARAMETER_SET_COUNT gibt die Anzahl von Parameter-Sets an, die in diesem Beispiel 100 ist. Der Grundgedanke hierbei ist, anschließend alle Parameter in einem Schritt einzufügen. Mit der Klasse CCommandEx ist dies tatsächlich ziemlich einfach: CCommandEx , CNoRowset> Command; hr = Command.OpenEx(Session,Command.parameters[0]);

7.5 Zusammenfassung In diesem Kapitel wurden die Konzepte Tabelle und Befehl in OLE DB vorgestellt. Tabellen lassen sich einfacher verwenden, aber Befehle bieten mehr Möglichkeiten. Vermutlich wird eine der Befehlsalternativen Ihren Anforderungen am besten gerecht. Wenn Sie keine Daten aus dem Datenspeicher benötigen, sind Sie inzwischen mit den wichtigsten Informationen über OLE DB versorgt. Wollen Sie jedoch Daten aus einem Datenspeicher abrufen, müssen Sie wissen, was sich hinter einem Zugriffsobjekt (Accessor) und einer Zeilenmenge (Rowset) verbirgt. Diese Konzepte werden in den nächsten beiden Kapiteln behandelt.

Sandini Bib

8 Zeilenmengen Wie Kapitel 7 bereits gezeigt hat, wird beim Arbeiten mit Tabellen und Befehlen in vielen Fällen eine Zeilenmenge (Rowset) zurückgegeben. Das vorliegende Kapitel ist dem Konzept der Zeilenmengen gewidmet und behandelt im Einzelnen folgende Themen: 왘 Gründe für ein Zeilenmengen-Objekt 왘 Die verschiedenen Zeilenmengen-Klassen 왘 Positionsmarken 왘 Die Eigenschaften von Zeilenmengen 왘 Einzelheiten zur Client-Cursor-Dienstkomponente 왘 Suche nach Zeilen in einer Zeilenmenge

8.1 Zeilenmengen-Objekte Grundsätzlich besteht eine Zeilenmenge aus einer Reihe von Zeilen, von denen jede eine Gruppe von Werten in Spalten oder Feldern enthält. Die Definition der Spalten ist für alle Zeilen gleich. In C++ wäre eine Zeilenmenge lediglich ein Array aus Zeilen. Eine Zeile wäre hier beispielsweise folgendermaßen definiert: class MyRow { int value1; char value2 [10]; } ;

Die entsprechende Zeilenmenge wäre dann wie folgt definiert: typedef MyRow [] MyRowset;

Bei OLE DB ist die ganze Sache ein wenig komplexer, da die Daten einer Zeilenmenge zwar vom Server kommen, aber vom Client verwendet werden. In der Regel empfiehlt es sich nicht, die gesamte Zeilenmenge systematisch vom Server zum Client zu übertragen, da sie zahlreiche Zeilen enthalten kann und der Client möglicherweise nur einige davon benötigt. Außerdem besteht die Gefahr, dass die Zeilenmenge zu viele Zeilen

Sandini Bib 194

8

Zeilenmengen

für den auf dem Client zur Verfügung stehenden Speicher enthält. Aus diesen Gründen wäre die Verwendung eines C++-Standard-Arrays nicht sinnvoll. Alternativ stellt OLE DB ein COM-Zeilenmengen-Objekt bereit, das über einen Mechanismus verfügt, mit dem auch nur eine Teilmenge der Zeilen abgerufen werden kann. OLE DB-Zeilenmengen-Objekte können beispielsweise zum Abrufen einer oder mehrerer Zeilen und zum Blättern von einer Zeile zur nächsten verwendet werden. In Abbildung 8.1 sind der Typ Zeilenmenge (Rowset) und seine Schnittstellen im Überblick dargestellt. Die obligatorischen Schnittstellen sind fett gedruckt, die optionalen Schnittstellen in normaler Schrift. Sie sehen, dass dieser OLE DB-Typ eine nicht unerhebliche Anzahl von Schnittstellen definiert. Er unterstützt sogar noch mehr Schnittstellen, als die Abbildung zeigt, in der nur die wichtigsten davon aufgeführt und in logischen Gruppen zusammengefasst sind. Zur Behandlung der gesamten Funktionalität sind weitere vier Kapitel erforderlich. Der Schwerpunkt dieses Kapitels liegt auf der ersten Gruppe, der Zeilenverwaltung. Diese umfasst folgende Operationen mit Zeilen: Abrufen (Get), Freigeben (Release), Holen (Fetch) und Blättern (Scroll). Ferner erstreckt sie sich auch auf die Definition und Verwendung von Positionsmarken, einem wirkungsvollen Verfahren, zu einer bestimmten Zeile zu springen. In diesem Kapitel wird auch erklärt, wie Zeilen und Zeilenmengen-Eigenschaften gesucht werden können. In Kapitel 9 wird das Abrufen von Spalteninformationen und der Zugriff auf Spaltenwerte erläutert, Kapitel 10 behandelt das Abrufen und Setzen von Spaltenwerten. In Kapitel 13 geht es schließlich um weitergehende Konzepte, Kapitelmarken und Sichten.

IColumnsRowset

IRowset

Zeilen verwaltung

IRowsetScroll

IColumnsInfo

IRowsetLocate

IColumnsInfo2

Spalteninformation

IRowsetIdentity

Zeilenmenge Eigenschaften

IRowsetInfo

Zeilen suchen

IRowsetFind

IConvertType

Datenzugriff

IAccessor IRowsetChange

Unterstützung für Sichten

IRowsetUpdate

IRowsetView

Unterstützung für Kapitelmarken

IRowsetRefresh

IChapteredRowset IRowsetChapterMember

Abbildung 8.1: Der Typ Zeilenmenge (Rowset)

Daten setzen

Sandini Bib 8.2

Zeilenmengen-Klassen

195

8.2 Zeilenmengen-Klassen Vor einem tieferen Einstieg in das Konzept ist es wichtig, sich klar zu machen, dass Zeilenmengen Bestandteil einer Architektur sind. Beim Arbeiten mit den OLE DB Consumer Templates verwenden Sie die Zeilenmengen-Klassen nicht direkt, sondern im Rahmen eines Befehls oder einer Tabelle, z.B. bei einem Befehl wie CCommand

oder bei einer Tabelle wie CTable

Die Klassen CCommand und CTable kapseln das OLE DB-Zeilenmengen-Objekt nicht. Statt dessen stellen sie eine Möglichkeit zur Verfügung, dieses abzurufen. Die ZugriffsobjektKlasse und die Zeilenmengen-Klassen (z.B. CDynamicAccessor und CRowset) kapseln zusammen das OLE DB-Zeilenmengen-Objekt: Die Zeilenmengen-Klasse kümmert sich um die Zeilen, die Zugriffsobjekt-Klasse um die Spalten. In diesem Kapitel konzentrieren wir uns auf die Zeilenmengen-Klassen, im nächsten Kapitel werden die Zugriffsobjekt-Klassen behandelt. Die OLE DB Consumer Templates stellen vier Zeilenmengen-Klassen zur Verfügung: 왘 CRowset ist die Basisklasse und wird in den meisten Fällen für Ihre Anforderungen genügen. Soweit nicht anders angegeben, wird in diesem Kapitel die Verwendung der Klasse CRowset beschrieben, die auch der Template-Standardparameter von CTable und CCommand ist. Daher ist beispielsweise die Zeile CCommand

gleichwertig mit der Zeile CCommand

왘 CBulkRowset enthält dieselbe Funktionalität wie CRowset, bietet aber noch Optimierungen beim Abrufen mehrerer aufeinander folgender Zeilen. 왘 CArrayRowset ermöglicht die Übertragung des Zeileninhalts an den Client, anschließend verhält sich diese Klasse wie ein Array aus Zeilen. 왘 CNoRowset ist dann zu verwenden, wenn ein Befehl keine Zeilenmenge zurückliefert. Diese Klasse enthält gerade so viel Funktionalität, wie zur Kompilierung als Template-Parameter erforderlich ist. Ansonsten bewirkt sie nichts. Die OLE DB-Erweiterungen stellen zusätzlich die folgenden neuen Klassen zur Verfügung:

Sandini Bib 196

8

Zeilenmengen

왘 CRowsetEx bietet Unterstützung für Eigenschaften und OLE DB-Kapitelmarken (Chapters). Eine Kapitelmarke stellt eine Gruppe von Zeilen in einer Zeilenmenge dar. Dieses Konzept wird in Kapitel 13 erläutert. 왘 CRowsetFind implementiert Suchoperationen, die weiter unten in diesem Kapitel behandelt werden.

8.3 Zeilenmengen und Zeilen Eine OLE DB-Zeilenmenge ist ein COM-Objekt, das Zeilen enthält. Man könnte sich vorstellen, dass eine Zeile auch ein COM-Objekt ist. Dies gilt aber nicht für TabellenProvider. Zwar definiert OLE DB ein COM-Zeilenobjekt, Zeilenmengen verwenden dieses im Gegensatz zu hierarchischen Providern allerdings nicht (siehe Kapitel 16). Sie nutzen einen anderen Mechanismus, und dies aus folgenden beiden Gründen: Erstens muss das Abrufen von Zeilen schnell erfolgen, selbst bei sehr vielen Zeilen. Mit einem COM-Objekt dauert dies normalerweise zu lange. Zweitens existiert ein Zeilenobjekt nur so lange wie das zugehörige Zeilenmengen-Objekt. Dies bedeutet, dass zum Zeitpunkt der Objektlöschung die Zeilenmenge alle ihre Zeilen freigibt. Da ein COMObjekt seine eigene Existenzdauer steuert, könnte eine Zeile, die als COM-Objekt implementiert ist, nach dem Löschen des Zeilenmengen-Objekts weiterexistieren. Zeilen werden über ein Handle vom Typ HROW definiert: typedef ULONG HROW;

Wie COM-Objekte weisen Zeilen einen Referenzzähler auf, der inkrementiert und dekrementiert werden kann. Bei unspezifischer Verwendung von OLE DB verfügt die Schnittstelle IRowset über Methoden zum Inkrementieren und Dekrementieren des mit einem Zeilen-Handle verknüpften Referenzzählers: virtual HRESULT STDMETHODCALLTYPE AddRefRows( ULONG cRows, const HROW rghRows[ ], ULONG rgRefCounts[ ], DBROWSTATUS rgRowStatus[ ]) = 0; virtual HRESULT STDMETHODCALLTYPE ReleaseRows( ULONG cRows, const HROW rghRows[ ], DBROWOPTIONS rgRowOptions[ ], ULONG rgRefCounts[ ], DBROWSTATUS rgRowStatus[ ]) = 0;

Zum Zeitpunkt der Objektlöschung gibt ein Zeilenmengen-Objekt alle seine ZeilenHandles unabhängig von ihrem Referenzzähler frei. Umgekehrt verhindert das Vorhandensein eines Referenzzählers mit einem Wert größer 1 nicht, dass die zugehörige Zeilenmenge gelöscht wird. In gewissem Sinne sind Zeilen »COM-Bürger zweiter Klasse«.

Sandini Bib 8.3

Zeilenmengen und Zeilen

8.3.1

197

Zeilenverwaltung

Der Umgang mit Zeilen-Handles ist nicht in jedem Fall einfach. Folglich enthalten die OLE DB Consumer Templates intern ein Zeilen-Handle, auf das sie jedes Mal zurückgreifen, wenn eine Operation durchgeführt wird, die ein solches Handle erfordert, z.B. beim Abrufen von Spaltenwerten. Ich nenne die entsprechende Zeile in der Folge die aktuelle Zeile. Die Zeilenmengen-Klasse kann jedoch aus Gründen der Effizienz zusätzliche Zeilen-Handles enthalten. Diese Zeilen nenne ich in der Folge aktive Zeilen. Es versteht sich von selbst, dass die aktuelle Zeile eine aktive Zeile ist. Die beiden folgenden Funktionen verwalten die Zeilen-Referenzzähler für die aktiven Zeilen: HRESULT AddRefRows()

inkrementiert den Referenzzähler aller aktiven Zeilen, HRESULT ReleaseRows()

dekrementiert sie. Die OLE DB Consumer Templates übernehmen die Verwaltung der Zeilen-Referenzzähler, so dass sich der Programmierer darum nicht zu kümmern braucht.

8.3.2

Zeilenanzahl und -position

Die natürlichste Frage, die sich bei einer Zeilenmenge stellt, ist die nach der Anzahl der darin enthaltenen Zeilen. OLE DB kann diese Frage nicht auf einfache Weise beantworten. Einerseits weiß OLE DB nur annähernd, wie viele Zeilen in einer Zeilenmenge vorhanden sind. Es ist nämlich möglich, dass ein Benutzer gerade Zeilen löscht oder einfügt, während Sie mit der Zeilenmenge arbeiten. Angenommen, Sie öffnen eine Tabelle, die 100 Zeilen enthält. Wenn Sie die Zeilenanzahl anfordern, müsste der Datenspeicher die Zahl 100 zurückgeben, aber gerade zu dem Zeitpunkt, an dem Sie beginnen, Zeilen zu holen, fügt ein Benutzer eine neue Zeile ein, so dass die Anzahl der Zeilen nun 101 ist. Wenn Sie dann Zeile 100 erreichen, muss noch eine weitere Zeile gelesen werden. Andererseits ist diese Funktion in eine andere, komplexere Methode eingebunden: HRESULT GetApproximatePosition(const CBookmarkBase* pBookmark, ULONG* pPosition, ULONG* pcRows)

Wir beachten zunächst einmal die ersten beiden Parameter nicht und setzen diese auf NULL. Der dritte Parameter, pcRows, enthält einen Zeiger auf die Zeilenanzahl. Schreiben Sie zum Abrufen der Zeilenanzahl folgenden Code: CRowset MyRowset; ULONG NumberOfRows; hr = MyRowset.GetApproximatePosition(NULL, NULL, &NumberOfRows);

Sandini Bib 198

8

Zeilenmengen

Mit der folgenden Methode können Sie eine bestimmte Zeile über einen Quotienten für die Zeilenmenge ansteuern: HRESULT MoveToRatio(ULONG nNumerator, ULONG nDenominator, bool bForward = true)

wobei nNumerator/nDenominator den Quotienten für die Definition der aktuellen Zeile darstellt. Anders ausgedrückt ermittelt sich der Index der aktuellen Zeile aus CurrentRowIndex = NumberOfRows * nNumerator / nDenominator

bForward gibt dabei an, ob die Zeile in Vorwärts- oder in Rückwärtsrichtung geholt werden soll.

Das folgende Beispiel zeigt, wie die aktuelle Zeile zum ersten Viertel, zum zweiten Zehntel oder zu einem bestimmten Zeilenindex wechseln soll: CRowsetMyRowset MyRowset.MoveToRatio(1, 4) MyRowset.MoveToRatio(2, 10) ULONG RowCount; GetApproximatePosition(NULL, NULL, &NumberOfRows); MyRowset.MoveToRatio(SpecificRowIndex, NumberOfRows);

8.3.3

Cursorbewegung

Für Zeilenmengen in OLE DB sind zwei Arten der Cursorbewegung definiert: Holen (Fetching) und Blättern (Scrolling). Beim Holen wird der Cursor bewegt und es werden Zeilen-Handles abgerufen. Beim Blättern wird der Cursor bewegt, ohne dass ZeilenHandles abgerufen werden. In den OLE DB Consumer Templates werden beide Arten der Cursorbewegung verwendet. Bei allen Zeilenmengen ist es möglich, in Vorwärtsrichtung Zeilen zu holen und durch Zeilen blättern, aber nicht alle können diese Vorgänge auch in Rückwärtsrichtung durchführen. In Tabelle 8.1 sind die beiden Eigenschaften für die Rückwärtsbewegung aufgeführt. CRowset und CBulkRowset definieren fünf Methoden zur Cursorbewegung, die in Tabelle 8.2 im Überblick dargestellt sind. HRESULT MoveNext(LONG lSkip, bool bForward) ist dabei die interessanteste Methode, weil sie die tatsächliche Implementierung aufruft und ihrerseits von anderen Cursorbewegungen aufgerufen wird. bForward gibt an, ob Zeilen in Vorwärts- oder Rückwärtsrichtung geholt werden sollen. Dieser Wert darf nur dann negativ sein, wenn die Eigenschaft DBPROP_ CANFETCHBACKWARDS auf True gesetzt ist. lSkip steht für die algebraische Anzahl der vor dem Holen zu durchblätternden Zeilen dar. Wenn dieser Wert 0 ist, holt MoveNext nur die nächste Zeile. Ist er positiv, blättert MoveNext um die in lSkip angegebene Anzahl von Zeilen vorwärts und holt die nächste Zeile. Ist er negativ, blättert MoveNext entsprechend rückwärts und holt dann die nächste Zeile. Daher darf lSkip nur dann negativ sein, wenn die Eigenschaft DBPROP_CANSCROLLBACKWARDS auf True gesetzt ist.

Sandini Bib 8.3

Zeilenmengen und Zeilen

199

Eigenschaft

Typ

Beschreibung

DBPROP_CANFETCHBACKWARDS

Boolesch

Gibt an, ob in einer Zeilenmenge Zeilen rückwärts geholt werden können.

DBPROP_CANSCROLLBACKWARDS

Boolesch

Gibt an, ob in einer Zeilenmenge rückwärts durch die Zeilen geblättert werden kann.

Tabelle 8.1: Eigenschaften zum Holen von Zeilen und zum Blättern in Rückwärtsrichtung

Methode

Beschreibung

HRESULT MovePrev()

Bewegt den Cursor zur vorherigen Zeile; wenn er sich bereits auf der ersten Zeile befindet, bleibt er dort.

HRESULT MoveNext()

Bewegt den Cursor zur nächsten Zeile; wenn er sich bereits auf der letzten Zeile befindet, bleibt er dort.

HRESULT MoveNext(LONG lSkip,

Bewegt den Cursor zur nächsten angegebenen Zeile.

bool bForward) HRESULT MoveFirst()

Bewegt den Cursor zur ersten Zeile.

HRESULT MoveLast()

Bewegt den Cursor zur letzten Zeile.

Tabelle 8.2: Cursorbewegungen

Wenn der Cursor in einer Zeilenmenge auf einem bestimmten Zeilenindex i steht, wie lautet dieser dann nach einem Aufruf von MoveNext? Am einfachsten stellt man sich vor, dass sich der Cursor jeweils zwischen zwei Zeilen befindet: Zunächst wird er während des Blätterns um den Wert von lSkip Zeilen verschoben. Anschließend rückt er eine Zeile nach vorne, sofern bForward auf True gesetzt ist. Andernfalls rückt er eine Zeile zurück. Die neue Zeile liegt zwischen den Positionen des Cursors vor und nach dem Aufruf von MoveNext. Das folgende Szenario zeigt, was die Parameter von MoveNext bewirken. 1. Die Zeilenmenge wird erzeugt. Der Cursor befindet sich vor der ersten Zeile (siehe Abbildung 8.2).

Zeile 1

Keine aktuelle Zeile

Zeile 2 Zeile 3 Zeile 4 Zeile 5 Abbildung 8.2: Vor dem Aufruf von MoveFirst()

Sandini Bib 200

8

Zeilenmengen

2. Während des ersten Aufrufs von MoveFirst wird der Cursor zwischen die erste und die zweite Zeile bewegt. Dies bedeutet, dass die Zeilenmenge die Werte der ersten Zeile aufweist (siehe Abbildung 8.3).

Zeile 1 (geholt)

Aktuelle Zeile

Zeile 2 Zeile 3 Zeile 4 Zeile 5

Abbildung 8.3: Nach dem Aufruf von MoveFirst()

3. Während eines Aufrufs von MoveNext (0, true) wird der Cursor zwischen die zweite und die dritte Zeile bewegt. Dies bedeutet, dass die Zeilenmenge jetzt die Werte der zweiten Zeile aufweist (Abbildung 8.4). Zeile 1 Row 2 (geholt)

Aktuelle Zeile

Zeile 3 Zeile 4 Zeile 5

Abbildung 8.4: Nach einem Aufruf von MoveNext()

4. Während eines Aufrufs von MoveNext (1, true) wird der Cursor durch eine BlätternOperation zwischen die dritte und die vierte Zeile bewegt und anschließend durch eine Holen-Operation zwischen die vierte und die fünfte Zeile. Dies bedeutet, dass die Zeilenmenge jetzt die Werte der vierten Zeile aufweist (siehe Abbildung 8.5).

Zeile 1 Zeile 2 Blättern Holen

Zeile 3 Zeile 4 (geholt)

Aktuelle Zeile

Zeile 5 Zeile 6

Abbildung 8.5: Nach einem Aufruf von MoveNext (1, true)

Sandini Bib 8.3

Zeilenmengen und Zeilen

201

5. Während eines Aufrufs von MoveNext (0, false) wird der Cursor zwischen die dritte und die vierte Zeile bewegt. Dies bedeutet, dass die Zeilenmenge jetzt die Werte der vierten Zeile aufweist (Abbildung 8.6).

Zeile 1 Zeile 2 Zeile 3 Holen

Zeile 4 (geholt)

Aktuelle Zeile

Zeile 5 Zeile 6 Abbildung 8.6: Nach einem Aufruf von MoveNext (0, false)

6. Während eines Aufrufs von MoveNext (1, false) wird der Cursor zunächst um eine Zeile nach vorne bewegt und holt dann mit einer Rückwärtsbewegung eine Zeile. Dies bedeutet, dass die Zeilenmenge nach wie vor die Werte der vierten Zeile aufweist (siehe Abbildung 8.7).

Zeile 1 Zeile 2 Zeile 3 Blättern

Holen

Zeile 4 (geholt)

Aktuelle Zeile

Zeile 5 Zeile 6 Abbildung 8.7: Nach einem weiteren Aufruf von MoveNext (1, false)

Daher hängt der Zeilenindex nach dem Aufruf von MoveNext() vom Wert für bForward während des vorherigen Aufrufs von MoveNext() ab. if (bForward) { if (bPreviousForward) NewRowIndex = PreviousRowIndex + lSkip + 1; else NewRowIndex = PreviousRowIndex + lSkip; }

Sandini Bib 202

8

Zeilenmengen

else { if (bPreviousForward) NewRowIndex = PreviousRowIndex + lSkip; else NewRowIndex = PreviousRowIndex + lSkip – 1; }

MoveNext() ist ganz einfach folgendermaßen definiert: HRESULT MoveNext() { return MoveNext(0, true); }

MovePrev ist folgendermaßen definiert: HRESULT MovePrev() { return MoveNext(-2, true); }

In diesem Fall muss die Eigenschaft DBPROP_CANSCROLLBACKWARDS auf True gesetzt sein. MoveFirst und MoveLast rufen intern IRowset::RestartPosition() auf. Diese Methode

müsste erfolgreich ausgeführt werden können, braucht aber möglicherweise etwas Zeit. Die Eigenschaft DBPROP_QUICKRESTART gibt an, ob RestartPosition schnell ausgeführt werden soll oder nicht. MoveLast verwendet auch negatives Blättern. In Tabelle 8.3 sind die einzelnen Metho-

den und ihre Anforderungen im Überblick dargestellt. Methode

Anforderungen

HRESULT MoveNext()

Keine

HRESULT MovePrev()

DBPROP_CANSCROLLBACKWARDS muss auf True gesetzt sein.

HRESULT MoveNext(LONG lSkip, bool bForward)

DBPROP_CANSCROLLBACKWARDS ist auf True gesetzt, wenn ISkip

kleiner als 0 ist. DBPROP_CANFETCHBACKWARDS muss auf True gesetzt sein, wenn bForward auf False gesetzt ist.

HRESULT MoveFirst()

DBPROP_QUICKRESTART sollte auf True gesetzt sein, damit die

HRESULT MoveLast()

DBPROP_QUICKRESTART sollte auf True gesetzt sein, damit die

Ausführung schnell genug abläuft. Ausführung schnell genug abläuft. DBPROP_CANSCROLLBACKWARDS muss auf True gesetzt sein.

Tabelle 8.3: Cursorbewegungen und Anforderungen

Sandini Bib 8.4

Positionsmarken

8.3.4

203

Zeilenidentität

Beim Bewegen des Cursors in der Zeilenliste ist es wichtig zu wissen, um welche Zeilen es sich jeweils handelt. Insbesondere muss bekannt sein, ob zwei Zeilen-Handles jeweils auf dieselbe Zeile verweisen. Wenn die Zeilen-Handles binär identisch sind, ist dies immer der Fall. Umgekehrt ist aber nicht gesagt, dass sich zwei unterschiedliche Zeilen-Handles immer auf zwei unterschiedliche Zeilen beziehen. Sie können auch auf ein und dieselbe Zeile verweisen, genauso wie sich zwei unterschiedliche Datei-Handles jeweils auf dieselbe Datei beziehen können. Zum Vergleichen von Zeilen-Handles dient die folgende Funktion: HRESULT IsSameRow(HROW hRow) const

IsSameRow liefert S_TRUE, wenn sich hRow auf die aktuelle Zeile bezieht, und S_FALSE,

wenn dies nicht der Fall ist. Dieser Vergleich ist nur gültig, wenn die Zeilen-Handles aus ein und derselben Zeilenmenge stammen.

8.4 Positionsmarken Wenn Sie beim Lesen eines Buches eine Pause machen, verwenden Sie ein Lesezeichen um später die Stelle wieder zu finden, an der Sie weiterlesen möchten. Dieses einfache Mittel hilft Ihnen, die gewünschte Seite ohne langes Herumsuchen sofort wieder zu finden. Bei Datenbanken erfüllen Positionsmarken (Bookmarks) diese Funktion, d.h. sie ermöglichen es Ihnen, schnell zu einer bestimmten Zeilen zurückzukehren. Bevor wir tiefer in dieses Thema einsteigen, sollte der Unterschied zwischen den Elementen Zeilen-Handle, Zeilenindex und Positionsmarke eindeutig geklärt werden. Alle drei beziehen sich jeweils auf eine bestimmte Zeile, sie werden jedoch für unterschiedliche Zwecke verwendet. Ein Zeilen-Handle kennzeichnet eine Zeile, während eine Positionsmarke zum schnellen Abrufen einer Zeile verwendet wird und daher die dafür erforderlichen Informationen enthält. Ein Zeilen-Handle enthält dagegen nicht notwendigerweise derartige Informationen. Zeilen-Handles und Positionsmarken unterscheiden sich auch von ihrer internen Struktur her. Eine OLE DB-Positionsmarke ist ein opaker Binärwert mit variabler Länge, den Sie nicht versuchen sollten zu interpretieren. Ein Zeilen-Handle ist dagegen eine feste, 4 Byte große Struktur. In OLE DB gibt es keinen bestimmten Typ für einen Zeilenindex. Ein Zeilenindex kennzeichnet hier lediglich die Position der Zeile in der Zeilenmenge. Zeilen-Handles entsprechen manchmal dem Zeilenindex, aber darauf sollten Sie sich nicht verlassen. In Tabelle 8.4 sind die Unterschiede zwischen Zeilen-Handles, Positionsmarken und Zeilenindizes im Überblick dargestellt.

Sandini Bib 204

8

Zeilenmengen

Merkmal

Zeilen-Handle

Zeilenindex

Positionsmarke

Ermöglicht schnelles Springen zu einer bestimmten Zeile

Nein

Nein

Ja

Größe

4 Byte

4 Byte

Variabel

Anzahl der offenen Elemente

Über DBPROP_MAXOPENROWS definiert

Keine Begrenzung

Keine Begrenzung

Opak

Ja

Nein

Ja

Tabelle 8.4: Zeilen-Handles, Zeilenindizes und Positionsmarken

Nicht alle Zeilenmengen unterstützen Positionsmarken. Über die ZeilenmengenEigenschaft DBPROP_BOOKMARKS wird diese Unterstützung angegeben. Ist diese Eigenschaft auf TRUE gesetzt, werden Positionsmarken unterstützt, lautet der Wert FALSE, werden sie nicht unterstützt. Eine Positionsmarke ist durch eine Länge und einen Puffer mit einem binären Wert definiert. In den OLE DB Consumer Templates sind die beiden folgende Klassen für Positionsmarken definiert: 왘 CBookmarkBase ist eine Implementierungsklasse, die in den Zeilenmengen-Klassen als Argument für eine Methode verwendet wird. Diese Klasse wird nicht direkt verwendet. 왘 CBookmark ist die Klasse, mit der Sie arbeiten. CBookmark ist folgendermaßen definiert: template < ULONG nSize = 0 > class CBookmark : public CBookmarkBase

wobei nSize die Größe der Positionsmarke in Bytes darstellt. Um beispielsweise eine Positionsmarke mit der Größe 4 Byte zu definieren, programmieren Sie Folgendes: CBookmark MyBookmark;

CBookmark weist eine folgendermaßen definierte Template-Spezialisierung auf: template < > class CBookmark< 0 > : public CBookmarkBase

CBookmark wird verwendet, wenn die Größe der Positionsmarke nicht feststeht, so

dass der Puffer dynamisch zugewiesen werden kann. Sie können diese Klasse auf zweierlei Weise verwenden. Entweder übergeben Sie zum Erzeugungszeitpunkt die Länge des Puffers: UINT Size; CBookmark MyBookmark (Size);

Sandini Bib 8.4

Positionsmarken

205

oder Sie setzen den Puffer direkt: CBookmark MyBookmark; BYTE* Bytes = new BYTE[Size]; MyBookmark.SetBookmark(Size, Bytes);

In beiden Fällen wird der Speicherpuffer von CBookmark verwaltet. Wenn Sie mit Zeilenmengen-Klassen arbeiten, können Sie über die Methode MoveToBookmark schnell zu einer bestimmten Zeile gelangen: HRESULT MoveToBookmark(const CBookmarkBase& bookmark, LONG lSkip = 0)

Hiermit gelangen Sie zu der Zeile, die ausgehend von bookmark um den Wert lSkip versetzt ist. Beispiel: Wenn lSkip gleich 0 ist, springen Sie mit MoveToBookmark genau zu der von bookmark angegebenen Zeile. Ist lSkip gleich 1, wird zur nächsten Zeile nach der Positionsmarke gesprungen, bei lSkip gleich -1 zur Zeile direkt vor der Positionsmarke.

8.4.1

Typen von Positionsmarken

In OLE DB sind folgende Typen von Positionsmarken definiert: numerische Positionsmarken und Schlüsselwert-Positionsmarken. Numerische Positionsmarken sind über den Zeilenindex oder einen anderen Zeilenbezeichner definiert. Dies bedeutet nicht, dass der tatsächliche Wert der Positionsmarke dem Zeilenindex entspricht, sondern nur, dass die Positionsmarke zur Berechnung ihres eigenen Wertes auf den Zeilenindex zugreift. Die Länge der Positionsmarke hängt von der zugrunde liegenden Datenbank ab. In den meisten Fällen wird eine numerische Positionsmarke 4 Byte lang sein, aber sie kann auch eine andere Länge aufweisen. Da diese Positionsmarken auf der Zeilenposition und nicht auf dem Zeileninhalt basieren, wird damit immer dieselbe Zeile angesprungen, auch wenn sich ihr Inhalt geändert hat. Schlüsselwert-Positionsmarken sind über eine Kombination aus Spaltenwerten definiert, die eine Zeile eindeutig identifizieren. Hierfür bietet sich natürlich ein Primärschlüssel an. Sie können Schlüsselwert-Positionsmarken nur verwenden, wenn die Zeilen über eine Kombination aus Spalten eindeutig identifiziert werden können. Versuchen Sie nicht, den Inhalt einer Schlüsselwert-Positionsmarke zu interpretieren. Auch wenn es möglich sein sollte, die Spaltenwerte aus dem Positionsmarkenpuffer zu erkennen, ist nicht gewährleistet, dass der Provider das betreffende Positionsmarkenformat beibehält. Da eine Schlüsselwert-Positionsmarke auf Spaltenwerten basiert, kann sie bei Änderungen an einem beteiligten Spaltenwert ungültig werden. Daher empfiehlt es sich, die Positionsmarke freizugeben, wenn Spaltenwerte geändert werden, und sie anschließend aus der Zeilenmenge wieder abzurufen.

Sandini Bib 206

8

Zeilenmengen

Die Zeilenmengen-Eigenschaft DBPROP_BOOKMARKTYPE gibt an, welcher Positionsmarkentyp zu verwenden ist. Sie kann einen der Werte DBPROPVAL_BMK_NUMERIC oder DBPROPVAL_BMK_KEY annehmen. Wenn Sie für DBPROP_BOOKMARKTYPE keine Angabe machen, wählt der OLE DB-Provider den am besten geeigneten Typ aus.

Positionsmarken vergleichen In einer Zeilenmenge liegen die Zeilen in einer bestimmten Reihenfolge vor, d.h. eine bestimmte Zeile befindet sich entweder vor oder hinter einer anderen Zeile. Über einen Positionsmarkenvergleich können Sie feststellen, ob sich eine Zeile, die mit einer bestimmten Positionsmarke verknüpft ist, vor oder hinter einer mit einer anderen Positionsmarke verknüpften Zeile befindet oder mit dieser identisch ist. Mit der Methode Compare kann ein solcher Positionsmarkenvergleich durchgeführt werden. Diese Methode ist folgendermaßen definiert: HRESULT Compare(const CBookmarkBase& bookmark1, const CBookmarkBase& bookmark2, DBCOMPARE* pComparison) const

pComparison stellt das Ergebnis des Vergleichs dar. Dieses kann einen der in Tabelle 8.5 aufgeführten Werte annehmen. Wenn als Ergebnis DBCOMPARE_NE zurückgegeben wird,

ist zwar bekannt, dass sich die Positionsmarken in der Zeilenmenge auf unterschiedliche Zeilen beziehen, es ist jedoch nicht möglich, deren Reihenfolge zu ermitteln. Symbolischer Wert

Beschreibung

DBCOMPARE_LT

Die Positionsmarke bookmark1 verweist auf eine Zeile vor der Positionsmarke bookmark2.

DBCOMPARE_EQ

Die Positionsmarke bookmark1 und die Positionsmarke bookmark2 verweisen auf dieselbe Zeile.

DBCOMPARE_GT

Die Positionsmarke bookmark1 verweist auf eine Zeile nach der Positionsmarke bookmark2.

DBCOMPARE_NE

Die Positionsmarke bookmark1 verweist nicht auf dieselbe Zeile wie die Positionsmarke bookmark2, es kann jedoch nicht ermittelt werden, welche Positionsmarke sich vor der anderen befindet.

DBCOMPARE_NOTCOMPARABLE

Die Positionsmarken bookmark1 und bookmark2 können nicht miteinander verglichen werden, d.h. es kann keine Aussage über die relative Position der Positionsmarken zueinander getroffen werden. Dies kann passieren, wenn eine der Positionsmarken ungültig ist.

Tabelle 8.5: Werte für DBCOMPARE

Sandini Bib 8.4

Positionsmarken

207

Wert für DBPROP_ORDEREDBOOKMARKS

Mögliche Werte für den Vergleich gültiger Positionsmarken

TRUE

DBCOMPARE_LT, DBCOMPARE_EQ,

FALSE

DBCOMPARE_EQ, DBCOMPARE_NE

DBCOMPARE_GT

Tabelle 8.6: Werte für DBPROP_ORDEREDBOOKMARKS

Sie können die Funktionen des Positionsmarkenvergleichs mit DBPROP_ORDEREDBOOKMARKS noch verfeinern. Wenn diese Eigenschaft auf TRUE gesetzt ist, muss die Zeilenmenge immer in der Lage sein, die Reihenfolge von zwei Positionsmarken zu ermitteln, vorausgesetzt, diese sind gültig. Ist sie auf FALSE gesetzt, erhalten Sie von der Zeilenmenge nur die Rückmeldung, ob die Positionsmarken identisch sind oder nicht. In Tabelle 8.6 sind die möglichen Werte für den Vergleich gültiger Positionsmarken zusammengefasst. Zur Beschleunigung der Vergleichsoperation kann in manchen Fällen der Wert (Inhalt) von zwei Positionsmarken vergleichend ausgewertet werden. Führen Sie Vergleiche von Positionsmarkenwerten unter Berücksichtigung des Typs dieser Werte durch. In der Regel handelt es sich um eine Byte-Folge (DBTYPE_BYTES) und es sind jeweils die Bytes miteinander zu vergleichen. Die Zeilenmengen-Eigenschaft DBPROP_LITERALBOOKMARKS bestimmt, ob Positionsmarken anhand ihres Inhalts miteinander verglichen werden können. Wenn die Positionsmarken in geordneter Reihenfolge vorliegen, kann ihr Inhalt zur Ermittlung der relativen Reihenfolge verglichen werden. Liegen sie nicht in geordneter Reihenfolge vor, können sie beim Vergleich nur auf identischen Inhalt geprüft werden. Eine entsprechende Übersicht zeigt Tabelle 8.7.

DBPROP_LITERALBOOKMARKS

hat den Wert TRUE

DBPROP_LITERALBOOKMARKS

hat den Wert FALSE

DBPROP_ORDEREDBOOKMARKS

DBPROP_ORDEREDBOOKMARKS

hat den Wert TRUE

hat den Wert FALSE

Die Positionsmarkenwerte können zur Prüfung auf identischen Inhalt und zur Ermittlung der relativen Reihenfolge verglichen werden.

Die Positionsmarkenwerte können nur zur Prüfung auf identischen Inhalt verglichen werden.

Die Positionsmarkenwerte können nicht miteinander verglichen werden.

Die Positionsmarkenwerte können nicht miteinander verglichen werden.

Tabelle 8.7: Werte für DBPROP_LITERALBOOKMARKS

Sandini Bib 208

8.4.2

8

Zeilenmengen

Position von Positionsmarken

In einem Abschnitt weiter vorne habe ich die Methode GetApproximatePosition vorgestellt und ihre ersten beiden Parameter zunächst links liegen gelassen. Diese Methode eignet sich zum Abrufen der annähernden Position einer Positionsmarke, wie im folgenden Beispiel gezeigt: CRowset MyRowset; CBookmark MyBookmark; ULONG BookmarkPosition; MyRowset.GetApproximatePosition(&MyBookmark, &BookmarkPosition, NULL);

Das Faszinierende an dieser Methode ist, dass sie sowohl die Position einer Positionsmarke als auch die Anzahl der Zeilen abrufen kann.

8.4.3

Standard-Positionsmarken

In OLE DB sind eine Positionsmarke für die erste Zeile, eine Positionsmarke für die letzte Zeile und eine ungültige Positionsmarke vordefiniert. Die ersten beiden Positionsmarken brauche ich nicht zu erläutern. Das dritte Objekt stellt eine Positionsmarke für eine ungültige (z.B. eine gelöschte) Zeile dar. Eine Schlüsselwert-Positionsmarke kann auch ungültig werden, wenn sich der Inhalt der zugrunde liegenden Zeile geändert hat und der Inhalt der Positionsmarke damit keiner Zeile mehr entspricht. Die Länge aller Standard-Positionsmarken wird durch STD_BOOKMARKLENGTH angegeben und beträgt 1 Byte: #define STD_BOOKMARKLENGTH 1

In Tabelle 8.8 sind die einzelnen Werte für die Standard-Positionsmarken aufgeführt. Symbolischer Wert

Beschreibung

DBBMK_INVALID

Wert der ungültigen Positionsmarke

DBBMK_FIRST

Wert der ersten Positionsmarke

DBBMK_LAST

Wert der letzten Positionsmarke

Tabelle 8.8: Standard-Positionsmarken

Verwendung von Standard-Positionsmarken Der Inhalt von Standard-Positionsmarken kann entweder statisch oder dynamisch zugewiesen werden. Der folgende Code zeigt, wie die Standard-Positionsmarken im jeweiligen Fall zu verwenden sind:

Sandini Bib 8.5

Zeilenmengen-Eigenschaften

209

CBookmark InvalidBookmark; BYTE byte = DBBMK_INVALID; memcpy (InvalidBookmark.GetBuffer(), &byte,STD_BOOKMARKLENGTH); CBookmark FirstBookmark; byte = DBBMK_FIRST; FirstBookmark.SetBookmark(STD_BOOKMARKLENGTH, &byte);

8.5 Zeilenmengen-Eigenschaften Wie viele andere OLE DB-Objekte weisen auch die Zeilenmengen in OLE DB Eigenschaften auf. Anders als bei den übrigen Objekten ist es jedoch nicht möglich, diese Eigenschaften nach der Erzeugung der Zeilenmenge zu ändern. Zu diesem Zeitpunkt können die Eigenschaften nur noch gelesen werden. Das Abrufen von ZeilenmengenEigenschaften ist in den Klassen der OLE DB Consumer Templates nicht implementiert. Hierfür brauchen Sie die Klasse CRowsetEx aus den OLE DB-Erweiterungen.

8.5.1

Eigenschaften bei der Erzeugung setzen

Die Zeilenmengen-Eigenschaften werden in der Open-Methode für die jeweilige Tabelle oder den jeweiligen Befehl übergeben. Beispielsweise zeigt der folgende Code, wie ein Befehl mit Eigenschaften geöffnet werden kann: CDBPropSet Properties(DBPROPSET_ROWSET); Properties.AddProperty(DBPROP_CANFETCHBACKWARDS, true); CCommand MyCommand; hr = MyCommand.Open(Session, "select * from TableName", &Properties);

Open weist NULL als Standardargument für die Eigenschaften auf. Daher können Sie die

Zeile auch folgendermaßen schreiben: hr = MyCommand.Open(Session, "select * from TableName");

In diesem Fall wird die Zeilenmenge mit den Standardeigenschaften erzeugt. Zu den wichtigsten Eigenschaften gehört die Angabe der COM-Schnittstellen, die von der Zeilenmenge unterstützt werden sollen. Die Namen dieser Eigenschaften beginnen in der Regel mit DBPROP_IRowset. Geben Sie z.B. DBPROP_IRowsetLocate an, so kann die Zeilenmenge nur dann erfolgreich erzeugt werden, wenn die Zeilenmengen des Providers die Schnittstelle IRowsetLocate unterstützen.

8.5.2

Eigenschaften abrufen

Nachdem eine Zeilenmenge erzeugt wurde, setzt CRowsetEx die OLE DB-Eigenschaften mit Hilfe der beiden Methoden

Sandini Bib 210

8

Zeilenmengen

HRESULT GetProperties(ULONG ulPropIDSets, const DBPROPIDSET* pPropIDSet, ULONG* pulPropertySets, DBPROPSET** ppPropsets)

und HRESULT GetProperty(const GUID& guid, DBPROPID propid, VARIANT* pVariant) const

Die Methode GetProperties ruft ein Eigenschaften-Set ab, GetProperty jeweils nur eine einzige Eigenschaft. Diese beiden Methoden sind »reguläre«, für Eigenschaften vorgesehene Methoden (siehe Kapitel 5). Das folgende Codebeispiel zeigt die Verwendung von GetProperties und GetProperty. CCommand MyRowset; // Zeilenmenge "MyRowset" öffnen CDBPropIDSet PropertiesIDs(DBPROPSET_ROWSET); PropertiesIDs.AddPropertyID(DBPROP_CANFETCHBACKWARDS); CDBPropSet Properties[]; ULONG PropertyCount; MyRowset.GetProperties(1, &PropertiesIDs, &PropertyCount, &Properties); // Eigenschaften verwenden // Eigenschaften freigeben CComVariant Variant; MyRowset.GetProperty(DBPROPSET_ROWSET, DBPROP_CANFETCHBACKWARDS, &Variant);

8.6 Zeilenmengen-»Sammelpackungen« Stellen Sie sich vor, Sie trinken jeden Tag eine Flasche Mineralwasser. Sie können nun jeden Tag eine einzelne Flasche kaufen oder alle zwölf Tage einen Kasten mit je einem Dutzend Flaschen. In der Regel ist der Kauf einer Sammelpackung billiger und er erspart Ihnen zusätzliche Einkaufswege. Beschränkungen gibt es hierbei nur, wenn Sie zu Hause nicht genügend Platz haben um die Kästen unterzubringen. Diese Vorgehensweise lässt sich auch auf das Abrufen von Zeilenmengen und Zeilen quasi in »Sammelpackungen« (Bulk Rowsets) übertragen. Wenn Sie aufeinander folgende Zeilen abrufen wollen, haben Sie zwei Möglichkeiten: Sie können die Zeilen einzeln nacheinander abrufen (siehe Abbildung 8.8). Dieses Verfahren wird von der Klasse CRowset angewandt, wobei ein eindeutiges Zeilen-Handle verfolgt wird. Dieses ist folgendermaßen definiert: HROW

m_hRow;

Wenn Sie MoveNext aufrufen, gibt CRowset das aktuelle Zeilen-Handle frei und ruft ein neues für die nächste Zeile ab (siehe Abbildung 8.9).

Sandini Bib 8.6

Zeilenmengen-»Sammelpackungen«

211

Zeile 1 CRowset

Zeilen-Handle 1

Consumer

Zeile 2

Provider

Abbildung 8.8: CRowset verweist auf Zeile 1

Zeilen-Handle 1

Zeile 1 Zeile 2

CRowset

Zeilen-Handle 2

Consumer

Provider

Abbildung 8.9: CRowset verweist auf Zeile 2

Alternativ können Sie Zeilen-Handles über die Klasse CBulkRowset gesammelt erfassen. CBulkRowset unterscheidet sich von CRowset nur durch die Implementierung. Daher können Sie leicht von der einen Klasse zu anderen wechseln. CBulkRowset verfolgt ein Array aus Zeilen-Handles. Diese Klasse weist die folgenden

Klassenelemente auf: HROW* ULONG ULONG ULONG

m_phRow; // Zeiger auf HROW-Array für alle Zeilen im // Puffer m_nRows; // Anzahl der Zeilen, die der Puffer aufnehmen // kann m_nCurrentRows; // Anzahl der aktuell im Puffer enthaltenen // Zeilen m_nCurrentRow; // Index der aktuellen Zeile im Puffer

Sandini Bib 212

8

Zeilenmengen

m_phRow ist ein zwischengespeichertes Array aus Zeilen-Handles vom Typ m_nRows. Es ist nicht immer voll, beispielsweise kann es nicht voll sein, wenn MoveNext am Ende der Zeilenmenge angekommen ist. Nur die ersten m_nCurrentRows-Elemente von m_phRow enthalten ein gültiges Zeilen-Handle. Wie bei CRowset gibt es auch bei CBulkRowset das Konzept einer »aktuellen Zeile«. Das aktuelle Zeilen-Handle befindet sich im Array aus Zeilen-Handles an der Position, die über m_nCurrentRow angegeben wird. Dies bedeutet, dass das aktuelle Zeilen-Handle m_hRow gleich m_phRow [m_nCurrentRow] ist.

In der Voreinstellung ist die Anzahl der Zeilen-Handles vom Typ m_nRows gleich 10. Dieser Wert kann durch folgenden Aufruf geändert werden: void SetRows(ULONG nRows)

Aus praktischen Gründen gehen wir im Folgenden davon aus, dass m_nRows gleich 3 ist. Beim ersten Aufruf von MoveNext ruft CBulkRowset ein Array ab, das die ersten drei Zeilen-Handles enthält und das aktuelle Zeilen-Handle auf das erste Array-Element setzt. Abbildung 8.10 zeigt die Klasse CBulkRowset in diesem Stadium.

CBulkRowset

Zeilen-Handle 1

Zeile 1

Zeilen-Handle 2

Zeile 2

Zeilen-Handle 3

Zeile 3 Zeile 4 Zeile 5 Zeile 6 Zeile 7

Consumer

Provider

Abbildung 8.10: CBulkRowset nach dem ersten Aufruf von MoveNext()

Während der folgenden Aufrufe von MoveNext aktualisiert CBulkRowset das aktuelle Zeilen-Handle im Array, vorausgesetzt, es wurde zwischengespeichert. Dies bedeutet, dass CBulkRowset das aktuelle Zeilen-Handle abruft, ohne eine Anforderung an den Provider abzusetzen. Dies funktioniert bis zum dritten Zeilen-Handle einwandfrei, wie Abbildung 8.11 zeigt. Wenn CBulkRowset bei der Zeile 4 angekommen ist, müssen neue Zeilen-Handles abgerufen werden. Zunächst werden alle zwischengespeicherten Zeilen-Handles freigegeben. Anschließend werden die Zeilen-Handles für die Zeilen 4, 5 und 6 abgerufen und das Zeilen-Handle 4 als aktuelles Zeilen-Handle gesetzt. Abbildung 8.12 zeigt das Objekt CBulkRowset in diesem Stadium.

Sandini Bib 8.6

Zeilenmengen-»Sammelpackungen«

213

Der Einfachheit halber zeigen die Abbildungen 8.10, 8.11 und 8.12 ein Beispiel mit drei zwischengespeicherten Zeilen-Handles. CBulkRowset kann jedoch eine beliebige Anzahl von Zeilen-Handles abrufen. Dabei stellt sich die Frage, wie groß die abzurufenden Sammelpackungen von Zeilen-Handles sein sollen?

CBulkRowset

Zeilen-Handle 1

Zeile 1

Zeilen-Handle 2

Zeile 2

Zeilen-Handle 3

Zeile 3 Zeile 4 Zeile 5 Zeile 6 Zeile 7

Consumer

Provider

Abbildung 8.11: CRowset verweist auf Zeile 3

Zeilen-Handle 1 Zeilen-Handle 2 CBulkRowset

Zeilen-Handle 3

Zeile 1 Zeile 2 Zeile 3

Zeilen-Handle 4

Zeile 4

Zeilen-Handle 5

Zeile 5

Zeilen-Handle 6

Zeile 6 Zeile 7

Consumer

Provider

Abbildung 8.12: CRowset verweist auf Zeile 4

Erstens bietet sich die Verwendung von CBulkRowset nur an, wenn Sie mit aufeinander folgenden Zeilen arbeiten. Anders ausgedrückt, beschleunigt dieses Verfahren die Ausführung bei Aufrufen von MoveNext oder MovePrev. Wenn Sie jedoch mit nicht aufeinander folgenden Zeilen arbeiten, z.B. mit Aufrufen von MoveToBookmark oder MoveToRatio, bringt dieses Verfahren keinen Geschwindigkeitsvorteil. Zweitens kann das Beibehalten von Zeilen-Handles aufwändig sein, so dass es sich nicht immer empfiehlt, eine große Anzahl davon zu setzen. Ein Maß hierbei ist die zum Holen einer Zeilenmenge benötigte Zeit im Verhältnis zur Anzahl der zwischengespeicherten Zeilen. In der Regel sieht die Kurve hierfür wie in Abbildung 8.13 aus.

Sandini Bib 8

Zeilenmengen

Zeit

214

Optimaler Wert

Anzahl ZeilenHandles

Abbildung 8.13: Verhältnis zwischen Verarbeitungszeit und Anzahl der Zeilen-Handles

Bei zunehmender Anzahl von Zeilen-Handles nimmt die Zeit zunächst ab, weil MoveNext weniger Aufrufe von GetNextRows durchführt. Bei weiterer Zunahme der ZeilenHandles wird es für den Provider jedoch immer schwieriger, diese zu verfolgen. Der durch die geringere Anzahl von GetNextRows-Aufrufen erzielte Leistungsgewinn wird durch den Leistungsverlust ausgeglichen, der durch die Anzahl aktiver Zeilen-Handles verursacht wird, so dass sich die benötigte Zeit erhöht. Die Kurve weist folglich auf einen Punkt, in dem die benötigte Zeit in einem optimalen Verhältnis zur Anzahl der Zeilen-Handles steht. Bei welcher Anzahl von Zeilen-Handles dieser Punkt erreicht ist, hängt vom jeweiligen Provider ab.

8.7 Zeilenmengen-Arrays Vom Konzept her stellen Zeilenmengen lediglich ein Array aus Zeilen dar. Mit der Klasse CArrayRowset kann ein Programmierer so auf die Zeilen zugreifen, als ob es sich um ein Array handelt. Diese Klasse ist folgendermaßen definiert: template class CArrayRowset : public CVirtualBuffer, public TRowset

wobei TRowset die Zeilenmengen-Klasse, z.B. CRowset oder CBulkRowset darstellt. CVirtualBuffer ist eine in der Bibliothek atlbase.h definierte ATL-Klasse, die ein Array von T mit einem aktuellen Element darstellt. CArrayRowset kombiniert die Funktionen der Zeilenmenge mit denen des virtuellen Puffers. Beim Erzeugen von CArrayRowset können Sie die Kapazität des virtuellen Puffers festlegen. In der Voreinstellung ist dieser Wert 100.000 Zeilen: CArrayRowset(int nMax = 100000)

Sandini Bib 8.7

Zeilenmengen-Arrays

215

Legen Sie immer eine Kapazität fest, die Ihren Anforderungen auf jeden Fall genügt. Wenn die Kapazität größer ist als die Anzahl der Zeilen in der Zeilenmenge, ersparen Sie sich so manches Problem. Während seiner Erzeugung enthält das Objekt der Klasse CArrayRowset keinen Wert. Optional können Sie die Methode Snapshot aufrufen, um alle in der Zeilenmenge enthaltenen Zeilen abzurufen. operator [] ermöglicht Ihnen, eine an einer bestimmten Position vorliegende Zeile abzurufen. Wenn die Zeile bereits durch die Methode Snapshot abgerufen wurde, ruft operator[] lediglich den Wert im virtuellen Puffer ab. Andernfalls ruft er so lange Zei-

len ab, bis er zur angeforderten Zeile gelangt ist. operator[] ist folgendermaßen definiert: T& operator[](int nRow)

Beachten Sie, dass Snapshot eine optionale Methode ist. Wenn Sie diese verwenden, werden alle Zeilen auf einmal abgerufen, andernfalls jeweils auf Anforderung. CArrayRowset unterscheidet sich auch von CBulkRowset. CBulkRowset verwaltet einen Puffer mit Zeilen-Handles, während CArrayRowset einen Puffer mit tatsächlichen Zeilenwerten verwaltet. Wenn diese Werte bereits einmal abgerufen wurden, ist kein OLE DB-Aufruf erforderlich um sie erneut abzurufen.

(Leer)

Zeile 1

CArrayRowset

Zeile 2 Zeile 3 Zeile 4 Zeile 5 Zeile 6 Zeile 7 Consumer

Provider

Abbildung 8.14: CArrayRowset vor dem Aufruf von operator []

Angenommen, es ist ein CArrayRowset-Objekt vorhanden, für das Snapshot bisher noch nicht aufgerufen wurde. Der zugehörige virtuelle Puffer enthält, wie in Abbildung 8.14 gezeigt, kein Element. Wenn der Benutzer die Werte für die erste Zeile durch einen Aufruf von operator [0] anfordert, stellt CArrayRowset fest, dass der virtuelle Puffer diese Werte nicht enthält und ruft den Wert für Zeile 1 ab, wie in Abbildung 8.15 gezeigt.

Sandini Bib 216

8

Zeilenmengen

Fordert der Benutzer anschließend die sechste Zeile durch einen Aufruf von operator [5] an, ruft CArrayRowset die Werte für alle Zeilen bis zur sechsten ab, wie in Abbildung 8.16 gezeigt. CArrayRowset bietet dabei folgende beiden Vorteile:

Wert 1

Zeile 1

CArrayRowset

Zeile 2 Zeile 3 Zeile 4 Zeile 5 Zeile 6 Zeile 7 Consumer

Provider

Abbildung 8.15: CArrayRowset nach dem Aufruf von operator [0]

Wert 1

Zeile 1

Wert 2

Zeile 2

Wert 3

Zeile 3

Wert 4

Zeile 4

Wert 5

Zeile 5

Wert 6

Zeile 6

CArrayRowset

Zeile 7 Consumer

Provider

Abbildung 8.16: CArrayRowset nach dem Aufruf von operator [5]

왘 Wenn die Werte für eine Zeile einmal abgerufen wurden, können sie ohne einen COM-Aufruf erneut abgerufen werden. Anders ausgedrückt, es geht schnell. 왘 Das Arbeiten mit dieser Klasse ist besonders einfach. Allerdings weist CArrayRowset auch die folgenden Nachteile auf: 왘 Es wird mehr Speicher belegt als mit CRowset. Die Speicherfrage kann ein wichtiger Faktor sein, wenn CArrayRowset eine Zeilenmenge mit vielen Zeilen verarbeitet. 왘 Änderungen an der Datenbank können nicht berücksichtigt werden, wenn sie beispielsweise nach einem Aufruf von Snapshot vorgenommen werden.

Sandini Bib 8.8

Die Client-Cursor-Dienstkomponente

217

8.8 Die Client-Cursor-Dienstkomponente Das Programmieren einer effizienten Zeilenmenge ist nicht einfach. Noch schwieriger ist es, wenn die Zeilenmenge das Holen von Zeilen in Rückwärtsrichtung, Suchoperationen und noch andere erweiterte Operationen (siehe Kapitel 13) unterstützen soll. Die Provider bieten in der Regel sogar nur die Funktionalität des zugrunde liegenden Datenspeichers und fügen keine weiteren Funktionen hinzu. Eines der Ziele von OLE DB besteht darin, ein hohes Funktionalitätsniveau für zahlreiche Provider zur Verfügung zu stellen. Wie üblich bei OLE DB, besteht die Lösung in einer Dienstkomponente, die mit der nativen Zeilenmenge eine Teile/Ganzes-Beziehung (Aggregation) herstellt und die zusätzlichen Schnittstellen implementiert. Die Dienstkomponente für das Zeilenmengen-Objekt ist der so genannte Client-Cursor. Seine Verwendung ist optional. Wenn der Consumer diese Dienstkomponente nicht verwendet, unterstützen die als Ergebnis erhaltenen Zeilenmengen nur einige native Funktionen. Nutzt der Consumer den Client-Cursor, so bilden die betreffenden Zeilenmengen eine Teile/Ganzes-Beziehung mit einem COM-Objekt, das mehr Funktionen unterstützt. Aus der Sicht des Consumers sieht es dann so aus, als ob das Zeilenmengen-Objekt die gesamte Funktionalität als native Funktionen implementiert. Ein weiterer Vorteil dieser Architektur liegt darin, dass die Client-Cursor-Dienstkomponente vermutlich eher auf dem neuesten Stand ist als die einzelnen Provider. Ein einfacher Provider bietet beispielsweise Zeilenmengen, die keine erweiterten Funktionen wie Zeilensuche und Sichten unterstützen. In der Implementierung entsteht dabei ein COM-Zeilenmengen-Objekt, das die Schnittstellen IRowsetFind und IRowsetView nicht unterstützt. Wenn jedoch die Client-Cursor-Dienstkomponente zur Verfügung steht, entsteht die in Abbildung 8.17 dargestellte Teile/Ganzes-Beziehung zwischen den beiden Objekten.

IRowsetFind IRowsetView

DienstKomponente

Native Zeilenmenge

IRowset

Abbildung 8.17: Zeilenmenge mit Dienstkomponente

Beachten Sie, dass die Implementierung der Dienstkomponente unter Umständen einen Client-seitigen Zwischenspeicherungsmechanismus ins Spiel bringt. Wenn Sie beispielsweise die Schnittstelle IRowsetFind anfordern, kann die Dienstkomponente die Zeilenwerte zum Suchen zwischenspeichern. Mit anderen Worten kann die Dienstkomponente Client-Cursor bewirken, dass der Client »fetter« wird.

Sandini Bib 218

8

Zeilenmengen

Die Frage, ob die Dienstkomponente verwendet werden soll oder nicht, entscheidet sich auf der Ebene der Datenquelle. In der Praxis ist es durchaus empfehlenswert, die Dienstkomponenten wie im folgenden Beispiel systematisch anzufordern: HRESULT hr; CDataSource DataSource; CDBPropSet dbinit(DBPROPSET_DBINIT); // Eigenschaften hinzufügen hr = DataSource.OpenWithServiceComponents(_T("SQLOLEDB.1"), &dbinit);

Weitere Informationen zu Datenquellen finden Sie in Kapitel 6. Wenn die Datenquelle die Client-Cursor-Dienstkomponente festlegt, enthalten alle daraus abgerufenen Zeilenmengen diese Dienstkomponente. Hierfür gibt es keinen zeilenmengenspezifischen Code.

8.9 Zeilen suchen OLE DB bietet drei Möglichkeiten zum Suchen einer bestimmten Zeile über den Wert aus einer der Spalten: 왘 Über SQL-Befehle mit der Form select * from mytable where mycolumn = Wert oder über Parameter, je nachdem, was jeweils gebräuchlicher ist. 왘 Über die Datenbankindizes. Mit diesem Verfahren greift der Client auf den Index auf dem Server zu, sucht nach einer bestimmten Menge von Werten und ruft die Positionsmarke für die Zeile ab. Anschließend kann der Client zur abgerufenen Positionsmarke in der Basistabelle blättern. (In Kapitel 13 werden OLE DB-Indizes behandelt.) 왘 Durchsuchen der Zeilenmenge mit Hilfe der Client-Cursor-Dienstkomponente. Das dritte Verfahren unterscheidet sich grundsätzlich von den beiden anderen: 왘 Die Suchoperation erfolgt auf dem Client und nicht auf dem Server. Folglich kann es vorkommen, dass der Client die gesamte Zeilenmenge abruft, bevor er die richtige Zeile gefunden hat. Außerdem kann die Suche nur anhand einer Spalte durchgeführt werden. 왘 Die Verarbeitung nutzt vermutlich die Datenbankindizes nicht. Anders ausgedrückt, die Suche erfolgt sequenziell, nicht über eine Baumstruktur, und dauert lange, wenn die Zeilenmenge viele Zeilen enthält. Alle diese Feststellungen scheinen gegen die Suchfunktion zu sprechen. In Verbindung mit anderen Mechanismen kann sie jedoch sehr nützlich sein. Beispielsweise ist es

Sandini Bib 8.9

Zeilen suchen

219

möglich, eine SQL-Anweisung abzusetzen und dann eine Suchoperation über die Ergebniszeilenmenge durchzuführen. Ferner ist die Suchoperation für die Suche in kleinen Zeilenmengen praktisch.

8.9.1

Beispiel für eine Suchoperation

Angenommen, eine Tabelle hat zwei Schlüssel, die beide der Einfachheit halber Daten vom Typ integer enthalten. Jetzt sollen zwei aufeinander folgende Suchoperationen anhand desselben ersten Schlüssels durchgeführt werden. Bei einem Beispiel aus der Praxis könnte der erste Schlüssel eine Produkt-ID und der zweite eine Kunden-ID sein. Der Suchvorgang läuft produktweise ab, so dass zwei aufeinander folgende Abfragen vermutlich auf dieselbe Produkt-ID zugreifen. Ein herkömmlicher Ansatz wäre, jedes Mal, wenn eine neue Zeile benötigt wird, eine SQL-Anweisung abzusetzen: SELECT * from table where product = ? and customer = ?

Diese Vorgehensweise ist jedoch nicht optimal, denn sie kann dazu führen, dass viele Abfragen an die Datenbank gesendet werden müssen. Es wäre besser, jeweils den letzten Wert von product zwischenzuspeichern. Wenn eine neue Zeile benötigt wird, überprüft die Komponente, ob sich der Wert für product geändert hat. Wenn ja, wird eine neue Abfrage ausgeführt. Anschließend führt die Komponente eine Suchoperation mit customer als Suchkriterium in der Ergebniszeilenmenge durch. Dies kann jedoch nur effizient ablaufen, wenn die folgenden Bedingungen erfüllt sind: 왘 Der Wert für product ändert sich nicht zu oft, so dass nicht zu viele Abfragen ausgeführt werden müssen. 왘 Die Ergebniszeilenmenge ist nicht zu groß, so dass die Suchoperation schnell genug ablaufen kann.

8.9.2

Verwendung der Suche mit CRowsetFind

Leider unterstützen die OLE DB Consumer Templates die Suchfunktion nicht. Diese Funktion ist nur in den OLE DB-Erweiterungen enthalten. Dies führt uns zu der Zeilenmengen-Klasse CRowsetFind in den OLE DB-Erweiterungen: template class CRowsetFind: public T

CRowsetFind erbt vom Template-Parameter T, so dass Sie diese Klasse mit CRowset, CBulkRowset oder einer anderen Zeilenmengen-Klasse Ihrer Wahl kombinieren können.

Normalerweise werden Sie einfach die Vorgabe verwenden: CCommand MyRowset;

Sandini Bib 220

8

Zeilenmengen

CRowsetFind verwendet die Schnittstelle IRowsetFind. Wenn Sie eine Zeilenmenge erzeugen, die mit der Suchfunktion durchsucht werden soll, muss zunächst die Eigenschaft DBPROP_IRowsetFind auf TRUE gesetzt sein. Diese Einstellung teilt dem Provider mit, dass Sie die Schnittstelle IRowsetFind benötigen. Wenn der Provider nicht in der Lage ist, eine Zeilenmenge zu erzeugen, die IRowsetFind unterstützt, liefert er gar keine Zeilenmenge zurück.

Wenn Ihr Provider IRowsetFind nicht von sich aus unterstützt, können Sie dieses Problem durch Verwendung der Client-Cursor-Engine umgehen. Hierfür müssen Sie folgende Schritte durchführen: 1. Öffnen Sie die Datenquelle mit Dienstkomponenten. In Kapitel 6 ist beschrieben, welche Methoden zum Öffnen einer Datenquelle mit und ohne Dienstkomponenten verwendet werden können. 2. Öffnen Sie die Datenquelle unter Verwendung der Eigenschaft DBPROPSET_DBINIT zur Datenquelleninitialisierung, einschließlich der Angabe DBPROPVAL_OS_ CLIENTCURSOR. Wenn Sie diese beiden Schritte durchgeführt haben, unterstützt jede von der Datenquelle erzeugte Zeilenmenge die Schnittstelle IRowsetFind, sobald diese angefordert wird. Wenn die Zeilenmenge erzeugt wurde, brauchen Sie nur die Methode FindNextRow aufzurufen: template HRESULT FindNextRow(VALUE v, ULONG iOrdinal, DBCOMPAREOP CompareOp = DBCOMPAREOPS_EQ, bool bBind = true, const CBookmarkBase* pBookmark = NULL, LONG lRowsOffset = 0)

wobei 왘 v der zu suchende Wert ist. Da es sich bei dieser Methode um eine TemplateMethode handelt, können Sie einen Wert eines beliebigen Typs übergeben, der von OLE DB erkannt wird, einschließlich integer, boolean und string. FindNextRow sucht automatisch den entsprechenden OLE DB-Typ und erzeugt das richtige Zugriffsobjekt dafür. 왘 iOrdinal die Ordnungsnummer der Spalte ist, zu der der Wert gehören müsste. Nach diesem Parameter haben alle Parameter einen Standardwert, der in den meisten Fällen zutrifft. 왘 CompareOp den zum Suchen der Zeile zu verwendenden Vergleichsoperator angibt (siehe Tabelle 8.9). In der Voreinstellung ist dies DBCOMPAREOPS_EQ, der festlegt, dass die Suche beendet werden soll, wenn der Wert in der Zeile exakt mit dem angeforderten Wert übereinstimmt.

Sandini Bib 8.9

Zeilen suchen

221

왘 bBind angibt, ob die Werte in der Zeilenmenge abgerufen werden sollen, wenn eine Zeilenmenge gefunden wird. In den meisten Fällen wird bBind auf True gesetzt sein (Standardwert). 왘 pBookmark und lRowsOffset die Position angeben, von der aus die Suche gestartet werden soll. Wenn für die Positionsmarke der Wert NULL gesetzt ist, beginnt die Suche an der aktuellen Cursorposition, und der aktuelle Cursor wird bei erfolgreicher Suche an die entsprechende Position verschoben. Ist für pBookmark ein anderer Wert als NULL gesetzt, beginnt die Suche an der Position, die um den Wert für lRowsOffset von der Positionsmarke pBookmark versetzt ist. Die Cursorposition ändert sich in diesem Fall nicht. Symbolischer Wert

Beschreibung

DBCOMPAREOPS_LT

Es wird der erste Wert gefunden, der kleiner als der gesuchte Wert ist.

DBCOMPAREOPS_LE

Es wird der erste Wert gefunden, der kleiner oder gleich dem gesuchten Wert ist.

DBCOMPAREOPS_EQ

Es wird der erste Wert gefunden, der gleich dem gesuchten Wert ist.

DBCOMPAREOPS_GE

Es wird der erste Wert gefunden, der größer oder gleich dem gesuchten Wert ist.

DBCOMPAREOPS_GT

Es wird der erste Wert gefunden, der größer als der gesuchte Wert ist.

DBCOMPAREOPS_BEGINSWITH

Es wird der erste Wert gefunden, der mit dem gesuchten Wert beginnt. Diese Einstellung ist nur für Strings gültig.

DBCOMPAREOPS_NOTBEGINSWITH

Es wird der erste Wert gefunden, der nicht mit dem gesuchten Wert beginnt. Diese Einstellung ist nur für Strings gültig.

DBCOMPAREOPS_CONTAINS

Es wird der erste Wert gefunden, der den gesuchten Wert enthält. Diese Einstellung ist nur für Werte gültig, die als Datentyp String gebunden sind.

DBCOMPAREOPS_NOTCONTAINS

Es wird der erste Wert gefunden, der den gesuchten Wert nicht enthält. Diese Einstellung ist nur für Strings gültig.

DBCOMPAREOPS_NE

Es wird der erste Wert gefunden, der nicht gleich dem gesuchten Wert ist.

DBCOMPAREOPS_IGNORE

Der gesuchte Wert wird ignoriert, d.h. jeder Vergleich ist erfolgreich. Diese Einstellung entspricht dem Blättern.

DBCOMPAREOPS_CASESENSITIVE

Bei der Suche wird die Groß-/Kleinschreibung berücksichtigt. Dieser Operator kann mit anderen Vergleichsoperatoren kombiniert werden.

DBCOMPAREOPS_CASEINSENSITIVE Bei der Suche wird die Groß-/Kleinschreibung nicht berücksichtigt.

Dieser Operator kann mit anderen Vergleichsoperatoren kombiniert werden. Tabelle 8.9: Werte für DBCOMPAREOPSENUM

Sandini Bib 222

8

Zeilenmengen

Bei diesem Beispiel wird in der Zeilenmenge der Wert 3 für die erste Spalte gefunden: HRESULT hr; CDataSource db; CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_AUTH_INTEGRATED, OLESTR("SSPI")); dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR("db")); dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("SERVER")); hr = db.OpenWithServiceComponents(_T("SQLOLEDB.1"), &dbinit); if (FAILED(hr)) return hr; CSession Session; hr = Session.Open(db); if (FAILED(hr)) return hr; CDBPropSet Properties(DBPROPSET_ROWSET); Properties.AddProperty(DBPROP_IRowsetFind , true); CCommand MyRowset; hr = MyRowset.Open(Session, "select * from FetchScroll", &Properties); if (FAILED(hr)) return hr; hr = MyRowset.FindNextRow(3, 1); return hr;

8.10 Zusammenfassung In diesem Kapitel wurden das Zeilenmengen-Objekt und die zugehörigen Klassen vorgestellt. Zeilenmengen haben leistungsfähige Funktionen und sind flexibel. Mit ihnen können Zeilen auf verschiedene Weise, je nach Situation und Anforderungen, abgerufen werden. Eine der leistungsfähigen Funktionen ist die Client-Cursor-Dienstkomponente, über die auch ein Zeilenmengen-Objekt, das vom Provider her nur wenig Möglichkeiten bietet, mit umfassender Funktionalität ausgestattet werden kann. Das Abrufen von Zeilen hilft nicht viel, solange Sie nicht auf die zugehörigen Spaltenwerte zugreifen können. Diesem Thema ist das nächste Kapitel gewidmet.

Sandini Bib

9 Zugriffsobjekte Zeilen nützen nicht viel, wenn nicht auf die Spaltendaten zugegriffen werden kann. Es muss möglich sein, die zwischen dem C++-Objekt und der OLE DB-Zeilenmenge ausgetauschten Daten abzurufen. An dieser Stelle kommt das Konzept des Zugriffsobjekts (Accessor) ins Spiel. Zugriffsobjekte sind OLE DB-spezifische Objekte, deren Funktion Sie unbedingt verstehen müssen, um die OLE DB Consumer Templates effektiv nutzen zu können, denn die Zugriffsobjekte bilden den Kern der Bibliothek. Für OLE DB-Neulinge sind Zugriffsobjekte schwer fassbar, denn es gibt in anderen Datenbank-APIs keine Entsprechung dazu. Sie sind jedoch nicht wirklich kompliziert, sondern stellen lediglich ein neues Konzept dar. Die volle Leistungsfähigkeit der OLE DB Consumer Templates entfaltet sich erst bei der Verwendung von Zugriffsobjekten: Ihre Implementierung ist elegant, flexibel und effektiv. Wenn Sie das Wesen der Zugriffsobjekte einmal verinnerlicht haben, werden Sie sich nicht mehr nach herkömmlichen Datenbank-APIs oder Produkten wie ADO zurücksehnen. Kurz gesagt beschreibt ein Zugriffsobjekt die Beziehung zwischen den Spalten einer Zeilenmenge und einer C++-Klasse. In diesem Kapitel werden Zugriffsobjekte unter folgenden Aspekten beleuchtet: 왘 Die Funktionsweise von Zugriffsobjekten 왘 Die verschiedenen Typen von Zugriffsobjekten 왘 Die Verwendung von Zugriffsobjekten

9.1 Übersicht Die Beschreibung von Zugriffsobjekten ergibt sich am einfachsten, indem man sich klar macht, welches Problem sie lösen. Angenommen, es liegt eine Zeilenmenge mit den drei Spalten A, B und C vor. Die entsprechende C++-Klasse weist die drei Klassenelemente m_a, m_b und m_c auf. Wie kann angegeben werden, welches Klassenelement welcher Spalte entspricht? Abbildung 9.1 zeigt die Zeilenmenge und das C++-Klassenobjekt.

Sandini Bib 224

9

A

B

Zugriffsobjekte

C

Bindung C++-Objekt m_a m_b

Zeilenmenge

m_c

Zugriffsobjekt Abbildung 9.1: Ein Zugriffsobjekt als Gruppe von Bindungen

Es gibt viele Möglichkeiten zur Lösung dieses Problems. Normalerweise wird eine Funktion zur Verfügung gestellt, die den Spaltenwert anhand einer Spaltennummer oder eines Spaltennamens abruft. Der Code zum Abrufen der Werte der Klassenelemente lautet folgendermaßen: // Pseudocode m_a = GetColumnValue(Rowset, "A"); m_b = GetColumnValue(Rowset, "B"); m_c = GetColumnValue(Rowset, "C");

OLE DB verwendet mit seinen Zugriffsobjekten einen grundsätzlich anderen Ansatz. Ein Zugriffsobjekt ist eine Gruppe von Bindungen, von denen jede ein Klassenelement und die zugehörige Spalte identifiziert. Abbildung 9.1 zeigt beispielsweise ein Zugriffsobjekt mit drei Bindungen. Bindungen enthalten zusätzliche Informationen für die Übertragung der Daten. Bei Verwendung einer Gruppe von Bindungen kann die Zeilenmenge die Spaltenwerte lesen und in das C++-Objekt schreiben. Es ist nicht notwendig, dass das C++-Objekt zum Austausch von Daten Iterationen durchführt.

9.1.1

Spalten- und Parameter-Zugriffsobjekte

Es gibt zwei Typen von Zugriffsobjekten: Spalten- und Parameter-Zugriffsobjekte. Ein Spalten-Zugriffsobjekt (Column Accessor) überträgt, wie im vorigen Abschnitt beschrieben, Spaltendaten aus einer Zeilenmenge in ein Consumer-Objekt. Ein ParameterZugriffsobjekt (Parameter Accessor) hat dieselbe Funktion, allerdings für Befehlsparameter. Für den folgenden Befehl wird beispielsweise ein Zugriffsobjekt mit einem Parameter benötigt: SELECT * FROM table where A = ?

Sandini Bib 9.1

Übersicht

225

Eingabe-Zugriffsobjekt Consumer

Provider Ausgabe-Zugriffsobjekt

Abbildung 9.2: Eingabe- und Ausgabe-Zugriffsobjekt

9.1.2

Eingabe- und Ausgabe-Zugriffsobjekte

Sie können Zugriffsobjekte in beide Richtungen verwenden: von der Zeilenmenge bzw. dem Befehlsparameter zum Consumer-Objekt oder umgekehrt. Ein Eingabe-Zugriffsobjekt (Input Accessor) überträgt Daten vom Provider-Objekt zum Consumer-Objekt, während ein Ausgabe-Zugriffsobjekt (Output Accessor) in umgekehrter Richtung funktioniert. In Abbildung 9.2 sind Eingabe- und Ausgabe-Zugriffsobjekte schematisch dargestellt. Behalten Sie die Tatsache, dass ein Zugriffsobjekt sowohl als Eingabe- wie als Ausgabeelement fungieren kann, im Hinterkopf. Wie ein Ausgabe-Zugriffsobjekt für Spalten funktioniert, kann man sich ohne weiteres vorstellen: Die Spaltendaten werden zu den C++-Klassenelementen übertragen. Spalten-Zugriffsobjekte können aber auch als Eingabe-Zugriffsobjekte verwendet werden, wenn eine Zeile in einer Zeilenmenge aktualisiert oder eine neue Zeile eingefügt werden soll. Dies bietet eine interessante Alternative zu UPDATE- und INSERT-Anweisungen (siehe Kapitel 10). Auch die Funktionsweise eines Eingabe-Zugriffsobjekts für Parameter ist offensichtlich. Parameter-Zugriffsobjekte können jedoch auch für Ausgabeparameter verwendet werden. Wenn beispielsweise eine Stored Procedure ein Ergebnis zurückliefert, handelt es sich hierbei um einen Ausgabeparameter. Für Zugriffsobjekte gelten die folgenden Grundregeln: 왘 Wenn Sie eine Spalte nicht binden, können Sie ihren Wert nicht abrufen. 왘 Sie können eine Spalte mit unterschiedlichen Klassenelementen doppelt binden, solange Sie sie nicht als Ausgabe-Zugriffsobjekt verwenden. 왘 Wenn Sie Daten einfügen oder aktualisieren, müssen alle Spalten gebunden werden, für die kein Standardwert gesetzt ist.

9.1.3

Bindungen

Mit einer Bindung wird eine Spalte oder ein Parameter an den Speicher des Consumers gebunden. In OLE DB sind drei Kategorien von Datentypen definiert, die gebunden werden können: Typen mit fester Länge, Typen mit variabler Länge und besonders große Typen.

Sandini Bib 226

9

Zugriffsobjekte

Bei einem Datentyp mit fester Länge ist die Größe immer gleich. Beispielsweise ist eine 4-Byte-Ganzzahl (Datentyp Integer) immer 4 Byte groß. Folgende Datentypen gehören zur Kategorie mit fester Länge: 왘 Die Integer-Typen (Ganzzahlen mit 1, 2 und 4 Byte Länge, mit oder ohne Vorzeichen) 왘 Die Real-Typen (reelle Zahlen mit 4 und 8 Byte Länge) 왘 Alle Date-Typen (Datumstypen) 왘 Die Boolean-Typen (boolesche Typen) 왘 Weitere numerische Typen (z.B. Currency) Typen mit variabler Länge, wie z.B. ein String, sind nicht immer gleich groß. Für den Consumer bedeutet dies, dass es in der Regel nicht ausreicht, einen Zeiger zu übergeben um einen solchen Wert zu setzen oder abzurufen. Sie müssen auch die Länge des Wertes an die Zeigeradresse übergeben. Typen mit variabler Länge können grundsätzlich an einem Stück gelesen werden. Daher sollten sie nicht zu lang sein. Dies bedeutet normalerweise, dass sie höchstens wenige Kilobytes groß sein dürfen. In manchen Fällen sind die Daten zu lang, um an einem Stück gelesen werden zu können, so dass mehrere Iterationen erforderlich sind. Hierbei handelt es sich um die so genannten besonders großen Typen. Bild- oder Soundobjekte oder auch sehr lange Strings gehören zu den besonders großen Typen. Diese Objekte werden in Kapitel 11 ausführlich behandelt. Die Struktur DBBINDING stellt eine Bindung dar. Sie ist folgendermaßen definiert: typedef struct tagDBBINDING { ULONG iOrdinal; ULONG obValue; ULONG obLength; ULONG obStatus; ITypeInfo * pTypeInfo; DBOBJECT * pObject; DBBINDEXT * pBindExt; DBPART dwPart; DBMEMOWNER dwMemOwner; DBPARAMIO eParamIO; ULONG cbMaxLen; DWORD dwFlags; DBTYPE wType; BYTE bPrecision; BYTE bScale; } DBBINDING;

Sandini Bib 9.1

Übersicht

227

wobei iOrdinal für die Nummer der Spalte oder des Parameters steht, zu der bzw. dem die Bindung gehört. Der Wert für iOrdinal reicht von 1 bis zur höchsten Spaltennummer. Der Wert 0 ist für die Eigen-Positionsmarke reserviert.

Datenbestandteile Die Daten setzen sich aus den Elementen Wert (Value), Länge (Length) und Status (Status) zusammen. Es können entweder eines, zwei oder alle drei Elemente gebunden werden. 왘 Value steht für den tatsächlichen Datenwert. Dies ist der offensichtlichste Datenbestandteil. 왘 Length steht für die Länge der Daten in Bytes. Für Datentypen mit fester Länge ist dieser Wert immer gleich der Größe des Typs. Dieses Element ist für Daten mit variabler Länge wie Strings sehr hilfreich. Bei ANSI-Strings ist die abschließende 0 nicht in den Wert für die Länge mit eingeschlossen. Bei UNICODE-Strings entspricht dieser Wert dem Doppelten der Zeichenanzahl, denn jedes UNICODE-Zeichen besteht aus 2 Byte. Bei COM-Objekten stellt dieser Wert die Größe des Zeigers auf die COM-Schnittstelle dar, was bei einer 32-Bit-Architektur gleich 4 ist. 왘 Status dient zur Angabe »spezieller« Werte wie z.B. NULL oder eines Standardwertes. Wie bei den OLE DB-Fehlerobjekten kann der Status eine erfolgreiche Aktion (der Wert wurde erfolgreich gelesen oder geschrieben, mit oder ohne entsprechende Warnmeldung) oder eine gescheiterte Aktion angeben. Ein Erfolgsstatus beginnt mit DBSTATUS_S*, ein Fehlerstatus mit DBSTATUS_E*. In Tabelle 9.1 sind die Konstanten für den Status aufgeführt. dwPart gibt an, welcher Datenbestandteil verwendet wird. Hierbei handelt es sich um eine Kombination der in Tabelle 9.2 aufgeführten Werte. Wenn beispielsweise dwPart = DBPART_VALUE | DBPART_LENGTH gilt, werden die Elemente für Wert und Länge verwen-

det, das Element für den Status dagegen nicht. Beim Lesen oder Schreiben von Daten empfängt die Zeilenmenge einen Puffer und eine Liste der Bindungen. Wenn Sie den Wert, die Länge oder den Status angeben, müssen Sie keinen Zeiger übergeben, sondern einen Offset-Wert in Bezug auf den Pufferanfang. Dieser Offset-Wert wird zum Zeiger auf das übergebene Objekt addiert. Symbolischer Wert

Beschreibung

DBSTATUS_S_OK

Der Wert ist gültig.

DBSTATUS_S_ISNULL

Der Wert ist ein NULL-Wert.

DBSTATUS_S_TRUNCATED

Der Wert wurde abgeschnitten.

Tabelle 9.1: Werte für den OLE DB-Status

Sandini Bib 228

9

Zugriffsobjekte

Symbolischer Wert

Beschreibung

DBSTATUS_E_BADACCESSOR

Das Zugriffsobjekt ist ungültig.

DBSTATUS_E_CANTCONVERTVALUE

Es steht keine Konvertierungsmöglichkeit zwischen den Typen des Consumers und des Providers zur Verfügung.

DBSTATUS_E_CANTCREATE

Der Provider konnte den Wert nicht erzeugen oder zuweisen.

DBSTATUS_E_DATAOVERFLOW

Datenüberlauf.

DBSTATUS_E_SIGNMISMATCH

Die Vorzeichen stimmen nicht überein.

DBSTATUS_E_UNAVAILABLE

Der Wert war nicht verfügbar.

DBSTATUS_S_DEFAULT

Bei dem Wert handelt sich um den im Datenspeicher definierten Standardwert.

DBSTATUS_S_IGNORE

Der Provider müsste diesen Wert ignorieren.

DBSTATUS_E_BADSTATUS

Der Status ist ungültig.

DBSTATUS_E_INTEGRITYVIOLATION

Der Wert hat die für die Spalte geltende Zwangsbedingung verletzt.

DBSTATUS_E_PERMISSIONDENIED

Der Provider hat keine Berechtigung zum Zugriff auf diese Spalte.

DBSTATUS_E_SCHEMAVIOLATION

Der Wert hat die im Schema definierte Zwangsbedingung für die Spalte verletzt.

Tabelle 9.1: Werte für den OLE DB-Status (Fortsetzung) Symbolischer Wert

Beschreibung

DBPART_VALUE

Die Bindung schließt das Datenelement für den Wert (Value) mit ein.

DBPART_LENGTH

Die Bindung schließt das Datenelement für die Länge (Length) mit ein.

DBPART_STATUS

Die Bindung schließt das Datenelement für den Status (Status) mit ein.

Tabelle 9.2: Flags für die Datenbestandteile

m_a

0

m_b m_c

4 8

C++-Objekt Abbildung 9.3: Bindungen

obValues

Sandini Bib 9.1

Übersicht

229

왘 obValue steht für den Offset des Datenwertes im Objekt in Bytes und wird ignoriert, wenn dwPart das Flag DBPART_VALUE nicht enthält. Sie müssen sicherstellen, dass obValue korrekt für die Prozessorarchitektur Ihres Rechners definiert ist. Wenn die Daten beispielsweise vom Typ ULONG sind, muss dieser Wert bei einer 32-Bit-Architektur ein Vielfaches von vier sein. 왘 obLength steht für den Offset der Datenlänge im Objekt in Bytes und muss ein Vielfaches von vier sein. obLength wird ignoriert, wenn dwPart das Flag DBPART_LENGTH nicht enthält. 왘 obStatus steht für den Offset des Datenstatus im Objekt in Bytes und muss ein Vielfaches von vier sein. obStatus wird ignoriert, wenn dwPart das Flag DBPART_STATUS nicht enthält Abbildung 9.3 zeigt die obValue-Felder im Beispiel von Abbildung 9.1, wobei davon ausgegangen wird, dass alle Klassenelemente dem Typ int angehören. Der erste Datenwert im Objekt ist 0, da m_a das erste Klassenelement ist, der zweite Datenwert im Objekt ist 4, da dies der Offset von m_b in der Klasse ist usw. Beim Abrufen und Setzen von Daten empfängt die Zeilenmenge einen Zeiger auf den Puffer (einen Zeiger auf das C++-Objekt) sowie eine Reihe von Bindungen. Die Zeilenmenge ist dann in der Lage, die Adressen der Klassenelemente neu zu erstellen. 왘 pTypeInfo wird derzeit noch nicht verwendet und sollte auf NULL gesetzt werden. 왘 pObject wird für BLOBs verwendet (siehe Kapitel 11). 왘 pBindExt wird derzeit noch nicht verwendet und sollte auf NULL gesetzt werden. 왘 dwMemOwner gibt an, ob der Consumer oder der Provider Eigner des Speichers ist. dwMemOwner wird auf einen der Werte DBMEMOWNER_CLIENTOWNED oder DBMEMOWNER_ PROVIDEROWNED gesetzt. Das Konzept des Speichereigners wird im weiteren Verlauf dieses Kapitels noch ausführlicher behandelt. 왘 eParamIO wird nur für Parameterbindungen verwendet und kann einen der Werte aus Tabelle 9.3 annehmen. 왘 cbMaxLen steht für die Länge der zugewiesenen Datenwerte in Bytes und gilt für Daten mit variabler Länge, wenn der Datenbestandteil für den Wert verwendet wird. (dwPart enthält den Wert DBPART_VALUE). Beim Schreiben des Wertes in den Datenpuffer schneidet der Provider die Daten ab, wenn der Puffer zu klein ist. Der Status wird dann auf DBSTATUS_S_TRUNCATED gesetzt, sofern er in der Bindung enthalten ist.

Sandini Bib 230

9

Zugriffsobjekte

Symbolischer Wert

Beschreibung

DBPARAMIO_NOTPARAM

Das Zugriffsobjekt wird nicht für Parameter verwendet. In den meisten Fällen wird eParamIO für Zeilen-Zugriffsobjekte auf diesen Wert gesetzt, um den Programmierer daran zu erinnern, dass dieses Feld ignoriert wird.

DBPARAMIO_INPUT

Der Parameter ist ein Eingabeparameter.

DBPARAMIO_OUTPUT

Der Parameter ist ein Ausgabeparameter.

Tabelle 9.3: Flags für Parameter-Zugriffsobjekte

Beachten Sie, dass cbMaxLen und obLength nicht dasselbe Konzept beschreiben. obLength bezieht sich auf die tatsächliche Datenlänge in der Zeilenmenge des Providers, während cbMaxLen die Größe der von Ihnen zugewiesenen Daten darstellt. Bei Strings wird in der Länge des Puffers die abschließende 0 berücksichtigt. Wenn Sie beispielsweise einen 5 Zeichen langen Puffer zuweisen, setzen Sie cbMaxLen auf 5. Beim Abrufen des Strings abcde versucht der Provider den Wert abcde\ 0 zu schreiben, der aus sechs Zeichen besteht. Somit schreibt er abcd\ 0 in den Puffer, setzt die Länge auf 5 und den Status auf DBSTATUS_S_TRUNCATED. Kurz gesagt, müssen Sie immer ein Zeichen mehr zuweisen, als die maximale Länge der Daten tatsächlich ist. Dies gilt nicht für DBTYPE_BYTES. 왘 dwFlags gibt das Format an, in dem die Daten zurückgeliefert werden. Derzeit wird nur DBBINDFLAG_HTML unterstützt. 왘 wType stellt den Typ von Consumer-Daten dar. Dieser braucht dem Typ der Provider-Daten nicht zu entsprechen. Wenn sich die Datentypen von Consumer und Provider unterscheiden, versucht OLE DB eine Konvertierung durchzuführen. In Anhang B sind die möglichen Typen und Konvertierungen aufgeführt. 왘 bPrecision stellt die maximale Genauigkeit dar, die beim Abrufen von Daten verwendet werden soll, wenn wType auf DBTYPE_NUMERIC gesetzt ist. Diese Einstellung wird in den folgenden Fällen ignoriert: beim Setzen von Daten, wenn wType nicht DBTYPE_NUMERIC ist oder wenn das DBPART_VALUE-Bit in dwPart nicht gesetzt ist. 왘 bScale stellt die Skalierung beim Abrufen von DBTYPE_NUMERIC oder auf DBTYPE_DECIMAL gesetzt ist. Einstellung in den folgenden Fällen ignoriert: beim nicht DBTYPE_NUMERIC oder DBTYPE_DECIMAL ist oder dwPart nicht gesetzt ist.

9.1.4

Daten dar, wenn wType auf Wie bei bPrecision wird diese Setzen von Daten, wenn wType wenn das DBPART_VALUE-Bit in

Zugriffsobjekte als Gruppen von Bindungen

Auf der OLE DB-Ebene wird nicht zwischen Eingabe- und Ausgabe-Zugriffsobjekten unterschieden. Wenn Sie eine bestimmte Spalte nicht binden (d.h. wenn das Zugriffs-

Sandini Bib 9.2

Zugriffsobjekt-Klassen

231

objekt keine Bindung mit der Positionsnummer der betreffenden Spalte aufweist), wird die darin enthaltene Information nicht an das C++-Objekt übergeben. Wenn Sie eine bestimmte Spalte mehrmals binden (d.h. wenn das Zugriffsobjekt mehrere Bindungen mit der Positionsnummer der betreffenden Spalte aufweist), werden die Informationen doppelt geschrieben. Die Situation stellt sich bei Parametern anders dar, da diese ihren Wert genau einmal abrufen müssen. Wenn Sie daher versuchen ein und denselben Parameter mehrmals zu binden, weiß das Zugriffsobjekt nicht, aus welchem es den Wert lesen soll. Wenn Sie dagegen einen Parameter überhaupt nicht binden, ist sein Wert unbestimmt. Diese Situation gilt es absolut zu vermeiden. Zugriffsobjekte haben etwas mit Zeilen gemeinsam: Es gibt in COM keine direkte Entsprechung für ein Zugriffsobjekt. Zugriffsobjekte werden vielmehr durch das Handle HACCESSOR dargestellt. typedef ULONG HACCESSOR;

Sie können Zugriffsobjekte über die Schnittstelle IAccessor erzeugen und bearbeiten. IAccessor stellt kein Zugriffsobjekt dar, sondern vielmehr ein Objekt, das ein Zugriffsobjekt bearbeiten kann. In Tabelle 9.4 sind die Methoden von IAccessor aufgeführt. Das Erzeugen von Bindungen und Zugriffsobjekten ist keine triviale Aufgabe und außerdem fehlerträchtig. Aus diesem Grund enthalten die OLE DB Consumer Templates Klassen, die Ihnen diese Aufgaben erleichtern. Methode

Beschreibung

AddRefAccessor

Fügt eine Referenz zu einem vorhandenen Zugriffsobjekt hinzu.

CreateAccessor

Erzeugt ein Zugriffsobjekt aus einer Reihe von Bindungen.

GetBindings

Liefert die Bindungen in einem Zugriffsobjekt zurück.

ReleaseAccessor

Gibt ein Zugriffsobjekt frei.

Tabelle 9.4: Methoden von IAccessor

9.2 Zugriffsobjekt-Klassen Die Zugriffsobjekt-Klassen der OLE DB Consumer Templates erleichtern Ihnen die Verwaltung von Zugriffsobjekt-Handles. Sie verfügen über einen Puffer zum Lesen und Schreiben der Daten sowie über ein Set von Zugriffsobjekt-Handles zur Definition der Bindung zwischen dem Puffer und der Zeilenmenge. Alle Zugriffsobjekt-Klassen erben von der Basisklasse CAccessorBase, die einige gemeinsam benutzte Funktionen implementiert.

Sandini Bib 232

9.2.1

9

Zugriffsobjekte

CAccessorBase

Sie brauchen nicht mit CAccessorBase direkt zu arbeiten. Wenn Sie jedoch eine Funktion programmieren müssen, die jeden beliebigen Typ von Zugriffsobjekt als Parameter akzeptiert, müssen Sie CAccessorBase als Parametertyp angeben. CAccessorBase weist die drei folgenden Klassenelemente auf: m_nAccessors enthält die Anzahl der Zugriffsobjekt-Handles, während m_pAccessorInfo ein Array aus Elementen mit der Anzahl m_nAccessors darstellt, die ihrerseits alle Informationen zum Zugriffsobjekt enthalten. Jedes Element ist ein Exemplar von ATL_ACCESSOR_INFO, wobei _ATL_ACCESSOR_INFO folgendermaßen definiert ist: struct _ATL_ACCESSOR_INFO { HACCESSOR hAccessor; bool bAutoAccessor; } ;

hAccessor ist das Zugriffsobjekt-Handle. bAutoAccessor gibt an, ob das Zugriffsobjekt ein automatisches Zugriffsobjekt ist. Ist dies der Fall, ruft die Zeilenmenge bei jeder Cursorbewegung die Daten für dieses Zugriffsobjekt ab. Andernfalls ruft die Zeilenmenge nur dann Daten für dieses Zugriffsobjekt ab, wenn es diese explizit anfordert. (Näheres hierzu finden Sie in Kapitel 10.)

Alle Nachkommen von CAccessorBase erben diese Unterstützung für mehrere Zugriffsobjekte. Einige Zugriffsobjekt-Klassen unterstützen nur ein einziges Zugriffsobjekt-Handle. In diesem Fall ist m_nAccessors immer gleich 1. CAccessorBase enthält auch einen Datenpuffer (m_pBuffer). Die Nachkommen dieser Klasse sind für das Setzen des Puffers über die Methode SetBuffer zuständig. Die Clients von CAccessorBase rufen den Puffer über die Methode GetBuffer ab.

9.2.2

Unterscheidung zwischen den Zugriffsobjekt-Klassen

Die OLE DB Consumer Templates stellen eine breite Palette von Zugriffsobjekt-Klassen zur Verfügung. Beim Auswählen einer Klasse lautet die erste Frage, ob Sie die Spalten der zugrunde liegenden Zeilenmenge kennen. Wenn Sie diese zum Zeitpunkt der Kompilierung kennen, empfiehlt sich die Verwendung der Klasse CAccessor, die Spalten und Parameter statisch bindet. Wenn Sie zum Zeitpunkt der Kompilierung nicht wissen, welche Spalten die Zeilenmenge enthält, haben Sie die Wahl zwischen CDynamicAccessor und CDynamicParameterAccessor. CDynamicAccessor weist den Datenpuffer dynamisch zu. CDynamicParameterAccessor funktioniert wie CDynamicAccessor, fügt aber zusätzlich noch Unterstützung für Parameter hinzu.

Sandini Bib 9.2

Zugriffsobjekt-Klassen

233

Die OLE DB Consumer Templates enthalten auch eine Zugriffsobjekt-Klasse, die Detailfunktionen auf unterer Ebene zur Verfügung stellt: CManualAccessor überlässt es wie CAccessor dem Benutzer, den Speicherpuffer zu reservieren. CManualAccessor und CAccessor unterstützen Spalten- und Parameterbindungen. In der Praxis bietet CManualAccessor kaum Vorteile oberhalb der unspezifischen OLE DB-Strukturen, daher wird diese Klasse hier nicht behandelt. In Tabelle 9.5 sind die einzelnen Zugriffsobjekt-Klassen und ihre Merkmale im Überblick dargestellt. Zugriffsobjekt-Klasse

Puffer

Typ

Unterstützung für Parameterbindung

CAccessor CDynamicAccessor

Benutzer

Statisch

Möglich

OLE DB Templates

Dynamisch

Nein

CDynamicParameterAccessor

OLE DB Templates

Dynamisch

Ja

CManualAccessor

Benutzer

Dynamisch

Möglich

Tabelle 9.5: Zugriffsobjekt-Klassen

9.2.3

CNoAccessor

CNoAccessor ist eine »unechte« Zugriffsobjekt-Klasse. Von außen betrachtet sieht sie

wie ein Zugriffsobjekt aus, aber sie bewirkt nichts. Verwenden Sie diese Klasse für Befehle, die keine Parameter verwenden bzw. keine Zeilenmengen zurückgeben. class CNoAccessor { public: // Hier werden keine Typendefinitionen benötigt, da die // Standard-Typendefinition festlegt, dass keine Parameter und // keine Ausgabespalten verwendet werden. HRESULT BindColumns(IUnknown*) { return S_OK; } HRESULT BindParameters(HACCESSOR*, ICommand*, void**) { return S_OK; } void Close() { } HRESULT ReleaseAccessors(IUnknown*) { return S_OK; } } ;

CNoAccessor ist aus einem Grund interessant: Diese Klasse stellt die erforderlichen

Methoden eines Zugriffsobjekts dar: 왘 BindColumns gibt der Zugriffsobjekt-Klasse die Gelegenheit, die OLE DB-Handles für Spalten-Zugriffsobjekte zu erstellen. 왘 BindParameters ist die entsprechende Methode für Parameter-Zugriffsobjekte. Die Zugriffsobjekt-Klassen behalten keine Referenz auf das Zugriffsobjekt-Handle bei. Die Referenz wird über den Parameter HACCESSOR* an die Befehlsklasse übergeben.

Sandini Bib 234

9

Zugriffsobjekte

왘 Close gibt beliebige Objekte, Handles oder Speicherabschnitte frei, die von der Zugriffsobjekt-Klasse belegt sind. 왘 ReleaseAccessors gibt alle Spalten-Zugriffsobjekte frei.

9.3 Verwendung von CAccessor Die Vererbung von CAccessor ist folgendermaßen definiert: template < class T > class CAccessor : public T, CaccessorBase

wobei T ein vom Benutzer deklarierter Typ ist. Auf den ersten Blick mag CAccessor schwer verständlich sein, weil diese Klasse von ihrem Template-Parameter erbt. T definiert die Klassenelemente und die Bindungen. CAccessor erbt dann die Klassenelemente von T und erstellt die Zugriffsobjekt-Handles über die Methoden, die von CAccessorBase vererbt werden, sowie die Bindungen, die von T vererbt werden. Abbildung 9.4 zeigt das Vererbungsdiagramm von CAccessor. T muss dabei die Spaltenbindungen und ggf. auch die Parameterbindungen definieren. Die BindColumnsImplementierung von CAccessor setzt den Puffer auf sich selbst: HRESULT BindColumns(IUnknown* pUnk) { /// Weiterer Code SetBuffer((BYTE*)this);

Somit sind die Objekte wie in Abbildung 9.5 gezeigt angeordnet.

(Klassenelemente und Bindungen)

(Verwaltung der Zugriffsobjekt-Handles)

T

CAccessorBase

CAccessor

Abbildung 9.4: Vererbung von Caccessor

Sandini Bib 9.3

Verwendung von CAccessor

235

Klassenelement 1 Klassenelement 2

Vererbt von T

Klassenelement 3 m_pAccessorInfo m_nAccessors

Vererbt von CAccessorBase

m_pBuffer

Array aus ZugriffsobjektInformationen Abbildung 9.5: Anordnung von CAccessor

9.3.1

Makros zur Spaltenbindung

Die OLE DB Consumer Templates definieren Bindungen über eine Reihe von Makros. Die Definition der Spaltenbindung beginnt mit der folgenden Zeile: BEGIN_COLUMN_MAP(T)

wobei T die gewählte Klasse ist. Die Definition endet dann mit der folgenden Zeile: END_COLUMN_MAP()

Über das Makro COLUMN_ENTRY können Sie die Spaltenbindung zwischen dem Anfang und dem Ende der Spaltenzuordnung (Column Map) definieren, COLUMN_ENTRY(nOrdinal, data )

wobei 왘 nOrdinal für die Nummer der Spalte steht 왘 data ein Element der gewählten Klasse ist COLUMN_ENTRY ist ein intelligentes Makro, das in der Lage ist, den OLE DB-Typ aus dem C++-Typ seines Datenarguments abzuleiten.

In Tabelle 9.6 sind die C++-Typen und die ihnen entsprechenden OLE DB-Typen aufgelistet. Wenn beispielsweise die Datenbank eine Tabelle mit einer Spalte vom Typ integer enthält, wäre die entsprechende Klasse folgendermaßen definiert: class CMyTable { int m_column1 BEGIN_COLUMN_MAP(CMyTable)

Sandini Bib 236

9

Zugriffsobjekte

COLUMN_ENTRY(1, m_column1) END_COLUMN_MAP() } ;

Denken Sie daran, dass die Spalten, anders als bei C-Arrays, mit 1 und nicht mit 0 beginnen, da dieser Wert für Positionsmarken reserviert ist. Die Bindungsmakros sind einfach in der Verwendung, allerdings wird bei ihrer Ausführung von den OLE DB Consumer Templates viel Hintergrundarbeit geleistet.

Interne Abläufe bei der Ausführung der Makros Wie Sie sich vermutlich denken können, arbeiten die Makros COLUMN_MAP und COLUMN_ENTRY sowie ihre Variationen Hand in Hand. C++-Typ

OLE DB-Typ

CHAR[]

DBTYPE_STR

WCHAR[]

DBTYPE_WSTR

signed char

DBTYPE_I1

SHORT

DBTYPE_I2

int

DBTYPE_I4

LARGE_INTEGER

DBTYPE_I8

BYTE

DBTYPE_UI1

unsigned short

DBTYPE_UI2

unsigned int

DBTYPE_UI4

unsigned long

DBTYPE_UI4

ULARGE_INTEGER

DBTYPE_UI8

float

DBTYPE_R4

double

DBTYPE_R8

DECIMAL

DBTYPE_DECIMAL

DB_NUMERIC

DBTYPE_NUMERIC

IDispatch*

DBTYPE_IDISPATCH

GUID

DBTYPE_GUID

SAFEARRAY*

DBTYPE_ARRAY

DBDATE

DBTYPE_DBDATE

DBTIME

DBTYPE_DBTIME

DBTIMESTAMP

DBTYPE_DBTIMESTAMP

FILETIME

DBTYPE_FILETIME

DBFILETIME

DBTYPE_DBFILETIME

PROPVARIANT

DBTYPE_PROPVARIANT

DB_VARNUMERIC

DBTYPE_VARNUMERIC

Tabelle 9.6: C++-Typen und die entsprechenden OLE DB-Typen

Sandini Bib 9.3

Verwendung von CAccessor

237

Das Makro COLUMN_MAP führt zwei interessante Vorgänge durch: Einerseits deklariert es den Typ _OutputColumnsClass als Makroparameter, was dazu führt, dass die globale Deklaration von _OutputColumnsClass, nämlich _CNoOutputColumns, in dieser Klasse außer Kraft gesetzt wird. Andererseits definiert es eine neue Deklaration von _GetBindEntries: inline static HRESULT _GetBindEntries(ULONG* pColumns, DBBINDING*pBinding, ULONG nAccessor, bool* pAuto, BYTE* pBuffer = NULL)

Der Parameter pColumns von _GetBindEntries empfängt die Anzahl der Spalten. CAccessor kann dann die geeignete Anzahl von Bindungen zuweisen. Wenn pBinding nicht auf NULL gesetzt ist, füllt _GetBindEntries die Strukturen entsprechend auf. Über das Argument pBuffer werden bei Bedarf die Spaltenwerte freigegeben. Da CAccessor mehrere Zugriffsobjekte unterstützt, muss der Zugriffsobjekt-Index in der Tabelle der Zugriffsobjekt-Handles angegeben werden. Wenn nur ein Zugriffsobjekt vorhanden ist, ist dieser Parameter gleich 0. Im Abschnitt über mehrere Zugriffsobjekte erfahren Sie Näheres zu diesem Thema. Der Parameter pAuto dient zur Festlegung, ob es sich bei dem Zugriffsobjekt um ein automatisches Zugriffsobjekt handelt (Näheres hierzu finden Sie im Abschnitt über automatische Zugriffsobjekte weiter unten in diesem Kapitel). Letztendlich wird jedes COLUMN_ENTRY-Makro in ein _COLUMN_ENTRY_CODE-Makro umgewandelt. Dieses ist folgendermaßen definiert: #define _COLUMN_ENTRY_CODE(nOrdinal, wType, nLength, nPrecision, nScale, dataOffset, lengthOffset, statusOffset) \ if (pBuffer != NULL) \ { \ CAccessorBase::FreeType(wType, pBuffer + dataOffset); \ } \ else if (pBinding != NULL) \ { \ CAccessorBase::Bind(pBinding, nOrdinal, wType, nLength, nPrecision, nScale, eParamIO, \ dataOffset, lengthOffset, statusOffset); \ pBinding++; \ } \ nColumns++;

Das Makro _COLUMN_ENTRY_CODE führt folgende drei Aktionen durch: 왘 Wenn der Wert für den Puffer nicht gleich NULL ist, führt es die notwendige Freigabeoperation durch. 왘 Wenn der Bindungszeiger nicht gleich NULL ist, setzt es dessen Felder und inkrementiert ihn auf die Position für das nächste COLUMN_ENTRY-Makro.

Sandini Bib 238

9

Zugriffsobjekte

왘 Es inkrementiert den Wert für nColumns. Am Ende aller Spalteneinträge enthält nColumns als Wert somit die Gesamtanzahl der Spalten. COLUMN_ENTRY ist nicht das einzige Makro, das _COLUMN_ENTRY_CODE aufruft. Im folgenden

Abschnitt werden wir uns mit weiteren solchen Makros beschäftigen.

9.3.2

Makros zur Längen- und Statusbindung

In der Regel empfiehlt es sich, den Status eines Datenwertes zu binden. Auf diese Weise erfahren Sie, ob der Wert korrekt gelesen oder geschrieben wurde und ob der Wert gleich NULL ist. Auf diese Weise können Sie auch den Wert NULL sowie Standardwerte setzen. Das Makro COLUMN_ENTRY_STATUS bindet den Datenwert und seinen Status. Der folgende Code zeigt diesen Vorgang am Beispiel der Spalte column1: LONG m_column1; DBSTATUS m_column1_status; //... BEGIN_COLUMN_MAP(CTable1Accessor5) COLUMN_ENTRY_STATUS(1, m_column1, m_column1_status)

Bei Daten mit variabler Länge empfiehlt es sich auch, die Datenlänge über das Makro COLUMN_ENTRY_LENGTH zu binden. Der folgende Code zeigt diesen Vorgang am Beispiel der Spalte column2: char m_column2 [51]; ULONG m_column2_length; //... COLUMN_ENTRY_LENGTH(2, m_column2, m_column2_length)

Im folgenden Beispiel ist dargestellt, wie sowohl der Status als auch die Länge für die Spalte column2 über das Makro COLUMN_ENTRY_LENGTH_STATUS gebunden werden: char m_column2 [51]; ULONG m_column2_length; DBSTATUS m_column2_status; //... COLUMN_ENTRY_LENGTH_STATUS(2, m_column2, m_column2_length, m_column2_status)

In Tabelle 9.7 sind die Makros mit Längen- oder Statusbindung für Spalteneinträge aufgeführt. Sie sehen, dass bei Längen- und/oder Statusbindung die Anzahl der Elemente in der gewählten Klasse mit 2 bzw. 3 multipliziert wird. Dies kann dazu führen, dass der Code im Klassenmodus schwierig zu lesen ist, weil der Namensbereich für die Klassenelemente übervoll wird.

Sandini Bib 9.3

Verwendung von CAccessor

239

Die OLE DB-Erweiterungen stellen drei Klassen zur Lösung des Benennungsproblems für die Klassenelemente zur Verfügung: 왘 ValueStatus enthält den Wert und den Status. 왘 ValueLength enthält den Wert und die Länge 왘 ValueLengthStatus enthält den Wert, die Länge und den Status. Makro

Beschreibung

COLUMN_ENTRY

Bindet den Wert.

COLUMN_ENTRY_LENGTH

Bindet den Wert und die Länge.

COLUMN_ENTRY_STATUS

Bindet den Wert und den Status.

COLUMN_ENTRY_LENGTH_STATUS

Bindet den Wert, die Länge und den Status.

Tabelle 9.7: Die wichtigsten Makros für die Bindung

Wenn Sie die Klasse ValueStatus verwenden wollen, müssen Sie das entsprechende Klassenelement My_type m_member;

folgendermaßen ändern: ValueStatus m_member;

Ändern Sie die Bindungsdefinition von COLUMN_ENTRY (ordinal, m_member)

in COLUMN_ENTRY_STATUS_AUTO (ordinal, m_member)

Das Makro COLUMN_ENTRY_STATUS_AUTO bindet den Wert und den Status automatisch. Die Klassen ValueLength und ValueLengthStatus werden analog verwendet. Ihre Bindungsmakros heißen COLUMN_ENTRY_LENGTH_AUTO bzw. COLUMN_ENTRY_LENGTH_STATUS_ AUTO. Zusätzlich stellt ValueStatus noch einige komfortable Methoden zum Abrufen und Setzen des Wertes zur Verfügung: operator T*() ValueStatus operator =(T* other) ValueStatus operator =(T other)

Mit der ersten Methode können Sie so auf den Wert zugreifen, als ob es sich um einen Zeiger handeln würde. Beispiel: ValueStatus m_value; LONG* value_pointer = m_value;

Sandini Bib 240

9

Zugriffsobjekte

wobei der Zeiger value_pointer den Wert enthält, wenn der Status korrekt ist. Andernfalls wird er auf NULL gesetzt. Mit den beiden anderen Methoden können Sie den Wert setzen. Wenn Sie einen Zeiger übergeben, wird geprüft, ob dieser auf NULL gesetzt ist. Wenn ja, setzt der Operator den Status auf DBSTATUS_S_ISNULL, andernfalls auf DBSTATUS_S_OK. Außerdem setzt er den Wert. Wenn Sie einen Wert übergeben, erhält er den Status DBSTATUS_S_OK, und der Wert wird gesetzt. Sie können hier beispielsweise Folgendes programmieren: LONG* value_ptr = NULL; my_accessor.m_column1 = value_ptr; LONG value; my_accessor.m_column1 = value

Makro

Beschreibung

COLUMN_ENTRY_PS

Analog zu COLUMN_ENTRY mit zusätzlicher Unterstützung von Genauigkeit und Skalierung.

COLUMN_ENTRY_PS_LENGTH

Analog zu COLUMN_ENTRY_LENGTH mit zusätzlicher Unterstützung von Genauigkeit und Skalierung.

COLUMN_ENTRY_PS_STATUS

Analog zu COLUMN_ENTRY_STATUS mit zusätzlicher Unterstützung von Genauigkeit und Skalierung.

COLUMN_ENTRY_PS_LENGTH_STATUS

Analog zu COLUMN_ENTRY_LENGTH_STATUS mit zusätzlicher Unterstützung von Genauigkeit und Skalierung.

Tabelle 9.8: COLUMN_ENTRY-Makros mit Unterstützung von Genauigkeit und Skalierung

9.3.3

Makros für Genauigkeit und Skalierung

Die OLE DB Consumer Templates stellen eine Reihe von Bindungsmakros für Datentypen zur Verfügung, die eine Angabe der Genauigkeit und der Skalierung erfordern. Diese Makros sind in Tabelle 9.8 aufgeführt.

9.3.4

Makros für Spaltentyp und Größe

Das Makro COLUMN_ENTRY und seine Variationen ermitteln den Typ der Variablen automatisch über das Makro _OLEDB_TYPE: #define COLUMN_ENTRY(nOrdinal, data) \ COLUMN_ENTRY_TYPE(nOrdinal, _OLEDB_TYPE(data), data)

_OLEDB_TYPE wandelt den C++-Typ in den entsprechenden OLE DB-Typ um. Die jeweili-

gen Zuordnungen finden Sie in Tabelle 9.6. Manchmal ist der zurückgegebene Typ jedoch nicht ausreichend oder nicht korrekt. In diesem Fall können Sie das Makro COLUMN_ENTRY umgehen und den Typ explizit über das Makro COLUMN_ENTRY_TYPE angeben.

Sandini Bib 9.3

Verwendung von CAccessor

241

COLUMN_ENTRY_TYPE kann über das Makro _SIZE_TYPE auch die Größe der Variablen ermitteln: #define COLUMN_ENTRY_TYPE(nOrdinal, wType, data) \ COLUMN_ENTRY_TYPE_SIZE(nOrdinal, wType, _SIZE_TYPE(data), data)

Sie können dieses Makro jedoch auch umgehen und die Größe der Variablen manuell über das Makro COLUMN_ENTRY_TYPE_SIZE angeben.

9.3.5

Automatische Zugriffsobjekte

In den OLE DB Consumer Templates sind zwei Arten von Zugriffsobjekten definiert: automatische und nicht automatische Zugriffsobjekte. Die Definition erfolgt über _ATL_ACCESSOR_INFO: struct _ATL_ACCESSOR_INFO { HACCESSOR hAccessor; bool bAutoAccessor; } ;

Wenn es sich beim einem Zugriffsobjekt um einen automatischen Typ handelt, wird der Consumer beim Holen einer Zeile automatisch aktualisiert. Handelt es sich um ein nicht automatisches Zugriffsobjekt, erfolgt eine Aktualisierung nur bei expliziter Anforderung. (Näheres hierzu finden Sie in Kapitel 10.)

9.3.6

Mehrere Zugriffsobjekte

CAccessor unterstützt auch die Verwendung mehrerer Zugriffsobjekt-Handles. Hierfür müssen Sie zunächst über das Makro BEGIN_ACCESSOR_MAP angeben, wie viele

Zugriffsobjekte die Klasse unterstützen soll. Anschließend definieren Sie, beginnend mit dem Makro BEGIN_ACCESSOR, die einzelnen Zugriffsobjekte: #define BEGIN_ACCESSOR(num, bAuto)

wobei num den Zugriffsobjekt-Index darstellt, während bAuto angibt, ob es sich um ein automatisches Zugriffsobjekt handelt. Die folgende Klasse enthält z.B. zwei Zugriffsobjekte: Das erste bindet nur die erste Spalte und ist ein automatisches Zugriffsobjekt, während das zweite nur die zweite Spalte bindet und ein nicht automatisches Zugriffsobjekt ist. class CMyTable { public: // Datenbestandteile int m_column1; CHAR m_column2 [2048];

Sandini Bib 242

9

Zugriffsobjekte

// Bindungszuordnungen für Ausgabe BEGIN_ACCESSOR_MAP(CMyTable, 2) //hat zwei Zugriffsobjekte BEGIN_ACCESSOR(0, true) COLUMN_ENTRY(1, m_column1) END_ACCESSOR() BEGIN_ACCESSOR(1, false) // dies ist kein automatisches // Zugriffsobjekt COLUMN_ENTRY(2, m_ column2) END_ACCESSOR() END_ACCESSOR_MAP() } ;

9.3.7

Binden über Referenz

Daten mit variabler Länge, wie z.B. Strings, können auf zweierlei Weisen gebunden werden: direkt oder über eine Referenz. Bei einer direkten Bindung werden die Daten direkt in den Puffer kopiert, wie in Abbildung 9.6 gezeigt. Wenn eine Spalte über eine Referenz gebunden wird, enthält der C++-Puffer eine Referenz auf die Daten, wie in Abbildung 9.7 für Spalte A gezeigt. Das Binden über Referenz ist praktisch, wenn Daten mit variabler Länge vorliegen. Da die C++-Klasse die Länge der Daten nicht im Voraus kennen muss, besteht nicht die Gefahr, dass Daten abgeschnitten werden. Außerdem weist der Puffer die exakte Länge auf, so dass Überreservierung von Speicher vermieden wird. Beim Binden über eine Referenz können Sie festlegen, welcher Teil für die Freigabe des Speichers zuständig ist. Wenn es der Provider ist, wird dieser als Eigner des Speichers betrachtet, es handelt sich dann also um Provider-Speicher. Ist der Consumer dafür zuständig, wird dieser als Eigner des Speichers betrachtet, es handelt sich dann um Client-Speicher bzw. um Consumer-Speicher.

a b c d 1 C++-Objekt (Consumer) Abbildung 9.6: Direktes Binden

Spalte A

Spalte B

abcd

1

Zeilenmenge (Provider)

Sandini Bib 9.3

Verwendung von CAccessor

243

Spalte A

Spalte B

abcd

1

Referenz 1

a b c d

C++-Objekt (Consumer)

Zeilenmenge (Provider)

Abbildung 9.7: Binden über Referenz

9.3.8

Provider-Speicher

Wenn der Speicher vom Provider verwaltet wird, erhält der Consumer eine direkte Referenz darauf, sollte aber nicht versuchen, den Speicher freizugeben (siehe Abbildung 9.8). Dafür ist in diesem Fall der Provider zuständig, sobald die Zeile freigegeben wurde. Wenn Sie über eine Referenz einen String verwenden wollen, der sich im vom Provider verwalteten Speicher befindet, deklarieren Sie das Klassenelement, z.B. char*, wie folgt: char* m_column1;

Binden Sie es anschließend mit dem Typ DBTYPE_STR|DBTYPE_BYREF: COLUMN_ENTRY_TYPE(1, DBTYPE_STR|DBTYPE_BYREF, m_column1)

Wenn Sie eine Bindung über eine Referenz durchführen, gehen die OLE DB Consumer Templates davon aus, dass es sich bei dem Speicher um Provider-Speicher handelt. Bei Betrachtung von CAccessorBase::Bind sehen Sie den Code, der dies möglich macht: if (wType & DBTYPE_BYREF) pBinding->dwMemOwner = DBMEMOWNER_PROVIDEROWNED; else pBinding->dwMemOwner = DBMEMOWNER_CLIENTOWNED;

Dies bedeutet, dass der Speicher bei Bindung über eine Referenz per Voreinstellung vom Provider verwaltet wird, d.h. Provider-Speicher ist, während er bei direkter Bindung per Voreinstellung vom Consumer verwaltet wird, d.h. Client-Speicher ist. Die Verwendung von Provider-Speicher hat drei Nachteile: 왘 Der Typ des Providers und der Typ des Consumers müssen übereinstimmen. Da der Speicher hierbei nicht kopiert wird, ist keine Konvertierung möglich.

Sandini Bib 244

9

Spalte A

Spalte B

abcd

1

Zugriffsobjekte

Referenz 1

C++-Objekt (Consumer)

Zeilenmenge (Provider)

Abbildung 9.8: Binden über Referenz, Provider-Speicher

왘 Der Provider legt fest, wie lange der betreffende Speicher existiert. Daher sollten Sie vermeiden, den Zeiger darauf mehrfach zu übergeben, weil er in dieser Zeit ungültig werden kann. 왘 Nicht alle Provider unterstützen Provider-Speicher. Wenn einer dieser Nachteile ein Problem für Sie darstellt, können Sie mit Client-Speicher arbeiten.

9.3.9

Client-Speicher

Client-Speicher wird vom Provider reserviert und an den Consumer übergeben, der dann für die Freigabe des Speichers zuständig ist. Da der Provider den COM-Allokator IMalloc verwendet, muss der Client den Speicher am Ende über IMalloc::Free freigeben. Wenn Sie aber free anstelle von IMalloc::Free verwenden, können Sie sicher sein, dass Ihre Anwendung abstürzt. In Abbildung 9.9 ist der Reservierungsvorgang für Client-Speicher schematisch dargestellt. Dieses Verfahren ist langsamer, weil der Inhalt des Strings vom Provider-Puffer in den Zwischenpuffer kopiert wird. Zudem wird es von den OLE DB Consumer Templates nicht unterstützt, da diese, wie wir bereits gesehen haben, davon ausgehen, dass nach jeglichem Binden über eine Referenz der Speicher vom Provider verwaltet wird. Die OLE DB-Erweiterungen unterstützen hingegen dieses Verfahren über die Makros CLIENT_OWNED und FREE_MEMORY. Fügen Sie zunächst nach dem Spalteneintrag die Angabe CLIENT_OWNED hinzu um die Verwendung von Client-Speicher zu erzwingen. Fügen Sie dann dahinter die Angabe FREE_MEMORY hinzu um anzugeben, dass der Speicher über die Methode FreeRecordMemory freigegeben werden soll. Andernfalls müssen Sie den Speicher manuell verwalten. char * m_column1; char * m_column2; BEGIN_COLUMN_MAP(CTable1Accessor4) //...

Sandini Bib 9.3

Verwendung von CAccessor

245

COLUMN_ENTRY_TYPE (1, DBTYPE_STR|DBTYPE_BYREF, m_column1) CLIENT_OWNED // Spalte 1 wird nicht durch FreeRecordMemory freigegeben COLUMN_ENTRY_TYPE (2, DBTYPE_STR|DBTYPE_BYREF, m_column2) CLIENT_OWNED FREE_MEMORY(m_column2) // Spalte 2 wird durch FreeRecordMemory freigegeben

IMalloc::Alloc IMalloc::Free

Referenz 1

C++-Objekt (Consumer)

Spalte A

Spalte B

abcd

1

abcd

Zeilenmenge (Provider)

Abbildung 9.9: Binden über Referenz, Client-Speicher

Wie bei der Verwendung von Provider-Speicher müssen Sie den Typ direkt angeben. Versuchen Sie nicht diesen vom Compiler ableiten zu lassen. Ferner müssen Sie nach jeder Cursorbewegung FreeRecordMemory aufrufen um ggf. Speicher freizugeben.

9.3.10 BSTR BSTR ist, einfach ausgedrückt, eine Möglichkeit um Strings zusammen mit COM verwenden zu können. Hierbei handelt es sich um einen UNICODE-String, der mit speziellen Funktionen bearbeitet werden kann.

Der folgende Code zeigt, wie Sie BSTR-Strings verwenden und bearbeiten können: BSTR m_column2; BEGIN_COLUMN_MAP(CTable1Accessor5) //... COLUMN_ENTRY_TYPE(2, DBTYPE_BSTR, m_column2) END_COLUMN_MAP()

Geben Sie den Typ explizit an, da ansonsten der generierte Code nicht erfolgreich ausgeführt werden kann. Dieser Ansatz ist einfach und komfortabel, beschränkt Sie jedoch auf die Verwendung des Typs BSTR, der nur mit den BSTR-Funktionen zugewiesen und gelöscht werden kann. Zudem müssen Sie in diesem Fall Doppelbyte-Zeichen verwenden. Bei Verwendung des Typs BSTR müssen Sie nach jeder Cursorbewegung FreeRecordMemory aufrufen um ggf. Speicher freizugeben.

Sandini Bib 246

9

Zugriffsobjekte

9.3.11 Zusammenfassung der Bindungsverfahren Die einzelnen Verfahren zum Binden lassen sich kurz zusammengefasst wie folgt charakterisieren: 왘 Verfahren 1 ermöglicht direktes Binden. 왘 Verfahren 2 ermöglicht das Binden über eine Referenz mit Provider-Speicher. 왘 Verfahren 3 ermöglicht das Binden über eine Referenz mit Client-Speicher. 왘 Verfahren 4 ermöglicht das Binden mit dem Typ BSTR. In Tabelle 9.9 sind die einzelnen Verfahren und ihre Merkmale in einer Übersicht dargestellt.

9.3.12 Parameterbindung Das Binden von Parametern funktioniert ähnlich wie das Binden von Spalten. Die Bindungsdeklaration beginnt mit BEGIN_PARAM_MAP(x) und endet mit END_PARAM_MAP(), wobei x für die Klasse selbst steht. Die Parameter werden wie Spalten deklariert. Beispiel: BEGIN_PARAM_MAP(CTable1Accessor9) COLUMN_ENTRY(1, m_column1) END_PARAM_MAP()

Verfahren

1

2

3

4

Speicherreservierung durch

Consumer

Provider

Provider

Provider

Gefahr, dass Daten abgeschnitten werden

Ja

Nein

Nein

Nein

Datenübertragung

Langsam

Schnell

Langsam

Langsam

Speicherfreigabe durch

Consumer

Provider

Consumer

Consumer

Automatische Speicherfreigabe

Ja, erfolgt über

Trifft nicht zu

Hängt von

Ja, erfolgt über

FREE_MEMORY ab

FreeRecordMemory

FreeRecordMemory

Unterstützung durch Provider

Ja

Nicht von allen

Ja

Ja

Unterstützung durch die OLE DB Consumer Templates

Ja

Ja

Nein

Ja

Einzelbyte-Zeichen zulässig

Ja

Ja

Ja

Nein

Tabelle 9.9: Bindungsverfahren

Sandini Bib 9.4

Dynamische Zugriffsobjekte

247

Symbolischer Wert

Beschreibung

DBPARAMIO_NOTPARAM

Das Zugriffsobjekt wird für Parameter nicht verwendet. In den meisten Fällen wird eParamIO auf diesen Wert gesetzt um den Programmierer daran zu erinnern, dass dieses Feld ignoriert wird.

DBPARAMIO_INPUT

Der Parameter ist ein Eingabeparameter.

DBPARAMIO_OUTPUT

Der Parameter ist ein Ausgabeparameter.

Tabelle 9.10: Flags für den Parametertyp

Zusätzlich können Sie den Parametertyp über SET_PARAM_TYPE angeben. Positionieren Sie dieses Makro vor der zu deklarierenden Spalte. Als Parametertyp können Sie einen der in Tabelle 9.10 aufgeführten Typen verwenden. Beachten Sie, dass BEGIN_PARAM_MAP das Element eParamIO auf DBPARAMIO_INPUT setzt. Somit brauchen Sie SET_PARAM_TYPE nicht zu verwenden, wenn alle von Ihnen verwendeten Parameter Eingabeparameter sind.

9.4 Dynamische Zugriffsobjekte Manchmal sind die Spalten oder die Parameter der Zeilenmenge zum Zeitpunkt der Kompilierung nicht bekannt. Daher muss das Zugriffsobjekt den Consumer-Puffer dynamisch reservieren. Zugriffsobjekte, die den Puffer ausgehend von der Zeilenmenge oder dem Parameter reservieren, werden dynamische Zugriffsobjekte genannt. Die OLE DB Consumer Templates enthalten die beiden folgenden dynamischen Zugriffsobjekte: CDynamicAccessor und CDynamicParameterAccessor. Das Zugriffsobjekt CDynamicAccessor bindet nur Spalten, während CDynamicParameterAccessor sowohl Spalten als auch Parameter bindet. CDynamicParameterAccessor ist von CDynamicAccessor abgeleitet. Abbildung 9.10 zeigt die Vererbungsbeziehungen. Beim dynamischen Reservieren des Puffers durch CDynamicAccessor werden zunächst die Anzahl der Spalten und die Länge der einzelnen Spalten angefordert. Zu jeder Spalte wird ausreichend Speicher für die eigentlichen Spaltendaten plus 4 Byte für die Längenangabe und 4 Byte für den Status angefordert. CDynamicAccessor bindet alle drei Datenbestandteile. Die entsprechende Anordnung ist in Abbildung 9.11 dargestellt. Beachten Sie, dass die Größe des eigentlichen Datenbestandteils von Spalte zu Spalte unterschiedlich sein kann. Zwar erbt die Klasse CDynamicAccessor die Funktionen zur Unterstützung mehrerer Zugriffsobjekt-Handles, doch sie verwendet nur ein solches Handle.

Sandini Bib 248

9

Zugriffsobjekte

CAccessorBase

CDynamicAccessor

CDynamicParameterAccessor

Abbildung 9.10: Vererbung bei dynamischen Zugriffsobjekten

Array aus Zugriffsobjekt-Infos m_pAccessorInfo m_nAccessors m_pBuffer

=1 Daten 1 Länge 1

Spalte 1

Status 1 Variable Länge

Daten 2 Länge 2

Spalte2

Status 2 Abbildung 9.11: Anordnung der Datenbestandteile bei CDynamicAccessor

9.4.1

Verwendung von dynamischen Zugriffsobjekten

In diesem Abschnitt wird die Verwendung eines Exemplars von CDynamicAccessor für eine zweispaltige Tabelle beschrieben. Der Datentyp für die erste Spalte ist integer, für die zweite Spalte string. Sie erfahren in diesem Abschnitt auch, wie Sie den Inhalt dieses dynamischen Zugriffsobjekts ermitteln. Zunächst müssen wir die Tabelle öffnen: CTable table; hr = table.Open(Session, "Table1");

Sandini Bib 9.4

Dynamische Zugriffsobjekte

249

Anzahl der Spalten Im nächsten Schritt ermitteln Sie die Anzahl der Spalten: ULONG ColumnCount = table.GetColumnCount(); OutputDebugString ("Anzahl der Spalten:\ n"); OutputDebugString (I2T (ColumnCount));

Wenn Sie das Programm ausführen, erhalten Sie die folgende Ausgabe: Anzahl der Spalten: 3

Warum wird der Wert 3 und nicht 2 ausgegeben, da die Tabelle ja nur zwei Spalten hat? In manchen Fällen fügt der Provider eine Spalte für eine Positionsmarke hinzu. Dies ist die einzige Spalte mit der Nummer 0. Die Nummern aller anderen Spalten sind größer oder gleich 1. Kurz zusammengefasst gibt es zwei mögliche Fälle: 왘 Der Provider hat eine Positionsmarkenspalte hinzugefügt, so dass die Spaltennummern von 0 bis zum Wert von ColumnCount() – 1 reichen. 왘 Der Provider hat keine Positionsmarkenspalte hinzugefügt, so dass die Spaltennummern von 1 bis ColumnCount() reichen. Wenn Sie herausfinden wollen, ob eine Positionsmarke vorhanden ist, brauchen Sie nur die Nummer der ersten Spalte zu prüfen: bool HasBookmark = table.m_pColumnInfo->iOrdinal == 0;

Sie können auch versuchen, eine Positionsmarke abzurufen, und prüfen, ob dieser Vorgang erfolgreich war: CBookmark Bookmark; HasBookmark = SUCCEEDED (table.GetBookmark(&Bookmark));

Eine andere Möglichkeit besteht darin zu prüfen, ob das Spalten-Flag den Wert DBCOLUMNFLAGS_ISBOOKMARK enthält. Dieses Verfahren wird im nächsten Abschnitt erläutert. Sobald Sie wissen, wie viele Spalten es sind und ob eine Positionsmarkenspalte vorhanden ist, kennen Sie den Spaltennummernbereich.

Typen, Namen und Flags Der nächste Schritt besteht darin, die Datentypen der einzelnen Spalten und ggf. auch ihre Namen zu ermitteln. In der Klasse CDynamicAccessor sind hierfür die drei folgenden Methoden definiert: 왘 GetColumnType ruft den Typ der Spalte entweder über ihren Namen oder ihre Nummer ab.

Sandini Bib 250

9

Zugriffsobjekte

왘 GetColumnName ruft den Namen der Spalte ab. 왘 GetColumnFlags ruft die Flags der Spalte entweder über ihren Namen oder ihre Nummer ab. Die Spalten-Flags bestehen aus einer Kombination der Werte aus Tabelle 9.11. Symbolischer Wert

Wert

Beschreibung

DBCOLUMNFLAGS_ISBOOKMARK

0x1

Die Spalte enthält eine Positionsmarke.

DBCOLUMNFLAGS_MAYDEFER

0x2

Spalte mit verzögerter Aktualisierung. Der Provider braucht die Daten erst dann abzurufen, wenn für diese Spalte GetData aufgerufen wird.

DBCOLUMNFLAGS_WRITE

0x4

In die Spalte kann geschrieben werden.

DBCOLUMNFLAGS_WRITEUNKNOWN

0x8

Der Provider weiß nicht, ob Daten in diese Spalte geschrieben werden können.

DBCOLUMNFLAGS_ISFIXEDLENGTH

0x10

Die Spalte hat eine feste Länge.

DBCOLUMNFLAGS_ISNULLABLE

0x20

In diese Spalte können NULL-Werte eingefügt werden.

DBCOLUMNFLAGS_MAYBENULL

0x40

Diese Spalte kann NULL-Werte enthalten.

DBCOLUMNFLAGS_ISLONG

0x80

Die Spalte enthält sehr große Objekte (BLOBs). Siehe auch Kapitel 11.

DBCOLUMNFLAGS_ISROWID

0x100

Dieser Wert wird gesetzt, wenn eine Spalte eine persistente Spalten-ID aufweist, in die nicht geschrieben werden kann. Diese weist keinen signifikanten Wert auf und dient nur zur Identifikation der Zeile.

DBCOLUMNFLAGS_ISROWVER

0x200

Dieser Wert wird gesetzt, wenn die Spalte eine Zeitangabe oder einen anderen Versionsmechanismus aufweist, in den nicht direkt geschrieben werden kann. Wenn die Zeile aktualisiert und festgeschrieben wird, erfolgt automatisch eine Aktualisierung dieser Angabe auf einen höheren Wert.

DBCOLUMNFLAGS_CACHEDEFERRED

0x1000

Dieser Wert wird gesetzt, wenn beim erstmaligen Lesen einer Spalte mit verzögerter Aktualisierung ihr Wert vom Provider zwischengespeichert wird.

Tabelle 9.11: Spalten-Flags

Wenn eine Funktion einen booleschen Wert zurückliefert, bedeutet True, dass die Funktion keinen Fehler festgestellt hat. Sie liefert False zurück, wenn ein Fehler erkannt wurde (z.B. die Spaltennummer liegt außerhalb des zulässigen Bereichs).

Sandini Bib 9.4

Dynamische Zugriffsobjekte

251

Der folgende Beispielcode führt Iterationen über die Spalten durch und ruft für jede Spalte den Typ, den Namen und das Flag ab: ULONG MinOrdinal, MaxOrdinal; if (HasBookmark) { MinOrdinal = 0; MaxOrdinal = ColumnCount – 1; } else { MinOrdinal = 1; MaxOrdinal = ColumnCount; } for (ULONG Ordinal = MinOrdinal;Ordinal GetData(m_hRow, m_pAccessor->GetHAccessor (nAccessor), m_pAccessor->GetBuffer()); }

Obwohl das Abrufen auf diese Weise nicht schwierig ist, geht die Klasse CRowset noch einen Schritt weiter, indem sie die Methode GetData bei jeder Cursorbewegung aufruft. Dies bedeutet, dass die folgenden Methoden alle GetData aufrufen: MoveToBookmark, MoveToRatio, MoveNext, MovePrev und MoveLast. Indirekt gilt dies auch für Methoden wie MoveFirst, die GetData über den Aufruf von MoveNext aufruft. Grundsätzlich bedeutet dies, dass der Inhalt des Puffers immer mit der aktuellen Zeile synchronisiert wird. Dies stellt jedoch ein Problem für Spalten dar, bei denen das Abrufen viel Aufwand bedeutet. Stellen Sie sich eine Tabelle mit zwei Spalten vor: Die erste enthält ganzzahlige Daten (Datentyp integer), die zweite lange Zeichenfolgen (Datentyp string). Wenn ein Zugriffsobjekt die zweite Spalte bindet, wird jede Cursorbewegung aufwändig, weil lange Strings übertragen werden müssen. Wenn dagegen die zweite Spalte nicht gebunden ist, kann ihr Wert nicht abgerufen werden. Somit brauchen wir einen Mechanismus, mit dem festgelegt wird, dass der Inhalt von bestimmten Spalten bei jeder Cursorbewegung übertragen werden soll, von anderen Spalten dagegen nur auf Anforderung. In unserem Beispiel könnte der Inhalt der Spalte mit den Ganzzahlen systematisch übertragen werden, der Inhalt der Spalte mit den langen Strings dagegen nur, wenn der Consumer weiß, dass er ihn benötigt. An dieser Stelle kommen automatische Zugriffsobjekte ins Spiel.

10.1.1 Automatische Zugriffsobjekte In Kapitel 9 wurden automatische Zugriffsobjekte bereits kurz beschrieben. Das automatische Zugriffsobjekt (Auto Accessor) ist allerdings nicht in OLE DB definiert, sondern nur auf C++-Ebene im Rahmen der OLE DB Consumer Templates. Ein Zugriffsobjekt ist nur dann als automatisch definiert, wenn eine Zeilenmenge die Daten für das Zugriffsobjekt bei jeder Cursorbewegung abruft. Mit anderen Worten umfassen automatische Zugriffsobjekte diejenigen Spalten, die einfach abgerufen werden können oder ständig benötigt werden. Nichtautomatische Zugriffsobjekte greifen dagegen auf Spalten zu, bei denen das Abrufen aufwändig ist und die nicht ständig benötigt werden. Bei jeder Cursorbewegung ruft die Zeilenmenge eine weitere Version von GetData auf: HRESULT GetData();

Diese Funktion akzeptiert keinen Zugriffsobjekt-Index. Die Methode GetData ruft die Daten für alle automatischen Zugriffsobjekte ab, indem sie entsprechende Iterationen für alle Zugriffsobjekte durchführt. Findet sie ein automatisches Zugriffsobjekt, ruft sie dafür GetData(int nAccessor) auf:

Sandini Bib 10.1

Daten abrufen

259

HRESULT GetData() { HRESULT hr = S_OK; ATLASSERT(m_pAccessor != NULL); ULONG nAccessors = m_pAccessor->GetNumAccessors(); for (ULONG i=0; iIsAutoAccessor(i)) { hr = GetData(i); if (FAILED(hr)) return hr; } } return hr; }

Es gibt keinen Grund dafür, GetData() explizit aufzurufen, da dies von den OLE DB Consumer Templates übernommen wird. Rufen Sie jedoch GetData() nach jeder Cursorbewegung auf, die nicht von den Templates implementiert ist. GetData(int nAccessor) eignet sich auch für das Abrufen von Daten für nichtautomatische Zugriffsobjekte. Betrachten wir beispielsweise die folgende Klasse: class CMyAccessor { public: LONG m_Column1; LONG m_Column2; LONG m_Column3; LONG m_Column4; BEGIN_ACCESSOR_MAP(CdboCustomerAccessor, 4) \ BEGIN_ACCESSOR(0, true) COLUMN_ENTRY(1, m_ Column1) END_ACCESSOR() BEGIN_ACCESSOR(1, true) COLUMN_ENTRY(2, m_ Column2) END_ACCESSOR() BEGIN_ACCESSOR(2, false) COLUMN_ENTRY(3, m_ Column3) END_ACCESSOR() BEGIN_ACCESSOR(3, false) COLUMN_ENTRY(4, m_ Column4) END_ACCESSOR() END_ACCESSOR_MAP() DEFINE_COMMAND(CMyAccessor, _T(" \ SELECT …der Rest wie gehabt"

Sandini Bib 260

10

Datenaustausch

Nur für Column1 und Column2 werden automatische Zugriffsobjekte verwendet. Column3 und Column4 werden hingegen über nicht automatische Zugriffsobjekte abgerufen. In der Voreinstellung werden nur Column1 und Column2 mit der aktuellen Zeile synchronisiert. Beispielsweise werden im folgenden Code nur diese Spalten mit Daten gefüllt. CCommand Command; Command.Open (Session); Command.MoveNext();

Wenn Sie GetData(2) aufrufen, wird die Spalte Column3 mit dem Wert aus der Zeile aktualisiert usw.: Command.GetData(2); // aktualisiert Column3 Command.GetData(3); // aktualisiert Column4

10.1.2 GetDataHere Bisher haben wir zum Aktualisieren der Daten im aktuellen Objekt (oder, genauer gesagt, im zugehörigen Puffer) mit GetData gearbeitet. Manchmal müssen aber auch Daten abgerufen werden, die nicht vom aktuellen Objekt stammen. Das bedeutet, dass ein entsprechender Puffer übergeben werden muss, in den die Zeilenmenge die Daten schreiben soll. Hierzu dient die Methode GetDataHere. Sie unterscheidet sich von GetData dadurch, dass sie ein zusätzliches Argument für den Puffer aufweist. Wie bei GetData gibt es auch von GetDataHere zwei Versionen: 왘 HRESULT GetDataHere(int nAccessor, void* pBuffer) 왘 HRESULT GetDataHere(void* pBuffer)

Beachten Sie, dass GetDataHere(int nAccessor, void* pBuffer) dieselbe Funktion hat wie GetData(int nAccessor). Das bedeutet, dass die beiden folgenden Zeilen gleichwertig sind: MyAccessor.GetData(nAccessor);

entspricht MyAccessor.GetDataHere(nAccessor, m_pAccessor->GetBuffer());

Dasselbe trifft auf GetDataHere(void* pBuffer) und GetData() zu. GetDataHere ist sehr nützlich, wenn Sie Daten außerhalb eines CCommand-Exemplars speichern wollen. Angenommen, Sie verfügen über eine Zugriffsobjekt-Klasse mit den folgenden Klassenelementen: class CMyAccessor { LONG m_Column1; LONG m_Column2; BEGIN_ACCESSOR(CMyAccessor)

Sandini Bib 10.2

Daten vom Consumer zum Datenspeicher übertragen

261

COLUMN_ENTRY(1, m_Column1) COLUMN_ENTRY(2, m_Column2) END_ACCESSOR() } ;

Sie müssen in diesem Fall jedoch einen Befehl zum Zugriff auf die Daten deklarieren: CCommand Command;

Angenommen, Sie wollen die Zeilenwerte in einem Array speichern. Wenn Sie das Array als CCommand MyArray [12];

deklarieren, enthält jedes Element einen Zeiger auf das von CCommandBase vererbte Befehlsobjekt. Dies führt zu einer ineffizienten Verarbeitung. Alternativ können Sie ein Array des folgenden Typs deklarieren: CMyAccessor MyArray [12];

Allerdings sind die Array-Elemente nun vom Typ her nicht kompatibel mit Command. Das bedeutet, dass es nicht möglich ist, die folgende Zeile zu programmieren: MyArray[3] = Command; // nicht möglich

Eine Lösung besteht darin, die Daten beim Füllen des Arrays von einem Puffer zum anderen zu kopieren: Command.MoveNext(); memcpy(MyArray[3], Command.GetBuffer(), sizeof(CMyAccessor));

Eine andere Lösung bietet die Verwendung von GetDataHere: Command.MoveNext(); Command.GetDataHere((void*) MyAccessor [3]);

10.2 Daten vom Consumer zum Datenspeicher übertragen Ein Consumer kann die drei folgenden Operationen an Zeilen durchführen: Daten setzen, einfügen und löschen. Diese Operationen können zwar auch mit Befehlen durchgeführt werden, aber OLE DB bietet eine attraktive Alternative hierzu. Die Zeilenmenge kann die optionale Schnittstelle IRowsetChange unterstützen, mit deren Methoden diese Operationen direkt durchgeführt werden können. Dies hat den Vorteil, dass weder Befehlstext noch Befehlsparameter benötigt werden. Der Code kann somit leichter gepflegt werden.

Sandini Bib 262

10

Datenaustausch

IRowsetChange verfügt über die drei folgenden Methoden:

왘 DeleteRows löscht eine oder mehrere Methoden in der Zeilenmenge. 왘 SetData ändert den Inhalt einer Zeile. 왘 InsertRow fügt eine Zeile ein. virtual HRESULT STDMETHODCALLTYPE DeleteRows( HCHAPTER hReserved, ULONGcRows, const HROW __RPC_FAR rghRows[ ], DBROWSTATUS __RPC_FAR rgRowStatus[ ]) = 0; virtual HRESULT STDMETHODCALLTYPE SetData( HROW hRow, HACCESSOR hAccessor, void __RPC_FAR *pData) = 0; virtual HRESULT STDMETHODCALLTYPE InsertRow( HCHAPTER hReserved, HACCESSOR hAccessor, void __RPC_FAR *pData, HROW __RPC_FAR *phRow) = 0;

10.2.1 Zeile löschen Es gibt drei Möglichkeiten zum Löschen einer Zeile aus einer Zeilenmenge: 왘 Verwendung eines Befehls. 왘 Verwendung eines Befehls mit Parametern. 왘 Verwendung der Methode Delete. Angenommen, eine Tabelle enthält eine Spalte mit dem Namen Value, die Daten vom Typ integer enthält. Weiter angenommen, diese Tabelle enthält zwei Zeilen mit den Werten 1 und 2. Im Folgenden erfahren Sie, wie die erste Zeile gelöscht werden kann. Das erste Verfahren besteht einfach darin, dass ein SQL-Befehl wie der Folgende abgesetzt wird: DELETE FROM TABLE_NAME WHERE VALUE

= 1

Dieses Verfahren ist nicht kompliziert und beim Debugging leicht zu testen. Allerdings zwingt es den Programmierer dazu, für jeden Befehl SQL-Text generieren zu lassen. Dies können Sie mit dem zweiten Verfahren vermeiden, bei dem ein Befehl mit Parametern verwendet wird. Hierbei besteht der erste Schritt in der Definition eines Zugriffsobjekts mit einer Parameterbindung:

Sandini Bib 10.2

Daten vom Consumer zum Datenspeicher übertragen

263

class CdboDataExchangeAccessor { public: LONG m_Value; // weiterer Code BEGIN_PARAM_MAP(CdboDataExchangeAccessor) COLUMN_ENTRY(1, m_Value) END_PARAM_MAP() // weiterer Code } ;

Im zweiten Schritt wird der Befehl ausgeführt: CCommand Command; Command.m_Value = 1; Command.Open("DELETE FROM DataExchange WHERE Value = ?");

Dieses Verfahren hat den Vorteil, dass ein und derselbe SQL-Text mehrmals mit unterschiedlichen Parametern ausgeführt werden kann. Je weniger SQL-Code vorhanden ist, umso leichter fällt die Verwaltung des Codes. Außerdem kann dieser Befehl vorbereitet werden, was eine schnellere Ausführung bewirkt. Es wäre gut, wenn man SQL ganz aus dem Code heraushalten könnte. Hier bietet IRowsetChange eine Lösung. Die Klasse CRowset verfügt über die Methode Delete, die IRowsetChange::Delete kapselt: HRESULT Delete() const

Vor der Verwendung von Delete müssen Sie allerdings sicherstellen, dass die Zeilenmenge die Schnittstelle IRowsetChange unterstützt. Der folgende Code zeigt, wie die erste Zeile mit Delete gelöscht wird: CDBPropSet propset(DBPROPSET_ROWSET); propset.AddProperty(DBPROP_IRowsetChange, true); CTable Table; Table.Open(Session, "DataExchange", &propset); Table.MoveFirst(); Table.Delete();

10.2.2 Daten setzen Ein Consumer kann Daten über einen Befehl oder über eine Methode der Klasse CRowset setzen. Zum Setzen der Daten einer Zeile ruft er eine der SetData-Methoden auf:

Sandini Bib 264

10

Datenaustausch

HRESULT SetData(int nAccessor) const

oder HRESULT SetData() const

Die erste SetData-Methode setzt alle Werte, die im Zugriffsobjekt mit dem Index nAccessor für die aktuelle Zeile definiert sind. Das Zugriffsobjekt braucht nicht alle Werte zu binden. Wenn es nur eine Teilmenge bindet, setzt der Provider nur diese Werte. Die anderen Werte bleiben dann unverändert. Die zweite SetData-Methode setzt alle Werte für alle Zugriffsobjekte (anders als die Methode GetData, die nur die Werte für automatische Zugriffsobjekte abruft). Wenn eine Spalte in mehreren Zugriffsobjekten gebunden ist, setzt SetData diesen Wert mehrmals. Denken Sie an diesen Umstand bei Spalten, bei denen das Setzen von Werten aufwändig ist, z.B. lange Textspalten oder BLOBs. Der folgende Code zeigt, wie für die erste Spalte der Wert 3 gesetzt werden kann: CDBPropSet propset(DBPROPSET_ROWSET); propset.AddProperty(DBPROP_IRowsetChange, true); CTable Table; Table.Open(Session, "DataExchange", &propset); Table.MoveFirst(); Table.m_Value = 3; Table.SetData();

10.2.3 Zeilen einfügen Ein Consumer fügt Zeilen durch Aufruf der Methode Insert ein. HRESULT Insert(int nAccessor = 0, bool bGetHRow = false)

Wie bei der Methode SetData kann der Consumer auch hier angeben, welches Zugriffsobjekt verwendet werden soll. In der Voreinstellung verwendet Insert das erste Zugriffsobjekt (Index 0). Es ist wichtig, ein Zugriffsobjekt zu wählen, das die notwendige Anzahl von Spalten bindet. Andernfalls ist der Provider möglicherweise nicht in der Lage zu entscheiden, welcher Wert eingefügt werden soll, und die Einfügeoperation scheitert. Der Consumer kann über den Parameter bGetHRow zusätzlich festlegen, ob die eingefügte Zeile abgerufen werden soll. Wenn bGetHRow auf False gesetzt ist, wechselt die aktuelle Zeile nicht. Andernfalls wird die neue Zeile zur aktuellen Zeile. Der linke Teil von Abbildung 10.1 zeigt das Einfügen einer neuen Zeile ohne Abrufen, so dass die aktuelle Zeile nicht wechselt. Auf der rechten Seite wird eine Zeile eingefügt und die neue Zeile wird abgerufen, so dass die neu eingefügte Zeile zur aktuellen Zeile wird. Der folgende Code zeigt das Einfügen einer Zeile, deren Wert auf 4 gesetzt ist.

Sandini Bib 10.2

Daten vom Consumer zum Datenspeicher übertragen

265

CDBPropSet propset(DBPROPSET_ROWSET); propset.AddProperty(DBPROP_IRowsetChange, true); CTable Table; Table.Open(Session, "DataExchange", &propset); Table.MoveFirst(); Table.m_Value = 4; Table.Insert();

1 Aktuelle Zeile

2

Insert(0, false)

Aktuelle Zeile

1 Aktuelle Zeile

2

Insert(0, true)

1

1

2

2

3

Aktuelle Zeile

3

Abbildung 10.1: Einfügen einer Zeile ohne und mit Abrufen der neuen Zeile

Methode von IRowsetChange/CRowset

Entsprechender SQL-Befehl

SetData()

UPDATE

Insert()

INSERT

Delete()

DELETE

Tabelle 10.1: Die Methoden von IRowsetChange

10.2.4 Soll ich Zeilenmengen-Methoden oder SQL-Befehle verwenden? Insgesamt stehen in OLE DB zwei Möglichkeiten zum Setzen von Daten zur Verfügung: Verwendung einer Sprache wie SQL oder Verwendung von OLE DB-Schnittstellenmethoden. Diese sind in Tabelle 10.1 einander gegenübergestellt. Im Allgemeinen ist es einfacher, die betreffende Methode von CRowset zu verwenden. Beachten Sie jedoch, dass die Operationen damit eher langsamer ablaufen. Wenn Sie Daten setzen oder aktualisieren, rufen Sie diese in der Regel zuvor ab. Dafür sind eine Lese- und eine Schreiboperation erforderlich. Diese sind beim Einfügen nicht mit enthalten.

Sandini Bib 266

10

Datenaustausch

10.3 Verzögerte Aktualisierungen Alle bisher behandelten Änderungen wirken sich sofort aus: Das Einfügen, Löschen oder Ändern einer Zeile wird sofort an den Datenspeicher weitergegeben. Einige OLE DB-Provider bieten aber auch die Möglichkeit einer verzögerten Aktualisierung. In diesem Modus überträgt der Consumer die Änderungen nicht sofort an den Datenspeicher, sondern speichert sie lokal im Cache. Wenn der Consumer dann die Methode Update aufruft, werden die Änderungen in den Datenspeicher übertragen.

Sofortige Aktualisierungen Zeilenmenge

SetData Insert Delete

Datenspeicher

Verzögerte Aktualisierungen Zeilenmenge

SetData Insert Delete

Lokaler Cache

Datenspeicher Update

Abbildung 10.2: Sofortige und verzögerte Aktualisierungen

Abbildung 10.2 zeigt den Unterschied zwischen sofortigen und verzögerten Aktualisierungen. Einer der Vorteile einer verzögerten Aktualisierung liegt in der größeren Flexibilität. Der Consumer kann besser steuern, wann welche Daten in den Datenspeicher geschrieben werden. Beispielsweise kann er mehrere Änderungen an einer Zeilenmenge vornehmen, diese aber erst nach der letzten Änderung in den Datenspeicher übertragen. Zudem können verzögerte Änderungen rückgängig gemacht werden. Dieses Verfahren ist auch hinsichtlich der Leistung vorteilhaft. Da mehrere Änderungen in einem Schritt übertragen werden, kann der Provider diese in einem Netzwerkaufruf gruppieren. Dieser Leistungsvorteil ist natürlich bei einem langsamen Netzwerk wie z.B. dem Internet ein ausschlaggebender Gesichtspunkt. Der Hauptnachteil verzögerter Aktualisierungen liegt in ihrer Komplexität. Der Programmierer muss, nachdem die Änderungen abgeschlossen sind, Update explizit aufrufen, da die Änderungen ansonsten nicht in den Datenspeicher übertragen werden. Außerdem können im Modus mit verzögerter Aktualisierung einzelne Zeilen für einen langen Zeitraum gesperrt sein. Daher sollte dieser Modus mit Bedacht verwendet werden.

Sandini Bib 10.3

Verzögerte Aktualisierungen

267

10.3.1 Verzögerte Änderungen definieren Wenn eine Zeilenmenge auf den Modus mit verzögerter Aktualisierung gesetzt ist, unterstützt sie die Schnittstelle IRowsetUpdate. Eine Zeilenmenge im Modus mit sofortiger Aktualisierung unterstützt diese Schnittstelle nicht. Der Consumer gibt den Aktualisierungsmodus über die Eigenschaft DBPROP_IRowsetUpdate an, die, wie jede andere Zeilenmengen-Eigenschaft auch, für die Zeit gültig ist, in der die Zeilenmenge existiert. Anders ausgedrückt: Wenn Sie einmal einen Aktualisierungsmodus für eine Zeilenmenge gewählt haben, besteht keine Möglichkeit mehr, einen anderen festzulegen. Das folgende Beispiel zeigt, wie eine Zeilenmenge im Modus mit sofortiger Aktualisierung und eine andere im Modus mit verzögerter Aktualisierung erzeugt wird: CCommand ImmediateRowset CDBPropSet Propset (DBPROPSET_ROWSET); Propset.AddProperty(DBPROP_IRowsetUpdate, false); ImmediateRowset.Open(Session, sometext, &Propset); /// CCommand DeferredRowset; CDBPropSet Propset (DBPROPSET_ROWSET); Propset.AddProperty(DBPROP_IRowsetUpdate, true); DeferredRowset.Open(Session, sometext, &Propset);

10.3.2 Verwendung von verzögerten Aktualisierungen In OLE DB ist keine Implementierung für verzögerte Aktualisierungen explizit definiert. Im folgenden Abschnitt erfahren Sie, wie verzögerte Aktualisierungen funktionieren.

10.3.3 Der Änderungsstatus bei verzögerter Aktualisierung Der Änderungsstatus bei verzögerter Aktualisierung (Pending Status) beschreibt, ob Änderungen an einer Zeile noch anstehen und um welche Änderungen es sich ggf. handelt. Die Werte für diesen Status sind in Tabelle 10.2 aufgeführt. Es ist wichtig, sich klar zu machen, dass der Änderungsstatus bei verzögerter Aktualisierung nur die noch anstehenden Operationen und nicht die übertragenen Änderungen beschreibt. Wenn beispielsweise eine Zeile mit noch anstehenden Änderungen in eine Zeilenmenge eingefügt wird, hat sie den Status DBPENDINGSTATUS_NEW. Wenn die Zeilenmenge die Änderungen dann überträgt, erhält die Zeile den Status DBPENDINGSTATUS_UNCHANGED. Der Änderungsstatus bei verzögerter Aktualisierung wird umso komplexer, je mehr aufeinander folgende Operationen für eine bestimmte Zeile durchgeführt werden. Was

Sandini Bib 268

10

Datenaustausch

dieser Status darstellt, lässt sich am einfachsten erklären, wenn man ganz unten, nämlich bei der Zwischenspeicherung von Zeilenmengen im Cache, beginnt. Abbildung 10.3 zeigt die einzelnen Cache-Konfigurationen und den jeweils dazugehörigen Änderungsstatus. Die Spalte »Cache« enthält den jeweils zwischengespeicherten Wert. Wenn hier kein Wert vorhanden ist, enthält der Cache keine Änderungen. Die Spalte »Datenspeicher« zeigt, welcher Wert jeweils im Datenspeicher enthalten ist. Symbolischer Wert

Beschreibung

DBPENDINGSTATUS_UNCHANGED

Es liegen keine anstehenden Änderungen vor.

DBPENDINGSTATUS_CHANGED

Die Werte der Zeile wurden geändert.

DBPENDINGSTATUS_DELETED

Die Zeile wurde gelöscht.

DBPENDINGSTATUS_NEW

Die Zeile wurde eingefügt.

DBPENDINGSTATUS_INVALIDROW

Die Zeile ist ungültig.

Tabelle 10.2: Änderungsstatus für verzögerte Aktualisierung

Cache

Datenspeicher a

DBPENDINGSTATUS_UNCHANGED

DBPENDINGSTATUS_CHANGED

b

DBPENDINGSTATUS_NEW

b

DBPENDINGSTATUS_DELETED

a

a

DBPENDINGSTATUS_INVALIDROW Abbildung 10.3: Bedeutungen der Werte für den Änderungsstatus bei verzögerter Aktualisierung

Im ersten Fall der Abbildung oben enthält der Cache keinen Wert, was bedeutet, dass keine Änderung ansteht. Der Änderungsstatus lautet daher DBPENDINGSTATUS_UNCHANGED. Dies kann einen der folgenden Gründe haben: 왘 Für diese Zeile wurde keine Änderung vorgenommen. 왘 Die Änderungen wurden bereits an den Datenspeicher übertragen und daher aus dem Cache gelöscht. 왘 Die Änderungen wurden rückgängig gemacht.

Sandini Bib 10.3

Verzögerte Aktualisierungen

269

Im zweiten Fall enthält der Datenspeicher den Wert a und der Cache den Wert b. Der Änderungsstatus lautet daher DBPENDINGSTATUS_CHANGED. Dieser Fall kann vorliegen, nachdem Daten gesetzt wurden. Im dritten Fall enthält der Cache einen Zeilenwert, zu dem es keinen vorherigen Wert im Datenspeicher gibt, so dass der Änderungsstatus DBPENDINGSTATUS_NEW lautet. Dieser Fall tritt auf, nachdem eine Zeile eingefügt wurde. Außerdem bleibt der Status auch dann erhalten, wenn sich die Werte einer neu eingefügten Zeile ändern. Im vierten Fall gibt der Cache an, dass die Zeile gelöscht werden soll. Dies ist in der Abbildung durch ein Kreuz dargestellt. Im Datenspeicher ist eine entsprechende Zeile vorhanden und der Änderungsstatus lautet DBPENDINGSTATUS_DELETED. Dieser Fall tritt auf, nachdem eine Zeile gelöscht wurde, unabhängig davon, ob die Werte der Zeile zuvor geändert wurden. Der letzte Fall – DBPENDINGSTATUS_INVALIDROW – zeigt das Beispiel eines ungültigen Änderungsstatus. Wenn Sie eine neue Zeile einfügen und anschließend wieder löschen, ist diese ungültig, da im Datenspeicher keine entsprechende Zeile vorhanden ist. Wenn Sie diese Änderung übertragen, geschieht nichts. Eine Zeile wird auch dann ungültig, wenn Sie diese löschen und anschließend versuchen ihre Daten zu setzen.

10.3.4 Zeilen mit bestimmtem Änderungsstatus abrufen: Navigation im Cache Eine Zeilenmenge kann eine Liste aller Zeilen mit einem bestimmten Änderungsstatus abrufen. Diese Funktionalität ist nicht in den OLE DB Consumer Templates enthalten, sondern über die Klasse CRowsetEx aus den OLE DB-Erweiterungen implementiert. Diese Methode ist folgendermaßen definiert: HRESULT GetPendingRows(DBPENDINGSTATUS dwRowStatus, ULONG *pcPendingRows, HROW **prgPendingRows, DBPENDINGSTATUS **prgPendingStatus)

wobei 왘 dwRowStatus den Änderungsstatus der angeforderten Zeilen darstellt. Dieser kann aus einer Kombination von DBPENDINGSTATUS_NEW, DBPENDINGSTATUS_CHANGED und DBPENDINGSTATUS_DELETED bestehen. 왘 pcPendingRows die Anzahl der Zeilen mit dem betreffenden Status aufnimmt. 왘 prgPendingRows die entsprechenden Zeilen-Handles aufnimmt. 왘 prgPendingStatus den Änderungsstatus der Zeilen aufnimmt. Die letzten beiden Parameter werden über den COM-Speicherallokator zugewiesen und müssen anschließend auch wieder über diesen freigegeben werden.

Sandini Bib 270

10

Datenaustausch

Der folgende Code zeigt das Abrufen von eingefügten oder gelöschten Zeilen: // Rowset erbt von CRowsetEx ULONG cPendingRows; HROW* rgPendingRows; DBPENDINGSTATUS* rgPendingStatus; hr = Rowset.GetPendingRows( DBPENDINGSTATUS_NEW | DBPENDINGSTATUS_ DELETED, &cPendingRows, &rgPendingRows, &rgPendingStatus); if (FAILED(hr)) return hr; // Verwendung von rgPendingRows und rgPendingStatus CoTaskMemFree(rgPendingStatus); CoTaskMemFree(rgPendingRows);

10.3.5 Die Änderungen übertragen Die Methode CRowset::Update überträgt die Änderungen aus dem Cache in den Datenspeicher und leert den Inhalt des Caches. Der Namensteil »Update« kann für SQL-Programmierer irreführend sein, denn diese Methode hat nichts mit einer UPDATEAnweisung von SQL zu tun. Die einer UPDATE-Anweisung entsprechende Methode ist SetData. Besser wäre es gewesen, die Methode nicht Update, sondern TransmitChanges zu nennen.

Sichtbarer Wert

Cache

1

Datenspeicher 1

SetData

2

2

1

Update

2

2

Abbildung 10.4: Zeile aktualisieren

Abbildung 10.4 zeigt ein Beispiel für die Verwendung dieser Methode. Die rechte Spalte zeigt den im Datenspeicher enthaltenen Wert. Da der Modus mit verzögerter Aktualisierung eingestellt ist, ist ein lokaler Cache vorhanden. Der grau unterlegte Wert in der mittleren Spalte zeigt den zwischengespeicherten Wert im Cache. (Wenn der Cache keinen Wert enthält, ist diese Spalte leer.) Die Spalte »Sichtbarer Wert« schließlich zeigt den Wert, den man durch Aufruf von GetData erhält. Es handelt sich

Sandini Bib 10.3

Verzögerte Aktualisierungen

271

bei dem »sichtbaren Wert« um den ggf. im Cache enthaltenen Wert oder – im anderen Fall – um den Wert im Datenspeicher. Der »sichtbare Wert« ist also nirgends tatsächlich gespeichert, d.h. er ist nur eine Art Platzhalter. Daher ist er mit einer gestrichelten Linie dargestellt. Da dieses Prinzip sowohl für einzelne als auch für mehrere Spalten gilt, werden wir der Einfachheit halber nur eine einzige Spalte betrachten. Am Anfang enthält der Datenspeicher den Wert 1. Da keine Aktion mit verzögerter Aktualisierung durchgeführt wurde, ist im Cache kein Wert für die Zeile enthalten. Wenn der Spaltenwert in der Zeilenmenge auf 2 gesetzt wird, wird im Cache der Wert 2 zwischengespeichert, während der Wert im Datenspeicher nach wie vor 1 lautet. Nach dem Aufruf von Update durch den Consumer überträgt der Provider den Wert aus dem Cache an den Datenspeicher und leert den Cache. Der Consumer überträgt Änderungen jeweils durch den Aufruf von CRowset:: Update(): HRESULT Update(ULONG* pcRows = NULL, HROW* phRow = NULL, DBROWSTATUS* pStatus = NULL) { ATLASSERT(m_spRowset != NULL); CComPtr spRowsetUpdate; HRESULT hr = m_spRowset->QueryInterface(&spRowsetUpdate); if (FAILED(hr)) return hr; HROW* prgRows; DBROWSTATUS* pRowStatus; if (phRow != NULL) hr = spRowsetUpdate->Update(NULL, 1, &m_hRow, pcRows, &prgRows, &pRowStatus); else hr = spRowsetUpdate->Update(NULL, 1, &m_hRow, pcRows, NULL, &pRowStatus); if (FAILED(hr)) return hr; if (phRow != NULL) { *phRow = *prgRows; CoTaskMemFree(prgRows); } if (pStatus != NULL) *pStatus = *pRowStatus; CoTaskMemFree(pRowStatus); return hr; }

Sandini Bib 272

10

Datenaustausch

Beachten Sie, dass mit Update hier nur die Änderungen für die aktuelle Zeile übertragen werden. Die Methode Update der Schnittstelle IRowsetUpdate aktualisiert jeweils nur bestimmte Zeilen, nicht alle Zeilen in der Zeilenmenge. Dabei ist der Consumer dafür zuständig, die richtigen Zeilen zu übergeben. Dies bedeutet, dass Sie Update für jede Zeile aufrufen müssen, die an den Datenspeicher übertragen werden soll. Im folgenden Beispiel ruft der Programmierer Update nur für die zweite Zeile auf, nicht aber für die erste: MyRowset.MoveFirst(); MyRowset.m_Column1 = 1; MyRowset.SetData(); MyRowset.MoveNext(); MyRowset.m_Column1 = 2; MyRowset.SetData(); MyRowset.Update();

Dies führt dazu, dass die Änderungen an der ersten Zeile verloren gehen.

10.3.6 Alle Änderungen übertragen Manchmal kann es schwierig sein, genau anzugeben, welche Zeile aktualisiert werden muss. Natürlich ist es immer möglich, vor jeder Cursorbewegung Update aufzurufen, aber auf diese Weise gehen alle spezifischen Vorteile der verzögerten Aktualisierung verloren. Die OLE DB-Erweiterungen bieten mit der Methode UpdateAll aus der Klasse CRowsetEx eine einfache Möglichkeit zur Aktualisierung aller Zeilen mit anstehenden Änderungen. UpdateAll übergibt einfach den Wert 0 für die Anzahl der zu aktualisierenden Zeilen, was von den OLE DB-Providern so interpretiert wird, dass alle Zeilen aktualisiert werden sollen. UpdateAll ist folgendermaßen definiert: HRESULT UpdateAll() { ATLASSERT(m_spRowset != NULL); CComPtr spRowsetUpdate; HRESULT hr = m_spRowset->QueryInterface(&spRowsetUpdate); if (FAILED(hr)) return hr; return spRowsetUpdate->Update(GetHChapter(), 0, NULL, NULL, NULL, NULL); }

Beispiel: CRowsetEx MyRowset; // Geeignete Aktionen durchführen MyRowset.UpdateAll(); // Alle Zeilen aktualisieren

Sandini Bib 10.3

Verzögerte Aktualisierungen

273

10.3.7 Änderungen rückgängig machen Einer der Vorteile von verzögerten Aktualisierungen liegt darin, dass die betreffenden Änderungen genauso rückgängig gemacht werden können, wie dies aus Office-Dokumenten bekannt ist. In der Regel ist die Implementierung einer Undo-Operation zum Rückgängigmachen ziemlich aufwändig. Für einen OLE DB-Provider ist dieser Vorgang jedoch einfach, da er nur darin besteht, den Cache-Inhalt für eine bestimmte Menge von Zeilen zu löschen. Abbildung 10.5 zeigt ein Beispiel für eine Undo-Operation. Wenn SetData mit dem Wert 2 aufgerufen wird, sucht der OLE DB-Provider nach einem Wert im Cache. Da dort kein Wert vorhanden ist, fügt der Provider den neuen Wert ein. Der Wert im Datenspeicher bleibt wegen der verzögerten Aktualisierung dabei unverändert. Der zwischengespeicherte Wert wird zum sichtbaren Wert. Sichtbarer Wert

Cache

1

Datenspeicher 1

SetData

2

2

1

3

3

1

SetData

Undo

1

1

Abbildung 10.5: Daten setzen und die Änderungen rückgängig machen

Bei einem erneuten Aufruf von SetData, dieses Mal mit dem Wert 3, sucht der Provider nach einem Wert im Cache, findet diesen und aktualisiert ihn. Auch in diesem Fall ändert sich der Wert im Datenspeicher nicht. Der Wert 3 wird zum sichtbaren Wert. Wenn anschließend Undo aufgerufen wird, sucht der Provider nach einem Wert im Cache, findet dort den Wert 3 und entfernt diesen. Dies hat zur Folge, dass der Wert im Datenspeicher – in diesem Beispiel 1 – zum sichtbaren Wert wird. Eigentlich sollte man meinen, dass Undo nur einen Schritt zurückgeht und den Wert auf 2 setzt. Der Einfach-

Sandini Bib 274

10

Datenaustausch

heit halber verfolgt der Cache die Änderungen seines Inhalts nicht, so dass er immer nur den aktuellen Wert kennt. Genauer wäre es daher, wenn die Methode nicht Undo sondern DeleteTheCacheValue heißen würde.

10.3.8 Alle Änderungen rückgängig machen Die Methode Undo macht nur die Änderungen an der aktuellen Zeile rückgängig. Die Klasse CRowsetEx stellt aber auch noch die Methode UndoAll zur Verfügung, die, wie die Methode UpdateAll für die Aktualisierung, beim Rückgängigmachen der Änderungen alle Zeilen berücksichtigt.

10.3.9 Ursprüngliche und zuletzt sichtbare Daten Stellen Sie sich folgenden Fall vor: Sie rufen eine Zeile mit einer Spalte ab, die den Wert 1 enthält. Im Modus mit verzögerter Aktualisierung ändern Sie den Wert auf 2. Zur selben Zeit wird dieser durch einen anderen Prozess ohne verzögerte Aktualisierung im Datenspeicher auf 3 geändert. Letztendlich liegen drei verschiedene Werte vor: die ursprünglichen (geholten) Daten, d.h. der Wert 1, der neue Wert 2 im Cache und der im Datenspeicher sichtbare Wert 3 (siehe Abbildung 10.6). Beim Holen einer Zeile ruft die Zeilenmenge die entsprechenden Spaltenwerte ab. Die Methode GetData sucht zunächst nach einem neuen Wert im Cache. Ist dort ein Wert vorhanden, wird dieser zurückgeliefert. Ansonsten ruft sie die ursprünglich zwischengespeicherten Werte (geholte Daten) ab. Für solche Fälle stellt CRowsetEx zwei zusätzliche Methoden zur Verfügung: GetOriginalData und GetLastVisibleData. Im Gegensatz zu GetData sucht GetOriginalData nicht nach dem Wert im Speicher, sondern ruft immer die ursprünglichen Daten ab. Das Verhalten dieser Methode hängt also nicht vom neuen Wert im Cache ab. Zeilenmenge Consumer

Cache

Ursprüngliche Daten

?

2

1

Datenspeicher Zuletzt sichtbare Daten 3

Abbildung 10.6: Cache-Daten, ursprüngliche Daten und zuletzt sichtbare Daten

GetLastVisibleData ruft die Daten aus dem Datenspeicher ab. Wie Sie in Kapitel 12 sehen werden, wirkt sich die Isolationsstufe für Transaktionen auf die Sichtbarkeit der Änderungen zwischen den einzelnen Transaktionen aus. GetLastVisibleData ruft einfach die Daten entsprechend der festgelegten Isolationsstufe aus dem Datenspeicher ab.

Sandini Bib 10.3

Verzögerte Aktualisierungen

275

Zeilenmenge Consumer

Cache

Datenspeicher

Ursprüngliche Daten

Zuletzt sichtbare Daten

GetOriginalData

?

2

1

3

GetData GetLastVisibleData GetOriginalData

?

GetData

1

3

GetLastVisibleData Abbildung 10.7: Verhalten der Methoden GetData, GetOriginalData und GetLastVisibleData

In Abbildung 10.7 ist das Verhalten der Methoden GetData, GetOriginalData und GetLastVisibleData mit und ohne Daten im Cache im Überblick dargestellt.

10.3.10 Verzögerte Aktualisierungen und Transaktionen In diesem Abschnitt wird davon ausgegangen, dass Sie mit dem Konzept der Transaktionen bereits vertraut sind. Wenn dies nicht der Fall ist, sollten Sie zunächst Kapitel 12 lesen und anschließend hierher zurückkehren. Verzögerte Aktualisierungen sind in gewissem Sinne mit Änderungen in einer Transaktion vergleichbar: Die Übertragung verzögerter Änderungen entspricht dem Festschreiben einer Transaktion, das Rückgängigmachen dieser Änderungen findet seine Entsprechung im Abbruch einer Transaktion. Für verzögerte Änderungen steht jedoch nicht dieselbe Funktionalität wie bei normalen Transaktionen zur Verfügung. Beispielsweise sind sie nicht so robust wie Transaktionen und gelten jeweils nur für eine Zeilenmenge. Man könnte verzögerte Änderungen gewissermaßen als »Schmalspurtransaktionen« bezeichnen. Da stellt sich ganz natürlicherweise die Frage, was geschieht, wenn Sie verzögerte Änderungen mit Transaktionen kombinieren. Zunächst muss man sich klar machen, dass verzögerte Aktualisierungen lokal innerhalb des Zeilenmengen-Objekts erfolgen. Transaktionen haben daher keine Kenntnis davon. Insbesondere stellt sich nicht die Frage nach der Sichtbarkeit der Änderungen zwischen den Transaktionen: Da eine Transaktion nichts von den Änderungen innerhalb einer Zeilenmenge weiß, können diese Änderungen auch nicht für eine andere Transaktion sichtbar sein.

Sandini Bib 276

10

Datenaustausch

Abbildung 10.8 zeigt die Beziehung zwischen einer Transaktion und den verzögerten Aktualisierungen.

Transaktion

(1)

Zeilenmenge

(2)

Cache

Datenspeicher

Abbildung 10.8: Verzögerte Aktualisierungen und Transaktionen

Zudem bemerkt die Transaktion nichts davon, wenn Sie Änderungen rückgängig machen. Dies bedeutet, dass die Änderungen auf keinen Fall zum Datenspeicher gelangen, unabhängig davon, ob die Transaktion erfolgreich ist oder nicht. Die Transaktion kennt auch den Änderungsstatus einer Zeilenmenge im Modus mit verzögerter Aktualisierung nicht. Wenn Sie daher verzögerte Änderungen übertragen, werden diese in der Transaktion genauso transportiert wie sofortige Änderungen. Am Ende kommt es auf das Ergebnis der Transaktion an: Wenn ihre Ausführung scheitert, werden die Änderungen in keinem Fall übertragen. Verzögerte Änderungen sind mit verschachtelten Transaktionen vergleichbar. Dennoch stellt sich die Frage, was mit anstehenden Änderungen geschieht, wenn eine Transaktion festgeschrieben bzw. abgebrochen wird. Hierbei hängt das Verhalten davon ab, ob die Zeilenmenge von der Transaktion beibehalten wird oder nicht (siehe Kapitel 12). Wenn die Transaktion die Zeilenmenge nicht beibehält, gehen die anstehenden Änderungen verloren. Wenn die Transaktion die Zeilenmenge beibehält, bleiben die anstehenden Änderungen weiterhin bestehen und verhalten sich so, als hätte es keine vorherige Transaktion gegeben. Zusammengefasst lässt sich sagen, dass für verzögerte Änderungen zwar nicht die Möglichkeiten von regulären Transaktionen zur Verfügung stehen, sie aber in einfachen Fällen durchaus ein praktisches und effektives Verfahren darstellen.

10.4 Benachrichtigungen Zeilenmengen sind ziemlich komplexe Objekte. Sie können Zeilen holen und Änderungen durchführen, rückgängig machen oder entsprechende Aktualisierungen durchführen. Die Verwaltung dieser Operationen bereitet keine Schwierigkeiten, wenn es sich jeweils um genau eine lokale Zeilenmenge handelt. Bei gemeinsam benutzten

Sandini Bib 10.4

Benachrichtigungen

277

Zeilenmengen liegt der Fall jedoch ganz anders. Beispiel: Zwei Komponenten A und B benutzen ein Zeilenmengen-Objekt gemeinsam. Wenn die Komponente A eine Zeile in die Zeilenmenge einfügt, muss die Komponente B darüber informiert werden, damit sie entsprechend reagieren kann. COM bietet als Lösung für dieses Problem das Benachrichtigungskonzept. Wenn Benachrichtigungen verwendet werden sollen, muss die betreffende Zeilenmenge die COM-Schnittstelle IConnectionPointContainer unterstützen, die eine Reihe von Verbindungspunkten (Connection Points) aufweist. Clients rufen dabei die Methode FindConnectionPoint auf um den benötigten Verbindungspunkt abzurufen. Bei einem Verbindungspunkt handelt es sich um ein COM-Objekt, das die COMSchnittstelle IConnectionPoint unterstützt. Objekte, die Zeilenmengen-Benachrichtigungen unterstützen, werden Zeilenmengen-Listener genannt. Ein Zeilenmengen-Listener übergibt sich selbst über die Methode Advise an den Verbindungspunkt. Wenn die Komponente nicht mehr über Ereignisse benachrichtigt werden will, ruft sie die Methode Unadvise auf. Bei Zeilenmengen müssen Objekte, die Zeilenmengen-Benachrichtigungen benötigen, die Schnittstelle IRowsetNotify unterstützen. IRowset IConnectionPointContainer

ZeilenmengenObjekt

FindConnectionPoint(...) IConnectionPoint

Verbindungspunkt

Advise(...)

IRowsetNotify

ZeilenmengenListener

Abbildung 10.9: Architektur der Verbindungspunkte

Abbildung 10.9 zeigt die Architektur der Verbindungspunkte für Zeilenmengen. Sie können auf Verbindungspunkte für Zeilenmengen ganz normal durch Aufruf einer unspezifischen COM-Schnittstelle oder über die ATL-Funktion zugreifen. In diesem Abschnitt wird davon ausgegangen, dass Sie hierzu die ATL-Funktion verwenden. Wenn eine Komponente für Benachrichtigungen empfangsbereit sein soll, die von einer Zeilenmenge kommen, müssen Sie die Funktion AtlAdvise verwenden. Im folgenden Beispiel fordert das Objekt spRowsetListener die Benachrichtigungen von spRowset an:

Sandini Bib 278

10

Datenaustausch

CComPtr spRowset; CComPtr spRowsetListener; // Erzeugung beider Objekte DWORD Cookie; spRowset.Advise(spNotif, IID_IRowsetNotify,&Cookie); // entspricht AtlAdvise(spRowset, spNotif, IID_IRowsetNotify,&Cookie);

Hierbei steht Cookie für eine Ganzzahl, welche die Benachrichtigung, die von der Zeilenmenge an den Zeilenmengen-Listener gesendet wird, eindeutig identifiziert. Dieses Objekt ist nützlich, wenn Sie mit der Funktion AtlUnadvise festlegen wollen, dass keine Benachrichtigungen mehr gesendet werden sollen: AtlUnadvise(spRowset, IID_IRowsetNotify,Cookie);

10.4.1 Zeilenmengen-Listener Die Implementierung eines Zeilenmengen-Listeners entspricht der Implementierung eines COM-Objekts, das die Schnittstelle IRowsetNotify unterstützt. IRowsetNotify behandelt alle Zeilenmengen-Ereignisse. Für Zeilenmengen sind zahlreiche Ereignisse definiert, die bestimmten Ereignisbereichen zugeordnet werden müssen. In OLE DB sind die drei folgenden Ereignisbereiche (Domains) definiert: Zeilenmenge (Rowset), Zeile (Row) und Spalte (Column). 왘 Der Zeilenmengen-Ereignisbereich umfasst alle Ereignisse, welche die gesamte Zeilenmenge betreffen, z.B. wenn ein Client eine Zeilenmengen-Schnittstelle freigibt. 왘 Der Zeilen-Ereignisbereich umfasst alle Ereignisse, die mit einer bestimmten Zeile zusammenhängen, z.B. wenn eine Zeile gelöscht wird. 왘 Der Spalten-Ereignisbereich umfasst alle Ereignisse, die sich auf eine bestimmte Spalte einer bestimmten Zeile beziehen, z.B. wenn Sie den Wert für eine Spalte setzen. Abbildung 10.10 stellt diese drei Ereignisbereiche anhand einer Tabellenstruktur dar. Die Schnittstelle IRowsetNotify deklariert für jeden Ereignisbereich eine Methode. 왘 OnRowsetChange wird für Ereignisse aufgerufen, welche die gesamte Zeilenmenge betreffen. 왘 OnRowChange wird für Ereignisse aufgerufen, die eine oder mehrere Zeilen betreffen. 왘 OnFieldChange wird für Ereignisse aufgerufen, die ein oder mehrere Felder einer bestimmten Zeile betreffen (»Feld« ist hierbei synonym für »Spalte«). Jede Methode verfügt über zumindest ein Argument für den Grund des Ereignisses und über zumindest ein Argument für die Ereignisphase.

Sandini Bib

Zeilenmengen-Ereignis

10.4

Benachrichtigungen

a

279

b

c

d

Zeilen-Ergebnis

Spalten-Ergebnis Abbildung 10.10: Ereignisbereiche für Zeilenmengen-, Zeilen- und Spaltenereignisse

10.4.2 Gründe für Ereignisse Der jeweilige Grund für ein Ereignis wird durch eine Ganzzahl dargestellt, die angibt, welcher Vorgang das Benachrichtigungsereignis ausgelöst hat. Das Argument für den Ereignisgrund kann einen der Werte aus Tabelle 10.3 annehmen. Symbolischer Wert

Ereignisbereich

DBREASON_ROWSET_CHANGED

Zeilenmenge

DBREASON_ROWSET_ FETCHPOSITIONCHANGE

Zeilenmenge

MoveNext

Die Position zum Holen der Zeilenmenge (Fetch) hat sich geändert.

DBREASON_ROWSET_RELEASE

Zeilenmenge

Release

Die Zeilenmenge wurde freigegeben.

DBREASON_ROW_ACTIVATE

Zeile

MoveNext

DBREASON_ROW_ASYNCHINSERT

Zeile

Insert

Die Zeile wurde asynchron eingefügt.

DBREASON_ROW_DELETE

Zeile

Delete

Die Zeile wurde gelöscht.

DBREASON_ROW_FIRSTCHANGE

Zeile

DBREASON_ROW_INSERT

Zeile

Insert

Die Zeile wurde eingefügt.

DBREASON_ROW_RELEASE

Zeile

ReleaseRows

Die Zeile wurde freigegeben.

DBREASON_ROW_RESYNCH

Zeile

DBREASON_ROW_UNDOCHANGE

Zeile

Undo

Die Zeilenänderung wurde rückgängig gemacht.

Tabelle 10.3: Gründe für Benachrichtigungsereignisse

Typische Methode der Zeilenmenge

Bemerkungen

Die Zeilenmenge wurde geändert.

Sandini Bib 280

10

Datenaustausch

Symbolischer Wert

Ereignisbereich

Typische Methode der Zeilenmenge

Bemerkungen

DBREASON_ROW_UNDODELETE

Zeile

Undo

Das Löschen der Zeile wurde rückgängig gemacht.

DBREASON_ROW_UNDOINSERT

Zeile

Undo

Das Einfügen der Zeile wurde rückgängig gemacht.

DBREASON_ROW_UPDATE

Zeile

Update

Die Zeile wurde aktualisiert.

DBREASON_ROWPOSITION_CHANGED

Zeile

DBREASON_ROWPOSITION_ CHAPTERCHANGED

Zeile

DBREASON_ROWPOSITION_CLEARED

Zeile

DBREASON_COLUMN_RECALCULATED

Spalte

DBREASON_COLUMN_SET

Spalte

SetData

Ein Spaltenwert wurde geändert.

Tabelle 10.3: Gründe für Benachrichtigungsereignisse (Fortsetzung)

10.4.3 Ereignisphasen Bei vielen Benachrichtigungs-Schnittstellen wird eine Methode pro Ereignis einmal aufgerufen und das Benachrichtigungsobjekt ist lediglich empfangsbereit. Die Schnittstelle IRowsetNotify funktioniert anders, da sie auch auf die Ereignisse reagiert bzw. Ereignisse verhindern kann. Der Zeilenmengen-Listener hat die Möglichkeit, das Ereignis abzubrechen, wenn bestimmte Gründe vorliegen. Dies führt dazu, dass das Zeilenmengen-Objekt für jedes Ereignis mehrere Benachrichtigungen absetzen muss, die jeweils einer bestimmten Ereignisphase entsprechen. In OLE DB sind folgende fünf Ereignisphasen definiert: 왘 DBEVENTPHASE_OKTODO 왘 DBEVENTPHASE_ABOUTTODO 왘 DBEVENTPHASE_SYNCHAFTER 왘 DBEVENTPHASE_FAILEDTODO 왘 DBEVENTPHASE_DIDEVENT

Wenn die Listener keinen Einfluss auf das Ereignis haben, gibt die Zeilenmenge nur für die Phase DBEVENTPHASE_DIDEVENT entsprechende Benachrichtigungen aus. Das Ganze wird jedoch komplizierter, wenn die Listener die Möglichkeit haben, das betreffende Ereignis zu verhindern. In diesem Fall sendet die Zeilenmenge Benachrichtigungen für vier Phasen, wie in Abbildung 10.11 gezeigt und nachfolgend beschrieben.

Sandini Bib 10.4

Benachrichtigungen

281

OKTODO

Alle Listener einverstanden

FAILEDTODO

ABOUTTODO

Aktion durchführen

FAILEDTODO

SYNCHAFTER

Alle Listener einverstanden

FAILEDTODO

DIDEVENT

Abbildung 10.11: Ereignisphasen

Zunächst benachrichtigt die Zeilenmenge alle Listener für die Phase DBEVENTPHASE_OKTODO. In dieser Phase stellt die Zeilenmenge sicher, dass alle Listener mit diesem Ereignis einverstanden sind. Sobald mindestens ein Listener den Wert S_FALSE zurückliefert, wird das Ereignis abgebrochen. Alle Listener, die DBEVENTPHASE_ OKTODO erhalten haben, empfangen in diesem Fall DBEVENTPHASE_FAILEDTODO. Wenn jedoch kein Listener den Wert S_FALSE zurückgeliefert hat, fährt die Zeilenmenge mit der nächsten Phase fort. Die Zeilenmenge benachrichtigt dann alle Listener für die Phase DBEVENTPHASE_ ABOUTTODO. Zu diesem Zeitpunkt haben alle Listener bei DBEVENTPHASE_OKTODO zugestimmt, daher ist die Wahrscheinlichkeit hoch, dass alle weiteren Phasen akzeptiert werden. Folglich müssen alle Listener die Vorbereitung in der Phase DBEVENTPHASE_ABOUTTODO durchführen. Wenn wie in der vorherigen Phase mindestens ein Listener den Wert S_FALSE zurückgeliefert hat, wird das Ereignis abgebrochen, und die Zeilenmenge sendet eine Benachrichtigung für DBEVENTPHASE_FAILEDTODO. Andernfalls wird die betreffende Aktion durchgeführt. Anschließend benachrichtigt die Zeilenmenge die Listener für die Phase DBEVENTPHASE_SYNCHAFTER, was bedeutet, dass die Aktion durchgeführt wurde. Wenn mindestens ein Listener den Wert S_FALSE zurückliefert, sendet die Zeilenmenge eine Benachrichtigung für DBEVENTPHASE_FAILEDTODO, andernfalls eine Benachrichtigung für DBEVENTPHASE_DIDEVENT. Diese Phase bedeutet, dass alle Listener erfolgreich synchronisiert wurden.

Sandini Bib 282

10

Datenaustausch

Für jede Phase kann die Zeilenmenge auch angeben, dass der Zeilenmengen-Listener nicht die Möglichkeit zum Abbruch des Ereignisses haben soll. Für jede Methode von IRowsetNotify gibt es zu diesem Zweck einen booleschen Parameter mit dem Namen bCantDeny. So weist beispielsweise die Methode OnRowsetChange die folgenden Parameter auf: HRESULT OnRowsetChange ( IRowset * pRowset, DBREASON eReason, DBEVENTPHASE ePhase, BOOL fCantDeny);

Wenn bCantDeny auf den Wert TRUE gesetzt ist, kann der Zeilenmengen-Listener das Ereignis nicht abbrechen. Durch Rückgabe des Wertes DB_S_UNWANTEDPHASE gibt ein Zeilenmengen-Listener an, dass er nicht an der Phase für einen bestimmten Benachrichtigungsgrund interessiert ist. Analog dazu gibt er durch den Wert DB_S_UNWANTEDREASON an, dass er für keine Phase an dem betreffenden Grund interessiert ist.

10.4.4 Beispiel für Benachrichtigungen Das folgende Beispiel zeigt, wie eine Zeilenmengen-Benachrichtigung konfiguriert wird. Beachten Sie, dass die Zeilenmenge mit Hilfe der Eigenschaft DBPROP_ IConnectionPointContainer erzeugt wird. Auf diese Weise ist gewährleistet, dass die Zeilenmenge Benachrichtigungen unterstützt. In diesem Beispiel ist der Zeilenmengen-Listener in einem separaten Server implementiert. CCommand DataExchange; CDBPropSet PropSet(DBPROPSET_ROWSET); PropSet.AddProperty(DBPROP_IRowsetChange, true); PropSet.AddProperty(DBPROP_IConnectionPointContainer, true); PropSet.AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE); hr = DataExchange.Open(Session, NULL, &PropSet); if (FAILED(hr)) return hr; CComPtr Notif; hr = Notif.CoCreateInstance(CLSID_NotifiedObject); if (FAILED(hr)) return hr; DWORD Cookie; hr = DataExchange.m_spRowset.Advise(Notif, IID_IRowsetNotify,&Cookie);

Der Zeilenmengen-Listener ist an den Ereignissen interessiert, sobald die Zeilenmenge geöffnet ist. Wenn der Cursor in der Zeilenmenge zur ersten Zeile bewegt wird, hr = DataExchange.MoveFirst();

Sandini Bib 10.5

Zusammenfassung

283

löst dies in allen Phasen das Ereignis DBREASON_ROWSET_FETCHPOSITIONCHANGE aus. Der Listener hat die Gelegenheit, das Holen der Zeile abzubrechen, aber in unserem Beispiel ist er passiv und stimmt allen Ereignissen zu. Anschließend löst die Zeilenmenge das Ereignis DBREASON_ROW_ACTIVATE erst in der Erledigt-Phase (Did Event) aus. Der Listener kann bei diesem Ereignis nicht eingreifen. Wenn der Spaltenwert gesetzt wird, DataExchange.m_Value = 123; hr = DataExchange.SetData();

löst die Zeilenmenge das Ereignis DBREASON_COLUMN_SET für alle Phasen aus. Wenn der Cursor in der Zeilenmenge zur nächsten Spalte bewegt wird, hr = DataExchange.MoveNext();

löst die Zeilenmenge das Ereignis DBREASON_ROW_RELEASE für die Erledigt-Phase aus. Der Listener kann bei diesem Ereignis nicht eingreifen. Löscht die Zeilenmenge die aktuelle Zeile, hr = DataExchange.Delete();

so löst sie das Ereignis DBREASON_ROW_DELETE für alle Phasen aus. Analog dazu löst sie das Ereignis DBREASON_ROW_INSERT aus, wenn eine Zeile eingefügt wird (Methode Insert). Rufen Sie nach Beendigung dieser Operationen die Funktion Unadvise auf: hr = AtlUnadvise(DataExchange.m_spRowset, IID_IRowsetNotify,Cookie);

10.5 Zusammenfassung In diesem Kapitel wurden das Abrufen und das Setzen von Daten erläutert. Wie in anderen Bereichen der OLE DB Consumer Templates auch, gibt es mehrere Ebenen für diese Funktionen. Die unterste Ebene ist die Verwendung von SetData, Insert und Delete im Modus mit sofortiger Aktualisierung, der ein einfaches Verfahren zum Setzen von Daten darstellt. Zugleich bietet OLE DB zahlreiche Möglichkeiten im Zusammenhang mit verzögerten Aktualisierungen, die etwas schwieriger zu programmieren sind, aber viele Vorteile bringen. Schließlich stellen Benachrichtigungen ein elegantes Standardverfahren zur Verwaltung von Zeilenmengen-Ereignissen dar. Mit diesem Kapitel ist die Behandlung des Kernbereichs der OLE DB Consumer Templates abgeschlossen. In den nächsten beiden Kapiteln werde ich Ihnen mit BLOBs und Transaktionen erweiterte Techniken vorstellen.

Sandini Bib

Sandini Bib

11 BLOBs Bisher haben wir immer mit einfachen Datentypen wie ganzen Zahlen (integer), reellen Zahlen (real), Datum (date) und Zeichenfolgen (string) gearbeitet. OLE DB unterstützt aber auch den Datentyp, der als BLOB (Binary Large Object, großes Datenobjekt) bekannt ist. Da nicht alle Provider BLOBs verwenden müssen, sollten Sie überprüfen, ob Ihr Provider diesen Datentyp unterstützt, bevor Sie weiterlesen. Wie der Name bereits andeutet, speichern BLOBs große Datenmengen. Daher eignen sie sich besonders für Multimedia-Typen wie Bitmaps, Sound- und Video-Daten oder umfangreiche Texte. Aber gerade deshalb, weil sie so viele Daten aufnehmen können, wird für BLOBs ein besonderer Mechanismus zum Speichern und Abrufen von Daten benötigt, damit keine Probleme durch Speichermangel auftreten. In diesem Kapitel werden die folgenden Themen behandelt: 왘 Die unterschiedlichen Typen von BLOBs. 왘 Die unterschiedlichen Methoden zum Zugriff auf BLOBs. 왘 Die Art und Weise, wie die Daten bei den einzelnen Methoden gelesen und geschrieben werden.

11.1 Die verschiedenen BLOB-Typen BLOBs können anhand ihres Inhalts und des Zugriffsverfahrens eingeteilt werden.

11.1.1 Inhalt Aus der Sicht von OLE DB gibt es, vom Inhalt her gesehen, zwei verschiedene BLOBTypen: reguläre BLOBs und COM-Objekte. Ein reguläres BLOB besteht lediglich aus einer Byte-Folge, die vom Consumer interpretiert werden muss. Dagegen können sich COM-Objekte in einen Datenstrom (Stream) oder in ein Speicherobjekt schreiben oder sich daraus auslesen und brauchen daher nicht interpretiert zu werden. Beachten Sie jedoch, dass es sich bei COM-Objekten eben-

Sandini Bib 286

11

BLOBs

falls um reguläre BLOBs handelt, daher können sie auch als nicht interpretierte ByteFolge gelesen und geschrieben werden, was allerdings nicht empfehlenswert ist. OLE DB-Provider können COM-Objekte genauso unterstützen wie reguläre BLOBs, in der Praxis gibt es jedoch nur wenige, die dies tun. Aus diesem Grund behandelt dieses Kapitel vor allem reguläre BLOBs. Die Eigenschaft für Datenquelleninformationen DBPROP_OLEOBJECTS gibt an, ob die Datenquelle BLOBs oder persistente COM-Objekte unterstützt. Sie kann auch eine Kombination aus den Werten DBPROPVAL_OO_BLOB und DBPROPVAL_OO_IPERSIST aufweisen, d.h. es werden beide Typen unterstützt.

11.1.2 Zugriffsverfahren Der Zugriff auf BLOBs kann über den globalen Speicher oder über ein Speicherobjekt erfolgen. Beim Zugriff über den Speicher wird der Inhalt des BLOBs in einem Schritt zum Consumer übertragen. Beim Zugriff über ein Speicherobjekt können die Daten paketweise gelesen und geschrieben werden. Der Zugriff über den Speicher eignet sich vor allem für kurze, der Zugriff über ein Speicherobjekt dagegen für lange BLOBs.

11.1.3 Lange und kurze BLOBs OLE DB unterscheidet zwischen kurzen und langen BLOBs. Es gibt jedoch keine exakte Trennlinie oder Formel, die besagt, ob sich in einer BLOB-Spalte kurze oder lange BLOBs befinden. Ein kurzes BLOB ist einfach dadurch definiert, dass es fehlerfrei in einem Schritt in den Speicher abgerufen werden kann. Im Gegensatz dazu sollte ein langes BLOB nicht in den Speicher, sondern in ein Speicherobjekt abgerufen werden. Dies bedeutet nicht, dass ein langes BLOB nie in den Speicher abgerufen werden kann, sondern nur, dass beim Lesen oder Schreiben von Daten auf diese Weise Fehler auftreten können. Grundsätzlich empfiehlt es sich, kurze BLOBs in den Speicher und lange BLOBs in ein Speicherobjekt abzurufen. Die Consumer können über das Spalten-Flag DBCOLUMNFLAGS_ISLONG festlegen, ob eine BLOB-Spalte lange oder kurze BLOBs enthält. CDynamicAccessor ist eine komfortable Klasse zum Abrufen von Spalten-Flags. Das folgende Beispiel zeigt, wie Sie herausfinden können, ob eine Spalte (in diesem Fall die erste Spalte) lange BLOBs enthält. CBLOBTable myTable; HRESULT hr = myTable.Open(); if (FAILED(hr)) return hr; DBCOLUMNFLAGS flags; myTable.GetColumnFlags(1, &flags); bool is_long = flags & DBCOLUMNFLAGS_ISLONG;

Sandini Bib 11.2

BLOBs als Daten im Speicher

287

Zugriffsverfahren

Kurzes BLOB mit fester Größe

Kurzes BLOB mit variabler Größe

Langes BLOB

Speicher

Bevorzugtes Verfahren

Möglich, aber es muss ausreichend Speicher reserviert werden.

Nicht empfehlenswert

Speicher mit PufferReferenz

Möglich, aber unnötig kompliziert.

Bevorzugtes Verfahren

Nicht empfehlenswert

Speicherobjekt

Möglich, aber unnötig kompliziert.

Möglich, aber unnötig kompliziert.

Bevorzugtes Verfahren

Tabelle 11.1: BLOB-Typen und Zugriffsverfahren

11.1.4 BLOBs mit fester und variabler Größe Kurze BLOBs können in zwei Kategorien eingeteilt werden: BLOBs mit fester und mit variabler Größe. In OLE DB wird diese Unterscheidung nicht getroffen, in der zugrunde liegenden Datenbank in der Regel schon. Kurze BLOBs mit fester Größe weisen eine in der Datenbank definierte, feste Größe auf. Bei SQL Server beispielsweise stellt binary (n) ein BLOB mit fester Größe dar (n steht für den Datenumfang). Die einfachste Möglichkeit um ein kurzes BLOB mit fester Größe abzurufen, besteht darin, einen Puffer zu reservieren und die Anforderung abzusetzen, dass der Provider den BLOB-Inhalt in diesen Puffer schreiben soll. Kurze BLOBs mit variabler Größe können in einem Stück abgerufen werden, wobei jedoch der Datenumfang von Zeile zu Zeile unterschiedlich ist. Bei SQL Server beispielsweise stellt varbinary (n) ein kurzes BLOB mit variabler Größe dar. In einer Spalte vom Typ varbinary (n) haben alle BLOBs die Länge n oder weniger. Die einfachste Möglichkeit zum Abrufen eines kurzen BLOBs mit variabler Größe besteht darin, anzufordern, dass der Provider den Speicherpuffer reservieren und den BLOBInhalt in diesen Puffer schreiben soll. Ich nenne dieses Verfahren »Speicher mit PufferReferenz«, da der Provider eine Referenz auf den Puffer liefert. Aus der Sicht von OLE DB gibt es keine direkte Möglichkeit um festzustellen, ob ein kurzes BLOB Daten mit fester oder variabler Größe enthält, der Consumer kann allerdings den Namen der Spalte abrufen und den BLOB-Typ daraus ableiten. In Tabelle 11.1 sind die einzelnen BLOB-Spalten und Zugriffsverfahren im Überblick dargestellt.

11.2 BLOBs als Daten im Speicher Wenn Sie Daten von BLOBs in den Speicher abrufen, reserviert der Consumer einen geeigneten Speicherabschnitt und gibt diesen an den Provider weiter. Der Provider

Sandini Bib 288

11

BLOBs

schreibt dann die BLOB-Daten in diesen Speicherabschnitt. Dieses Verfahren ist unkompliziert und ermöglicht ein einfaches Debugging. Wenn der Consumer jedoch einen zu kleinen Speicherabschnitt reserviert, werden die Daten abgeschnitten. Aus diesem Grund eignet sich dieses Verfahren ideal für BLOB-Spalten mit fester Länge, da der Programmierer einen Puffer mit exakt der benötigten Größe reservieren kann. Dieses Verfahren ist auch sehr effizient, da der Consumer nicht wiederholt Speicher zu reservieren braucht. Das folgende Beispiel zeigt eine Zugriffsobjekt-Klasse mit Bindung an eine BLOBSpalte mit fester Länge: class CBLOBAccessorFixedLength { public: BYTE m_Column1 [50]; BEGIN_COLUMN_MAP(CBLOBAccessorFixedLength) COLUMN_ENTRY(1, m_Column1) END_COLUMN_MAP() DEFINE_COMMAND(CBLOBAccessorFixedLength, _T(" \ SELECT \ Column1 \ FROM TABLE1")) } ;

11.3 BLOBs als Daten im Speicher mit Puffer-Referenz Wenn Sie das Verfahren »Speicher mit Puffer-Referenz« verwenden, ist der Provider und nicht der Consumer dafür zuständig, einen Speicherabschnitt zu reservieren und die BLOB-Daten dort hineinzuschreiben. Der Consumer ruft eine Referenz auf diesen Puffer ab und kann auswählen, ob er selbst oder der Provider (Voreinstellung) Eigner dieses Speicherabschnitts sein soll. Weitere Informationen über Eigner und die Verwaltung von Speicher finden Sie in Kapitel 9. Dieses Verfahren ist für kurze BLOBs mit variabler Größe gut geeignet, da hier nicht die Gefahr besteht, dass Daten abgeschnitten werden. Wenn der Consumer als Eigner des Speicherabschnitts festgelegt wurde, ist es möglicherweise weniger effizient, da der Speicher für jede einzelne Zeile neu reserviert und freigegeben werden muss. Das folgende Beispiel zeigt eine Zugriffsobjekt-Klasse mit Bindung an eine BLOBSpalte mit variabler Länge: class CBLOBAccessorVariableLength { public: BYTE* m_Column1;

Sandini Bib 11.4

BLOBs als Daten in einem Speicherobjekt

289

ULONG m_Column1Length; BEGIN_COLUMN_MAP(CBLOBAccessorVariableLength) COLUMN_ENTRY_TYPE_LENGTH(1, DBTYPE_BYTES | DBTYPE_BYREF, m_Column1, m_Column1Length) END_COLUMN_MAP() DEFINE_COMMAND(CBLOBAccessorVariableLength, _T(" \ SELECT \ Column1 \ FROM TABLE1")) } ;

11.4 BLOBs als Daten in einem Speicherobjekt Ein Speicherobjekt ist ein COM-Objekt, das zum Speichern und Abrufen von BLOBDaten dient. Es verfügt über Methoden zum Lesen und Schreiben kleiner Speicherabschnitte. Wie in Abbildung 11.1 gezeigt, erzeugt der Provider ein Exemplar eines Speicherobjekts und gibt dieses an den Consumer weiter. Der Consumer ist dann dafür zuständig, das BLOB abschnittsweise zu lesen. Der Vorteil dieses Verfahrens liegt darin, dass große Objekte in kleinen Abschnitten gelesen werden können. Wenn eine BLOB-Spalte beispielsweise einen umfangreichen Video-Clip enthält, kann der Consumer diesen während des Abspielens nach und nach in kleinen Portionen abrufen. Dieses Verfahren kann zwar auch für kurze BLOBs angewendet werden, macht aber das Abrufen aufgrund der Komplexität der Speicherobjekte unnötig kompliziert. Beachten Sie, dass das Speicherobjekt nicht in der Datenquelle gespeichert wird, sondern dazu dient, abschnittsweise auf die BLOB-Daten zuzugreifen.

11.4.1 Speicherschnittstellen Ein Speicherobjekt unterstützt mindestens eine der vier folgenden Speicherschnittstellen: 왘 ISequentialStream 왘 IStream 왘 IStorage 왘 ILockBytes

Die Eigenschaft für Datenquelleninformationen DBPROP_STRUCTUREDSTORAGE gibt an, welche Speicherschnittstelle unterstützt wird. Der Wert für diese Schnittstelle setzt sich aus den in Tabelle 11.2 enthaltenen Werten zusammen. ISequentialStream ist die einfachste Schnittstelle, sie verfügt nur über die Methoden Read und Write:

Sandini Bib 290

11

(1) Erzeugt (3) Liest oder schreibt Zeiger

Speicherobjekt

ConsumerPuffer Consumer-Objekt

BLOBs

ProviderZeilenmenge

(4) Überträgt Daten

BLOB

(2) Reserviert Abbildung 11.1: Die Architektur für Speicherobjekte HRESULT Read( void * pv, //Zeiger auf den Puffer, in den der Datenstrom // eingelesen wird ULONG cb, //Gibt die Anzahl der zu lesenden Bytes an ULONG * pcbRead //Zeiger auf den Speicherort, der die tatsächliche // Anzahl der gelesenen Bytes enthält ); HRESULT Write( void const* pv, //Adresse des Puffers, aus dem der Datenstrom //herausgeschrieben wird ULONG cb, //Gibt die Anzahl der zu schreibenden Bytes an ULONG * pcbWritten //Gibt die tatsächliche Anzahl der geschriebenen Bytes an );

Objekte, die ISequentialStream implementieren, müssen jeweils die aktuelle Position verfolgen, die sich bei der Erzeugung des Speicherobjekts am Anfang des BLOBs befindet. Beide Methoden, d.h. sowohl Read als auch Write inkrementieren die aktuelle Position um die Anzahl der bereits verarbeiteten Bytes. Abbildung 11.2 zeigt ein Beispiel, bei dem der Consumer die Methode Read zweimal aufruft. Beim ersten Mal fordert er 3 Byte an und verschiebt die aktuelle Position um 3 Byte nach vorne. Dann ruft er Read erneut auf und fordert dabei 4 Byte an. ISequentialStream ist eine ziemlich primitive Schnittstelle. Sie bietet beispielsweise

keine Möglichkeit zum Lesen oder Schreiben von Daten in Rückwärtsrichtung. Positiv zu vermerken ist, dass dieser Umstand eine einfache Implementierung für den Provider ermöglicht. Häufig ist ISequentialStream die einzige vom Provider unterstützte Schnittstelle, daher bildet sie den Schwerpunkt für die weiteren Ausführungen in diesem Kapitel.

Sandini Bib 11.4

BLOBs als Daten in einem Speicherobjekt

291

Die Schnittstelle IStream ist ein Nachkomme von ISequentialStream. Zusätzlich zu den Methoden Read und Write weist sie Methoden zum Suchen einer bestimmten Position, zum Kopieren von Speicher und zum Klonen von sich selbst auf. Anders als ISequentialStream ermöglicht IStream dem Client das Lesen oder Schreiben von Daten in Rückwärtsrichtung. Ihre Methoden haben große Ähnlichkeit mit den Datei-API-Funktionen, so dass diese Schnittstelle für den Zugriff auf Dateien hervorragend geeignet ist. Symbolischer Wert

Beschreibung

DBPROPVAL_SS_ISEQUENTIALSTREAM

ISequentialStream wird unterstützt.

DBPROPVAL_SS_ISTREAM

IStream wird unterstützt.

DBPROPVAL_SS_ISTORAGE

IStorage wird unterstützt.

DBPROPVAL_SS_ILOCKBYTES

ILockBytes wird unterstützt.

Tabelle 11.2: Werte für die Eigenschaft DBPROP_STRUCTUREDSTORAGE

Read (3) Read (4) Aktuelle Position Datenstrom

Abbildung 11.2: Lesen eines sequenziellen Datenstroms

IStorage ist eine aus Datenstrom-Objekten (Streams) und einem Speicherobjekt gebildete Schnittstelle. IStream und IStorage stellen zusammen eine Struktur dar, die mit

einem Dateisystem vergleichbar ist. ILockBytes ist eine Schnittstelle, die ein Byte-Array im Speicher, auf der Festplatte oder

in einem anderen Gerät wie z.B. einem Datenbank-BLOB darstellt. ISequentialStream ist die bevorzugte Schnittstelle und bildet daher die Basis für das folgende Beispiel. Beispiele unter Verwendung einer anderen Schnittstelle für Speicherobjekte sähen ähnlich aus.

11.4.2 BLOB-Daten über ein Speicherobjekt abrufen Zum Zugriff auf ein BLOB-Objekt sind die folgenden beiden Schritte erforderlich: 1. Sie müssen eine Zugriffsobjekt-Klasse deklarieren, die ein Speicherobjekt bindet. 2. Sie müssen ein Exemplar erzeugen, den Datenstrom abrufen und die Daten lesen.

Sandini Bib 292

11

BLOBs

Es bietet sich an, die Zugriffsobjekt-Klasse über die Makros aus den OLE DB Consumer Templates zu definieren. Das Basis-Makro BLOB_ENTRY ist dabei folgendermaßen definiert: #define BLOB_ENTRY(nOrdinal, IID, flags, data) \

...

wobei 왘 nOrdinal die Positionsnummer der Spalte in der Zeilenmenge darstellt. 왘 data das Klassenelement ist, das die Referenz auf das Speicherobjekt enthält. 왘 IID der Schnittstellenbezeichner (Schnittstellen-ID) des Speicherobjekts ist, beispielsweise IID_IsequentialStream. 왘 flags den Zugriffsmodus für das Speicherobjekt angibt. Der Speichermodus kann eine Reihe verschiedener Werte aufweisen, von denen die am häufigsten vorkommenden in Tabelle 11.3 aufgelistet sind. Die OLE DB Consumer Templates definieren auch BLOB_ENTRY_STATUS, ein Makro, das BLOB_ENTRY ähnelt und zusätzlich noch den Spaltenstatus bindet: #define BLOB_ENTRY_STATUS(nOrdinal, IID, flags, data, status)

wobei status das Klassenelement ist, das den Status der Spalte angibt. Symbolischer Wert

Beschreibung

STGM_READ

Es sind nur Leseoperationen möglich.

STGM_WRITE

Es sind nur Schreiboperationen möglich.

STGM_READWRITE

Es sind sowohl Lese- als auch Schreiboperationen möglich.

Tabelle 11.3: Flags für Speicherobjekte

Wie im folgenden Beispiel gezeigt, bindet die Klasse ein BLOB über die Schnittstelle ISequentialStream: class CBLOBAccessorSequentialStreamRead { public: ISequentialStream* m_Column1; BEGIN_COLUMN_MAP(CBLOBAccessorSequentialStreamRead) BLOB_ENTRY(1, IID_ISequentialStream, STGM_READ, m_Column1) END_COLUMN_MAP() DEFINE_COMMAND(CBLOBAccessorSequentialStreamRead, _T(" \ SELECT \ Column1 \ FROM TABLE1"))

Sandini Bib 11.4

BLOBs als Daten in einem Speicherobjekt

293

void ClearRecord() { memset(this, 0, sizeof(*this)); } } ;

Hier geben wir STGM_READ als Modus an, da in diesem Fall nur Daten gelesen werden müssen. Auf den ersten Blick mag die folgende Zeile überraschen: ISequentialStream* m_Column1;

Im ersten Kapitel habe ich die Vorteile von COM-Zeigern (Klasse CComPtr) für die automatische Referenzzählung erläutert. In diesem Kontext besteht jedoch nicht die Notwendigkeit, CComPtr zu verwenden, da FreeRecordMemory den Referenzzähler des COM-Objekts freigibt und ClearRecord das Klassenelement m_Column1 mit dem Wert NULL initialisiert. Sobald die Bindung des Speicherobjekts definiert ist, können Sie es wie folgt verwenden: HRESULT ReadWithSequentialStream() { CBLOBTable myTable; HRESULT hr = myTable.Open(); if (FAILED(hr)) return hr; hr = myTable.MoveFirst(); if (hr == S_OK) { CComPtr myStream; myStream = myTable.m_Column1; BYTE bytes [50]; ULONG ActualLength = sizeof(bytes); while(ActualLength == sizeof(bytes) || hr == S_OK) { hr = myStream->Read(bytes,sizeof(bytes),&ActualLength); // eine Aktion mit den Daten durchführen } myTable.FreeRecordMemory(); } return S_OK; }

In diesem Codebeispiel sollten wir einige Punkte genauer betrachten: 왘 bytes stellt ein Array dar, das die aus dem sequenziellen Datenstrom gelesenen Bytes empfängt. Es ist klar, dass dieses Array vor dem Aufruf von ISequentialStream::Read zugewiesen werden muss.

Sandini Bib 294

11

BLOBs

왘 Wenn das Ende des Datenstroms erreicht ist, muss ISequentialStream zwei Aktionen durchführen: Zunächst muss es die Länge der tatsächlich gelesenen Bytes setzen (ActualLength im Code). Dieser Wert wird geringer sein als die angeforderte Byte-Anzahl. Zweitens muss es den Erfolgscode S_FALSE zurückliefern. Manche Provider führen eine dieser Aktionen nicht aus, daher sollten Sie sowohl hr als auch ActualLength überprüfen. 왘 Sobald die Zeile gelesen wurde, müssen Sie FreeRecordMemory aufrufen. Andernfalls bleibt das Speicherobjekt bestehen und blockiert unnötigerweise Speicher. 왘 In diesem Beispiel wird eine Variable vom Typ CComPtr verwendet. Es wäre auch möglich (und geringfügig schneller), eine Variable des Typs ISequentialStream* zu verwenden. Eine der Unzulänglichkeiten dieses Ansatzes ist, dass wir die Länge der im Speicherobjekt enthaltenen Daten nicht kennen, bevor die Daten nicht vollständig gelesen wurden. Es kann mühevoll sein, nach jedem Aufruf von ISequential::Read die Länge zu ermitteln. Außerdem ist es nicht machbar, dass der Benutzer nur diejenigen Speicherobjekte verarbeitet, deren Daten eine bestimmte Länge aufweisen. Es ist eigentlich überraschend, dass die OLE DB Consumer Templates keine Makros zur Verfügung stellen, welche die Länge der Daten in einem Speicherobjekt binden, obwohl die Implementierung nicht schwierig ist. Sowohl COLUMN_ENTRY als auch COLUMN_ENTRY_STATUS basieren auf dem Makro _BLOB_ENTRY_CODE, das folgendermaßen definiert ist: #define _BLOB_ENTRY_CODE(nOrdinal, IID, flags, dataOffset, statusOffset) \ if (pBuffer != NULL) \ { \ CAccessorBase::FreeType(DBTYPE_IUNKNOWN, pBuffer + \ dataOffset); \ } \ else if (pBinding != NULL) \ { \ DBOBJECT* pObject = NULL; \ ATLTRY(pObject = new DBOBJECT); \ if (pObject == NULL) \ return E_OUTOFMEMORY; \ pObject->dwFlags = flags; \ pObject->iid = IID; \ CAccessorBase::Bind(pBinding, nOrdinal, DBTYPE_IUNKNOWN, sizeof(IUnknown*), 0, 0, eParamIO, \ dataOffset, 0, statusOffset, pObject); \ pBinding++; \ } \ nColumns++;

Sandini Bib 11.4

BLOBs als Daten in einem Speicherobjekt

295

Wie andere Makros zum Binden von Spalten hat auch _BLOB_ENTRY_CODE zwei Aufgaben: Binden der Klassenelemente und Freigeben der Daten. Die ersten Zeilen geben das Speicherobjekt frei, sobald es nicht mehr benötigt wird. Die restlichen Zeilen bewirken nichts anderes als den Aufruf von CAccessorBase::Bind mit den richtigen Parametern. Da der Längen-Offset immer 0 ist (der neunte Parameter von Bind()), bindet _BLOB_ENTRY_CODE nie die Länge der im Speicherobjekt enthaltenen Daten. Die OLE DB-Erweiterungen stellen das Makro _BLOB_ENTRY_CODE_EX zur Verfügung, das außer den Funktionen von _BLOB_ENTRY_CODE auch noch das Binden der Datenlänge übernimmt. Es ist folgendermaßen definiert: #define _BLOB_ENTRY_CODE_EX(nOrdinal, IID, flags, dataOffset, lengthOffset, statusOffset) \ if (pBuffer != NULL) \ { \ CAccessorBase::FreeType(DBTYPE_IUNKNOWN, pBuffer + \ dataOffset); \ } \ else if (pBinding != NULL) \ { \ DBOBJECT* pObject = NULL; \ ATLTRY(pObject = new DBOBJECT); \ if (pObject == NULL) \ return E_OUTOFMEMORY; \ pObject->dwFlags = flags; \ pObject->iid = IID; \ CAccessorBase::Bind(pBinding, nOrdinal, DBTYPE_IUNKNOWN, sizeof (IUnknown*), 0, 0, eParamIO, \ dataOffset, lengthOffset, statusOffset, pObject); \ pBinding++; \ } \ nColumns++;

Somit ist es möglich, BLOB-Makros zu definieren, die auch die Länge der Daten berücksichtigen: #define BLOB_ENTRY_LENGTH(nOrdinal, IID, flags, data, length) \ _BLOB_ENTRY_CODE_EX(nOrdinal, IID, flags, offsetbuf(data), \ offsetbuf (length), 0);

und #define BLOB_ENTRY_LENGTH_STATUS(nOrdinal, IID, flags, data, \ length, status) \ _BLOB_ENTRY_CODE_EX(nOrdinal, IID, flags, offsetbuf(data), \ offsetbuf (length), offsetbuf (status));

Das erste Makro berücksichtigt die Länge des BLOBs, das zweite die Datenlänge und den Status.

Sandini Bib 296

11

BLOBs

Das folgende Codebeispiel zeigt eine BLOB-Bindung unter Einbeziehung der Datenlänge: class CBLOBAccessorSequentialStreamReadWithLength { public: ISequentialStream* m_Column1; ULONG m_Column1Length; BEGIN_COLUMN_MAP(CBLOBAccessorSequentialStreamReadWithLength) BLOB_ENTRY_LENGTH(1, IID_ISequentialStream, STGM_READ, m_Column1, m_Column1Length) END_COLUMN_MAP() DEFINE_COMMAND(CBLOBAccessorSequentialStreamReadWithLength, _T(" \ SELECT \ Column1 \ FROM TABLE1")) } ;

Der Aufruf von MoveNext setzt m_Column1Length auf die Länge der im Speicherobjekt enthaltenen Daten. Einige Provider setzen m_Column1Length fälschlicherweise auf 4. Dieser Wert gibt die Länge des Speicherobjekt-Zeigers und nicht der Daten an. Es ist muss jedoch die Länge der gespeicherten Daten übergeben werden.

11.4.3 BLOB-Daten mit Hilfe eines Speicherobjekts speichern Theoretisch gibt es zwei Möglichkeiten um ein BLOB-Objekt mit Hilfe von Speicherobjekten zu speichern. Im einen Fall stellt der Provider ein Speicherobjekt zur Verfügung, in das der Consumer direkt schreibt. Im anderen Fall erzeugt der Consumer ein neues Speicherobjekt und gibt es an den Provider weiter. Der Provider liest dann die im Speicherobjekt enthaltenen Daten und speichert sie in der Datenquelle. OLE DB-Provider müssen nicht beide Verfahren unterstützen und in der Regel steht nur eines davon zur Verfügung. Da Sie beim Speichern der BLOBs vermutlich nicht die Wahl haben, ist es wichtig, dass Sie mit beiden Verfahren vertraut sind.

11.4.4 BLOBs in ein Speicherobjekt schreiben Dieses Verfahren besteht aus folgenden vier Schritten: 1. Der Consumer bindet ein Speicherobjekt mit Schreibzugriff. 2. Wenn aus der Zeilenmenge eine bestimmte Zeile abgerufen wird, erzeugt der Provider das Speicherobjekt und gibt es an den Consumer weiter.

Sandini Bib 11.4

BLOBs als Daten in einem Speicherobjekt

297

3. Der Consumer ruft die Write-Methode für das Speicherobjekt auf. 4. Das Speicherobjekt überträgt den Schreibvorgang auf den Datenspeicher. Der Vorteil dieses Verfahrens liegt darin, dass es einfach anzuwenden ist. Der folgende Code zeigt, wie Sie dabei vorgehen müssen: class CBLOBAccessorSequentialStreamWrite { public: ISequentialStream* m_Column1; BEGIN_COLUMN_MAP(CBLOBAccessorSequentialStreamWrite) BLOB_ENTRY(1, IID_ISequentialStream, STGM_WRITE, m_Column1) END_COLUMN_MAP() DEFINE_COMMAND(CBLOBAccessorSequentialStreamWrite, _T(" \ SELECT \ Column1 \ FROM TABLE1")) } ;

Sobald die Klasse für das Binden definiert ist, brauchen Sie nur noch den Schreibvorgang für das Speicherobjekt zu programmieren: HRESULT WriteWithSequentialStream() { CBLOBTable myTable; HRESULT hr = myTable.Open(); if (FAILED(hr)) return hr; hr = myTable.MoveFirst(); if (hr == S_OK) { CComPtr myStream; myStream = myTable.m_Column1; BYTE bytes [50]; // die Bytes mit den entsprechenden Daten initialisieren ULONG ActualLength = sizeof(bytes); myStream->Write(bytes, sizeof(bytes), &ActualLength); myTable.FreeRecordMemory(); } return S_OK; }

11.4.5 BLOBs in ein vom Consumer erzeugtes Speicherobjekt schreiben Dieses Verfahren ist zwar ein wenig komplexer, aber Sie müssen darauf zurückgreifen, wenn das andere nicht zur Verfügung steht. Der Hauptunterschied liegt darin, dass

Sandini Bib 298

11

BLOBs

der Consumer das Speicherobjekt zur Verfügung stellt, so dass dieses in manchen Fällen auch vom Consumer implementiert werden muss. Dieses Verfahren besteht aus den folgenden vier Schritten: 1. Der Consumer definiert eine Zugriffsobjekt-Klasse mit Bindung eines Speicherobjekts. Für den Zugriff bietet sich der Nur-Lese-Modus an. 2. Der Consumer erzeugt ein Speicherobjekt. 3. Der Consumer ruft eine Methode auf, wie z.B. SetData oder Insert. 4. Der Provider liest anschließend den Inhalt des Speicherobjekts und fügt das BLOBObjekt in die Datenbank ein. Abbildung 11.3 veranschaulicht dieses Verfahren.

ProviderZeilenmenge Consumer-Objekt

(2) Ruft Insert oder Update auf Liest

(1) Erzeugt Zeiger

Speicherobjekt Schreibt

BLOB Abbildung 11.3: BLOB mit Hilfe eines vom Consumer gelieferten Speicherobjekts speichern

11.4.6 Eigenschaften von BLOBs In einer vollkommenen Welt könnten alle Provider alles. In der Realität weisen einige Provider jedoch eine eingeschränkte Funktionalität auf. Eine dieser Einschränkungen ist die Anzahl von Speicherobjekten pro Zeilenmenge, die von einem Provider unterstützt werden. Diese Anzahl wird über die Eigenschaft DBPROP_MULTIPLESTORAGEOBJECTS angegeben. Wenn diese Eigenschaft auf True gesetzt ist, brauchen Sie sich über die Anzahl der vorhandenen Speicherobjekte keine Gedanken zu machen. Leider unterstützen zahlreiche Provider mehrere gleichzeitig vorhandene Speicherobjekte nicht. In diesem Fall müssen Sie die Zeilen nacheinander verarbeiten und das jeweils letzte Speicherobjekt freigeben, bevor die nächste Zeile verarbeitet werden kann. Dies bedeutet vor allem, dass »Momentaufnahmen« (Verwendung der

Sandini Bib 11.4

BLOBs als Daten in einem Speicherobjekt

299

Methode Snapshot) nicht möglich sind. Das Problem wird noch kniffliger, wenn eine Tabelle mehr als eine Spalte mit BLOBs enthält. Wenn ein Zugriffsobjekt mehrere BLOB-Spalten bindet, scheitert die Methode GetData. Eine weitere Einschränkung sind blockierende Speicherobjekte. Hierbei handelt es sich um Speicherobjekte, die während ihrer Existenz andere Zeilenmengenoperationen verhindern. Dies bedeutet, dass Sie das betreffende Speicherobjekt freigeben müssen, bevor Sie zur nächsten Zeile wechseln können. Die Eigenschaft für Datenquelleninformationen DBPROP_MULTIPLESTORAGEOBJECTS gibt an, ob es sich bei den Speicherobjekten eines bestimmten Providers um blockierende Speicherobjekte handelt oder nicht.

11.4.7 Mit mehreren BLOBs arbeiten Wenn die Eigenschaft DBPROP_MULTIPLESTORAGEOBJECTS auf False gesetzt ist, sollten Sie nicht versuchen, mehrere Speicherobjekte gleichzeitig abzurufen, da sich dies auf die Behandlung von Zeilen und Spalten auswirkt. Lassen Sie keine zwei Zeilen gleichzeitig bestehen, wenn Sie in einer Zeilenmenge eine BLOB-Spalte mit Hilfe eines Speicherobjekts öffnen, da sonst beim Abrufen der ersten Zeile eine Referenz auf das erste Speicherobjekt erzeugt und beibehalten wird. Für die zweite abgerufene Zeile kann dann kein Speicherobjekt erzeugt werden. Verwenden Sie folglich CRowset, da diese Klasse jeweils nur eine Zeile abruft und bestehen lässt. CArrayRowset hingegen ist für das Abrufen mehrerer Zeilen gleichzeitig vorgesehen und sollte in diesem Fall nicht verwendet werden. Wenn es wirklich nötig sein sollte, dass mehrere Zeilen gleichzeitig existieren, empfiehlt es sich, die Bindung über den Speicher vorzunehmen. Ein weiteres Problem tritt auf, wenn eine Zeilenmenge mehrere BLOB-Spalten enthält. Wenn Sie zwei oder mehr Speicherobjekte binden, kann der Provider sie nicht abrufen. Eine Möglichkeit, dieses Problem zu umgehen, besteht in der Deklaration einiger Bindungen über den Speicher, wobei höchstens eine Bindung über ein Speicherobjekt definiert bleiben darf. Das folgende Beispiel zeigt eine Zugriffsobjekt-Klasse mit zwei Speicherobjekten: class CMultipleBLOBAccessorSequentialStreamRead { public: ISequentialStream* m_Column1; ISequentialStream* m_Column2; BEGIN_COLUMN_MAP(CMultipleBLOBAccessorSequentialStreamRead) BLOB_ENTRY(1, IID_ISequentialStream, STGM_READ, m_Column1) BLOB_ENTRY(2, IID_ISequentialStream, STGM_READ, m_Column2) END_COLUMN_MAP() ///... } ;

Sandini Bib 300

11

BLOBs

Diese könnte dann mit der oben beschriebenen Notlösung folgendermaßen umgewandelt werden: class CMultipleBLOBAccessorSequentialStreamRead1 { public: ISequentialStream* m_Column1; BYTE m_Column2 [50]; BEGIN_COLUMN_MAP(CMultipleBLOBAccessorSequentialStreamRead1) BLOB_ENTRY(1, IID_ISequentialStream, STGM_READ, m_Column1) COLUMN_ENTRY(2, m_Column2) END_COLUMN_MAP() //... } ;

Wenn eine der BLOB-Spalten normalerweise mehr Daten enthält als die anderen, empfiehlt es sich, auf die Daten dieser Spalte über ein Speicherobjekt zuzugreifen. Dies setzt voraus, dass alle Spalten außer einer in den Speicher abgerufen werden können. Ist dies nicht gewährleistet, können Sie auf eine zweite Notlösung zurückgreifen. Im Kapitel 9, Zugriffsobjekte, habe ich eine Möglichkeit vorgestellt, mehrere Zugriffsobjekte in derselben Klasse zu definieren. Bei einfachen Typen ist dies nicht besonders sinnvoll, für die Behandlung mehrerer BLOBs dagegen bietet diese Möglichkeit Vorteile. Dahinter steht der Gedanke, mehrere Zugriffsobjekte mit je höchstens einem Speicherobjekt zu deklarieren. Dies würde beispielsweise die Deklaration der Klasse aus dem vorherigen Beispiel folgendermaßen ändern: class CMultipleBLOBAccessorSequentialStreamRead2 { public: ISequentialStream* m_Column1; ISequentialStream* m_Column2; BEGIN_ACCESSOR_MAP(CMultipleBLOBAccessorSequentialStreamRead2,2) BEGIN_ACCESSOR(0, false) BLOB_ENTRY(1, IID_ISequentialStream, STGM_READ, m_Column1) END_ACCESSOR() BEGIN_ACCESSOR(1, false) BLOB_ENTRY(2, IID_ISequentialStream, STGM_READ, m_Column2) END_ACCESSOR() END_ACCESSOR_MAP() DEFINE_COMMAND(CMultipleBLOBAccessorSequentialStreamRead2, _T(" \ SELECT \ Column1, \ Column2 \ FROM TABLE1")) } ;

Sandini Bib 11.4

BLOBs als Daten in einem Speicherobjekt

301

Zur Erinnerung: Der zweite Parameter von BEGIN_ACCESSOR_MAP gibt die Anzahl der Zugriffsobjekte an, in diesem Fall zwei. Der zweite Parameter von BEGIN_ACCESSOR gibt an, ob es sich um ein automatisches Zugriffsobjekt handelt. In diesem Fall weist er den Wert False auf. Wenn Sie eine der Methoden zur Cursorbewegung für die Zeilenmenge aufrufen, werden nur die Daten für das automatische Zugriffsobjekt abgerufen. Es sollte höchstens ein automatisches Zugriffsobjekt zum Binden eines Speicherobjekts vorhanden sein. Wenn Sie Daten aus dem Datenspeicher lesen, müssen Sie GetData explizit für das angegebene Zugriffsobjekt aufrufen und dann vor einem weiteren Aufruf von GetData die Methode FreeRecordMemory aufrufen. Das folgende Beispiel zeigt, wie Sie eine Klasse mit mehreren Zugriffsobjekten verwenden können. Öffnen Sie zunächst die Tabelle und rufen Sie eine Zeile ab. Rufen Sie anschließend GetData (0) auf um Spalte 1 abzurufen und den Datenstrom zu verarbeiten. Sobald die Verarbeitung der ersten Spalte beendet ist, rufen Sie dann FreeRecordMemory auf und geben die ggf. vorhandene Referenz auf den Datenstrom frei. Die gleiche Schrittfolge ist auch für die zweite Spalte durchzuführen. HRESULT ReadMultipleWithSequentialStream() { CBLOBTable myTable; HRESULT hr = myTable.Open(); if (FAILED(hr)) return hr; hr = myTable.MoveFirst(); if (hr == S_OK) { myTable.GetData(0); CComPtr myStream; myStream = myTable.m_Column1; BYTE bytes [50]; ULONG ActualLength = sizeof(bytes); while(ActualLength == sizeof(bytes) || hr == S_OK) { hr = myStream->Read(bytes,sizeof(bytes),&ActualLength); // eine Aktion mit den Daten durchführen } myTable.FreeRecordMemory(); myStream = NULL; myTable.GetData(1); myStream = myTable.m_Column2; while(ActualLength == sizeof(bytes) || hr == S_OK) { hr = myStream->Read(bytes,sizeof(bytes),&ActualLength); // eine Aktion mit den Daten durchführen }

Sandini Bib 302

11

BLOBs

myTable.FreeRecordMemory(); } return S_OK; }

Es ist noch anzumerken, dass bei der Verwendung dynamischer Zugriffsobjekte das Konzept mit mehreren Zugriffsobjekten nicht unterstützt wird. Folglich scheitert CDynamicAccessor bei dem Versuch, mehrere Speicherobjekte für eine Datenquelle abzurufen, da dies nicht unterstützt wird.

11.4.8 Schnittstellen, Speicher und Dateien konvertieren Bisher haben Sie die Vorzüge von Speicherobjekten kennen gelernt. Diese weisen jedoch auch einen Nachteil auf, nämlich die Schnittstelle ISequentialStream, mit der Sie sicherlich nicht auf Dauer direkt arbeiten wollen. In der Regel verwendet der Benutzer zur Behandlung von BLOB-Daten den Speicher, Dateien und weitere Schnittstellen. Im folgenden Abschnitt erfahren Sie, wie Sie diese Elemente nach ISequentialStream und zurück konvertieren können.

IStream Da IStream von ISequentialStream erbt, kann ein IStream-Zeiger verlustfrei in einen ISequentialStream-Zeiger konvertiert werden. Sie können diese Konvertierung beispielsweise folgendermaßen programmieren: IStream * MyStream; //MyStream abrufen CComPtr MySequentialStream = MyStream;

Der umgekehrte Vorgang, nämlich einem ISequentialStream-Zeiger direkt einen IStream-Zeiger zuzuweisen, ist nicht möglich. Sie können jedoch die ISequentialStream-Schnittstelle in den Speicher einlesen und daraus eine IStream-Schnittstelle erzeugen. Die Windows-API stellt eine sehr komfortable Funktion zur Verfügung, die für den globalen Speicher einen Datenstrom erzeugt: WINOLEAPI CreateStreamOnHGlobal( HGLOBAL hGlobal, //Speicher-Handle für das Datenstromobjekt BOOL fDeleteOnRelease, //Festlegung, ob der Speicher nach der // Freigabe des Objekts freigegeben // werden soll LPSTREAM * ppstm //Adresse der Ausgabevariablen, die den // Zeiger auf die IStream-Schnittstelle //empfängt );

Sandini Bib 11.4

BLOBs als Daten in einem Speicherobjekt

303

Wenn Sie beispielsweise einen Speicherabschnitt über ein Speicherobjekt in eine BLOBSpalte einfügen wollen, können Sie das folgende Verfahren anwenden: 1. Globalen Speicher reservieren (Aufruf von GlobalAlloc). 2. In den Speicher schreiben (Aufruf von GlobalLock, memcpy und GlobalUnLock). 3. Im globalen Speicher einen Datenstrom erzeugen (Aufruf von CreateStreamOnHGlobal). 4. Zeilen wie üblich einfügen oder aktualisieren. Das folgende Beispiel dient zur Veranschaulichung dieses Verfahrens: HRESULT WriteInMemory() { CBLOBTable myTable; HRESULT hr = myTable.Open(); if (FAILED(hr)) return hr; byte memory [123]; HGLOBAL GlobalMemoryHandle = GlobalAlloc(GMEM_FIXED, sizeof(memory)); void * GlobalMemoryPointer = GlobalLock(GlobalMemoryHandle); memcpy(GlobalMemoryPointer, memory, sizeof(memory)); GlobalUnlock(GlobalMemoryHandle); IStream * Stream= NULL; hr = CreateStreamOnHGlobal(GlobalMemoryHandle, TRUE,&Stream); if (FAILED(hr)) return hr; myTable.m_Column1 = Stream; myTable.m_Column1Length = sizeof(memory); hr = myTable.Insert(); myTable.FreeRecordMemory(); return S_OK; }

Die Windows-API stellt auch die Umkehrfunktion zur Verfügung: WINOLEAPI GetHGlobalFromStream( IStream * pstm, //Zeigt auf das Datenstromobjekt HGLOBAL * phglobal //Zeigt auf das aktuelle Speicher-Handle // für den angegebenen Datenstrom

ILockBytes Auf die gleiche Weise ist die Konvertierung des Inhalts des globalen Speichers in eine ILockBytes-Schnittstelle möglich. Die Windows-API stellt zwei Funktionen zur Konvertierung eines ILockBytes-Zeigers in ein globales Speicher-Handle und umgekehrt zur Verfügung:

Sandini Bib 304

11

BLOBs

WINOLEAPI CreateILockBytesOnHGlobal( HGLOBAL hGlobal, //Speicher-Handle für das Byte-Array-Objekt BOOL fDeleteOnRelease, // Festlegung, ob der Speicher nach der // Freigabe des Objekts freigegeben //werden soll ILockBytes ** ppLkbyt //Adresse der Ausgabevariablen, die den // Zeiger auf die ILockBytes-Schnittstelle empfängt );

und WINOLEAPI GetHGlobalFromILockBytes( ILockBytes * pLkbyt, //Zeigt auf das Byte-Array-Objekt HGLOBAL * phglobal //Zeigt auf das aktuellen Speicher-Handle // für das angegebene Byte-Array );

Über diesen Umweg ist es folglich möglich, eine ILockBytes-Schnittstelle über ein globales Speicher-Handle in eine IStream-Schnittstelle zu konvertieren. Es versteht sich von selbst, dass dieses Verfahren nicht für BLOBs angewendet werden kann, die wegen ihrer Größe nicht in den globalen Speicher passen.

IStorage IStorage ist die komplexeste und leistungsfähigste Schnittstelle und daher ist ihre Handhabung am schwierigsten. Es gibt jedoch zwei Funktionen zum Abrufen eines IStorage-Zeigers. Bei der ersten ist das Ausgangsobjekt ein ILockBytes-Zeiger: WINOLEAPI StgOpenStorageOnILockBytes( ILockBytes * plkbyt, //Zeigt auf die ILockBytes-Schnittstelle // im zugrunde liegenden Byte-Array IStorage * pStgPriority, //Zeigt auf das vorherige Öffnen eines // Stamm-Speicherobjekts DWORD grfMode, //Gibt den Zugriffsmodus für das Objekt an SNB snbExclude, //Zeigt auf eine SNB-Struktur, wobei // auszuschließende Elemente angegeben werden DWORD reserved, //Reserviert, muss 0 sein IStorage ** ppstgOpen //Zeigt an, wohin das Speicherobjekt // zurückgeliefert werden soll );

Mit dem folgenden Code kann beispielsweise ein IStorage-Schnittstellenzeiger abgerufen werden:

Sandini Bib 11.5

Auf persistente COM-Objekte zugreifen

305

ILockBytes * LockBytes; // LockBytes abrufen IStorage* Storage; StgOpenStorageOnILockBytes(LockBytes, NULL, mode, NULL, 0, &Storage);

Bei der zweiten Funktion ist das Ausgangsobjekt eine Datei: HRESULT StgOpenStorageEx( const WCHAR * pwcsName, //Zeigt auf den Pfad der Datei, die das // Speicherobjekt enthält DWORD grfMode, //Gibt den Zugriffsmodus für das Objekt an STGFMT stgfmt, //Gibt das Speicherformat für die Datei an DWORD grfAttrs, //Reserviert, muss 0 sein STGOPTIONS ** ppStgOptions, //Adresse des STGOPTIONS-Zeigers void * reserved2, //Reserviert, muss 0 sein REFIID riid, //Gibt die GUID des Schnittstellenzeigers an void ** ppObjectOpen //Adresse eines Schnittstellenzeigers );

Mit dem folgenden Code kann beispielsweise ein IStorage-Zeiger auf die Daten in einer Datei mit dem angegebenen Namen abgerufen werden: WCHAR * filename = L"c:\ whatever"; IStorage* Storage; StgOpenStorageEx (filename, STGM_READ, STGFMT_STORAGE, 0, NULL, 0, IID_IStorage, &Storage);

11.5 Auf persistente COM-Objekte zugreifen Bisher haben wir mit BLOBs in ihrer einfachsten Form, nämlich als Byte-Folge, gearbeitet. Wie ich bereits am Anfang des Kapitels erläutert habe, definiert OLE DB auch eine Möglichkeit für COM-Objekte, sich selbst in ein BLOB zu schreiben oder sich aus einem BLOB auszulesen. Dies entbindet den Consumer von der Aufgabe, den BLOBInhalt zu interpretieren. Diese persistenten COM-Objekte unterstützen eine der folgenden Schnittstellen: 왘 IPersistStorage 왘 IPersistStream 왘 IPersistStreamInit

Sandini Bib 306

11

BLOBs

COM-Objekte, die IPersistStream oder IPersistStreamInit unterstützen, können sich selbst in einem IStream-Zeiger speichern und sich auch daraus abrufen. Für COMObjekte, die IPersistStorage unterstützen, gilt dasselbe in Bezug auf einen IStorageZeiger. Wenn Sie mit der Behandlung von Speicherobjekten vertraut sind, werden Sie feststellen, dass die Behandlung von COM-Objekten in ähnlicher Weise erfolgt. Sie ist sogar noch einfacher, da hierbei der Provider alle Leseoperationen durchführt. Gehen Sie zum Abrufen eines persistenten COM-Objekts folgendermaßen vor: 1. Definieren Sie eine Zugriffsobjekt-Klasse, die das persistente COM-Objekt bindet. 2. Öffnen Sie die Zeilenmenge und navigieren Sie wie üblich durch die Zeilen. Der folgende Code bindet beispielsweise ein persistentes COM-Objekt mit der Spalte 1: class CBLOBAccessorIPersistStreamRead { public: IPersistStream* m_Column1; BEGIN_COLUMN_MAP(CBLOBAccessorIPersistStreamRead) BLOB_ENTRY(1, IID_IPersistStream, STGM_READ, m_Column1) END_COLUMN_MAP() DEFINE_COMMAND(CBLOBAccessorIPersistStreamRead, _T(" \ SELECT \ Column1 \ FROM TABLE1")) } ;

Nach dem Aufruf einer Methode wie z.B. MoveFirst oder MoveNext zeigt m_Column1 auf ein persistentes COM-Objekt.

11.6 Zusammenfassung BLOBs bieten zahlreiche Möglichkeiten zum Lesen und Schreiben von Daten. In diesem Kapitel wurden ihre Vor- und Nachteile erläutert. Die Implementierung einer effizienten BLOB-Unterstützung ist keine einfache Aufgabe und Sie werden dabei schnell an die Grenzen des von Ihnen verwendeten Providers stoßen. Grundsätzlich ist es am günstigsten, die einzelnen Verfahren auszuprobieren um festzustellen, welche den jeweiligen Anforderungen am besten gerecht wird.

Sandini Bib

12 Transaktionen Beim Aktualisieren von Zeilenmengen werden Zeilen eingefügt, aktualisiert oder gelöscht. Client-Programme bestehen in der Regel aus mehreren Aktualisierungsoperationen, die wir bisher einzeln nacheinander ausgeführt haben. In der Praxis besteht ein solches Client-Programm üblicherweise aus mehreren, miteinander zu koordinierenden Aktionen, die durch eine Transaktion ordnungsgemäß ausgeführt werden können. In diesem Kapitel werden die folgenden Themen behandelt: 왘 Warum sind Transaktionen so wichtig? 왘 Merkmale von Transaktionen 왘 Die verschiedenen Transaktionstypen: einfache, verschachtelte und verteilte Transaktionen 왘 Verwendung von OLE DB zusammen mit Komponenten aus Microsoft Transaction Server (oder COM+)

12.1 Warum werden Transaktionen benötigt? Es ist wichtig zu verstehen, warum Transaktionen ein so wesentliches Element für Anwendungen sind. Dies will ich anhand eines Beispiels erklären. Jeder kennt heute die Online-Banking-Anwendungen. Sicher können auch Sie online auf Ihr Bankkonto zugreifen. Stellen Sie sich folgendes Szenario vor: Ein Benutzer hat ein Girokonto und ein Sparkonto und kann Geld von einem Konto zum anderen überweisen. Da er für sein Geld mehr Zinsen bekommen möchte, will er 100 _ von seinem Girokonto auf das Sparkonto überweisen. Auf der Anwendungsseite besteht dieser Vorgang aus zwei Aktionen (siehe Abbildung 12.1): 왘 100 € vom Girokonto abbuchen 왘 100 € dem Sparkonto gutschreiben

Sandini Bib 308

12

Girokonto 500 €

Aktion1: 100 € abbuchen

Sparkonto 500 €

Aktion2: 100 € gutschreiben

Transaktionen

Girokonto 400 €

Sparkonto 600 €

Abbildung 12.1: Die beiden Aktionen beim Bank-Szenario

Girokonto 500 €

Sparkonto 500 €

Aktion1: 100 € abbuchen

Girokonto 400 €

Aktion2: Sparkonto 500 € 100 € gutschreiben

Abbildung 12.2: Eine Aktion scheitert

Theoretisch ist das schon alles. In der Praxis kann eine dieser Aktionen jedoch aus den folgenden Gründen scheitern: 왘 Hardware-Fehler. Es könnte vorkommen, dass es ein Problem mit einer Festplatte gibt oder dass der Computer ausgeschaltet ist. 왘 Betriebssystemfehler. Ein solcher Fehler kann auftreten, wenn die Festplatte voll ist oder für das Betriebssystem nicht genügend Speicher zur Verfügung steht. 왘 Datenbankfehler. Eine der Aktionen kann die Integrität der Datenbank verletzen. 왘 Fehlerbedingungen in der Geschäftsanwendung, d.h. ein durch die Programmierung bedingter, gewollter Fehler, der in bestimmten Situationen ausgelöst werden soll. So könnte beispielsweise ein Höchstbetrag für Überweisungen festgelegt werden oder eine Überprüfung auf andere Berechtigungen. Ein (für den Sparer) glücklicher Fall würde eintreten, wenn die Gutschrift auf dem Sparkonto erfolgreich vorgenommen wird, während die Abbuchung vom Girokonto scheitert, wobei dies der Bank sicher nicht so sehr gefallen würde. Im anderen (für den Sparer ungünstigen) Fall würde die Gutschrift scheitern, die Abbuchung allerdings erfolgreich ablaufen. Abbildung 12.2 zeigt, was in diesem Fall geschieht. Dieses Problem ist zwar sehr leicht nachzuvollziehen, aber von der Programmierung her nur sehr schwierig zu lösen.

Sandini Bib 12.2

Transaktionen als Lösung

309

Das Programmverhalten beim Scheitern der ersten Operation lässt sich einfach programmieren. Der Code würde wie folgt aussehen: HRESULT PerformTwoOperations() { HRESULT hr = PerformFirstOperation(); if (FAILED(hr)) return hr; hr = PerformSecondOperation(); return hr; }

Beim Scheitern der zweiten Operation würde ein naiver Ansatz darin bestehen, Aktionen zum Rückgängigmachen zu programmieren: HRESULT PerformTwoOperations() { HRESULT hr = PerformFirstOperation(); if (FAILED(hr)) return hr; hr = PerformSecondOperation(); if (FAILED(hr)) UndoFirstOperation(); Return hr; }

Dabei treten jedoch zwei Probleme auf: 왘 Der Programmierer muss eine Funktion zum Rückgängigmachen zur Verfügung stellen. 왘 Damit wird das Problem nicht wirklich gelöst, weil die Funktion zum Rückgängigmachen (hier UndoFirstOperation) scheitern kann. In diesem Fall bleiben die Änderungen, die durch die erste Operation vorgenommen wurden, bestehen.

12.2 Transaktionen als Lösung Hier kommen nun Transaktionen ins Spiel. Eine Transaktion besteht aus einer Reihe von Aktionen, die entweder alle erfolgreich ausgeführt werden oder alle scheitern. Aus Sicht des Programmierers erfolgt eine Transaktion in drei Schritten: 1. Transaktion starten 2. Aktionen durchführen 3. Transaktion beenden Zum Beenden einer Transaktion gibt es zwei Möglichkeiten: Entweder wird die Transaktion festgeschrieben oder abgebrochen. Beim Festschreiben der Transaktion werden alle

Sandini Bib 310

12

Transaktionen

Aktionen in die Datenbank geschrieben, beim Abbruch werden automatisch alle Aktionen komplett rückgängig gemacht. Dieser Vorgang wird manchmal auch als Rollback bezeichnet. Transaktion starten

Transaktion beenden Aktion 1

Aktion 2

Transaktion

Zeit Abbildung 12.3: Phasen einer Transaktion

Abbildung 12.3 zeigt die Phasen einer Transaktion im Überblick. Die Implementierung von Transaktionen ist zwar eine schwierige Aufgabe für den Datenbank-Provider, ihre Verwendung durch den Programmierer ist jedoch glücklicherweise sehr einfach. Dies bedeutet, dass sie das Schreiben einer ausgefeilten Geschäftsanwendung vereinfachen. Der Pseudocode zum Durchführen von zwei Aktionen sieht folgendermaßen aus: HRESULT PerformTwoOperations() { HRESULT hr = StartTransaction(); if (FAILED(hr)) return hr; hr = PerformFirstOperation(); if (FAILED(hr)) { Abort(); return hr; } hr = PerformSecondOperation(); if (FAILED(hr)) { Abort(); return hr; } return Commit() }

Sandini Bib 12.3

Merkmale von Transaktionen

311

12.3 Merkmale von Transaktionen Der Zweck einer Transaktion besteht darin, die Konsistenz eines bestimmten Systems dauerhaft zu gewährleisten. Transaktionen weisen vier wesentliche Merkmale auf, die unter der Abkürzung ACID (Atomicity, Consistency, Isolation und Durability) bekannt sind. Diese will ich im Folgenden erläutern: 왘 Atomicity (Alles-oder-nichts-Prinzip). Entweder werden alle Aktionen erfolgreich durchgeführt oder gar keine. Wenn auch nur eine der Aktionen scheitert, wird alles, was durch die übrigen Aktionen bewirkt wurde, rückgängig gemacht. 왘 Consistency (Konsistenz). Wenn sich das System vor dem Start einer Transaktion in einem konsistenten Zustand befunden hat, gilt dies auch für den Zustand nach der Transaktion, und zwar sowohl für den Fall, dass die Transaktion scheitert, als auch für den Fall, dass sie erfolgreich durchgeführt werden konnte. 왘 Isolation. Eine Transaktion weiß nichts von den Änderungen, die durch andere Transaktionen vorgenommen wurden. Laufen zwei Transaktionen gleichzeitig ab, so ist jede der Auffassung, sie sei die Einzige. 왘 Durability (Dauerhaftigkeit). Sobald eine Transaktion abgeschlossen wurde, sind die Änderungen dauerhaft. Dies bedeutet, dass sie nicht verloren gehen, wenn es zu einem Systemabsturz kommt. Die ACID-Merkmale einer Transaktion lassen sich besser verstehen, wenn man sich vergegenwärtigt, was passiert, wenn eines davon nicht gesichert ist.

12.3.1 Atomicity (Alles-oder-nichts-Prinzip) Dieses Prinzip bildet den Kern jeder Transaktion. Ohne dieses Prinzip wäre es möglich, dass eine Aktion erfolgreich durchgeführt wird, während eine andere scheitert.

12.3.2 Consistency (Konsistenz) Hierbei handelt es sich um ein abstraktes Merkmal, das sich auf die Definition der Konsistenz des Systems bezieht und von den übrigen Merkmalen einer Transaktion abgeleitet werden kann. Wenn nach einer Reihe von Aktionen die Konsistenz des Systems beibehalten werden kann, geht dieses in einen anderen konsistenten Zustand über, sobald die Transaktion festgeschrieben wird. Wird die Transaktion abgebrochen, so bleibt das System in demselben konsistenten Zustand.

12.3.3 Isolation Wenn eine Transaktion nicht isoliert ablaufen würde, könnte sie die Änderungen sehen, die von anderen Transaktionen vorgenommen werden. Dies würde unweiger-

Sandini Bib 312

12

Transaktionen

lich zu Inkonsistenzen führen. Im Abschnitt über die Isolationsstufen werde ich noch genauer auf dieses Thema eingehen.

12.3.4 Durability (Dauerhaftigkeit) Wenn eine Transaktion nicht dauerhaft wäre, könnten die durch sie vorgenommenen Änderungen verloren gehen, selbst nachdem die Transaktion erfolgreich abgeschlossen wurde. Wenn beispielsweise die Änderungen nach dem Festschreiben nur im Speicher verbleiben würden, gingen diese bei einem Stromausfall verloren. Einfach ausgedrückt bedeutet Dauerhaftigkeit, dass die festgeschriebenen Änderungen auf der Festplatte gespeichert werden.

12.4 Verwendung von Transaktionen Bisher haben wir Sitzungsobjekte nur erzeugt, ohne sie für irgendwelche Bearbeitungsvorgänge einzusetzen. Die Nützlichkeit der Klasse CSession zeigt sich erst richtig in deren Transaktionsmethoden: StartTransaction dient zum Starten der Transaktion, Commit zum Festschreiben der Änderungen und Abort zum Abbrechen der Transaktion. Für diese Methoden gilt die folgende Signatur: HRESULT StartTransaction(ISOLEVEL isoLevel = ISOLATIONLEVEL_READCOMMITTED, ULONG isoFlags = 0, ITransactionOptions* pOtherOptions = NULL, ULONG* pulTransactionLevel = NULL) HRESULT Abort(BOID* pboidReason = NULL, BOOL bRetaining = FALSE, BOOL bAsync = FALSE) HRESULT Commit(BOOL bRetaining = FALSE, DWORD grfTC = XACTTC_SYNC, DWORD grfRM = 0)

12.4.1 Einfache Transaktionen Eine einzelne Transaktion in einer Sitzung wird als einfache Transaktion bezeichnet. Dieser Transaktionstyp wird normalerweise folgendermaßen verwendet: CSession MySession MySession.Open(...) MySession.StartTransaction(); CCommand MyCommand; MyCommand.Open(MySession); HRESULT hr =..// Aktionen durchführen if (FAILED(hr)) MySession.Abort(); else MySession.Commit();

Sandini Bib 12.4

Verwendung von Transaktionen

313

12.4.2 Erneuerung von Transaktionen Über den Flag-Parameter fRetaining in der Methode Abort bzw. Commit lässt sich angeben, ob bei Beendigung einer Transaktion eine neue Transaktion gestartet werden soll oder nicht. Wenn dieser Parameter auf FALSE gesetzt ist, wird bei Beendigung einer Transaktion keine neue Transaktion erzeugt. Wird dieser Parameter auf TRUE gesetzt, wird direkt nach der Beendigung der aktuellen Transaktion eine neue Transaktion gestartet. Dies gilt sowohl für Commit als auch für Abort. Die Erneuerung von Transaktionen hat drei Vorteile: 왘 Die Methode StartTransaction braucht nicht aufgerufen zu werden. 왘 Der OLE DB-Provider kann Optimierungen vornehmen. So kann er beispielsweise die Sperrungen zwischen der alten und der neuen Transaktion beibehalten. 왘 Der OLE DB-Provider kann den Status einiger Objekte über das Ende einer Transaktion hinaus beibehalten. Wird beispielsweise ein Befehl vorbereitet, so wird dessen Status auch für die nächste Transaktion beibehalten. Der folgende Code zeigt eine Transaktion ohne Erneuerung: HRESULT hr = S_OK; CSession Session; Session.Open(aDataSource); Session.StartTransaction(); CCommandMyCommand; MyCommand.Execute ...; /// Session.Abort(); // oder Session.Commit(); // Zu diesem Zeitpunkt ist die Transaktion beendet. // Es ist nicht möglich, die Transaktion erneut // abzubrechen oder festzuschreiben //Der folgende Code ist ungültig. hr = Session.Abort(); // Zu diesem Zeitpunkt: hr = XACT_E_NOTRANSACTION

Der folgende Code zeigt eine Transaktion mit Erneuerung: HRESULT hr = S_OK; CSession Session; Session.Open(adataSource); Session.StartTransaction(); // Aktionen durchführen Session.Abort(NULL, TRUE); // Die Transaktion wurde abgebrochen, aber eine neue // wurde gestartet // weitere Aktionen durchführen Session.Commit(TRUE); // Die Transaktion wurde festgeschrieben, eine neue wurde gestartet

Sandini Bib 314

12

Transaktionen

//..weitere Aktionen durchführen Session.Commit(FALSE); // Die Transaktion wurde festgeschrieben, keine weitere wurde // gestartet hr = Session.Commit(FALSE); // Der letzte Aufruf war ungültig, weil keine Transaktion zur // Verarbeitung anstand. Zu diesem Zeitpunkt: hr = XACT_E_NOTRANSACTION

12.4.3 Beibehaltung von Zeilenmengen Über diese Einstellung wird das Verhalten der während einer Transaktion erzeugten Zeilenmenge nach Beendigung der Transaktion festgelegt. Eine beibehaltene Zeilenmenge bleibt in diesem Fall voll funktionsfähig, eine nicht beibehaltene Zeilenmenge unterstützt dann nur noch die Schnittstelle IUnknown. Wenn Sie einen Zeiger auf andere Referenzen beibehalten, wird dieser nur das Inkrementieren und Dekrementieren der Referenzzähler für Zugriffsobjekte und Zeilen-Handles unterstützen. Alle übrigen Methoden liefern den Fehler E_UNEXPECTED zurück. Abbildung 12.4 zeigt die Beibehaltung der Zeilenmenge (durch den grauen Bereich gekennzeichnet).

Eigenschaften für die Beibehaltung von Zeilenmengen festlegen OLE DB unterstützt zwei Zeilenmengen-Eigenschaften, die für die Beibehaltung von Zeilenmengen relevant sind und unabhängig voneinander gesetzt werden können. Diese Eigenschaften sind in Tabelle 12.1 aufgeführt. Mit dem folgenden Code lässt sich die Beibehaltung der Zeilenmenge beim Abbruch der Transaktion auf true setzen und die Beibehaltung beim Festschreiben auf false.

Zeilenmenge Zeilenmenge erzeugen

Zeilenmenge löschen

Transaktion

Transaktion starten

Festschreiben oder abbrechen Zeit

Abbildung 12.4: Beibehaltung von Zeilenmengen

Sandini Bib 12.4

Verwendung von Transaktionen

315

CDBPropSet propset(DBPROPSET_ROWSET); propset.AddProperty(DBPROP_ABORTPRESERVE, true); propset.AddProperty(DBPROP_COMMITPRESERVE,false); m_Rowset.Open(m_Session, NULL, &propset);

Beachten Sie, dass dieses Verhalten auf der Ebene der Zeilenmenge und nicht der Transaktion definiert wird. Damit ist es möglich, Transaktionen zu definieren, bei denen einige Zeilenmengen beibehalten werden und andere nicht. Wenn die Eigenschaften für die Beibehaltung nicht explizit programmiert werden, gelten die Voreinstellungen, d.h. Provider-spezifische Wert. Eigenschaft

Typ

Beschreibung

DBPROP_ABORTPRESERVE

Boolesch

Gibt an, ob Zeilenmengen nach dem Abbrechen einer Transaktion beibehalten werden sollen oder nicht.

DBPROP_COMMITPRESERVE

Boolesch

Gibt an, ob Zeilenmengen nach dem Festschreiben einer Transaktion beibehalten werden sollen oder nicht.

Tabelle 12.1: Eigenschaften für die Beibehaltung von Zeilenmengen

Abbildung 12.5: Hauptdialogfenster von Preservation Demo

Demoprojekt für die Beibehaltung von Zeilenmengen Für dieses Buch wurde das Online-Projekt PRESERVATION DEMO erstellt, das die Beibehaltung von Zeilenmengen beim Abbrechen und Festschreiben einer Transaktion demonstriert.

Sandini Bib 316

12

Transaktionen

Starten Sie zunächst eine Transaktion, ohne das Kontrollkästchen PRESERVE ON COMMIT zu aktivieren. Damit aktualisieren Sie die Listenanzeige, so dass der Inhalt der zugehörigen Tabelle sichtbar ist. Klicken Sie dann zum Festschreiben auf die Schaltfläche COMMIT. Da die Zeilenmenge nicht beibehalten wird, kann die Listenanzeige nicht korrekt aktualisiert werden und ist leer (siehe Abbildung 12.5). Damit Sie sehen können, was im Falle der Beibehaltung geschieht, aktivieren Sie das Kontrollkästchen PRESERVE ON COMMIT. Starten Sie dann eine Transaktion, indem Sie auf START TRANSACTION klicken, und schreiben Sie die Transaktion fest, indem Sie auf COMMIT klicken. Nachdem die Transaktion festgeschrieben wurde, wird die Listenanzeige anhand des Tabelleninhalts korrekt aktualisiert. Das Beibehalten von Zeilenmengen beim Abbrechen einer Transaktion funktioniert genauso.

12.5 Isolationsstufen Wie bereits erwähnt, gelten für Transaktionen die vier ACID-Merkmale. Dabei muss allerdings angemerkt werden, dass eine Transaktion immer nur bis zu einem gewissen Grad isoliert ist. Die Isolationsstufen für eine Transaktion geben an, in welchem Maß die Änderungen, die außerhalb einer Transaktion vorgenommen werden, für diese sichtbar sind. OLE DB unterstützt vier Isolationsstufen: 왘 Nicht festgeschriebene Daten lesen (Readuncommitted, wird auch als »Blättern« bezeichnet) 왘 Festgeschriebene Daten lesen (Readcommitted, wird auch als »Cursorstabilität« bezeichnet) 왘 Wiederholbares Lesen (Repeatableread) 왘 Serialisierbar (Serializable, wird auch als »isoliert« bezeichnet) Eine Transaktion, für welche die Isolationsstufe Serialisierbar gilt, ist in höherem Maße isoliert als eine, für welche die Stufe Wiederholbares Lesen gilt usw. Bevor wir uns jedoch mit den Isolationsstufen im Einzelnen beschäftigen, müssen wir die Phänomene betrachten, die bei der Interaktion zwischen zwei Transaktionen auftreten können. Diese Isolationsphänomene heißen unsauberes Lesen (Dirty Read), nicht wiederholbares Lesen (Nonrepeatable Read) und Phantome. 왘 Unsauberes Lesen liegt vor, wenn eine Transaktion Daten lesen kann, die von einer anderen Transaktion noch nicht festgeschrieben wurden. Nehmen wir einmal an, dass Transaktion A eine Zeile ändert und Transaktion B diese geänderte Zeile liest,

Sandini Bib 12.5

Isolationsstufen

317

bevor A die Änderung festgeschrieben hat. Wenn Transaktion A die Änderung abbricht, hat Transaktion B somit Daten gelesen, die eigentlich nie existiert haben. 왘 Nicht wiederholbares Lesen tritt auf, wenn eine Transaktion dieselbe Zeile zweimal liest und dabei jedes Mal andere Daten abruft. Nehmen wir einmal an, dass Transaktion A eine Zeile liest, Transaktion B diese Zeile dann ändert oder löscht und diese Änderung bzw. Löschung festschreibt. Wenn Transaktion A dann versucht, dieselbe Zeile nochmals abzurufen, enthält diese andere Werte, oder die Zeile ist nicht mehr vorhanden, weil sie gelöscht wurde. 왘 Ein Phantom liegt vor, wenn eine Zeile den Suchkriterien entspricht, am Anfang aber nicht vorhanden zu sein scheint. Nehmen wir an, Transaktion A liest eine Zeilenmenge, die bestimmten Suchkriterien entspricht. Transaktion B fügt eine neue Zeile ein, die diesen Kriterien ebenfalls entspricht. Wenn Transaktion A dann den Befehl ausführt, für den die Zeilen gelesen werden, wird eine abweichende Zeilenmenge abgerufen. In OLE DB sind die folgenden Isolationsstufen anhand des Vorhandenseins bzw. Fehlens dieser Phänomene definiert. 왘 Wenn eine Transaktion vom Typ Nicht festgeschriebene Daten lesen durchgeführt wird, kann die Transaktion die noch nicht von anderen Transaktionen festgeschriebenen Änderungen sehen. Auf dieser Stufe können unsauberes Lesen, nicht wiederholbares Lesen und Phantome auftreten. 왘 Wenn eine Transaktion vom Typ Festgeschriebene Daten lesen verwendet wird, kann die Transaktion die Änderungen, die von anderen Transaktionen vorgenommen wurden, erst sehen, wenn diese festgeschrieben worden sind. Auf dieser Stufe ist unsauberes Lesen nicht möglich, nicht wiederholbares Lesen und Phantome können jedoch auftreten. 왘 Bei Durchführung einer Transaktion vom Typ Wiederholbares Lesen sieht diese keine Änderungen in bereits gelesenen Zeilen, die von anderen Transaktionen durchgeführt werden. Auf dieser Stufe sind unsauberes Lesen und nicht wiederholbares Lesen nicht möglich, Phantome können jedoch auftreten. 왘 Eine Transaktion mit der Isolationsstufe Serialisierbar (bzw. Isoliert) stellt sicher, dass bei der Interaktion aller gleichzeitig auftretenden Transaktionen dieselben Ergebnisse entstehen, als wenn sie gänzlich nacheinander ausgeführt worden wären. Auf dieser Stufe sind unsauberes Lesen, nicht wiederholbares Lesen und Phantome nicht möglich. In Tabelle 12.2 sind die Isolationsstufen und das mögliche Auftreten von Isolationsphänomenen im Überblick dargestellt.

Sandini Bib 318

12

Transaktionen

Isolationsstufe

Unsauberes Lesen

Nicht wiederholbares Lesen

Phantome

Nicht festgeschriebene Daten lesen

Ja

Ja

Ja

Festgeschriebene Daten lesen

Nein

Ja

Ja

Wiederholbares Lesen

Nein

Nein

Ja

Serialisierbar

Nein

Nein

Nein

Tabelle 12.2: Isolationsstufen und Isolationsphänomene

12.5.1 Demoprogramme für die Transaktionsisolation Auf den ersten Blick ist das Konzept der Isolationsstufen für die Transaktionsverarbeitung vielleicht nicht ganz einsichtig. Es ist jedoch zwingend erforderlich, dass Sie es verstehen, bevor Sie mit Transaktionen arbeiten. Eine Möglichkeit hierzu besteht darin, dieses Verhalten einfach auszuprobieren. Zu diesem Buch sind zwei Online-Beispielprogramme vorhanden (A und B), die zusammen ausgeführt werden sollen. Sie bringen Ihnen die verschiedenen Isolationsstufen und die vorgesehenen Sperrmechanismen näher.

Programm A Programm A nimmt Änderungen in einer Datenbank vor. Es weist nur das in Abbildung 12.6 gezeigte Dialogfenster auf. Wenn dieses Fenster aufgerufen wird, erstellt das Programm automatisch eine neue Sitzung, die so lange bestehen bleibt, wie das Dialogfenster geöffnet ist. Über das Dialogfenster von Programm A werden Transaktionen gesteuert und Daten aktualisiert, wobei die in Tabelle 12.3 aufgeführten Schaltflächen verwendet werden.

Abbildung 12.6: Hauptdialogfenster von Programm A

Sandini Bib 12.5

Isolationsstufen

319

Schaltfläche

Beschreibung

START TRANSACTION

Startet eine Transaktion in der aktuellen Sitzung.

COMMIT

Schreibt die aktuelle Transaktion ggf. fest.

ABORT

Bricht die aktuelle Transaktion ggf. ab.

Tabelle 12.3: Schaltflächen für Transaktionen

Schaltfläche

Beschreibung

INSERT

Fügt eine neue Zeile mit dem Wert 0 in die Tabelle ein.

DELETE

Löscht die letzte Zeile der Tabelle.

UPDATE

Erhöht den Wert der letzten Zeile um 1.

Tabelle 12.4: Schaltflächen für das Aktualisieren von Daten

Die von Programm A gestartete Transaktion soll Transaktion A heißen. Die drei übrigen Schaltflächen, die in Tabelle 12.4 beschrieben sind, dienen zum Ändern des Inhalts einer Tabelle (transac), die aus einer einzigen Spalte vom Typ integer besteht. Die Operationen zum Einfügen, Löschen und Aktualisieren werden in der aktuellen Sitzung ausgeführt. Wenn in der aktuellen Sitzung eine Transaktion ausgeführt wird, gehören diese Operationen dazu. Andernfalls erfolgen sie außerhalb von jeglicher Transaktion.

Programm B Programm B ist ein einfaches Programm, das die von Programm A vorgenommenen Änderungen liest. Es weist ebenfalls nur ein Dialogfenster auf, in dem der Inhalt der Tabelle (transac) aufgelistet wird. Abbildung 12.7 zeigt dieses Dialogfenster. Wenn dieses Fenster aufgerufen wird, erstellt das Programm eine neue Sitzung, die so lange bestehen bleibt, wie das Dialogfenster geöffnet ist. Der Benutzer kann eine Transaktion mit der Schaltfläche START TRANSACTION starten, diese mit der Schaltfläche COMMIT festschreiben oder mit der Schaltfläche ABORT abbrechen. Die Transaktion wird mit derjenigen Isolationsstufe gestartet, die jeweils über eines der vier folgenden Optionsfelder gewählt wurde: 왘 READUNCOMMITTED 왘 READCOMMITTED 왘 REPEATABLEREAD 왘 SERIALIZABLE

Sandini Bib 320

12

Transaktionen

Abbildung 12.7: Hauptdialogfenster von Programm B

In einer Liste wird der Inhalt der Tabelle angezeigt. Diese Listenanzeige wird nicht automatisch aktualisiert. Hierzu muss der Benutzer auf die Schaltfläche UPDATE klicken. Wenn zuvor eine Transaktion gestartet wurde, erfolgt die Aktualisierungsoperation innerhalb der aktuellen Transaktion, wobei die gewählte Isolationsstufe verwendet wird. Andernfalls wird die Aktualisierungsoperation außerhalb der Transaktion durchgeführt.

12.5.2 Transaktionen mit der Isolationsstufe »Nicht festgeschriebene Daten lesen« Ein unsauberer Lesevorgang tritt auf, wenn eine Transaktion neue, von einer anderen Transaktion eingefügte Zeilen sehen kann, bevor die betreffende Transaktion festgeschrieben bzw. abgebrochen wurde. Um diesen Sachverhalt nachzuvollziehen, führen Sie die folgenden Schritte aus: 1. Öffnen Sie die Programme A und B. 2. Wählen Sie in Programm B das Optionsfeld READUNCOMMITTED und starten Sie dann eine Transaktion. 3. Aktualisieren Sie die Listenanzeige in B, indem Sie auf die Schaltfläche UPDATE klicken. Zu diesem Zeitpunkt ist die leere Tabelle transac sichtbar. 4. Starten Sie eine Transaktion in Programm A. 5. Fügen Sie in Programm A eine Zeile in die Tabelle ein, indem Sie auf die Schaltfläche INSERT klicken. Zu diesem Zeitpunkt ist die Zeile zwar in die Tabelle transac eingefügt worden, die Änderung wurde aber bisher weder festgeschrieben noch abgebrochen. 6. Aktualisieren Sie die in Programm B angezeigte Listenanzeige, indem Sie auf UPDATE klicken. In der Liste ist nun das soeben eingefügte Element zu sehen. Dies

Sandini Bib 12.5

Isolationsstufen

321

bedeutet, dass die Transaktion in Programm B die Zeile sehen kann, die von der Transaktion in Programm A eingefügt worden ist, obwohl diese Änderung noch gar nicht festgeschrieben wurde. 7. Brechen Sie die Transaktion in A ab, indem Sie auf die Schaltfläche ABORT klicken. 8. Aktualisieren Sie die in Programm B angezeigte Listenanzeige, indem Sie auf UPDATE klicken. In der Liste ist nun wieder der ursprüngliche Zustand der Tabelle zu sehen. Dies bedeutet, dass die Transaktion in Programm B die Zeile nicht mehr sehen kann. Bei einem unsauberen Lesevorgang sieht eine Transaktion die von einer anderen Transaktion vorgenommenen Änderungen »live«. Für den Fall, dass Transaktion A abgebrochen wird, führt dies zu einem Problem, denn in diesem Fall hat die Transaktion in Programm B Zeilen gelesen, die gar nicht existiert haben sollten. Dies kann auch zu einem Problem führen, wenn die Transaktion schließlich festgeschrieben wird. Führen Sie die folgenden Schritte durch, um dies auszuprobieren: 1. Öffnen Sie die Programme A und B. 2. Wählen Sie in Programm B das Optionsfeld READUNCOMMITTED und starten Sie dann eine Transaktion. 3. Aktualisieren Sie die Listenanzeige in B, indem Sie auf die Schaltfläche UPDATE klicken. Zu diesem Zeitpunkt ist die leere Tabelle transac sichtbar. 4. Starten Sie eine Transaktion in Programm A. 5. Fügen Sie in Programm A eine Zeile in die Tabelle ein, indem Sie auf die Schaltfläche INSERT klicken. Zu diesem Zeitpunkt ist die Zeile zwar in die Tabelle transac eingefügt worden, die Änderung wurde aber weder festgeschrieben noch abgebrochen. 6. Aktualisieren Sie die in Programm B angezeigte Listenanzeige, indem Sie auf UPDATE klicken. In der Liste ist nun das eingefügte Element zu sehen. 7. Fügen Sie über INSERT eine weitere Zeile ein. Zu diesem Zeitpunkt sind zwar zwei Zeilen in die Tabelle transac eingefügt worden, die Änderung wurde aber noch nicht festgeschrieben. 8. Aktualisieren Sie die in Programm B angezeigte Listenanzeige, indem Sie auf UPDATE klicken. In der Liste sind jetzt zwei Elemente zu sehen. 9. Schreiben Sie die Transaktion in Programm A fest, indem Sie auf die Schaltfläche COMMIT klicken. Wenn es sich um eine wirklich isolierte Transaktion gehandelt hätte, würde die Transaktion in Programm B entweder keine oder zwei Zeilen sehen. Die teilweise vorgenomme-

Sandini Bib 322

12

Transaktionen

nen Änderungen sind aber sichtbar. Dies kann zu einer Inkonsistenz führen, wodurch die Anforderung verletzt wird, dass die Datenbank konsistent sein muss. Dieses Beispiel ist trivial, weil nur eine Tabelle mit einer Spalte vorhanden ist. Stellen Sie sich aber nun vor, dass eine Tabelle für Aufträge und eine für Produkte vorhanden ist und dass in der Auftragstabelle eine Spalte für die Produkt-ID enthalten ist. Stellen Sie sich weiterhin vor, dass eine erste Transaktion einen neuen Auftrag für ein neues Produkt einfügt und anschließend ein neues Produkt in die Produkttabelle aufnimmt. Bei einem unsauberen Lesevorgang kann eine andere Transaktion den Auftrag für das neue Produkt sehen, bevor die Daten für das Produkt selbst eingefügt worden sind. Selbst wenn die erste Transaktion festgeschrieben wurde, kann dies zu einer Inkonsistenz in der zweiten Transaktion führen. In den Beispielen oben haben wir Zeilen eingefügt. Derselbe Mechanismus gilt aber auch beim Löschen oder Aktualisieren von Zeilen.

12.5.3 Transaktionen mit der Isolationsstufe »Festgeschriebene Daten lesen« Bei einer Transaktion der Isolationsstufe Festgeschriebene Daten lesen sind unsaubere Lesevorgänge nicht möglich. Führen Sie die folgenden Schritte aus um dies nachzuvollziehen: 1. Öffnen Sie die Programme A und B. 2. Wählen Sie in Programm B das Optionsfeld READCOMMITTED und starten Sie dann eine Transaktion. 3. Aktualisieren Sie die Listenanzeige in B, indem Sie auf die Schaltfläche UPDATE klicken. Zu diesem Zeitpunkt weist die Tabelle transac noch keine Zeilen auf. 4. Starten Sie eine Transaktion in Programm A. 5. Fügen Sie in Programm A eine Zeile in die Tabelle transac ein, indem Sie auf die Schaltfläche INSERT klicken. Zu diesem Zeitpunkt wurde das Einfügen der Zeile noch nicht festgeschrieben oder abgebrochen. 6. Aktualisieren Sie die in Programm B angezeigte Listenanzeige, indem Sie auf UPDATE klicken. Zu diesem Zeitpunkt wird Programm B »eingefroren«. Da für Programm B die Isolationsstufe Festgeschriebene Daten lesen festgelegt wurde, kann es die neu eingefügte Zeile nicht lesen und wartet, bis Transaktion A entweder abgebrochen oder festgeschrieben wird. 7. Schreiben Sie die Transaktion in Programm A fest, indem Sie auf die Schaltfläche COMMIT klicken. Direkt danach reagiert Programm B wieder und zeigt die Tabelle in der Listenanzeige an. Alternativ kann Transaktion A auch abgebrochen werden, wodurch Programm B ebenfalls wieder aktiviert wird.

Sandini Bib 12.5

Isolationsstufen

323

Bei diesem Beispiel wurde eine Zeile eingefügt. Beim Löschen oder Aktualisieren einer Zeile würde das Gleiche geschehen. Bei einer Transaktion mit der Isolationsstufe Festgeschriebene Daten lesen sind zwar keine unsauberen Lesevorgänge möglich, dafür kann es aber zu nicht wiederholbarem Lesen kommen. Um dies nachzuvollziehen, führen Sie die folgenden Schritte aus: 1. Starten Sie die Programme A und B. 2. Wählen Sie in Programm B das Optionsfeld READCOMMITTED und starten Sie dann eine Transaktion. 3. Fügen Sie in Programm A entweder innerhalb oder außerhalb einer Transaktion eine Zeile ein. Wenn dies innerhalb einer Transaktion erfolgt, müssen Sie anschließend die Transaktion über die Schaltfläche COMMIT festschreiben. 4. Aktualisieren Sie die in Programm B angezeigte Listenanzeige, indem Sie auf UPDATE klicken. Zu diesem Zeitpunkt wird in der Listenanzeige nur eine Zeile mit dem Wert column1 = 0 angezeigt. 5. Starten Sie eine Transaktion in Programm A. Erhöhen Sie den Wert für die Zeile, indem Sie auf die Schaltfläche UPDATE klicken. Schreiben Sie dann die Transaktion in Programm A fest, indem Sie auf die Schaltfläche COMMIT klicken. 6. Aktualisieren Sie die Listenanzeige, indem Sie auf die Schaltfläche UPDATE klicken. Zu diesem Zeitpunkt zeigt die Listenanzeige nur eine Zeile mit dem Wert column1 = 1. 7. Starten Sie eine Transaktion in Programm A und löschen Sie eine Zeile, indem Sie auf die Schaltfläche DELETE klicken. Schreiben Sie dann die Transaktion über die Schaltfläche COMMIT fest. 8. Aktualisieren Sie die Listenanzeige. Zu diesem Zeitpunkt werden hier keine Zeilen angezeigt. Während dieses Beispiels liest Transaktion B dieselbe Zeile drei Mal. Beim ersten Mal hat die Zeile den Wert 0, beim zweiten Mal den Wert 1, und beim dritten Mal ist die Zeile nicht mehr vorhanden. Dies ist das Phänomen des nicht wiederholbaren Lesens.

12.5.4 Transaktionen mit der Isolationsstufe »Wiederholbares Lesen« Wie der Name schon sagt, sind nicht wiederholbare Lesevorgänge bei Transaktionen mit der Isolationsstufe Wiederholbares Lesen nicht möglich. Um dies nachzuvollziehen, führen Sie die folgenden Schritte aus: 1. Starten Sie die Programme A und B. 2. Wählen Sie in Programm B das Optionsfeld REPEATABLEREAD und starten Sie dann eine Transaktion.

Sandini Bib 324

12

Transaktionen

3. Aktualisieren Sie die in Programm B angezeigte Listenanzeige, indem Sie auf UPDATE klicken. Zu diesem Zeitpunkt wird in der Listenanzeige keine Tabellenzeile angezeigt. 4. Starten Sie eine Transaktion in Programm A. Fügen Sie eine Zeile ein, indem Sie auf die Schaltfläche INSERT klicken, und schreiben Sie dann die Transaktion in Programm A fest, indem Sie auf COMMIT klicken. 5. Aktualisieren Sie die in Programm B angezeigte Listenanzeige, indem Sie auf UPDATE klicken. Zu diesem Zeitpunkt wird in der Listenanzeige eine Tabellenzeile angezeigt. 6. Starten Sie eine Transaktion in Programm A, ändern Sie den Inhalt der Zeile, indem Sie auf UPDATE klicken. Zu diesem Zeitpunkt wird Programm A »eingefroren«, weil es darauf wartet, dass Programm B die Transaktion beendet. 7. Schreiben Sie mit COMMIT die Transaktion in Programm B fest. Damit müsste Programm A wieder aktiviert werden und Sie können die Transaktion dort mit COMMIT festschreiben. Dieses Beispiel zeigt das Aktualisieren einer Zeile. Das Verhalten beim Löschen ist das Gleiche. Beachten Sie, dass beim Einfügen einer Zeile keine Sperrung erfolgt. Eine eingefügte Zeile kann nicht von B gesperrt werden, weil sie neu ist (und noch nicht von B gelesen wurde). Dies bedeutet, dass eine Transaktion mit wiederholbarem Lesen bei nochmaliger Ausführung derselben Abfrage ein anderes Ergebnis haben kann. Dieses Phänomen ist als Phantom bekannt. Um diesen Sachverhalt nachzuvollziehen, führen Sie die folgenden Schritte aus: 1. Starten Sie die Programme A und B. 2. Starten Sie in Programm B unter Verwendung des Optionsfelds REPEATABLEREAD eine Transaktion, aktualisieren Sie aber zu diesem Zeitpunkt die Listenanzeige nicht. 3. Starten Sie eine Transaktion in Programm A. Fügen Sie dann nach Belieben Zeilen ein, aktualisieren Sie diese und löschen Sie Zeilen. Schreiben Sie die Transaktion mit COMMIT fest. Zu diesem Zeitpunkt dürfte keine Sperrung vorhanden sein. 4. Aktualisieren Sie die Listenanzeige in Programm B. Zu diesem Zeitpunkt werden alle in der Tabelle enthaltenen Zeilen durch Transaktion B gesperrt. 5. Starten Sie eine Transaktion in Programm A und fügen Sie eine Zeile ein. Für diese Zeile ist keine Sperrung vorhanden, weil es sich um eine neue Zeile handelt. 6. Schreiben Sie die Transaktion in Programm A mit COMMIT fest. Starten Sie die Transaktion in Programm A erneut. Ändern Sie den Inhalt der letzten Zeile und

Sandini Bib 12.5

Isolationsstufen

325

schreiben Sie die Transaktion mit COMMIT fest. Auch in diesem Fall ist keine Sperrung vorhanden, weil Transaktion B die letzte Zeile nicht gelesen hat. 7. Starten Sie eine Transaktion in Programm A. Löschen Sie die letzte Zeile und schreiben Sie die Transaktion mit COMMIT fest. Auch in diesem Fall erfolgt keine Sperrung, weil Programm B die jetzt gelöschte Zeile nicht gelesen hat. Zu diesem Zeitpunkt wurden alle verbleibenden Zeilen von Programm B gelesen und sind daher gesperrt. Starten Sie eine Transaktion in Programm A und löschen Sie eine beliebige Zeile. Damit wird Programm A »eingefroren«. Zum erneuten Aktivieren von Programm A müssen Sie die Transaktion in B mit COMMIT festschreiben. Danach ist es dann möglich, die Transaktion in Programm A festzuschreiben.

12.5.5 Transaktionen mit der Isolationsstufe »Serialisierbar« Bei Transaktionen der Isolationsstufe Serialisierbar können Phantome, nicht wiederholbare Lesevorgänge und unsauberes Lesen nicht auftreten. Von einer Transaktion mit dieser Isolationsstufe aus sind die Auswirkungen anderer Transaktionen nicht sichtbar. Um diesen Sachverhalt nachzuvollziehen, führen Sie die folgenden Schritte aus: 1. Starten Sie die Programme A und B. 2. Starten Sie in Programm B unter Verwendung des Optionsfeldes SERIALIZABLE eine Transaktion, aktualisieren Sie aber zu diesem Zeitpunkt die Listenanzeige nicht. 3. Starten Sie eine Transaktion in Programm A. Fügen Sie dann nach Belieben Zeilen ein, aktualisieren Sie diese und löschen Sie Zeilen. Schreiben Sie die Transaktion mit COMMIT fest. 4. Aktualisieren Sie die Listenanzeige in Programm B. Versuchen Sie nun, in Programm A eine Zeile einzufügen, zu aktualisieren oder zu löschen. In Programm B sehen Sie keine Änderung. Wenn Programm A »eingefroren« ist, müssen Sie die Transaktion in Programm B festschreiben um A wieder zu aktivieren.

12.5.6 Isolationsstufen und Sperrungen In OLE DB ist ein Standard für die Isolationsstufen von Transaktionen definiert. Gemäß der Philosophie von COM wird allerdings die Implementierung dem Provider überlassen. In diesem Abschnitt soll das Verhalten von SQL Server untersucht werden. Andere Provider verhalten sich möglicherweise jedoch anders. SQL Server stellt die Isolation dadurch sicher, dass die entsprechenden Zeilen gesperrt werden. Je höher die Isolationsstufe ist, umso mehr Sperrungen erfolgen. Wenn eine

Sandini Bib 326

12

Transaktionen

Zeile geändert wird, markiert SQL Server diese als gesperrt. Die Sperrung wird aufgehoben, wenn die Transaktion entweder festgeschrieben oder abgebrochen wurde. 왘 Auf der Isolationsstufe Nicht festgeschriebene Daten lesen führt SQL Server keine weitere Sperrung ein und wartet auch auf keine. 왘 Auf der Isolationsstufe Festgeschriebene Daten lesen führt SQL Server keine Sperrung ein, wartet aber, bis die Zeilen entsperrt worden sind, bevor Daten gelesen werden. 왘 Auf der Isolationsstufe Wiederholbares Lesen fügt SQL Server Sperrungen für alle Zeilen hinzu, die gelesen werden. Wenn eine Transaktion mit dieser Isolationsstufe Zeilen liest, kann eine andere Transaktion diese erst ändern, nachdem die Transaktion die Sperrungen aufgehoben hat. 왘 Auf der Isolationsstufe Serialisierbar sperrt SQL Server auf der Server-Seite alle Zeilen, die den Kriterien einer SELECT-Anweisung entsprechen. Isolationsstufe der Transaktion

Wartet auf andere Transaktionssperrung

Gelesene Zeilen werden gesperrt

Gelesene und den Kriterien entsprechende Zeilen werden gesperrt

Nicht festgeschriebene Daten lesen

Nein

Nein

Nein

Festgeschriebene Daten lesen

Ja

Nein

Nein

Wiederholbares Lesen

Ja

Ja

Nein

Serialisierbar

Ja

Ja

Ja

Tabelle 12.5: Isolationsstufen und Sperrungen

In Tabelle 12.5 sind die Isolationsstufen für Transaktionen und die zugehörigen Sperrungen unter SQL Server im Überblick dargestellt.

Welche Isolationsstufe soll ich verwenden? Vom Gefühl her würden Sie sicher sagen, dass die Verwendung einer hohen Isolationsstufe gut ist. Dies ist allerdings nicht immer der Fall. Eine starke Isolation bedeutet, dass viele Zeilen gesperrt werden, was sich dann auch auf die Systemleistung auswirkt. Eine starke Isolation ist positiv, die Verwendung vieler Sperrungen und damit eine sinkende Ausführungsgeschwindigkeit der Anwendung wirken sich negativ aus. Da es keinen Königsweg bei der Wahl der gewünschten Isolationsstufe gibt, bietet OLE DB mehrere zur Auswahl an. Wenn Sie nichts dagegen haben, dass eine Transaktion nicht festgeschriebene Daten lesen kann, können Sie für eine Transaktion die Isolationsstufe Nicht festgeschriebene Daten lesen wählen. Wenn Sie andererseits die größtmögliche Isolation verwenden möchten, wählen Sie die Stufe Serialisierbar, in diesem

Sandini Bib 12.5

Isolationsstufen

327

Fall müssen Sie jedoch mit vielen Sperrungen und den sich daraus ergebenden Konsequenzen rechnen. In der Voreinstellung verwenden die OLE DB Consumer Templates die Stufe Festgeschriebene Daten lesen, was einen Kompromiss zwischen zu starker Isolation und zu häufigen Sperrungen darstellt.

Unterstützte Isolationsstufen Es ist nicht erforderlich, dass die OLE DB-Provider alle Isolationsstufen unterstützen. Je nach Provider können es auch nur einige davon sein. Die Eigenschaft für Datenquelleninformationen DBPROP_SUPPORTEDTXNISOLEVELS gibt an, welche Isolationsstufen unterstützt werden. Logischerweise handelt es sich hierbei um eine Nur-Lese-Eigenschaft. Der einzige darin enthaltene Wert ist eine 4 Byte lange Ganzzahl, der eine or-Kombination der in Tabelle 12.6 enthaltenen Werte darstellt. Beachten Sie hierbei Folgendes: 왘 DBPROPVAL_TI_BROWSE entspricht DBPROPVAL_TI_READUNCOMMITTED. 왘 DBPROPVAL_TI_CURSORSTABILITY entspricht DBPROPVAL_ TI_READCOMMITTED. 왘 DBPROPVAL_TI_ISOLATED entspricht DBPROPVAL_TI_ SERIALIZABLE.

Symbolischer Wert

Wert

Bedeutung

DBPROPVAL_TI_ READUNCOMMITTED

0x00000100L

ISOLATIONLEVEL_READUNCOMMITTED wird unterstützt.

DBPROPVAL_TI_ READCOMMITTED

0x00001000L

ISOLATIONLEVEL_READCOMMITTED wird unterstützt.

DBPROPVAL_TI_ REPEATABLEREAD

0x00010000L

ISOLATIONLEVEL_REPEATABLEREAD wird unterstützt.

DBPROPVAL_TI_ SERIALIZABLE

0x00100000L

ISOLATIONLEVEL_SERIALIZABLE wird unterstützt.

Tabelle 12.6: Eigenschaftenwerte für die unterstützten Isolationsstufen von Transaktionen

Der folgende Beispielcode zeigt, wie die unterstützten Isolationsstufen abgerufen werden: CDataSource DataSource; HRESULT hr = DataSource.OpenFromFileName(L"Transac.udl"); CComVariant Variant; DataSource.GetProperty(DBPROPSET_DATASOURCEINFO, DBPROP_SUPPORTEDTXNISOLEVELS, &Variant);

Bei SQL Server ist der Wert für die Eigenschaft 0x00111100, was bedeutet, dass vier Isolationsstufen unterstützt werden.

Sandini Bib 328

12

Transaktionen

12.5.7 Sitzungen ohne Transaktionen Wenn für eine Sitzung keine Transaktion aktiv ist, wird jede Aktion festgeschrieben, nachdem sie beendet wurde. Der folgende Code CSession Session; Session.Open(..); PerformAction1(Session); PerformAction2(Session); PerformAction3(Session); Session.Close();

ist gleichbedeutend mit CSession Session; Session.Open(..); Session.StartTransaction(IsolationLevel); PerformAction1(Session); Session.Commit(); Session.StartTransaction(IsolationLevel);

Eigenschaftenwert

Isolationsstufe

DBPROPVAL_TI_READUNCOMMITTED

ISOLATIONLEVEL_READUNCOMMITTED

DBPROPVAL_TI_READCOMMITTED

ISOLATIONLEVEL_READCOMMITTED

DBPROPVAL_TI_REPEATABLEREAD

ISOLATIONLEVEL_REPEATABLEREAD

DBPROPVAL_TI_SERIALIZABLE

ISOLATIONLEVEL_SERIALIZABLE

Tabelle 12.7: Isolationsstufen für automatisches Festschreiben PerformAction2(Session); Session.Commit(); Session.StartTransaction(IsolationLevel); PerformAction3(Session); Session.Commit(); Session.Close();

Dieses Verhalten wird als automatisches Festschreiben bezeichnet. Die Sitzungseigenschaft DBPROP_SESS_AUTOCOMMITISOLEVELS definiert die hierfür gültige Isolationsstufe (siehe Tabelle 12.7). Im folgenden Beispiel wird die voreingestellte Isolationsstufe für automatisches Festschreiben abgerufen, auf einen anderen Wert gesetzt und anschließend nochmals abgerufen: CDataSource DataSource; HRESULT hr = DataSource.OpenFromFileName(L"Transac.udl"); CSessionEx Session; hr = Session.Open(DataSource); CComVariant Variant;

Sandini Bib 12.6

Asynchrone Transaktionen

329

Session.GetProperty(DBPROPSET_SESSION, DBPROP_SESS_AUTOCOMMITISOLEVELS, &Variant); Session.SetProperty(DBPROPSET_SESSION, DBPROP_SESS_AUTOCOMMITISOLEVELS, DBPROPVAL_TI_ISOLATED); Session.GetProperty(DBPROPSET_SESSION, DBPROP_SESS_AUTOCOMMITISOLEVELS, &Variant);

Per Voreinstellung ist bei SQL Server für automatisches Festschreiben der Daten die Isolationsstufe ISOLATIONLEVEL_READCOMMITTED festgelegt.

12.6 Asynchrone Transaktionen Wenn eine Transaktion viele Aktionen umfasst, kann es lange dauern, bis sie entweder festgeschrieben oder abgebrochen wird. Wenn Sie diese Wartezeiten vermeiden möchten, können Sie die das Abbrechen oder Festschreiben asynchron ausführen lassen. Zum asynchronen Abbrechen einer Transaktion müssen Sie für den Parameter bAsync den Wert TRUE übergeben. Der entsprechende Code könnte folgendermaßen aussehen: CSession MySession; // Aktionen durchführen MySession.Abort (NULL, FALSE, TRUE);

Zum asynchronen Festschreiben einer Transaktion übergeben Sie XACTTC_ASYNC_PHASEONE als grfTC-Parameter. Beispiel: CSession MySession; // Aktionen durchführen MySession.Commit(FALSE, XACTTC_ASYNC_PHASEONE);

Wird die Transaktion synchron beendet (festgeschrieben oder abgebrochen), so können Sie das Ergebnis anhand des HRESULT-Elements erkennen, das von der Methode zurückgegeben wurde. Dies funktioniert nicht, wenn Sie die Transaktion asynchron festschreiben oder abbrechen lassen. Eine Lösung hierfür besteht darin, eine Benachrichtigung zu verwenden, wenn die Transaktion tatsächlich beendet ist. OLE DB verwendet hierfür die Schnittstelle ITransactionOutcomeEvents, die über die vier in Tabelle 12.8 beschriebenen Methoden verfügt. Für diese Methoden gilt die folgende Signatur: virtual HRESULT STDMETHODCALLTYPE Committed(BOOL fRetaining, XACTUOW *pNewUOW, HRESULT hr) = 0; virtual HRESULT STDMETHODCALLTYPE Aborted(BOID *pboidReason, BOOL fRetaining, XACTUOW *pNewUOW, HRESULT hr) = 0; virtual HRESULT STDMETHODCALLTYPE HeuristicDecision(

Sandini Bib 330

12

Transaktionen

DWORD dwDecision, BOID *pboidReason, HRESULT hr) = 0; virtual HRESULT STDMETHODCALLTYPE Indoubt( void) = 0;

Weitere Informationen zu diesen Methoden finden Sie im Microsoft SDK für Transaktions-Server. In den OLE DB-Erweiterungen steht die Schnittstelle ITransactionOutcomeEventsImpl zur Verfügung, eine Standardimplementierung von ITransactionOutcomeEvents, die für die jeweilige Methode einfach E_NOTIMPL (nicht implementiert) zurückliefert. Methode

Beschreibung

Committed

Wird aufgerufen, wenn die Transaktion festgeschrieben wird.

Aborted

Wird aufgerufen, wenn die Transaktion abgebrochen wird.

HeuristicDecision

Wird aufgerufen, wenn einer der Teilnehmer eine heuristische Entscheidung über den Ausgang der Transaktion trifft.

Indoubt

Wird aufgerufen, wenn es Zweifel bezüglich der Transaktion gibt.

Tabelle 12.8: Methoden für ITransactionOutcomeEvents

12.6.1 Verwendung des Transaktionsergebnisses Die Verwendung des Transaktionsergebnisses erfolgt analog zum Arbeiten mit einem Zeilenmengen-Listener (siehe Kapitel 10). Der erste Schritt besteht darin, die Komponente für das Transaktionsergebnis zu implementieren, im zweiten Schritt wird dann eine Benachrichtigung durch die Sitzung angefordert.

COM-Klasse für die Ereignisse von Transaktionsergebnissen definieren Diese Klasse muss die Schnittstelle ITransactionOutcomeEvents unterstützen. Der folgende Code soll als Beispiel dienen. Wenn die Transaktion festgeschrieben wird, erscheint ein Meldungsfenster. Bei anderen Ereignissen wird das Standardverhalten programmiert. class CMyNotif: public CComObjectRoot, public ITransactionOutcomeEventsImpl { BEGIN_COM_MAP(CMyNotif) COM_INTERFACE_ENTRY(ITransactionOutcomeEvents) END_COM_MAP() STDMETHOD(Committed)( BOOL fRetaining, XACTUOW *pNewUOW, HRESULT hr) { MessageBox (NULL, "Die Transaktion wurde festgeschrieben", NULL, NULL);

Sandini Bib 12.7

Verschachtelte Transaktionen

331

return S_OK; } ; } ;

12.6.2 Ereignisse einer Transaktion abrufen Die Methode AtlAdvise dient zum Hinzufügen einer Benachrichtigung zu einer Transaktion, wie der folgende Beispielcode zeigt: CComObject * MyNotification = new CComObject; DWORD dwCookie; hr = AtlAdvise(Transaction, MyNotification, IID_ITransactionOutcomeEvents, &dwCookie);

Beachten Sie, dass dwCookie verwendet werden kann um die Ausgabe einer Benachrichtigung am Verbindungspunkt zu unterbrechen. Beispiel: hr = AtlUnadvise(Transaction, IID_ITransactionOutcomeEvents, dwCookie);

Beachten Sie ferner, dass SQL Server Verbindungspunkte für Transaktionen nicht unterstützt, DTC (Distributed Transaction Coordinator) hingegen tut dies (weitere Informationen zu diesem Thema finden Sie weiter unten).

12.7 Verschachtelte Transaktionen Unter einer verschachtelten Transaktion versteht man eine Transaktion, die innerhalb einer anderen Transaktion ausgeführt wird. Abbildung 12.8 zeigt drei solche Transaktionen. Die Änderungen, die innerhalb der verschachtelten Transaktion erfolgen, sind nur für diese Transaktion sowie die darüber befindlichen Transaktionen sichtbar. Es ist möglich, Transaktionen mehrfach ineinander zu verschachteln. Die Verschachtelungsstufe gibt die Anzahl von Transaktionen von der Basis-Transaktion bis hin zur aktuellen Transaktion an. Für die Basis-Transaktion gilt die Verschachtelungsstufe 1. Für die Provider kann festgelegt werden, ob sie verschachtelte Transaktionen unterstützen oder nicht und bis zu welcher maximalen Verschachtelungsstufe dies der Fall ist. Beachten Sie, dass SQL Server keine verschachtelten Transaktionen unterstützt.

12.7.1 Verwendung von verschachtelten Transaktionen Wenn für eine Sitzung keine Transaktion aktiv ist, kann die Basis-Transaktion mit StartTransaction gestartet werden. Wenn für die Sitzung bereits eine Transaktion aktiviert ist, wird mit StartTransaction eine verschachtelte Transaktion gestartet. Diese wird oberhalb der Transaktion erstellt, die bisher die höchste Verschachtelungsstufe aufgewiesen hat.

Sandini Bib 332

12

Transaktionen

Der folgende Code zeigt, wie drei ineinander verschachtelte Transaktionen erstellt werden. Die Transaktionen der Verschachtelungsstufen 1 und 3 werden festgeschrieben, die Transaktion der Stufe 2 wird abgebrochen. Transaktion starten

Festschreiben

Verschachtelte Transaktion (Stufe = 3) Transaktion starten

Festschreiben

Verschachtelte Transaktion (Stufe = 2) Transaktion starten

Festschreiben

Basis-Transaktion (Stufe = 1)

Abbildung 12.8: Verschachtelte Transaktionen CSession MySession; // Basis-Transaktion starten, Verschachtelungsstufe = 1 MySession.StartTransaction(); // Operationen in der Basis-Transaktion durchführen // Erste verschachtelte Transaktion starten, // Verschachtelungsstufe = 2 MySession.StartTransaction(); // Operationen in der ersten verschachtelten Transaktion // durchführen // Zweite verschachtelte Transaktion starten, // Verschachtelungsstufe = 3 MySession.StartTransaction(); // Operationen in der zweiten verschachtelten Transaktion // durchführen // In der zweiten verschachtelten Transaktion vorgenommene // Änderungen festschreiben MySession.Commit(); // In der ersten verschachtelten Transaktion vorgenommene // Änderungen abbrechen; dies bewirkt, dass die in der // zweiten verschachtelten Transaktion vorgenommenen // Änderungen ebenfalls abgebrochen werden MySession.Abort(); // Basis-Transaktion festschreiben. Nur die außerhalb der ersten

Sandini Bib 12.8

Verteilte Transaktionen

333

// verschachtelten Transaktion vorgenommenen Änderungen werden // festgeschrieben MySession.Commit();

12.8 Verteilte Transaktionen Im vorherigen Abschnitt haben wir uns mit Transaktionen beschäftigt, die sich auf eine einzige Datenquelle beziehen. Einer der Grundgedanken hinter OLE DB ist jedoch, dass die Daten auf mehrere verschiedene Datenquellen verteilt sein können. In diesem Fall wird eine Transaktion benötigt, die auf mehrere Datenquellen zugreifen kann.

12.8.1 Microsoft Distributed Transaction Coordinator Microsoft Distributed Transaction Coordinator (MS DTC) ist ein Windows-Dienst, der Transaktionen koordiniert, die auf verschiedene Datenquellen zugreifen. MS DTC ist sogar in der Lage, Transaktionen zu koordinieren, die auf unterschiedlichen Rechnern ausgeführt werden. Der Dienst implementiert ein zweiphasiges Protokoll, über das sichergestellt wird, dass alle Transaktionen entweder festgeschrieben oder aber abgebrochen werden. Zum Arbeiten mit verteilten Transaktionen muss der DTC-Dienst auf dem ClientRechner sowie auf allen Rechnern ausgeführt werden, auf denen eine Datenquelle vorhanden ist, auf die zugegriffen werden soll. Abbildung 12.9 zeigt ein Beispiel mit einem Client und zwei Datenquellen. DTC wurde anfänglich mit SQL Server 6.5 ausgeliefert und dann als Teil eines Optionspakets für Windows NT 4.0 und Windows 95 im Microsoft Transaction Server integriert. DTC ist nun in Windows 2000 Professional und Windows 2000 Server enthalten und kann Datenquellen verwenden, die eines der folgenden Transaktionsprotokolle implementieren: 왘 OLE Transactions 왘 X/Open XA 왘 LU 6.2 Sync Level 2 DTC muss installiert sein und ausgeführt werden. Dies müssen Sie unter Umständen in der SYSTEMSTEUERUNG über das Applet DIENSTE entsprechend einstellen. Öffnen Sie unter Windows NT 4.0, Windows 95 und Windows 98 die SYSTEMSTEUERUNG und doppelklicken Sie auf das Symbol DIENSTE. Unter Windows 2000 müssen Sie START, PROGRAMME, VERWALTUNG und dann KOMPONENTENDIENSTE wählen. Abbildung 12.10 zeigt dieses Tool.

Sandini Bib 334

12

Transaktionen

Client DTC

DTC

Datenquelle 1

DTC

Datenquelle 2

Abbildung 12.9: DTC-Architektur

12.8.2 Verwendung von MS DTC In den OLE DB-Erweiterungen sind zwei Klassen enthalten, die das Arbeiten mit verteilten Transaktionen erleichtern: CTransactionManager und CSessionEx. Die Verwaltung der verteilten Transaktionen erfolgt über CTransactionManager, CSessionEx erweitert die Funktionalität von CSession. Zum Arbeiten mit dem Transaktions-Manager müssen Sie die Methode CTransactionManager::Open aufrufen: HRESULT Open(char * pszHost = NULL, char * pszTmName = NULL)

Wenn Sie die Standardparameter (NULL und NULL) übergeben, wird der voreingestellte Transaktions-Manager auf dem Rechner geöffnet, auf dem die Komponente ausgeführt wird. Wenn DTC nicht gestartet wird, liefert die Methode Open den Wert XACT_E_ TMNOTAVAILABLE (= 0x8004d01b) zurück. Sobald DTC geöffnet wurde, können Sie über die Methode BeginTransaction eine verteilte Transaktion starten: CComPtr Transaction; TransactionManager.BeginTransaction(&Transaction);

Es folgt der vollständige Code für CTransactionManager: class CTransactionManager { public: HRESULT Open(char * pszHost = NULL, char * pszTmName = NULL) { return DtcGetTransactionManager (pszHost, pszTmName,

Sandini Bib 12.8

Verteilte Transaktionen

335

IID_ITransactionDispenser, 0, 0, NULL, reinterpret_cast (&m_spTransactionDispenser)); } HRESULT BeginTransaction(ITransaction ** ppTransaction, IUnknown * punkOuter = NULL, ISOLEVEL isoLevel = ISOLATIONLEVEL_READCOMMITTED, ULONG isoFlags = 0, ITransactionOptions * pOptions = NULL) { ATLASSERT(m_spTransactionDispenser != NULL); return m_spTransactionDispenser ->BeginTransaction( punkOuter, isoLevel, isoFlags, pOptions,ppTransaction); } CComPtr m_spTransactionDispenser ; } ;

Abbildung 12.10: Konsole zur Verwaltung der Komponentendienste

CSessionEx ist ein Nachkomme von CSession, der alle in dieser Klasse enthaltenen

Transaktionsfunktionen enthält und zusätzlich in der Lage ist, an verteilten Transaktionen teilzunehmen. Der Code für CSessionEx::JoinTransaction sieht folgendermaßen aus: HRESULT JoinTransaction(IUnknown* Transaction, ISOLEVEL isoLevel = ISOLATIONLEVEL_READCOMMITTED, ULONG isoFlags = 0, ITransactionOptions * Options = NULL) { ATLASSERT(m_spOpenRowset != NULL); CComPtr spTransactionJoin; HRESULT hr = m_spOpenRowset->QueryInterface( &spTransactionJoin); if (FAILED(hr)) return hr;

Sandini Bib 336

12

Transaktionen

return spTransactionJoin->JoinTransaction(Transaction, isoLevel, isoFlags, Options); }

Mit dem folgenden Code wird eine verteilte Transaktion erstellt, an der zwei verschiedene Sitzungen teilnehmen. In jeder Sitzung wird eine Operation ausgeführt, anschließend wird die verteilte Transaktion festgeschrieben. CDataSource DataSource1; HRESULT hr = DataSource1.OpenFromFileName(L"transaction1.udl"); CSessionEx Session1; hr = Session1.Open(DataSource1); CDataSource DataSource2; hr = DataSource2.OpenFromFileName(L"transaction2.udl"); CSessionEx Session2; hr = Session2.Open(DataSource2); CTransactionManager TransactionManager; TransactionManager.Open(); CComPtr Transaction; hr = TransactionManager.BeginTransaction(&Transaction); hr = Session1.JoinTransaction(Transaction); hr = Session2.JoinTransaction(Transaction); // In Session1 und Session2 jeweils einige Aktionen durchführen hr = Transaction->Commit(FALSE, XACTTC_SYNC_PHASEONE, 0);

Versuchen Sie nicht, die Transaktionen, die an der verteilten Transaktion teilnehmen, einzeln festzuschreiben oder abzubrechen. Diese Vorgänge werden vom TransaktionsManager verwaltet.

12.9 Transaktionen und Komponenten Inzwischen haben Sie die Vorzüge des Arbeitens mit Transaktionen kennen gelernt. Aber noch fehlt etwas. Bei COM ist die Wiederverwendung von Komponenten das Ziel. Wie können wir jetzt die Transaktionen mit den Komponenten zusammenbringen? Bisher konnten wir die zu verwendenden Transaktionen steuern. Die Komponenten, die wir schreiben, starten eine Transaktion und schreiben diese anschließend fest oder brechen sie ab. Aber da dies dem Konzept der Wiederverwendbarkeit entgegensteht, müssen die Komponenten Teil einer Transaktion sein, die sie nicht gestartet haben. Dies lässt sich anhand der Granularität der Komponenten besser verstehen.

12.9.1 Granularität von Komponenten Die Granularität einer Komponente, d.h. der Grad ihrer Verdichtung, hängt von der Anzahl der von ihr ausgeführten Aufgaben ab. Eine fein granulierte Komponente führt nur eine oder wenige Aufgaben durch, eine grob granulierte Komponente viele Aufgaben.

Sandini Bib 12.9

Transaktionen und Komponenten

337

Wenn wir als Bild den Bau eines Hauses verwenden, so stehen die fein granulierten Komponenten für die Ziegelsteine, die grob granulierten Komponenten für ganze Wände. Intuitiv wissen wir, dass Ziegel besser wieder verwendet werden können als Wände. Es ist jedoch praktischer, eine gesamte Wand zu verwenden, wenn diese genau den Erfordernissen entspricht. Ziegel hingegen sind einfacher zu reparieren, weil sie kleiner sind. Idealerweise besteht ein System sowohl aus grob granulierten als auch aus fein granulierten Komponenten. Grob granulierte Komponenten verwenden fein granulierte Komponenten für ihre Implementierung, wie in Abbildung 12.11 zu sehen ist. Mit einem solchen Ansatz wird die Komplexität der Komponenten aufgebrochen, so dass sich diese leichter verwalten lassen: 왘 Grob granulierte Komponenten sind weniger komplex, weil sie die grundlegenden Aktionen nicht selbst implementieren. 왘 Das System lässt sich leichter verwalten, weil es aus mehr Einzelbestandteilen besteht. Wenn beispielsweise ein Fehler in einer fein granulierten Komponente behoben wird oder darin Verbesserungen vorgenommen werden, kann die grob granulierte Komponente dies automatisch ausnutzen. Der Code für grob granulierte Komponenten sieht folgendermaßen aus: HRESULT PerformCoarseGrainedAction() { CComPtr Component1; CComPtr Component2; Component1.CoCreateInstance(..); Component2.CoCreateInstance(..); Component1.PerformFinedGrainedAction1(); Component2.PerformFinedGrainedAction2(); return S_OK; }

Grob granulierte Komponente

Verwendet

Fein granulierte Komponente 1

Verwendet

Fein granulierte Komponente 2

Abbildung 12.11: Grob und fein granulierte Komponenten

Die einzelnen fein granulierten Komponenten sind wie folgt definiert: HRESULT PerformFinedGrainedActioni() { UpdateTablei(); }

Sandini Bib 338

12

Transaktionen

Wo sollte in diesem Fall eine Transaktion beginnen und wo sollte sie enden? Wenn sie in einer fein granulierten Komponente beginnt und endet, ist es nicht möglich, das Ergebnis der verschiedenen fein granulierten Aktionen zu koordinieren. Aus diesem Grund müssen die Transaktionen in der grob granulierten Komponente beginnen und enden. Nach dieser Erkenntnis sieht der Code wie folgt aus: HRESULT PerformCoarseGrainedAction() { CComPtr Component1; CComPtr Component2; CComPtr Transaction; //Transaktion abrufen Component1.CoCreateInstance(..); Component2.CoCreateInstance(..); HRESULT hr; hr = Component1.PerformFinedGrainedAction1(Transaction, other parameters); if(FAILED(hr)) { Transaction->Abort(..); return hr; } hr = Component2.PerformFinedGrainedAction1(Transaction, other parameters); { Transaction->Abort(..); return hr; } Transaction->Commit(..); return S_OK; }

Die einzelnen fein granulierten Komponenten sehen folgendermaßen aus: HRESULT PerformFineGrainedActioni(ITransaction* Transaction,...) { CSessionEx MySession; //Meine Sitzung (mysession) abrufen MySession.JoinTransaction(Transaction); // Beliebige Aktion mit MySession durchführen }

Der Nachteil bei diesem Ansatz ist, dass am Schluss jede Methode als Parameter eine Transaktion aufweist, was zu einer großen Belastung werden kann.

12.9.2 Microsoft Transaction Server und COM+-Komponenten Microsoft Transaction Server (MTS) führte das Konzept ein, nach dem ein COM-Objektexemplar mit einem Kontext verknüpft sein kann. Darunter versteht man ein COMObjekt, das Informationen über die Umgebung des COM-Objektexemplars, einschließ-

Sandini Bib 12.9

Transaktionen und Komponenten

339

lich einer ggf. vorhandenen Transaktion, verwaltet. Das Ergebnis hierbei ist, dass nicht für jede Methode eine Transaktion als Parameter vorhanden sein muss, denn der Kontext beinhaltet die Transaktionen, und die Koordinierung dieser beiden Elemente erfolgt durch MTS. Mit Windows 2000 und COM+ wurde dieses Konzept noch erweitert, so dass es jetzt auch Bereiche wie Gleichzeitigkeit (Concurrency) umfasst. Abgesehen von geringen Unterschieden bei der Programmierung bleibt das Grundkonzept bezüglich der Transaktionen jedoch unverändert. Dieser Abschnitt soll nur eine kurze Einführung in Microsoft Transaction Server/ COM+ bieten, MTS aber nur in jenen Aspekten behandeln, die für OLE DB wichtig sind.

Der Objektkontext Mit GetObjectContext erhalten Sie Zugriff auf den Objektkontext. HRESULT GetObjectContext (IObjectContext** ppInstanceContext);

Der folgende Beispielcode zeigt, wie der Objektkontext abgerufen wird: CComPtr spObjectContext; GetObjectContext(&spObjectContext);

GetObjectContext mag den Programmierer zunächst verwirren, weil das aktuelle Objekt nicht als Argument übergeben wird. Anders gesagt, müssen Sie sich fragen, welchen Objektkontext es zurückliefert. Die Antwort lautet: Es gibt den Objektkontext des aufrufenden Objekts zurück. MTS/COM+ übernimmt die Verfolgung, welches Objekt GetObjectContext zur Laufzeit aufruft.

Wenn es sich bei einem COM-Objekt nicht um ein MTS-Objekt handelt, so wird als Objektkontext NULL zurückgeliefert Im Gegensatz zum traditionellen Ansatz, bei dem ein einziges Objekt die Transaktion steuert, übernimmt hier jedes MTS-Objekt über seinen Kontext einen Teil der Steuerung. Der Objektkontext unterstützt die Schnittstelle IObjectContext, deren Methoden in Tabelle 12.9 aufgeführt sind. Der Objektkontext gibt an, ob die Transaktion festgeschrieben werden kann. Dies ist allerdings nur ein Hinweis. Wenn die Methode SetComplete aufgerufen wird, so gibt dies an, dass das aktuelle Objekt damit einverstanden ist, die Transaktion mit Commit festzuschreiben. Wenn ein anderes Objekt damit nicht einverstanden ist, wird die Transaktion auf jeden Fall abgebrochen. In Tabelle 12.10 sind die Methoden für Objektkontexte im Überblick dargestellt.

Sandini Bib 340

12

Transaktionen

Methode

Beschreibung

SetComplete

Gibt an, dass das Objekt seine Aufgabe erfolgreich beendet hat.

SetAbort

Gibt an, dass ein nicht behebbarer Fehler aufgetreten ist und dass der ganze Vorgang abgebrochen werden muss.

EnableCommit

Gibt an, dass sich der von dem Objekt durchgeführte Vorgang in einem Zustand befindet, in dem er festgeschrieben werden könnte, das Objekt hat seine Aufgabe jedoch noch nicht beendet.

DisableCommit

Gibt an, dass der von dem Objekt durchgeführte Vorgang jetzt noch nicht festgeschrieben werden kann, das Objekt hat seine Aufgabe noch nicht beendet.

Tabelle 12.9: Methoden für die Schnittstelle IObjectContext

Methode

Kann die Transaktion festgeschrieben werden?

Ist der Vorgang beendet?

SetComplete

Ja

Ja

SetAbort

Niemals

Ja, ist aber gescheitert

EnableCommit

Ja

Nein

DisableCommit

Noch nicht

Nein

Tabelle 12.10: Methoden und Transaktionen für die Schnittstelle IObjectContext

Transaktionsattribute Jede MTS/COM+-Komponente kann ein Attribut aufweisen, welches das Verhalten der zugehörigen Transaktion angibt. Dieses Attribut wird auf COM-Klassen-Ebene angegeben, so dass alle Exemplare einer Klasse dasselbe Attribut verwenden. Ferner kann das Attribut während der Lebensdauer eines Objekts nicht geändert werden. Wenn ein Objekt A ein Objekt B erzeugt, so kann der Objektkontext von B dieselbe Transaktion verwenden, die auch im Objektkontext von A enthalten ist. Das Transaktionsattribut von B gibt an, in welcher Beziehung die Objektkontexte von A und B zueinander stehen. In diesem Beispiel ist A der Client von B. Für das Transaktionsattribut kann einer der folgenden Werte gesetzt werden: 왘 TRANSACTION_REQUIRED (Transaktion erforderlich). Dieser Wert gibt an, dass das Objekt immer in einem Kontext mit Transaktion ausgeführt werden muss. Bei der Erzeugung wird geprüft, ob der Client in einem Kontext mit Transaktion ausgeführt wird. Wenn dies der Fall ist, wird das Objekt in diesem Kontext ausgeführt. Andernfalls erzeugt MTS eine neue Transaktion für das Objekt.

Sandini Bib 12.9

Transaktionen und Komponenten

341

왘 TRANSACTION_REQUIRES_NEW (Neue Transaktion erforderlich). Dieser Wert gibt an, dass das Objekt immer in seiner eigenen Transaktion ausgeführt werden muss. Beim Erzeugen des Objekts erstellt MTS immer eine neue Transaktion für dieses. 왘 TRANSACTION_SUPPORTED (Unterstützt Transaktionen). Dieser Wert gibt an, dass das Objekt in einer Transaktion ausgeführt werden kann, dies ist allerdings nicht unbedingt erforderlich. Beim Erzeugen eines neuen Objekts erbt der Kontext die Transaktion vom Kontext des Clients. Wenn der Client bereits eine Transaktion verwendet, wird das neue Objekt in dieser Transaktion ausgeführt. Wenn der Client ohne Transaktion ausgeführt wird, verwendet das neue Objekt auch keine. 왘 TRANSACTION_NOT_SUPPORTED (Unterstützt keine Transaktionen). Dieser Wert gibt an, dass das Objekt niemals in einer Transaktion ausgeführt wird. Beim Erzeugen des Objekts wird der Kontext ohne Transaktion erstellt. Man unterscheidet drei Arten der Erzeugung von MTS-Objekten: 왘 Das MTS-Objekt kann ein Basisobjekt sein. In diesem Fall handelt es sich beim Client nicht um ein MTS-Objekt, so dass kein Attribut für den Objektkontext vererbt werden kann. 왘 Das MTS-Objekt wird von einem anderen MTS-Objekt ohne Transaktion erzeugt. Aus Sicht der Transaktion entspricht dieser Fall dem ersten Fall. 왘 Das MTS-Objekt wird von einem anderen MTS-Objekt erzeugt, das innerhalb eines Transaktionskontexts ausgeführt wird. In Tabelle 12.11 sind die Transaktionsattribute für die verschiedenen Arten der Erzeugung von Objekten zusammengefasst. Dabei sind drei Punkte besonders wichtig: 왘 Beim Wert TRANSACTION_REQUIRES_NEW ist die neue Transaktion nicht verschachtelt, sondern unabhängig. Daher kann sie erfolgreich festgeschrieben werden, auch wenn die Client-Transaktion abgebrochen wird. Andersherum ist es aber auch möglich, sie abzubrechen, während die Client-Transaktion erfolgreich festgeschrieben wird. 왘 Beim Wert TRANSACTION_NOT_SUPPORTED scheitert das Erzeugen eines neuen Objekts nicht, wenn der Client eine Transaktion aufweist, sondern die Transaktion wird einfach nicht berücksichtigt. Dies bedeutet, dass dieser Attributwert besser TRANSACTION_NOT_USED (Transaktion wird nicht verwendet) oder ähnlich hätte heißen sollte. 왘 Die MTS-Komponente kann Ressourcen mit und ohne Transaktion außerhalb von MTS-Transaktionen verwendet. In diesem Fall gelten die Transaktionsattribute nicht, so dass die MTS-Komponente beispielsweise Dateioperationen durchführen kann. Da das Dateisystem keine Transaktionen unterstützt, können Dateioperationen nicht abgebrochen werden. Gleichfalls ist es möglich, dass die MTS-Kompo-

Sandini Bib 342

12

Transaktionen

nente eine OLE DB-Datenquelle zwar öffnen, aber die Transaktion nicht einbinden kann. In diesem Fall ist das Ergebnis der Transaktion vollkommen unabhängig von der Transaktion des Objekts. Transaktionsattribut

TRANSACTION_ REQUIRED

TRANSACTION_ REQUIRES _NEW

TRANSACTION_ SUPPORTED

TRANSACTION_NOT _SUPPORTED

Basisobjekt

Neue Transaktion

Neue Transaktion

Keine Transaktion

Keine Transaktion

Kein Basisobjekt, für den Client ist keine Transaktion vorhanden

Neue Transaktion

Neue Transaktion

Keine Transaktion

Keine Transaktion

Kein Basisobjekt, für den Client ist eine Transaktion vorhanden

Geerbte Transaktion

Neue Transaktion

Geerbte Transaktion

Keine Transaktion

Tabelle 12.11: Transaktionsattribute und Transaktionen

12.9.3 Transaktionseinbindung Die letzte Dienstkomponente ist die Transaktionseinbindung (Transaction Enlistment). Wenn Sie eine Datenquelle mit Transaktionseinbindung öffnen, geben Sie an, dass jede Sitzung versuchen sollte, an der Transaktion des Exemplars teilzunehmen, das die Sitzung erstellt hat. Wenn die Komponente, welche die Sitzung erstellt hat, keine Transaktion aufweist, hat die Transaktionseinbindung keine Auswirkungen. Andernfalls erzeugt die Sitzung eine Transaktion und nimmt an der Transaktion des Objekts teil. Aus diesem Grund sollten Sie die Transaktion der Sitzung nicht festschreiben oder abbrechen, sondern statt dessen für die Transaktion des Objekts die Funktionen SetComplete und SetAbort verwenden. Bei der Transaktionseinbindung werden die folgenden Elemente verwendet: 왘 Ein Ressourcen-Manager, der dauerhafte Daten verwaltet. SQL Server und Oracle sind Beispiele für Ressourcen-Manager. 왘 Ein Ressourcen-Verteiler verwaltet die Verbindungen zu den Ressourcen-Managern. Die Verbindungen selbst sind nicht dauerhaft, sie gehen beim Beenden des Ressourcen-Verteilers verloren. 왘 Der Verteiler-Manager verwaltet die verschiedenen Ressourcen-Verteiler. 왘 MTS/COM+ verwendet MS DTC als Transaktions-Koordinator. Abbildung 12.12 zeigt die verschiedenen Elemente, die bei der Transaktionseinbindung eine Rolle spielen.

Sandini Bib 12.9

Transaktionen und Komponenten

343

MTS/COM+ fängt zur Laufzeit die Erzeugung des Transaktionsexemplars ab und überträgt das Transaktionsattribut gemäß den oben beschriebenen Regeln.

TransaktionsKoordinator (MS DTC)

Fordert Transaktion an

MTS/COM+ zur Laufzeit Erstellt

Erzeugt MTS/COM+Komponente Koordiniert

Transaktion

Sitzung Teilnahme Fordert Verteiler an VerteilerManager

Bindet ein

RessourcenVerteiler

Fordert Teilnahme an

RessourcenManager

Abbildung 12.12: Architektur für die Transaktionseinbindung

Wenn die Transaktionskomponente eine OLE DB-Sitzung öffnet, fordert diese beim Verteiler-Manager einen Ressourcen-Verteiler an. Der Ressourcen-Verteiler fordert den Ressourcen-Manager auf, an der Transaktion der Komponente teilzunehmen. Von dort ab sind alle über die Sitzung erfolgenden Aktionen Bestandteil der Transaktion des Objekts. Die Verwendung der Transaktionseinbindung ist für den Programmierer kein großes Problem. Wesentlich ist dabei allein die Aktivierung: { CDataSource db; CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_AUTH_INTEGRATED, OLESTR("SSPI")); dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR(database)); dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR(servername)); dbinit.AddProperty(DBPROP_INIT_OLEDBSERVICES, DBPROPVAL_OS_RESOURCEPOOLING | DBPROPVAL_OS_TXNENLISTMENT); db.OpenWithServiceComponents(_T("SQLOLEDB.1"), &dbinit); CSession Session; Session.Open(db) // Aktionen der Sitzung (Session) durchführen }

Sandini Bib 344

12

Transaktionen

Es ist auch möglich, außerhalb der Transaktion des Objekts zu arbeiten. Hierzu wird die Datenquelle ohne Transaktionseinbindung wie folgt geöffnet: { CDataSource db; CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_AUTH_INTEGRATED, OLESTR("SSPI")); dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR(database)); dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR(servername)); dbinit.AddProperty(DBPROP_INIT_OLEDBSERVICES, 0); db.Open (_T("SQLOLEDB.1"), &dbinit); CSession Session; Session.Open(db) Session.StartTransaction(); // Aktionen der Sitzung (Session) durchführen // Transaktion abbrechen oder festschreiben }

12.10 Zusammenfassung In diesem Kapitel wurden einfache, verschachtelte und verteilte Transaktionen, das Isolieren von Transaktionen sowie die Transaktionskomponenten von Microsoft Transaction Server und COM+ behandelt. Microsoft Transaction Server und COM+ sind die Eckpfeiler beim Erzeugen von Komponenten. Zunächst erscheint das Arbeiten mit Transaktionen nicht natürlich. Wenn Sie diese Konzepte jedoch einmal verstanden haben, werden Sie sich fragen, wie Sie bisher ohne sie auskommen konnten.

Sandini Bib

13 Indizes, Kapitelmarken und Sichten In diesem Kapitel werden die vier erweiterten Konzepte Indizes, OLE DB-Kapitelmarken, hierarchische Zeilenmengen und Sichten vorgestellt. Indizes stellen eine Kapselung von Datenbankindizes dar und stehen mit den anderen Konzepten in keinem Zusammenhang. Ich habe sie nur deshalb hier mit aufgenommen, weil dieses Konzept nicht genügend Stoff für ein eigenes Kapitel bietet. OLE DB-Kapitelmarken bieten eine komfortable und effiziente Möglichkeit, eine Gruppe von Zeilen in einer Zeilenmenge anzugeben. Für sie gibt es zwei Anwendungsbereiche: hierarchische Zeilenmengen und Sichten. In den OLE DB Consumer Templates sind diese Konzepte nicht berücksichtigt. Daher haben Sie zwei Möglichkeiten: Entweder Sie arbeiten direkt mit unspezifischen OLE DB-Schnittstellen oder Sie verwenden die mit diesem Buch zur Verfügung gestellten OLE DB-Erweiterungen. In diesem Kapitel gehe ich davon aus, dass Sie die zweite Möglichkeit nutzen. Es ist allerdings auch nicht schwer, den Code für die unspezifischen OLE DB-Schnittstellen aus den OLE DB-Erweiterungen zu ermitteln.

13.1 Indizes Der hinter Tabellen-Datenbanken stehende Grundgedanke ist, dass die Daten in einer Reihe von Tabellen vorliegen, aus denen sie mühelos extrahiert werden können. Angenommen, Sie führen eine Abfrage wie die Folgende aus: SELECT * FROM mytable WHERE column1 = 3

Um zu erreichen, dass hierbei die Antwortzeit nicht proportional zur Anzahl der Zeilen in der Tabelle ansteigt, können Sie einen Index verwenden. Ein Index ist eine geordnete, sinnvoll aufgebaute Baumstruktur für eine oder mehrere Spalten. Wenn Sie einen bestimmten Wert suchen, führt Sie der Index schnell zu den Zeilen, die diesen Wert enthalten. Die Datenbank-Engine findet die entsprechenden Indizes und optimiert auf diese Weise die Ausführung der Abfrage. Unter anderem dadurch zeichnet sich ein guter Abfrageprozessor aus.

Sandini Bib 346

13

Indizes, Kapitelmarken und Sichten

Normalerweise brauchen Sie sich beim Zugriff auf Daten keine Gedanken über Indizes zu machen, da hierfür die Datenbank-Engine zuständig ist und diese Aufgabe in der Regel sogar viel besser erledigt. Außerdem stärkt die Verwendung von Indizes ausdrücklich die Verbindung zwischen Ihrem Programm und dem zugrunde liegenden Datenspeicher. Es gibt jedoch auch Fälle, in denen Sie direkt auf Indizes zugreifen müssen. Beispielsweise können Indizes für die Optimierung von Abfragen über mehrere Datenbanken hilfreich sein. Sie bieten Ihnen auch mehr Funktionalität als normale Befehle. Schließlich unterstützen einige Provider Indizes als einzige Möglichkeit zum effektiven Zugriff auf Daten. In OLE DB sind zwei Arten von Indizes definiert: integrierte Indizes und separate Indizes. Ein integrierter Index ist Bestandteil der Tabelle, die er indiziert. In diesem Fall können die Consumer eine Zeilenmenge öffnen, die sowohl den Index als auch den Tabelleninhalt enthält. Ein separater Index ist nicht Bestandteil irgendeiner Tabelle, sondern verweist lediglich auf Zeilen einer anderen Tabelle. In der Praxis unterstützen die Provider Indizes eher nicht. Der Jet Provider 4.0 für Access 2000-Datenbanken unterstützt allerdings integrierte Indizes, auf die wir uns in diesem Abschnitt konzentrieren werden.

13.1.1 Der Indextyp Für Indizes gibt es einen eigenen OLE DB-Typ, der mit dem Zeilenmengen-Typ verwandt ist. Schließlich ist ein Datenbankindex mit einer Datenbanktabelle vergleichbar und so überrascht es nicht, das OLE DB-Indizes und OLE DB-Zeilenmengen zahlreiche gemeinsame Schnittstellen unterstützen. Genauer gesagt unterstützen OLE DB-Indizes viele der Zeilenmengen-Schnittstellen wie z.B. IRowset und IAccessor oder IRowsetLocate. Außerdem unterstützen sie die Schnittstelle IRowsetIndex, die Such- und Bereichsoperationen ermöglicht. Abbildung 13.1 zeigt die von Indizes unterstützten Schnittstellen. Die obligatorischen Schnittstellen sind fett gedruckt, die optionalen in normaler Schrift dargestellt.

13.1.2 Verwendung von Indizes Die Klasse CIndex kapselt einen integrierten Index. Sie ist der Klasse CTable sehr ähnlich und wird auch in ähnlicher Weise verwendet. Allerdings weist sie einige Funktionen mehr auf als CTable. CIndex ist folgendermaßen deklariert: template class CIndex : public CAccessorRowset

Sandini Bib 13.1

Indizes

347

IRowsetIndex IRowset IAccessor

Indexobjekt

IRowsetInfo IConvertType

Viele IRowset*-Schnittstellen ISupportErrorInfo

Abbildung 13.1: Index-Schnittstellen

Wie CTable benötigt auch CIndex eine Zugriffsobjekt- und eine Zeilenmengen-Klasse. Intern verwaltet diese Klasse einen Zeiger auf die IRowsetIndex-Schnittstelle des OLE DB-Index. Als TRowset-Klasse sollte CRowset oder eine beliebige andere ZeilenmengenKlasse verwendet werden, die einen m_spRowset-Zeiger auf die OLE DB-Zeilenmenge enthält. Wie CTable setzt auch CIndex dieses Klassenelement. Abbildung 13.2 zeigt die Anordnung der Klassenelemente. Während die Zeilenmenge derjenigen ähnelt, die bei Tabellen verwendet wird, gilt für das Zugriffsobjekt eine Einschränkung. Zugriffsobjekte dienen zum Abrufen von Spaltenwerten und zum Übertragen eines Kriteriums. Es empfiehlt sich, die ZugriffsobjektEinträge in der Reihenfolge zu binden, in der sie im Index vorliegen, und mit der Indexspalte zu beginnen. Sehen Sie sich das Beispiel einer Tabelle mit den Spalten column1, column2 und column3 an. Wenn man annimmt, dass der Index die Spalten column2 und column3 in dieser Reihenfolge verwendet, wären folgende Bindungen angebracht: BEGIN_COLUMN_MAP(CTableAccessor) COLUMN_ENTRY(1, m_Column2) COLUMN_ENTRY(2, m_Column3) COLUMN_ENTRY(3, m_Column1) END_COLUMN_MAP()

oder BEGIN_COLUMN_MAP(CTableAccessor) COLUMN_ENTRY(1, m_Column2) COLUMN_ENTRY(2, m_Column1) END_COLUMN_MAP()

Die folgende Bindung wäre jedoch nicht korrekt, weil sie nicht mit einer Spalte beginnt, die Bestandteil des Index ist.

Sandini Bib 348

13

Indizes, Kapitelmarken und Sichten

CRowset IRowset m_spRowset

OLE DB-Index

CIndex IRowsetIndex m_spRowsetIndex

Abbildung 13.2: Anordnung der Klassenelemente von CIndex und CRowset BEGIN_COLUMN_MAP(CTableAccessor) COLUMN_ENTRY(1, m_Column1) COLUMN_ENTRY(2, m_Column2) COLUMN_ENTRY(3, m_Column3) END_COLUMN_MAP()

Auch die folgende Bindung wäre nicht korrekt, weil die Spalten nicht in der im Index definierten Reihenfolge erscheinen: BEGIN_COLUMN_MAP(CTableAccessor) COLUMN_ENTRY(1, m_Column3) COLUMN_ENTRY(2, m_Column2) COLUMN_ENTRY(3, m_Column1) END_COLUMN_MAP()

13.1.3 Index öffnen Das Öffnen eines Index entspricht dem Öffnen einer Tabelle, abgesehen davon, dass Sie hier einen Index- und einen Tabellennamen übergeben. Die Klasse CIndex::Open ist folgendermaßen deklariert: HRESULT Open(const CSession& session, LPCTSTR szIndexName, LPCTSTR szTableName, DBPROPSET* pPropSet = NULL)

wobei 왘 session die Sitzung darstellt, für die der Index geöffnet werden soll. 왘 szIndexName und szTableName für den Indexnamen bzw. den Tabellennamen stehen. Wenn der Indexname zur eindeutigen Identifizierung des Index ausreicht, können Sie für den Tabellennamen den Wert NULL übergeben. Andernfalls ist die Angabe des Tabellennamens erforderlich. 왘 pPropSet stellt ein optionales Eigenschaften-Set dar. Der folgende Code zeigt, wie der Index PrimaryKey für die Tabelle Table1 geöffnet wird: CIndex index; hr = index.Open (mysession, "PrimaryKey", "Table1");

Sandini Bib 13.1

Indizes

349

Intern ruft Open die Funktion IOpenRowset::OpenRowset auf um den Zeiger auf die Schnittstelle IRowsetIndex abzurufen. Ist dieser Aufruf erfolgreich, werden weitere Schnittstellen aufgerufen und Spalten gebunden: hr = session.m_spOpenRowset->OpenRowset(NULL, TableID, &IndexID, __uuidof(m_spRowsetIndex), (pPropSet) ? 1 : 0, pPropSet, (IUnknown**)&m_spRowsetIndex); if (SUCCEEDED(hr)) { SetupOptionalRowsetInterfaces(); // Wenn Ausgabespalten vorliegen, dann binden if (_OutputColumnsClass::HasOutputColumns()) hr = Bind(); }

SetupOptionalRowsetInterfaces fordert eine Schnittstelle für die Zeilenmenge an (GetInterfacePtr erbt von TRowset) und leitet den Aufruf dann an TRowset weiter: void SetupOptionalRowsetInterfaces() { if (m_spRowsetIndex != NULL) m_spRowsetIndex.p->QueryInterface (GetIID(), GetInterfacePtr()); TRowset::SetupOptionalRowsetInterfaces(); }

Sobald der Index geöffnet ist, können Sie unter Verwendung der ZeilenmengenMethoden wie MoveFirst und MoveNext genauso navigieren wie in einer Tabelle. Sie können auch zwei indexspezifische Aktionen durchführen: Indexsuche nach einer bestimmten Zeile und Beschränken des Indexbereichs. Diese Aktionen werden in den folgenden Abschnitten erläutert.

13.1.4 Indexsuche nach einer Zeile Über einen Index kann eine Zeile angesteuert werden, d.h. es kann eine Indexsuche nach einer Zeile durchgeführt werden, deren Spalteninhalt mit einem bestimmten Wert übereinstimmt. Voraussetzung ist, dass die Spalte im Index enthalten ist. Die Methode Seek bewegt den Zeilenmengen-Cursor zu einer neuen aktuellen Zeile und ist daher mit Methoden wie MoveNext vergleichbar. Für die Indexsuche nach einer Zeile müssen Sie die Werte setzen, für die in der betreffenden Spalte Entsprechungen gefunden werden sollen, und dann müssen Sie die Methode Seek mit der Anzahl der signifikanten Spalten aufrufen. Die Methode Seek weist die folgenden zusätzlichen Parameter auf: HRESULT Seek (ULONG cKeyValues, DBSEEK dwSeekOptions = DBSEEK_FIRSTEQ, int nAccessor = 0)

Sandini Bib 350

13

Indizes, Kapitelmarken und Sichten

dwSeekOptions enthält die Option für die Indexsuche und kann einen der in Tabelle 13.1 aufgeführten Werte annehmen. In der Regel soll die erste Zeile abgerufen werden, die dem Suchkriterium entspricht. Daher ist der Wert DBSEEK_FIRSTEQ die Voreinstellung. nAccessor stellt den Index des Zugriffsobjekts dar, für das der Vergleich durchgeführt werden soll. Anders ausgedrückt berücksichtigt Seek die ersten Werte für cKeyValues in diesem Zugriffsobjekt. Die Voreinstellung für nAccessor ist 0.

Symbolischer Wert

Beschreibung

DBSEEK_FIRSTEQ

Sucht nach den ersten Schlüsseln, die mit den gesuchten Werten übereinstimmen.

DBSEEK_LASTEQ

Sucht nach den letzten Schlüsseln, die mit den gesuchten Werten übereinstimmen.

DBSEEK_AFTEREQ

Sucht nach den ersten Schlüsseln, die in der vom Index definierten Reihenfolge gleich den gesuchten Werten sind oder direkt nach ihnen kommen.

DBSEEK_AFTER

Sucht nach den ersten Schlüsseln, die in der vom Index definierten Reihenfolge direkt nach den gesuchten Werten kommen.

DBSEEK_BEFOREEQ

Sucht nach den letzten Schlüsseln, die in der vom Index definierten Reihenfolge gleich den gesuchten Werten sind oder vor ihnen kommen.

DBSEEK_BEFORE

Sucht nach den letzten Schlüsseln, die in der vom Index definierten Reihenfolge direkt vor den gesuchten Werten kommen.

Tabelle 13.1: Optionen für die Indexsuche (Seek)

Intern ruft Seek die Methode IRowsetIndex::Seek auf und übergibt zur Angabe des Kriteriums das Zugriffsobjekt-Handle und den Zugriffsobjekt-Puffer. Diese Operation ist ein wenig mit der Verwendung von Befehlsparametern vergleichbar. Der Code dafür lautet folgendermaßen: HRESULT hr = m_spRowsetIndex->Seek(m_pAccessor->GetHAccessor (nAccessor), cKeyValues, m_pAccessor->GetBuffer(), dwSeekOptions); if (FAILED (hr)) return hr; return GetData();

Wenn beispielsweise ID die erste Indexspalte ist und Sie den Wert 3 abrufen wollen, schreiben Sie folgende Zeilen: index.m_ID = 3; hr = index.Seek (1);

Wenn außerdem ID2 die zweite Indexspalte ist und Sie die erste Zeile abrufen wollen, die der Wertekombination ID = 3 und ID2 = 5 entspricht, schreiben Sie folgende Zeilen:

Sandini Bib 13.1

Indizes

351

index.m_ID = 3; index.m_ID2 = 5; hr = index.Seek (2);

Zum Abrufen der letzten Zeile, in der die Wertekombination ID = 3 und ID2 = 5 enthalten ist, schreiben Sie folgende Zeilen: index.m_ID = 3; index.m_ID2 = 5; hr = index.Seek (2, DBSEEK_LASTEQ);

Nachdem eine Zeile über die Indexsuche gefunden wurde, können Sie durch Aufruf von MoveNext schließlich zu einer Zeile gelangen, die das Kriterium nicht erfüllt. Diesbezüglich bietet Seek eine Funktionalität, die von den herkömmlichen Befehlen nicht zur Verfügung gestellt wird.

13.1.5 Indexbereich beschränken Manchmal weisen Indizes eindeutige Werte auf: Nur eine Zeile kann dann ein bestimmtes Kriterium erfüllen. In diesem Fall ist die Verwendung von Seek geeignet. In anderen Fällen sind die Indexwerte nicht eindeutig: Mehrere Zeilen können dann ein bestimmtes Kriterium erfüllen. In diesem Fall eignet sich Seek weniger gut, weil diese Methode nur eine Zeile und keine Gruppe von Zeilen abrufen kann. Wie der vorherige Abschnitt gezeigt hat, wird keine Warnung ausgegeben, wenn MoveNext zu einer Zeile gelangt, die das Kriterium nicht mehr erfüllt. Zum Glück unterstützen Indizes aber auch Bereiche. Jeder Index enthält einen aktuellen Bereich, der bei Erzeugung des Index gleich dem gesamten Index ist. Auf den aktuellen Bereich kann nicht explizit zugegriffen werden. Der aktuelle Bereich ändert jedoch das Bewegungsverhalten des Indexcursors so, dass beispielsweise MoveFirst den Cursor zur ersten Zeile im Bereich bewegt, während MoveNext den Cursor zur nächsten Zeile verschiebt. Der linke Teil von Abbildung 13.3 zeigt einen Index, bei dem der aktuelle Bereich gleich dem gesamten Index ist. Hier ist das Verhalten von MoveFirst und MoveNext unverändert. Der rechte Teil der Abbildung zeigt den gleichen Index nach dem Setzen eines aktuellen Bereichs auf die Zeilen, die den Wert 2 und 3 aufweisen (die Zeilen im aktuellen Bereich sind grau unterlegt). Hierbei wird das Verhalten von MoveFirst und MoveNext so geändert, dass nur die Zeilen im aktuellen Bereich durchlaufen werden. Die Methode SetRange gibt den aktuellen Bereich mit einem Anfangs- und einem Endkriterium an: HRESULT SetRange ( ULONG cStartKeyColumns, void * pStartData, ULONG cEndKeyColumns, void * pEndData,

Sandini Bib 352

13

Indizes, Kapitelmarken und Sichten

DBRANGE dwRangeOptions = DBRANGE_INCLUSIVESTART | DBRANGE_INCLUSIVEEND, int nAccessor = 0)

MoveFirst

1

MoveFirst

1

MoveNext

2

MoveNext

2

MoveNext

3

MoveNext

3

MoveNext

3

MoveNext

3

MoveNext

5

5

MoveNext

7

7

MoveNext

Zeile im Bereich

Zeile nicht im Bereich

Abbildung 13.3: Aktuellen Bereich eines Index ändern

Im obigen Code ist SetRange eine direkte Kapselung von IRowsetIndex::SetRange. Das Argument pStartData ist ein Array aus Daten, das den Anfang des Bereichs angibt, und cStartKeyColumns steht für die Anzahl von Elementen in pStartData. Dasselbe gilt für das Ende des Bereichs mit den beiden Argumenten cEndKeyColumns und pEndData. dwRangeOptions ist die Bereichsoption. Sie enthält eine Kombination der Werte von

Tabelle 13.2. Im folgenden Beispiel wird ein Wertebereich zwischen 2 und 4 gesetzt: int start [] = { 2} ; int end [] = { 4} ; hr = index.SetRange(1, start, 1, end); hr = index.MoveFirst(); while (hr = S_OK) { hr = index.MoveNext(); }

Sie erhalten einen vollständigen Bereich, indem Sie, wie nachfolgend gezeigt, ein leeres Kriterium übergeben: hr = index.SetRange(0, NULL, 0, NULL);

Symbolischer Wert

Beschreibung

DBRANGE_INCLUSIVESTART

Der Anfangswert ist Bestandteil des Bereichs.

DBRANGE_INCLUSIVEEND

Der Endwert ist Bestandteil des Bereichs.

DBRANGE_EXCLUSIVESTART

Der Anfangswert ist nicht Bestandteil des Bereichs.

Tabelle 13.2: Bereichs-Flags

Sandini Bib 13.2

Kapitelmarken

353

Symbolischer Wert

Beschreibung

DBRANGE_EXCLUSIVEEND

Der Endwert ist nicht Bestandteil des Bereichs.

DBRANGE_EXCLUDENULLS

NULL-Werte sind nicht Bestandteile des Bereichs.

DBRANGE_PREFIX

Der Bereich schließt alle Werte mit ein, die mit dem für pStartData angegebenen Wert beginnen. Dies eignet sich hauptsächlich für Strings; pEndData wird ignoriert.

DBRANGE_MATCH

Der Bereich schließt alle Werte mit ein, die dem Wert für pStartData entsprechen. pEndData wird ignoriert.

Tabelle 13.2: Bereichs-Flags

13.2 Kapitelmarken In Kapitel 8 wurden Positionsmarken für Zeilenmengen behandelt, die jeweils eine Zeile identifizieren und es Ihnen ermöglichen, schnell zur betreffenden Zeile zu wechseln. Während eine Positionsmarke einer einzelnen Zeile entspricht, steht eine Kapitelmarke (Chapter) für eine Gruppe von Zeilen. So zeigt Abbildung 13.4 eine Zeilenmenge mit einer Positionsmarke auf der zweiten Zeile und einer Kapitelmarke auf den Zeilen 3, 4 und 5. Genau wie Positionsmarken sind auch Kapitelmarken opake Werte. Die Abbildung soll nur das Prinzip der Kapitelmarken zeigen. Es ist nicht gewährleistet, dass die jeweilige Implementierung eine Art von Array verwendet. Positionsmarken ließen sich gut mit Lesezeichen in einem Buch vergleichen. Für Kapitelmarken funktioniert dieser Vergleich trotz der Namensverwandtschaft nicht, da eine Kapitelmarke aus folgenden Gründen nicht einem Kapitel eines Buches entspricht: Erstens brauchen die in einer Kapitelmarke zusammengefassten Zeilen, wie in Abbildung 13.5 gezeigt, nicht wie die Abschnitte eines Buchkapitels in aufeinander folgender Reihenfolge vorzuliegen. Somit stellt eine OLE DB-Kapitelmarke mehr als nur einen Zeilenbereich dar. Zweitens brauchen die Zeilen einer Kapitelmarke nicht die Reihenfolge der Zeilen in der Zeilenmenge wiederzugeben. Mit anderen Worten organisiert eine Kapitelmarke die Reihenfolge, in der die Zeilen erscheinen sollen. Abbildung 13.6 zeigt eine Kapitelmarke mit »ungeordneten« Zeilen, d.h. die Zeilenreihenfolge ist anders als in der Zeilenmenge. Kapitelmarken dienen vor allem zum Filtern und Sortieren von Zeilen. Da eine Kapitelmarke eine beliebige Teilmenge von Zeilen enthalten kann, ist sie ideal zur Kennzeichnung der Ergebnisse einer Filteroperation geeignet. Da sie auch die Reihenfolge der Zeilen angibt, eignet sie sich gut zum Speichern der Ergebnisse einer Sortieroperation. In einer Kapitelmarke kann auch das Ergebnis einer kombinierten Sortier-/Filteroperation festgehalten werden.

Sandini Bib 354

13

Indizes, Kapitelmarken und Sichten

Zeilenmenge 1

Positionsmarke für Zeile 2

2

Kapitelmarke für die Zeilen 3, 4 und 5

3

3

4

4

5

5

2

6 Abbildung 13.4: Zeilenmenge, Positionsmarke und Kapitelmarke

Zeilenmenge 1 Kapitelmarke für die Zeilen 1, 3 und 5

1

2

3

3

5

4 5 6

Abbildung 13.5: Die Zeilen in der Kapitelmarke brauchen nicht aufeinander zu folgen

Zeilenmenge 1 Kapitelmarke für die Zeilen 1, 5 und 3

1

2

5

3

3

4 5 6

Abbildung 13.6: Kapitelmarke für ungeordnete Zeilen

13.2.1 Verwendung von Kapitelmarken Wie eine Zeile wird auch eine Kapitelmarke nicht durch ein COM-Objekt, sondern über ein Handle dargestellt: typedef ULONG HCHAPTER;

Der Grund dafür ist, dass eine Kapitelmarke nicht ohne zugrunde liegende Zeilenmenge existieren kann. Außerdem ist ein Handle effektiver.

Sandini Bib 13.2

Kapitelmarken

355

Kapitelmarken-Handles gehören immer zu einer bestimmten Zeilenmenge und eine Zeilenmenge sollte immer nur die zugehörigen Kapitelmarken-Handles verwenden. Kapitelmarken sind opake Werte, es hat daher keinen Zweck, diese zu interpretieren. In OLE DB ist eine besondere NULL-Kapitelmarke definiert: #define DB_NULL_HCHAPTER 0x00

Das Konzept der NULL-Kapitelmarke ist nicht auf Anhieb verständlich. Man kann es sich aber so vorstellen, dass das zugrunde liegende Objekt diese Kapitelmarke ignoriert, oder man kann sich vorstellen, dass die NULL-Kapitelmarke alle Zeilen in derselben Reihenfolge wie in der Zeilenmenge enthält. Auf OLE DB-Ebene akzeptieren zahlreiche Funktionen ein Kapitelmarken-Handle als Parameter. Die OLE DB Consumer Templates nutzen diesen Parameter jedoch nicht und übergeben statt dessen als Parameter DB_NULL_HCHAPTER. Die folgenden Methoden der Klasse CRowset übergeben DB_NULL_HCHAPTER an die jeweilige OLE DB-Funktion: 왘 GetRowCount 왘 Compare 왘 MoveNext 왘 MoveFirst 왘 MoveLast 왘 MoveToBookmark 왘 Insert 왘 Delete 왘 GetRowStatus 왘 MoveToRatio 왘 Undo 왘 Update 왘 RefreshVisibleData 왘 GetApproximatePosition

Betrachten wir als Beispiel einmal den Code für CRowset::MoveFirst: HRESULT MoveFirst() { HRESULT hr; // Überprüfen, ob die Daten erfolgreich geöffnet wurden und // das Zugriffsobjekt gesetzt wurde. ATLASSERT(m_spRowset != NULL); ATLASSERT(m_pAccessor != NULL);

Sandini Bib 356

13

Indizes, Kapitelmarken und Sichten

// Eine ggf. bereits vorliegende Zeile freigeben ReleaseRows(); hr = m_spRowset->RestartPosition(NULL); if (FAILED(hr)) return hr; // Die Daten abrufen return MoveNext(); }

m_spRowset->RestartPosition positioniert den Cursor am Anfang der Zeilen im Kapitelmarken-Parameter. Die OLE DB Consumer Templates übergeben NULL (d.h. den Wert DB_NULL_HCHAPTER) als Parameter. Daher bewegt MoveFirst den Cursor an den Anfang der gesamten Zeilenmenge.

13.2.2 CRowsetEx CRowsetEx ist eine erweiterte Version von CRowset, die Kapitelmarken unterstützt. Diese Klasse weist einen Template-Parameter auf, der definiert, in welchem Maße Kapitelmarken unterstützt werden. template class CRowsetEx: public CRowset, public ChapterClass

ChapterClass kann jede beliebige Klasse sein, die zumindest die folgende Methode enthält: HCHAPTER GetHChapter() const

Abbildung 13.7 zeigt die Vererbung für CRowsetEx. Die Implementierung von CRowsetEx ähnelt der von CRowset, mit dem Unterschied, dass sie an die jeweilige OLE DB-Funktion nicht den Wert NULL übergibt, sondern das Ergebnis von GetHChapter. Beispielsweise wird GetHChapter()von MoveFirst als Parameter für RestartPosition übergeben: HRESULT MoveFirst() { HRESULT hr; // Überprüfen, ob die Daten erfolgreich geöffnet wurden und // das Zugriffsobjekt gesetzt wurde. ATLASSERT(m_spRowset != NULL); ATLASSERT(m_pAccessor != NULL); // Eine ggf. bereits vorliegende Zeile freigeben ReleaseRows(); hr = m_spRowset->RestartPosition(GetHChapter()); if (FAILED(hr))

Sandini Bib 13.2

Kapitelmarken

357

return hr; // Die Daten abrufen return MoveNext(); }

Dies führt dazu, dass CRowsetEx::MoveFirst den Cursor an den Anfang der Kapitelmarke setzt, der nicht unbedingt mit dem Anfang der Zeilenmenge übereinstimmen muss.

CRowset

ChapterClass Definiert GetHChapter()

CRowsetEx Abbildung 13.7: Vererbung von CRowsetEx

CNoChapter ist die trivialste Klasse, da sie keine Unterstützung für Kapitelmarken definiert. Wenn sie verwendet wird, liefert die Methode GetHChapter immer den Wert DB_NULL_HCHAPTER zurück: class CNoChapter { public: HCHAPTER GetHChapter() const { return DB_NULL_HCHAPTER; } } ;

Die Verwendung von CNoChapter ist unaufwändig, das diese Klasse kein neues Klassenelement definiert. CRowsetEx entspricht daher faktisch CRowset. CHasChapter ist eine interessantere Klasse hinsichtlich der Unterstützung von Kapitelmarken. Sie weist ein Klassenelement auf, das die aktuelle Kapitelmarke darstellt. class CHasChapter { public: CHasChapter() { m_hChapter = DB_NULL_HCHAPTER; } HRESULT SetChapter(HCHAPTER Chapter, IRowset* Rowset) // weiterer Code HRESULT ReleaseChapter(IRowset* Rowset) // weiterer Code HCHAPTER GetHChapter() const

Sandini Bib 358

13

Indizes, Kapitelmarken und Sichten

{ return m_hChapter; } void Close() // weiterer Code HCHAPTER m_hChapter; } ;

Abbildung 13.8 zeigt die Vererbung von CRowsetEx. Die Methoden von CRowsetEx berücksichtigen die gewählte Kapitelmarke. MoveFirst bewegt den Cursor zur ersten Zeile der Kapitelmarke, MoveNext zur nächsten Zeile, wobei die von der Kapitelmarke definierte Zeilenreihenfolge berücksichtigt wird. Für Kapitelmarken gibt es zwei Anwendungsbereiche: Sie dienen zum Bewegen des Cursors innerhalb von hierarchischen Zeilenmengen und sie bieten Unterstützung für das Filtern und Sortieren von Zeilen mit Hilfe von Sichten. In den nächsten beiden Hauptabschnitten gehe ich näher auf diese Anwendungsbereiche ein.

CRowset

CHasChapter

Klassenelemente von CRowset

m_HChapter

CRowsetEx Klassenelemente von CRowset m_HChapter

Abbildung 13.8: Vererbung von CRowsetEx

13.3 Hierarchische Zeilenmengen Eine relationale Datenbank enthält eine Reihe von Tabellen. In der Regel werden Beziehungen zwischen diesen Tabellen definiert. Zwei Zeilen in zwei verschiedenen Tabellen weisen eine Beziehung zueinander auf, wenn die Werte in den jeweiligen Spalten identisch sind. Es ist möglich, Eins-zu-Eins-, Eins-zu-Viele- und Viele-zu-Viele-Beziehungen zu definieren.

Sandini Bib 13.3

Hierarchische Zeilenmengen

359

13.3.1 Beispiel In den folgenden Abschnitten werden wir mit einem typischen Beispiel arbeiten: ein vereinfachtes Tabellenschema mit Kunden, Aufträgen und Produkten. Die Kundentabelle (Customer) weist die beiden folgenden Spalten auf: Name enthält den Namen des Kunden, Id den eindeutigen Bezeichner für den Kunden. (In der Praxis wären in einer solchen Tabelle noch Spalten für zusätzliche Informationen wie Postadresse, E-MailAdresse und Telefonnummer enthalten.) Die Auftragstabelle (Order) weist folgende drei Spalten auf: Id identifiziert den Auftrag eindeutig, CustomerId identifiziert den Kunden, der den Auftrag erteilt hat und Date gibt das Erteilungsdatum für den Auftrag an. Zu jedem Auftrag ist eine Reihe von Artikeln vorhanden, die in der Tabelle OrderItems zusammengefasst sind. Wie erwartet gibt hier die Spalte OrderId den Auftrag an, zu dem die Artikel gehören, während die Spalte ProductId das Produkt enthält, zu dem die einzelnen Artikel gehören. Die Produkttabelle (Product) schließlich weist die beiden folgenden Spalten auf: Id identifiziert das Produkt eindeutig und Description enthält eine kurze Produktbeschreibung. Abbildung 13.9 zeigt eine graphische Darstellung dieses Schemas im Diagramm-Editor von SQL Server. Im Folgenden werde ich erläutern, wie die Datenbank ausgehend von der Kundentabelle durchlaufen wird. Dabei konzentrieren wir uns auf die Aufträge der einzelnen Kunden, die mit den einzelnen Aufträgen bestellten Produkte und die jeweils bestellte Menge. Diese Punkte lassen sich in einer hierarchischen Struktur wie in Abbildung 13.10 gut darstellen. In den nächsten Abschnitten werde ich die Verfahren zur Navigation in dieser Struktur beschreiben.

Abbildung 13.9: Schematische Darstellung der Beispieldatenbank

Abbildung 13.10: Graphische Darstellung der Tabellenhierarchie

Sandini Bib 360

13

Indizes, Kapitelmarken und Sichten

13.3.2 Herkömmlicher Ansatz Der herkömmliche Ansatz kennt keine Kapitelmarken. Statt dessen wird für jede untergeordnete Zeile eine Zeilenmenge geöffnet, wie in Abbildung 13.11 gezeigt. In dieser Abbildung sind die Linien zwischen den Zeilenmengen gestrichelt dargestellt und stellen abstrakte Verknüpfungen dar. Bei diesem Ansatz muss der Programmierer wissen, welche Zeilenmenge welcher Zeile entspricht. Der folgende Code zeigt, wie die Kunden- und die Auftragstabelle durchlaufen werden: CCommand Customers; hr = Customers.Open (Session); if (FAILED(hr)) return hr; hr = Customers.MoveFirst(); while (hr == S_OK) { { CCommand Orders; Orders.m_CustomerId = Customers.m_Id; Orders.Open(Session); HRESULT hr2 = Orders.MoveFirst(); while (hr2 = S_OK) { // Eine Aktion mit dem Auftrag durchführen. hr2 = Orders.MoveNext(); } } hr = Customers.MoveNext(); }

Eltern-Zeilenmenge (Customers)

Kind-Zeilenmenge 1 (Aufträge für Albert)

Name

Id

OrderId

CustomerId

Albert

1

1

1

Bob

2

3

1

Kind-Zeilenmenge 2 (Aufträge für Bob) OrderId

CustomerId

2

2

Abbildung 13.11: Eltern- und Kind-Zeilenmenge ohne Kapitelmarken

Sandini Bib 13.3

Hierarchische Zeilenmengen

361

Bei diesem Code wird davon ausgegangen, dass der dem Auftrags-Zugriffsobjekt zugeordnete Befehlstext folgendermaßen lautet: DEFINE_COMMAND(CdboOrdersAccessor2, _T(" \ SELECT \ Id, \ CustomerId, \ date \ FROM dbo.Orders WHERE CustomerId = ?"))

Dieses Verfahren ist zwar nicht schlecht, doch es kann noch verbessert werden. Der hauptsächliche Nachteil liegt darin, dass letztendlich so viele Auftrags-Zeilenmengen geöffnet werden, wie Kunden vorhanden sind, was nicht nur schwierig zu verwalten ist, sondern auch zu Leistungseinbußen führen kann.

13.3.3 Ansatz mit Verwendung von Kapitelmarken Wenn Sie Kapitelmarken verwenden, muss nicht für jede übergeordnete Zeile eine eigene Zeilenmenge geöffnet werden, sondern die übergeordnete Zeilenmenge (ElternZeilenmenge) weist eine untergeordnete Zeilenmenge (Kind-Zeilenmenge) auf, die alle Zeilen der zugrunde liegenden Tabelle enthält. Jedoch gibt es in der Eltern-Zeilenmenge (Customers) eine Kapitelmarken-Spalte (Chapter), die für jede Zeile aus der Eltern-Zeilenmenge ein Kapitelmarken-Handle auf die entsprechende Teilmenge der Kind-Zeilenmenge enthält. Abbildung 13.12 zeigt dies für das Beispiel mit den Kunden und den Aufträgen. Beachten Sie, dass hier nur eine Zeilenmenge für die Aufträge (Orders) vorhanden ist, die während der Existenz der Eltern-Zeilenmenge geöffnet bleibt. Auf die gestrichelt dargestellten Komponenten hat der Programmierer keinen Zugriff. Sie sind nur zur Veranschaulichung der Struktur mit aufgeführt.

13.3.4 Der Data Shape-Dienst Zahlreiche Provider bieten keine native Unterstützung für hierarchische Zeilenmengen. Dieser Umstand ist jedoch kaum von Bedeutung, weil OLE DB einen Dienst zur Verfügung stellt, der es Ihnen erleichtert, diese Art von Zeilenmengen zu erstellen: der Data Shape-Dienst. Wie in Kapitel 2 bereits erläutert, ist der Data Shape-Dienst selbst kein Daten-Provider, sondern er wandelt Abfragen um und nutzt einen Daten-Provider zum Speichern und Verwalten der tatsächlichen Daten. Der Data Shape-Dienst übersetzt seine eigene Data Shape-Sprache in eine Reihe von SQL-Abfragen und sendet diese an den Daten-Provider. Anschließend verwendet er die vom Daten-Provider zurückgelieferte Zeilenmenge zum Erstellen einer hierarchischen Zeilenmenge. Abbildung 13.13 veranschaulicht diese Architektur.

Sandini Bib 362

13

Eltern-Zeilenmenge (Customers)

KindKapitelmarke 1

Indizes, Kapitelmarken und Sichten

Kind-Zeilenmenge (Orders)

Id

Chapter

1

OrderId

CustomerId

Albert

1

Chapter 1

3

1

1

Bob

2

Chapter 2

2

2

3

1

Name

2 KindKapitelmarke 2

Abbildung 13.12: Eltern- und Kind-Zeilenmenge mit Kapitelmarken

Data ShapeSprache

Hierarchische Zeilenmenge

Data Shape-Dienst

Zeilenmenge

SQL

Daten-Provider Abbildung 13.13: Architektur mit Data Shape-Dienst

Eine Beschreibung der Data Shape-Sprache würde den Rahmen dieses Buches sprengen. Sie können hierzu jedoch die Microsoft-Dokumentation konsultieren, in der diese Sprache ausführlich beschrieben ist. Das Öffnen einer Datenquelle mit dem Data Shape-Dienst und mit einem normalen Provider unterscheidet sich kaum. Wenn Sie die Datenquelle mit einer CLSID und einem Eigenschaften-Set öffnen, verwenden Sie den Data Shape-Provider CLSID_ DataShapeProvider, der in MSDShape.h definiert ist. Eine wichtige Eigenschaft ist DBPROP_ MSDS_DBINIT_DATAPROVIDER. Diese ist Bestandteil des Eigenschaften-Sets DBPROPSET_ MSDSDBINIT, das die ProgID des zugrunde liegenden Providers darstellt. Beispiel: Sie schreiben den folgenden Code zum Öffnen einer Datenquelle für den zugrunde liegenden Provider: CdataSourcedb; CDBPropSet dbinit; dbinit.SetGUID(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("server name")); dbinit.AddProperty(DBPROP_AUTH_INTEGRATED, OLESTR("SSPI")); dbinit.AddProperty(DBPROP_INIT_CATALOG, OLESTR("database name")); hr = db.Open(_T("SQLOLEDB"), &dbinit);

Sandini Bib 13.3

Hierarchische Zeilenmengen

363

Der entsprechende Code zum Abrufen des Data Shape-Dienstes für denselben zugrunde liegenden Provider lautet folgendermaßen: CDataSource db; CDBPropSet dbinit[2]; dbinit[0].SetGUID(DBPROPSET_DBINIT); dbinit[0].AddProperty(DBPROP_INIT_DATASOURCE, OLESTR("server name")); dbinit[0].AddProperty(DBPROP_AUTH_INTEGRATED, OLESTR("SSPI")); dbinit[0].AddProperty(DBPROP_INIT_CATALOG, OLESTR("database name")); dbinit[1].SetGUID(DBPROPSET_MSDSDBINIT); dbinit[1].AddProperty(DBPROP_MSDS_DBINIT_DATAPROVIDER, OLESTR("SQLOLEDB")); hr = db.Open(CLSID_DataShapeProvider, dbinit, 2);

13.3.5 Cursorbewegung in hierarchischen Zeilenmengen Beim Bewegen des Cursors in hierarchischen Zeilenmengen haben Sie es mit einer übergeordneten Zeilenmenge (Eltern-Zeilenmenge) und einer untergeordneten Zeilenmenge (Kind-Zeilenmenge) zu tun. Die übergeordnete Zeilenmenge kann jedem beliebigen Typ angehören, die untergeordnete Zeilenmenge kann nur aus der übergeordneten Zeilenmenge heraus geöffnet werden. Die Klasse CChildRowset implementiert die Unterstützung für Kind-Zeilenmengen. Sie ist folgendermaßen deklariert: template class CChildRowset: public CAccessorRowset

CChildRowset ist mit CTable vergleichbar. Ein wichtiger Unterschied ist der TemplateParameter Ordinal, der in der Zeilenmenge mit der Kapitelmarken-Spalte die Spaltennummer angibt, die der Kind-Zeilenmenge entspricht. Sie können diesen Wert entweder bei null belassen oder ihn dynamisch angeben.

Die Kind-Zeilenmenge wird mit der Methode Open geöffnet. Diese akzeptiert zwei Parameter, nämlich den Zeiger auf die Eltern-Schnittstelle IRowset und die Spaltennummer (Parameter Ordinal). Wenn Sie die Spaltennummer nicht angeben, wird die Voreinstellung, d.h. die Spaltennummer, aus dem Template-Parameter übernommen. Open ist folgendermaßen definiert: HRESULT Open(IRowset* Parent, ULONG iOrdinal = Ordinal) { ATLASSERT(Parent != NULL); CComPtr RowsetInfo; HRESULT hr = Parent->QueryInterface(&RowsetInfo); if (FAILED(hr)) return hr; hr = RowsetInfo->GetReferencedRowset(iOrdinal,GetIID(), reinterpret_cast (GetInterfacePtr())); if (FAILED(hr))

Sandini Bib 364

13

Indizes, Kapitelmarken und Sichten

return hr; SetupOptionalRowsetInterfaces(); if (_OutputColumnsClass::HasOutputColumns()) hr = Bind(); return hr; }

Abbildung 13.14 veranschaulicht, wie Open den Schnittstellenzeiger auf die Kind-Zeilenmenge abruft. Das folgende Beispiel zeigt zwei gleichwertige Alternativen zum Öffnen einer Kind-Zeilenmenge: CTable ParentRowset; CChildRowset ChildRowset1; ChildRowset1.Open(ParentRowset.m_spRowset); // oder CChildRowset ChildRowset2; ChildRowset2.Open(ParentRowset.m_spRowset, 3);

Es gibt zwei Ansätze zum Bewegen des Cursors in hierarchischen Zeilenmengen. Beim ersten werden zwei separate Exemplare für die Eltern- und die Kind-Zeilenmenge erzeugt. Beim zweiten wird die Kind-Zeilenmenge als Teil der Eltern-Zeilenmenge definiert.

Separate Exemplare für Eltern- und Kind-Zeilenmenge Wenn ein Exemplar für die Eltern-Zeilenmenge und eines für die Kind-Zeilenmenge vorhanden ist, besteht die Schwierigkeit darin, dass die Eltern-Zeilenmenge das Kapitelmarken-Handle abruft, obwohl die Kind-Zeilenmenge dieses Handle benötigt. Dies führt dazu, dass Sie das Kapitelmarken-Handle bei jeder Cursorbewegung von der Eltern- in die Kind-Zeilenmenge kopieren müssen. In Abbildung 13.15 ist die zugehörige Objektanordnung dargestellt. IRowset

ElternZeilenmenge

OLE DBZeilenmenge IRowsetInfo

KindZeilenmenge

OLE DBZeilenmenge

GetReferencedRowset

Abbildung 13.14: Kind-Zeilenmenge abrufen

Sandini Bib 13.3

Hierarchische Zeilenmengen

365

Eltern-Zeilenmenge Zeilenmengen-Zeiger Kind-Kapitelmarke

Kind-Zeilenmenge Zeilenmengen-Zeiger Kind-Kapitelmarke Abbildung 13.15: Architektur mit separater Eltern- und Kind-Zeilenmenge

Bei diesem Ansatz sind die Eltern- und die Kind-Zeilenmenge voneinander unabhängig. Die Eltern-Zeilenmenge bindet lediglich die Kapitelmarke, für deren Aktualisierung die Kind-Zeilenmenge zuständig ist. Der Vorteil dieses Ansatzes liegt in der guten Lesbarkeit des Codes. Beispielsweise kann das Zugriffsobjekt für die Klasse der Auftragsobjekte (Orders) folgendermaßen definiert sein: class CdboOrdersAccessor { public: LONG m_Id; LONG m_CustomerId; DBTIMESTAMP m_date; HCHAPTER m_OrderItemChapter; BEGIN_COLUMN_MAP(CdboOrdersAccessor) COLUMN_ENTRY(1, m_Id) COLUMN_ENTRY(2, m_CustomerId) COLUMN_ENTRY(3, m_date) COLUMN_ENTRY_TYPE(4, DBTYPE_HCHAPTER, m_OrderItemChapter) END_COLUMN_MAP()

Da HCHAPTER den Typ ULONG aufweist, ist es erforderlich den OLE DB-Typ DBTYPE_HCHAPTER explizit anzugeben. Für ein bestimmtes Exemplar eines Auftragsobjekts (Order) werden mit dem folgenden Codebeispiel die zugehörigen Artikel durchlaufen: CChildRowset OrderItems; OrderItems.Open (Customers.m_Orders.GetInterface(), 4); OrderItems.SetChapter(Customers.m_Orders.m_OrderItemChapter);

Sandini Bib 366

13

Indizes, Kapitelmarken und Sichten

HRESULT hr3 = OrderItems.MoveFirst(); while (hr3 == S_OK) { // Eine Aktion mit OrderItems durchführen hr3 = OrderItems.MoveNext(); }

Die Kind-Zeilenmenge als Teil der Eltern-Zeilenmenge Wenn die Kind-Zeilenmenge als Klassenelement der Eltern-Zeilenmenge vorliegt, kann die Eltern-Zeilenmenge die Kapitelmarke der Kind-Zeilenmenge in ihre eigene Bindung miteinbeziehen. Dies führt dazu, dass das Kapitelmarken-Handle nicht mehr bei jeder Cursorbewegung kopiert werden muss. Abbildung 13.16 zeigt die Objektanordnung bei diesem Verfahren. CChildRowset wird in Verbindung mit dem Makro CHILD_ROWSET_ENTRY verwendet, das

die Kapitelmarke der Kind-Zeilenmenge in der Elternklasse bindet. Wenn sich die Zeilenposition in der Eltern-Zeilenmenge ändert, wird die Kapitelmarke der Kind-Zeilenmenge automatisch aktualisiert. Die Verwendung von CHILD_ROWSET_ENTRY erfolgt hier einfach durch Übergabe der Variablen. Intern ruft CHILD_ROWSET_ENTRY die Nummer der Kapitelmarke aus der Variablen ab und bindet die zugehörige Kapitelmarke. Dieses Makro ist folgendermaßen definiert: #define CHILD_ROWSET_ENTRY(variable) \ COLUMN_ENTRY_TYPE(_GetChapterOrdinal(((_classtype*)0)-> ##variable##), DBTYPE_CHAPTER, variable##.m_hChapter)

Die Zugriffsobjekt-Klasse für die Kundentabelle (Customers) ist beispielsweise folgendermaßen definiert: class CdboCustomersAccessor { public: LONG m_Id; TCHAR m_Name[11]; CChildRowset m_Orders; BEGIN_COLUMN_MAP(CdboCustomersAccessor) COLUMN_ENTRY(1, m_Id) COLUMN_ENTRY(2, m_Name) CHILD_ROWSET_ENTRY(m_Orders) END_COLUMN_MAP()

Der folgende Code zeigt, wie die Kundentabelle (Customers) und die Auftragstabelle (Orders) durchlaufen werden können: CCommand Customers; hr = Customers.Open (Session, CommandText); if (FAILED(hr))

Sandini Bib 13.3

Hierarchische Zeilenmengen

367

return hr; hr = Customers.m_Orders.Open(Customers.GetInterface()); hr = Customers.MoveFirst(); while (hr == S_OK) { HRESULT hr2 = Customers.m_Orders.MoveFirst(); while (hr2 == S_OK) { // Eine Aktion mit dem Auftrag durchführen hr2 = Customers.m_Orders.MoveNext(); } hr = Customers.MoveNext(); } Customers.m_Orders.Close(); Customers.Close();

Eltern-Zeilenmenge Zeilenmengen-Zeiger Kind-Zeilenmenge Zeilenmengen-Zeiger Kind-Kapitelmarke

Abbildung 13.16: Architektur, bei der die Kind-Zeilenmenge Teil der Eltern-Zeilenmenge ist

Abschließend werden in diesem Beispiel die einzelnen Verfahren zum Durchlaufen der gesamten Hierarchie aus den Tabellen Customers, Orders, OrderItems und Product kombiniert: CCommand Customers; hr = Customers.Open (Session, CommandText); if (FAILED(hr)) return hr; hr = Customers.m_Orders.Open(Customers.GetInterface()); hr = Customers.MoveFirst(); while (hr == S_OK) { OutputDebugString(_T("------\ n")); OutputDebugString(Customers.m_Name); OutputDebugString(_T(" has ")); ULONG RowCount; Customers.m_Orders.GetRowCount(RowCount); OutputDebugString(I2T(RowCount)); OutputDebugString(_T(" orders:\ n")); HRESULT hr2 = Customers.m_Orders.MoveFirst();

Sandini Bib 368

13

Indizes, Kapitelmarken und Sichten

while (hr2 == S_OK) { CChildRowset OrderItems; OrderItems.Open (Customers.m_Orders.GetInterface(), 4); OrderItems.SetChapter( Customers.m_Orders.m_OrderItemChapter); HRESULT hr3 = OrderItems.MoveFirst(); OutputDebugString(_T("order ")); OutputDebugString(I2T(OrderItems.m_OrderId)); OutputDebugString(_T("\ n")); while (hr3 == S_OK) { OutputDebugString(I2T(OrderItems.m_Quantity)); CChildRowsetProducts; Products.Open (OrderItems.GetInterface(), 4); Products.SetChapter(OrderItems.m_ProductChapter); HRESULT hr4 = Products.MoveFirst(); OutputDebugString(_T(Products.m_Description)); OutputDebugString(_T("\ n")); hr3 = OrderItems.MoveNext(); } hr2 = Customers.m_Orders.MoveNext(); } hr = Customers.MoveNext(); } Customers.m_Orders.Close(); Customers.Close();

13.4 Sichten Sprachen wie SQL sind sehr leistungsfähig, da sie komplexe Operationen wie Verknüpfungen (Joins) zwischen mehreren Tabellen ermöglichen. SQL wird jedoch häufig auch für sehr viel einfachere Abfragen verwendet, bei denen z.B. nur eine Tabelle abgefragt und eine Kombination aus Filtern und Sortieren durchgeführt wird. Solche Abfragen ermitteln aus einer Tabelle eine bestimmte Zeilenmenge. Dabei stellt sich die Frage, wie diese Zeilenmenge für eine erneute Abfrage verwendet werden kann. Die Antwort: mit Sichten. Wie der Name bereits andeutet, sind Sichten OLE DB-Objekte, die so auf Zeilenmengen angewendet werden, dass diese anhand einer Kombination aus Filtern und Sortieren »unter einem besonderen Licht« betrachtet werden können. In dieser Hinsicht sind OLE DB-Sichten für Zeilenmengen das Gleiche, was Datenbanksichten für Datenbanktabellen sind. Abbildung 13.17 zeigt die gesamte Architektur: Die Datenbankabfrage filtert alle Elemente mit einem Wert kleiner oder gleich 3 und erzeugt die entsprechende Zeilenmenge, für die eine Sicht erzeugt wird. In diesem Beispiel filtert die Sicht alle Elemente heraus, die größer als 2 sind, und ordnet die Zeilenmenge in absteigender Wertefolge neu.

Sandini Bib 13.4

Sichten

369

Datenbankabfrage

OLE OB-Sicht

Tabelle

Zeilenmenge

Zeilenmenge

1

1

2

2

2

1

3

3

4 5 Abbildung 13.17: Tabellen, Zeilenmengen und Sichten

13.4.1 Wann sollten Sichten verwendet werden? Sichten sind besonders gut geeignet für Operationen mit bereits geöffneten Zeilenmengen, besonders dann, wenn Sie keinen Einfluss auf die Datenquelle der Zeilenmenge haben. Ferner können mit Sichten bestimmte Sortier- und Filteroperationen durchgeführt werden, die grundlegenden SQL-Befehlen entsprechen, wenn der jeweilige Provider keine Befehle in einer komplexen Sprache wie SQL unterstützt. Mit Hilfe von Sichten können Sie auch in vielen Fällen die Verwendung von SQL-Befehlen vermeiden. Die Client-Cursor-Dienstkomponente, die Bestandteil von OLE DB ist, unterstützt Sichten. Somit brauchen die Provider Sichten nicht als nativen Code implementieren. Wenn ein Consumer Unterstützung für Sichten benötigt, braucht er nur die Datenquelle mit dem Client-Cursor-Dienst zu öffnen und die richtige Schnittstelle anzufordern. In den folgenden Abschnitten wird die Verwendung von Sichten ausführlich behandelt. Grundsätzlich kann aber jetzt schon gesagt werden, dass Sichten für jeden beliebigen OLE DB-Tabellen-Provider verwendet werden können. Sichten haben allerdings auch den Nachteil, dass der Client zur Durchführung von Operationen mit Sichten die gesamte Zeilenmenge laden muss.

13.4.2 Sicht öffnen Theoretisch gibt es zwei Möglichkeiten um eine Sicht zu öffnen. Einerseits kann sie direkt für die Datenbanktabelle geöffnet werden. Andererseits kann sie für eine bereits offene Zeilenmenge geöffnet werden. In Abbildung 13.18 sind diese beiden Verfahren dargestellt. Dabei ist es immer möglich, das erste Verfahren durch Öffnen einer Zeilenmenge, die den gesamten Tabelleninhalt darstellt, zu simulieren und anschließend eine Sicht für diese Zeilenmenge zu öffnen. Die meisten OLE DB-Provider unterstützen jedoch das Öffnen einer Sicht direkt für eine Tabelle nicht. Folglich wird in diesem Kapitel vornehmlich das zweite Verfahren behandelt.

Sandini Bib 370

13

Indizes, Kapitelmarken und Sichten

13.4.3 Der Sichtentyp Wie für Indizes gibt es auch für OLE DB-Sichten einen eigenen Typ. Dieser Typ weist die folgenden vier neuen Schnittstellen auf: IViewSort definiert Sortieroperationen, IViewFilter unterstützt Filteroperationen, IViewChapter gewährt Zugriff auf die Kapitelmarke, welche die Sicht definiert, und IViewRowset. Letztere wird in der Regel nicht unterstützt und auch in den OLE DB-Erweiterungen nicht verwendet. In Abbildung 13.19 sind die einzelnen Schnittstellen des OLE DB-Sichtentyps im Überblick dargestellt.

Datenbanktabelle

Datenbanktabelle

Sicht

Zeilenmenge

Sicht

Abbildung 13.18: Zwei Verfahren zum Öffnen einer Sicht

IColumnsInfo IAccessor IViewSort

Sichtobjekt

IViewFilter IViewchapter IViewRowset ISupportErrorInfo

Abbildung 13.19: Die Schnittstellen für Sichten

Die Klasse CView kapselt den OLE DB-Sichtentyp. Um eine Sicht zu öffnen, übergeben Sie einfach ein Zeilenmengen-Objekt. Diese Zeilenmenge muss allerdings Sichten auch unterstützen. Im COM-Sprachgebrauch bedeutet dies, sie muss die Schnittstelle IRowsetView unterstützen. Das Abrufen einer Zeilenmenge, die Sichten unterstützt, ist nicht immer ganz unkompliziert. Dies hat zwei Gründe: Erstens unterstützt der OLE DB-Provider in vielen Fällen von sich aus keine Sichten. Daher müssen Sie beim Erzeugen der Datenquelle die Client-Cursor-Dienstkomponente laden. Hierzu können Sie beispielsweise folgenden Code programmieren:

Sandini Bib 13.4

Sichten

371

CDataSource db; CDBPropSet dbinit(DBPROPSET_DBINIT); dbinit.AddProperty(DBPROP_INIT_OLEDBSERVICES, (long)DBPROPVAL_OS_ENABLEALL); // weitere Eigenschaften hr = db.OpenWithServiceComponents(_T("SQLOLEDB.1"), &dbinit); if (FAILED(hr)) return hr; CSession Session; hr = Session.Open(db); if (FAILED(hr)) return hr;

Zweitens muss in den Zeilenmengen-Eigenschaften die Unterstützung für IRowsetView definiert sein. Die betreffende Eigenschaft heißt DBPROP_IRowsetView. Dies scheint ausreichend zu sein, aber manche Provider erfordern, dass Sie explizit über die Eigenschaft DBPROP_CLIENTCURSOR angeben, dass der Cursor Client-seitig ist. Hierfür können Sie beispielsweise folgenden Code programmieren: CDBPropSet PropSet(DBPROPSET_ROWSET); PropSet.AddProperty (DBPROP_IRowsetView, true); PropSet.AddProperty (DBPROP_CLIENTCURSOR, true); CCommand Orders; hr = Orders.Open(Session, NULL, &PropSet);

IRowsetView

Zeilenmenge

Erzeugt

IViewSort

(gibt Sortieroperationen an)

Sicht IViewFilter

Kapitelmarke anwenden

IViewChapter

(gibt Filteroperationen an)

GetViewChapter

Abbildung 13.20: Zusammenspiel der Schnittstellen für Sichten

Nachdem die Zeilenmenge erfolgreich geöffnet wurde, können Sie die Sicht für die Zeilenmenge öffnen: CView View; hr = View.Open(Orders);

Zu diesem Zeitpunkt ist die Sicht mustergültig, d.h. sie enthält keine Transformationen. Sie können jetzt jedoch Sortier- und Filteroperationen angeben, die in der Sicht ausgeführt werden sollen. In Abbildung 13.20 sind die einzelnen Schnittstellen mit den entsprechenden Operationen im Überblick dargestellt.

Sandini Bib 372

13

Indizes, Kapitelmarken und Sichten

13.4.4 Sicht sortieren Die Methode SetSortOrder stellt eine direkte Kapselung von IViewSort::SetSortOrder dar. Sie gibt die Sortierreihenfolge anhand einer Reihe von Spalten und deren Sortierreihenfolge an: HRESULT SetSortOrder(LONG cValues, ULONG rgColumns[ ], DBSORT rgOrders[ ])

wobei 왘 cValues die Anzahl der zu sortierenden Spalten angibt. 왘 rgColumns ein Array darstellt, das die Positionsnummern der zu sortierenden Spalten angibt, deren Anzahl durch cValues angegeben ist. 왘 rgOrders ein Array darstellt, das die Sortierreihenfolgen der zu sortierenden Spalten enthält. Für die Sortierreihenfolge kann einer der Werte DBSORT_DESCENDING (Sortieren in absteigender Folge) oder DBSORT_ASCENDING (Sortieren in aufsteigender Reihenfolge) gesetzt sein. Das folgende Beispiel zeigt den Code für eine Sortierung, bei der die erste Spalte in absteigender und die zweite Spalte in aufsteigender Reihenfolge sortiert wird: ULONG rgColumns [] = { 1, 2} ; DBSORT rgOrders [] = { DBSORT_DESCENDING, DBSORT_ASCENDING} ; hr = View.SetSortOrder(2, rgColumns, rgOrders);

Häufig wird jedoch nur anhand einer Spalte sortiert. Für diesen Fall gibt es eine weitere Version von SetSortOrder: HRESULT SetSortOrder(ULONG Column, DBSORT Order)

Das folgende Beispiel zeigt, wie anhand der ersten Spalte sortiert wird: hr = View.SetSortOrder(1, DBSORT_DESCENDING);

Es ist auch möglich, die Sortierreihenfolge über die Methode GetSortOrder abzurufen.

13.4.5 Sicht filtern Das Sortieren einer Sicht ist relativ einfach, das Filtern hingegen ist komplexer, weil viele Kombinationen mit OR und AND möglich sind. Beispielsweise können Sie die Werte filtern, die das folgende Kriterium erfüllen: Column1 > 3 and column2 < 4 or column1 < 2

CView weist zwei SetFilter-Methoden auf. Die erste Methode ist eine direkte Kapselung von IViewFilter::SetFilter:

Sandini Bib 13.4

Sichten

373

HRESULT SetFilter(HACCESSOR hAccessor, ULONG cRows, DBCOMPAREOP CompareOps[ ], void *pCriteriaData)

Da diese Methode einigermaßen komplex ist und die OLE DB-Erweiterungen hier keine Erleichterungen zur Verfügung stellen, ist es am besten, die Dokumentation von Microsoft zu Rate zu ziehen, wenn Sie weitere Einzelheiten zu dieser Methode wissen möchten. In vielen Fällen brauchen Sie jedoch nur anhand einer einzigen Spalte zu filtern. An dieser Stelle kommt die zweite SetFilter-Methode ins Spiel: template HRESULT SetFilter(VALUE v, ULONG iOrdinal, DBCOMPAREOP CompareOp = DBCOMPAREOPS_EQ)

wobei 왘 v den Wert der Filterbedingung darstellt. 왘 iOrdinal die Nummer der Spalte darstellt, für welche die Bedingung zutrifft. 왘 CompareOp der Vergleichsoperator ist. Wenn Sie beispielsweise alle Werte filtern möchten, die der Bedingung Column2 = 1 entsprechen, programmieren Sie Folgendes: CView View; // die Sicht öffnen hr = View.SetFilter(1, 2);

13.4.6 Kapitelmarken für Sichten Wenn eine Sicht eingerichtet ist, besteht der nächste Schritt darin, das Ergebnis abzurufen. Es ist wichtig, sich klar zu machen, dass das Sichtobjekt keine Zeilenmenge ist und keine Daten enthält. Es arbeitet vielmehr mit einer Zeilenmenge zusammen um ein Ergebnis zu liefern. Sie haben sich sicher schon gedacht, dass das Sichtobjekt eine Kapitelmarke enthält, welche die binäre Entsprechung für die Sortier- und Filteroperation an der Sicht darstellt. Es bietet sich an, die Kapitelmarke auf die Zeilenmenge anzuwenden, anhand derer die Sicht erzeugt wurde. Abbildung 13.21 zeigt die Beziehung zwischen Zeilenmenge, Sicht und Kapitelmarke. Beachten Sie, dass letztendlich sowohl die Sicht als auch die Zeilenmenge eine Referenz auf die Kapitelmarke aufweisen. Das folgende Beispiel zeigt, wie dies im Code umgesetzt wird: CCommand Orders; hr = Orders.Open(Session, NULL, &PropSet); CView View; hr = View.Open(Orders);

Sandini Bib 374

13

Indizes, Kapitelmarken und Sichten

//Filter und Sortierreihenfolge auf die Sicht anwenden hr = View.SetViewChapter(); Orders.SetChapter(View.GetHChapter());

Die Methode SetViewChapter der Klasse CView ruft die Kapitelmarke ab und setzt das Cview-Klassenelement m_HChapter. Durch den Aufruf von SetChapter für die Zeilenmenge setzen Sie die Kapitelmarke der Zeilenmenge auf die Kapitelmarke der Sicht.

13.4.7 Sichten stapeln In den vorherigen Abschnitten haben Sie erfahren, wie Sichten auf Zeilenmengen angewendet werden können. Es ist auch möglich, für Sichten weitere Sichten zu erzeugen. Beispielsweise kann die erste Sicht eine Filteroperation und die zweite Sicht eine Sortieroperation am Ergebnis der Filteroperation aus der ersten Sicht durchführen. Dies kann zwar auch über eine einzige Sicht erreicht werden, aber es ist vorteilhaft, dafür mehrere Sichten zu verwenden und diese zu stapeln. Einerseits haben Sie dadurch mehr Flexibilität beim Programmieren, andererseits sind auf diese Weise Szenarien möglich, bei denen Sichten gestapelt werden, ohne dass den Sichten jeweils die anderen Sichten bekannt sind. (1) Erzeugt (2) Sortierung und

Zeilenmenge

Sicht

m_hChapter

m_hChapter

Filterung angeben

(3) Kapitelmarke

abrufen (3) Kapitelmarke

anwenden Abbildung 13.21: Beziehungen zwischen Zeilenmenge, Sicht und Kapitelmarke

Dies wird durch einen einfachen Trick erreicht: Die Methode SetViewChapter akzeptiert eine Kapitelmarke als Argument: HRESULT SetViewChapter(HCHAPTER hSource = DB_NULL_HCHAPTER)

wobei hSource für die Kapitelmarke steht, anhand derer die Stapelung erfolgen soll. Auf diese Weise können Sie Sichten aufeinander stapeln, indem Sie einfach die Kapitelmarke der ersten Sicht an die zweite Sicht übergeben. Der folgende Code zeigt, wie die erste Sicht (View1) erzeugt und eine zweite Sicht (View2) darauf gestapelt wird:

Sandini Bib 13.5

Zusammenfassung

375

CCommand Orders; hr = Orders.Open(Session, NULL, &PropSet); CView View1; hr = View1.Open(Orders); //Filter und Sortierreihenfolge auf die Sicht anwenden hr = View1.SetViewChapter(); CView View2; hr = View2.Open(Orders); //Filter und Sortierreihenfolge auf die Sicht anwenden hr = View2.SetViewChapter(View1.GetHChapter()); Orders.SetChapter(View2.GetHChapter());

Am Ende dieses Beispiels enthält die Zeilenmenge eine Kapitelmarke, welche die Operationen von View1 und View2 in sich vereint. Beachten Sie, dass solche Stapeloperationen nicht auf Kapitelmarken beschränkt sind, die von Sichten erzeugt wurden. Sie können auf jede Kapitelmarke angewendet werden, die zur richtigen Zeilenmenge gehört.

13.5 Zusammenfassung Indizes bieten auf niedriger Ebene erweiterte Zugriffsmöglichkeiten auf den Datenspeicher. Sie sollten Indizes nur dann verwenden, wenn Befehle nicht zur Verfügung stehen oder für die betreffende Aufgabe nicht geeignet sind. Kapitelmarken stellen ein leistungsfähiges Konzept für zwei Anwendungsbereiche dar, nämlich für hierarchische Zeilenmengen und für Sichten. Durch die Verwendung von Kapitelmarken stehen Ihnen ganz neue Möglichkeiten für die Vorgehensweise beim Zugriff auf relationale Daten sowie bei der Zwischenspeicherung von und der Cursorbewegung in relationalen Daten zur Verfügung.

Sandini Bib

Sandini Bib

14 Schemata, Definitionen und Synchronisation In den vorherigen Kapiteln, in denen das Navigieren durch Daten, das Abrufen und Setzen von Daten erklärt wurde, bin ich von der impliziten Annahme ausgegangen, dass dem Consumer die Struktur des zugrunde liegenden Datenspeichers bereits bekannt war. So musste der Consumer wissen, welche Tabelle sich im Datenspeicher befindet und meistens darüber hinaus noch die Spalten der Tabelle kennen. Diese Annahme ist in vielen Fällen gültig, aber es kann auch vorkommen, dass der Consumer die Metadaten (das Schema) eines Datenspeichers kennen und bearbeiten muss. Dieses Kapitel besteht aus drei Teilen: Im ersten Teil erläutere ich, wie Sie das Schema eines Datenspeichers abfragen und durch die zugehörigen Daten navigieren, im zweiten Teil zeige ich, wie Sie das Schema ändern können, und im dritten Teil schlage ich Verfahren vor, mit denen Sie den Code des Consumers und das Schema synchronisieren können.

14.1 Das Schema abfragen und durch dessen Daten navigieren In den OLE DB Consumer Templates sind zwei Include-Dateien enthalten: atldbcli.h enthält den gesamten Code der OLE DB Consumer Templates, mit dem wir arbeiten, atldbsch.h enthält alle Schema-Klassen. Während atldbsch.h von atldbcli.h abhängt, ist es möglich, atldbcli.h allein zu verwenden. Da atldbsch.h die Schema-Klassen enthält, müssen Sie diese Datei explizit einbinden, wenn Sie die Schema-Klassen benötigen. Die Schema-Daten entsprechen in vieler Hinsicht anderen Informationselementen. So werden Schema-Daten in vielen Datenspeichern in Form von Tabellen gespeichert. Bei SQL Server ist beispielsweise eine Tabelle vorhanden, die eine Liste aller im Datenspeicher vorhandenen Tabellen enthält, und eine weitere Tabelle, die eine Liste aller Spalten enthält. Viele Datenspeicher ermöglichen den Zugriff auf diese Tabellen und Sie können auf diese Weise Schema-Daten abrufen. Es gibt allerdings kein Standardverfahren zur Abfrage dieser Tabellen und der für einen Datenspeicher geschriebene

Sandini Bib 378

14

Schemata, Definitionen und Synchronisation

Code funktioniert nicht notwendigerweise auch bei einem anderen Datenspeicher. OLE DB stellt jedoch einen Standard für den Zugriff auf Schema-Daten zur Verfügung. Dieser bietet zwar nicht die Flexibilität der Systemtabellen, stellt aber die wichtigsten Funktionen zur Verfügung.

14.1.1 Restriktionen In den meisten Fällen müssen die Consumer sehr einfache Schema-Abfragen durchführen. So wird beispielsweise eine Liste der Spalten für eine bestimmte Tabelle benötigt. Die entsprechende Abfrage würde folgendermaßen aussehen: SELECT * FROM columns where table_name = a_name

In diesem Fall ist a_name eine Restriktion (Restriction), weil diese Angabe die Gesamtmenge der Spalten auf diejenigen beschränkt, die der Benutzer benötigt. In der Regel sind in solchen Fällen keine komplexen Abfragen, beispielsweise mit den Operatoren AND oder OR, erforderlich. Die in OLE DB definierten Schema-Zeilenmengen unterstützen nur solche einfachen Abfragen. Aufgrund der einfachen Struktur der Abfragen wird für diese keine Befehlssprache benötigt, sondern es werden OLE DB API-Funktionen wie die Folgende verwendet: //(Pseudocode) Rowset = GetRowset(ColumnsGUID, "a_name");

Der in OLE DB definierte Mechanismus zur Abfrage von Schema-Daten geht etwas weiter, denn er lässt diverse Restriktionen unterschiedlicher Art zu: CComVariant * Restrictions = { Restriction1, Restriction2} ; Rowset = GetRowset(DBSCHEMA_COLUMNS, Restrictions, RestrictionCount);

In diesem Beispiel mit Pseudocode enthält Restrictions ein Array aus Varianten, von denen jede den Wert für eine Restriktion enthält. Abbildung 14.1 zeigt die Restriktionen und die dadurch abgerufenen Schema-Zeilenmengen. Dieses Vorgehen entspricht einer SQL-Abfrage wie der Folgenden: SELECT Column1, Column2, Column3 from xx where Column1 = Restriction1 AND Column2 = Restriction2 AND Column3 = Restriction3

Wenn Sie für eine Restriktion den Wert NULL übergeben, wird diese nicht berücksichtigt und alle übrigen Werte werden übergeben. Im Allgemeinen stellen Restriktionen die ersten Spalten der entsprechenden Schema-Zeilenmengen dar.

CRestrictions Bei der Klasse CRestrictions handelt es sich um eine generische Klasse, die den für alle Schema-Zeilenmengen-Klassen gültigen Code implementiert:

Sandini Bib 14.1

Das Schema abfragen und durch dessen Daten navigieren

Spalte 1 Restriktion 1

Spalte 2

379

Spalte 3

GetRowset

Restriktion 2 Restriktion 3

Schema-Zeilenmenge Abbildung 14.1: Restriktionen und Schema-Zeilenmengen template class CRestrictions : public CSchemaRowset

wobei 왘 T die zu verwendende Zugriffsobjekt-Klasse darstellt. 왘 nRestriction die maximale Anzahl von Restriktionen angibt. 왘 pguid eine GUID ist, die zur Identifikation der Schema-Zeilenmenge dient. In OLE DB sind eine Reihe von Standard-GUIDs definiert, es ist aber möglich, dass die Provider eigene GUIDs definieren. (Dieses Kapitel befasst sich ausschließlich mit den über OLE DB definierten Schema-Zeilenmengen.) Die Methode Open der Klasse CRestrictions ist wie folgt definiert: HRESULT Open(const CSession& session, LPCTSTR lpszParam2 = NULL, LPCTSTR LPCTSTR lpszParam4 = NULL, LPCTSTR LPCTSTR lpszParam6 = NULL, LPCTSTR

LPCTSTR lpszParam1 = NULL, lpszParam3 = NULL, lpszParam5 = NULL, lpszParam7 = NULL)

wobei lpszParami die i-te Restriktion ist. In fast allen Fällen gehören Restriktionen dem Typ string an und CRestrictions deckt auch nur diesen Fall ab. Für CRestrictions sind bis zu sieben Restriktionen definierbar, aber es wird nur die Anzahl von Restriktionen berücksichtigt, die über nRestriction angegeben wurde. Alle übrigen Restriktionen werden ignoriert. Alle Schema-Zeilenmengen-Klassen erben von CRestrictions. Diese Klassen werde ich in den folgenden Abschnitten noch eingehend darstellen. Abbildung 14.2 zeigt die Vererbungs-Hierarchie für CRestrictions. Jede der Klassen hat eine eigene Funktion: 왘 _CStoreRestrictions verwaltet eine Referenz auf das Array von n Restriktionen und kann eine Schema-Zeilenmenge abrufen. Wie der Unterstrich am Namensanfang andeutet, handelt es sich hier um eine Implementierungsklasse, die der Consumer nicht direkt verwendet. 왘 CSchemaRowset bindet die Merkmale von CAccessorRowset und _CStoreRestrictions. Insbesondere ruft diese Klasse die Methode Bind auf, nachdem die Zeilenmenge von _CStoreRestrictions abgerufen wurde.

Sandini Bib 380

14

T

Schemata, Definitionen und Synchronisation

CRowset

CAccessorRowset

_CStoreRestrictions

CSchemaRowset

CRestrictions

Abbildung 14.2: Vererbung von CRestrictions

왘 CRestrictions fügt eine Open()-Methode zu CSchemaRowset hinzu. Diese Klassenhierarchie könnte auch weniger aufwändig gestaltet werden, wenn man die relativ einfache Struktur der Schema-Zeilenmengen betrachtet. Insbesondere wäre es möglich gewesen, CSchemaRowset mit CRestrictions zusammenzuführen.

14.2 Das Katalog/Schema-Modell OLE DB geht davon aus, dass der zugrunde liegende Datenspeicher dem von ANSISQL definierten Katalog/Schema-Modell entspricht. Unter einem Schema versteht man eine Sammlung von Datenbankobjekten, die ein bestimmter Benutzer besitzt. Man könnte auch sagen, dass ein Schema einem Eigentümer entspricht. Bei SQL Server ist dbo ein typischer Name für ein Schema. Ein Katalog stellt eine Sammlung miteinander in Beziehung stehender Schemata dar. Einfach gesagt ist »Katalog« ein anderer Name für »Datenbank«. Dies lässt sich beispielsweise daran erkennen, dass der Bezeichner der Datenquellen-Initialisierungseigenschaft für eine Datenbank DBPROP_INIT_CATALOG heißt. Bei Providern wie Microsoft Access, die weder Schemata noch Kataloge unterstützen, sind die Werte für diese beiden Spalten NULL. Restriktionen, die für Kataloge und Schemata definiert wurden, werden bei Abfragen nicht berücksichtigt.

14.2.1 Kataloge Der erste Schritt besteht in der Regel darin, die Kataloge aufzuzählen. Hierzu dient die Klasse CCatalogs, die zwei Klassenelemente aufweist:

Sandini Bib 14.2

Das Katalog/Schema-Modell

381

왘 m_szName gibt den Namen des Katalogs an. 왘 m_szDescription enthält eine Beschreibung des Katalogs. Das folgende Beispiel zeigt, wie Sie durch eine Liste von Katalogen navigieren können: CCatalogs Catalogs; hr = Catalogs.Open (Session); hr = Catalogs.MoveFirst(); while (hr == S_OK) { hr = Catalogs.MoveNext(); }

CCatalogs weist nur eine Spalte zur Angabe von Restriktionen auf: Diese enthält den

Namen des Katalogs. Zum Abrufen der Beschreibung eines bestimmten Katalogs können Sie den folgenden Code schreiben: CCatalogs Catalogs; hr = Catalogs.Open (Session, CatalogName); hr = Catalogs.MoveFirst(); // Die Beschreibung ist in Catalogs.m_szDescription enthalten while (hr == S_OK) { hr = Catalogs.MoveNext(); }

14.2.2 Tabellen Die Klasse CTables dient zum Auflisten der Tabellen, die auf einem Daten-Server vorhanden sind. Sie weist die folgenden Klassenelemente auf: 왘 m_szCatalog gibt den Namen des Katalogs an, zu dem die Tabelle gehört. 왘 m_szSchema enthält den Namen des Schemas, das für die Tabelle gilt. 왘 m_szName gibt den Namen der Tabelle an. 왘 m_szType ist eine Textdarstellung des Tabellentyps, wie z.B. Tabelle vom Typ "TABLE" oder "SYSTEM". Diese vier Klassenelemente stellen gleichzeitig die vier Restriktionen für die SchemaZeilenmenge von Tabellen dar. Außerdem sind für CTables die folgenden Klassenelemente definiert: 왘 m_guidTable gibt die GUID der Tabelle an. 왘 m_szDescription enthält die Beschreibung der Tabelle.

Sandini Bib 382

14

Schemata, Definitionen und Synchronisation

Zum Abrufen aller Tabellen eines bestimmten Katalogs müssten Sie folgenden Code schreiben: CTables Tables; hr = Tables.Open (Session, CatalogName); hr = Tables.MoveFirst(); // der Rest wie gehabt

Zum Abrufen des Typs und der Beschreibung einer Tabelle würden Sie Folgendes schreiben: CTables Tables; hr = Tables.Open (Session, CatalogName, SchemaName, TableName); hr = Tables.MoveFirst(); // der Rest wie gehabt

Zum Abrufen aller Systemtabellen eines Servers programmieren Sie Folgendes: CTables Tables; hr = Tables.Open (Session, NULL, NULL, NULL, "SYSTEM TABLE"); hr = Tables.MoveFirst(); // der Rest wie gehabt

14.2.3 Spalten Über die Klasse CColumns wird die Schema-Zeilenmenge für Spalten implementiert. Sie ist analog zum Katalog/Schema-Modell aufgebaut und beginnt daher mit den Klassenelementen für den Namen des Katalogs und den Schema-Namen sowie mit den Restriktionen. Als Klassenelemente und Restriktionen sind der Tabellen- und der Spaltenname definiert. Zum Abrufen aller Spalten einer bestimmten Tabelle schreiben Sie beispielsweise den folgenden Code: CColumns Columns; hr = Columns.Open (Session, CatalogName, SchemaName, TableName); hr = Columns.MoveFirst(); while (hr == S_OK) { hr = Columns.MoveNext (); }

Wenn alle Attribute einer bestimmten Spalte abgerufen werden sollen, können Sie Folgendes programmieren: CColumns Columns; hr = Columns.Open (Session, CatalogName, SchemaName, TableName, ColumnName); hr = Columns.MoveFirst();

CColumns weist noch weitere Klassenelemente auf:

왘 m_guidColumn und m_nColumnPropID, d.h. die GUID der Spalte sowie die Spalteneigenschaften-ID, sofern vorhanden. Da die meisten Provider die Spalten anhand des Namens identifizieren, sind diese beiden Klassenelemente oft leer.

Sandini Bib 14.2

Das Katalog/Schema-Modell

383

왘 m_nOrdinalPosition gibt die Positionsnummer der Spalte in der Tabelle an (1 für die erste Spalte, 2 für die zweite Spalte usw.). 왘 m_nDataType dient zur Angabe des OLE DB-Typs der Spalte (beispielsweise DBTYPE_I4).

14.2.4 Sichten In den Schema-Daten zu Sichten sind die Sichten des Datenspeichers definiert. Bei diesen Sichten handelt es sich nicht um OLE DB-Sichten, denn sie befinden sich auf dem Server und nicht auf dem Client. In Microsoft Access werden Sichten als »Abfragen« bezeichnet, in anderen Datenbanken, wie beispielsweise SQL Server, heißen sie »Sichten«. Auch Sichten sind gemäß dem Katalog/Schema-Modell strukturiert. Daher stellen die beiden ersten Klassenelemente bzw. Restriktionen den Katalog und das Schema dar. Das nächste Klassenelement bzw. die nächste Restriktion ist der Name der Sicht (m_szTableName), m_szDefinition ist eine Textdefinition der Sicht. Bei einem SQL-Datenspeicher handelt es sich hierbei einfach um die SQL-Anweisung, mit der die aktuelle Sicht erzeugt wurde. Das folgende Beispiel zeigt, wie alle Sichten eines Katalogs/Schemas abgerufen werden: CViews Views; hr = Views.Open(Session, CatalogName, SchemaName); hr = Views.MoveFirst(); while (hr == S_OK) { hr = Views.MoveNext(); }

Der folgende Code dient zum Abrufen der Definition einer Sicht: CViews Views; hr = Views.Open(Session, CatalogName, SchemaName, ViewName); hr = Views.MoveFirst(); // Die Definition ist in Views.m_szDefinition enthalten.

14.2.5 Prozeduren Die Schema-Daten zu Prozeduren definieren Stored Procedures (gespeicherte Prozeduren), deren Spalten und Parameter. Prozeduren verhalten sich ganz ähnlich wie Sichten. Sie weisen nur ein weiteres Klassenelement auf: m_nType gibt den Typ der Prozedur an und enthält einen der in Tabelle 14.1 angegebenen Werte. Mit dem folgenden Code werden die Stored Procedures in einem Katalog aufgezählt:

Sandini Bib 384

14

Schemata, Definitionen und Synchronisation

CProcedures Procedures; hr = Procedures.Open(Session, CatalogName); hr = Procedures.MoveFirst(); while (hr == S_OK) { hr = Procedures.MoveNext(); }

Symbolischer Wert

Beschreibung

DB_PT_UNKNOWN

Unbekannter Typ.

DB_PT_PROCEDURE

Es handelt sich um eine Prozedur, die keinen Wert zurückliefert.

DB_PT_FUNCTION

Es handelt sich um eine Funktion, die einen Wert zurückliefert.

Tabelle 14.1: Werte für Prozeduren

Die Beziehung zwischen Prozeduren und ihnen zugehörigen Spalten ist dieselbe wie zwischen Tabellen und Spalten. Die Klassenelemente von CProcedureColumns sind demzufolge quasi identisch mit denen von CColumns. Die Parameter einer Stored Procedure können über die Klasse CProcedureParameters abgefragt werden. Zusätzlich zu den üblichen Klassenelementen/Restriktionen für Katalog und Schema weist die Klasse CProcedureParameters die folgenden Klassenelemente auf: 왘 m_szName definiert den Namen der Prozedur, die den aktuellen Parameter enthält. 왘 m_szParameterName gibt den Namen des Parameters selbst an. Über den Code im folgenden Beispiel lassen sich die Parameter einer Prozedur aufzählen: CProcedureParameters ProcedureParameters; hr = ProcedureParameters.Open(Session, CatalogName, SchemaName, ProcedureName); hr = ProcedureParameters.MoveFirst(); while (hr == S_OK) { hr = ProcedureParameters.MoveNext(); }

14.2.6 Indizes Die Klasse CIndexes dient zum Aufzählen der Indizes, wobei jeder Index für eine Tabelle gültig ist. OLE DB geht dabei nicht davon aus, dass sich die jeweilige Tabelle im selben Schema bzw. Katalog befinden muss. Daher enthält CIndexes Schema- und Katalog-Daten sowohl zur Tabelle als auch für den Index (m_szTableCatalog, m_szTableSchema und m_szIndexCatalog, m_szIndexSchema).

Sandini Bib 14.2

Das Katalog/Schema-Modell

385

m_szTableName enthält den Namen der Tabelle, auf die sich der Index bezieht, m_szIndexName den Namen des Index selbst.

Indizes beziehen sich auf eine oder mehrere Spalten. Wenn der Index sich nur auf eine Spalte bezieht, ist in der CIndexes-Zeilenmenge nur eine Zeile dafür vorhanden. Gilt der Index für mehrere Spalten, so ist in der Klasse CIndexes pro Spalte eine Zeile vorhanden. Genauso wie bei CColumns wird jede Spalte durch den Inhalt von m_szColumnName, m_guidColumn und m_nColumnPropID identifiziert. Mit dem folgenden Code können Sie die Indizes einer Tabelle aufzählen: CIndexes Indexes; Indexes.Open(Session, CatalogName, SchemaName, TableName); hr = Indexes.MoveFirst(); while (hr == S_OK) { hr = Indexes.MoveNext(); }

CIndexes stellt noch weitere Informationen zur Verfügung: m_bPrimaryKey gibt an, ob es sich bei dem Index um einen Primärschlüssel handelt, m_bUnique sagt aus, ob der Index nur eindeutige Werte enthält, und m_bClustered gibt an, ob es sich um einen gruppen-

bildenden Index (Clustered Index) handelt.

14.2.7 Schlüssel Ein Datenspeicher kann Primärschlüssel und Fremdschlüssel enthalten. CPrimaryKeys dient zum Aufzählen der Primärschlüssel, wobei Restriktionen zu Katalog, Schema und Tabellennamen angegeben werden können. Pro Tabelle ist nur ein Primärschlüssel vorhanden, dieser kann aber mehrere Spalten umfassen. In diesem Fall enthält die CPrimaryKeys-Zeilenmenge eine Zeile pro Spalte. Ein Beispiel für die Programmierung einer solchen Aufzählung: CPrimaryKeys PrimaryKeys; PrimaryKeys.Open(Session, CatalogName, SchemaName, TableName); hr = PrimaryKeys.MoveFirst(); while (hr == S_OK) { hr = PrimaryKeys.MoveNext(); // der Spaltenname ist in PrimaryKeys.m_szColumnName enthalten; }

Zusätzlich enthält CPrimaryKeys noch die üblichen Klassenelemente zur Angabe der Spalte sowie das Klassenelement m_nOrdinal, das die Nummer der Spalte im Schlüssel (nicht die der Spalte selbst) angibt. Über die Klasse CForeignKeys lassen sich die Fremdschlüssel aufzählen. Da ein Fremdschlüssel sich immer auf einen Primärschlüssel bezieht, enthält die Klasse CForeignKeys

Sandini Bib 386

14

Schemata, Definitionen und Synchronisation

die Klassenelemente von CPrimaryKeys (dies sind die Klassenelemente, welche die Zeichenfolge PK enthalten, wie z.B. m_szPKTableName). Ferner enthält sie Informationen zum Fremdschlüssel selbst (Klassenelemente mit der Zeichenfolge FK, wie z.B. m_szFKTableName). Abbildung 14.3 zeigt ein Beispiel für drei Tabellen mit zwei Fremdschlüsseln. Tabelle 2 ForeignID2

Tabelle 1

Tabelle 3

ID

ForeignID3

Abbildung 14.3: Fremdschlüssel

m_szPKTableName

m_szPKColumnName m_szFKTableName

m_szFKColumnName

Table1

ID

Table2

ForeignID2

Table1

ID

Table3

ForeignID3

Tabelle 14.2: Fremdschlüssel

So fragen Sie die Fremdschlüssel für Table1 ab: CForeignKeys ForeignKeys; ForeignKeys.Open(Session, CatalogName, SchemaName, _T("Table1")); hr = ForeignKeys.MoveFirst(); while (hr == S_OK) { hr = ForeignKeys.MoveNext(); }

In diesem Beispiel enthält ForeignKeys die in Tabelle 14.2 aufgeführten Zeilen.

14.2.8 Berechtigungen Bei einigen Datenspeichern werden Berechtigungen für Tabellen und Spalten definiert. Diese dienen dazu anzugeben, ob ein Benutzer eine bestimmte Operation mit einem Objekt (Tabelle oder Spalte) durchführen darf. Diese Berechtigung wird von einem anderen Benutzer erteilt. In den OLE DB Consumer Templates stehen hierfür zwei Klassen zur Verfügung: CTablePrivileges enthält die Berechtigungsinformationen für Tabellen und CColumnPrivileges die Berechtigungsinformationen für Spalten. Beide enthalten das Klassenelement m_szGrantor, über das angegeben wird, wer die Berechti-

Sandini Bib 14.2

Das Katalog/Schema-Modell

387

gung erteilt, und das Klassenelement m_szGrantee, das angibt, wem die Berechtigung erteilt wird. Außerdem sind noch Katalog- und Schema-Daten abrufbar. 왘 m_szType bzw. m_szPrivilegeType ist eine Textdarstellung des Typs der Berechtigung. In der Regel enthalten sie einen der Werte INSERT, DELETE, UPDATE, SELECT oder REFERENCES. 왘 m_bIsGrantable gibt an, ob der Benutzer, dem die Berechtigung erteilt wurde, diese an andere Benutzer weitergeben kann. Wenn hier der Wert false enthalten ist, kann er diese Berechtigung zwar erhalten, aber nicht an einen anderen Benutzer weitergeben. Ist der Wert auf true gesetzt, so verfügt der Benutzer über die Berechtigung und kann sie an andere Benutzer weitergeben. CTablePrivileges kombiniert die Berechtigungsinformationen mit denen für die Tabelle. Wenn Sie z.B. die Berechtigungen für eine Tabelle abrufen möchten, schreiben Sie den folgenden Code: CTablePrivilegeInfo TablePrivilegeInfo; TablePrivilege.Open(Session, GrantorName, GranteeName, CatalogName, SchemaName, TableName); hr = TablePrivilege.MoveFirst(); while (hr == S_OK) { hr = TablePrivilege.MoveNext(); }

CColumnPrivileges kombiniert die Berechtigungsinformationen mit den Spalteninformationen. Zum Abrufen der Berechtigungen für eine Spalte programmieren Sie beispielsweise Folgendes: CColumnPrivileges ColumnPrivileges; ColumnPrivileges.Open(Session, GrantorName, GranteeName, CatalogName, SchemaName, TableName, ColumnName); hr = ColumnPrivilege.MoveFirst(); while (hr == S_OK) { hr = ColumnPrivilege.MoveNext(); }

14.2.9 Provider-Typen In OLE DB sind eine Menge von Typen definiert, die jeweils durch eine ganzzahlige Konstante bezeichnet werden. So steht DBTYPE_I4 beispielsweise für eine 4 Byte lange Ganzzahl mit Vorzeichen. Die Schema-Zeilenmenge für den Provider-Typ gibt die Beziehungen zwischen OLE DB und den Provider-Typen an, die jeweils durch einen Namen spezifiziert sind. So könnte der Provider-Typ für eine Ganzzahl int heißen.

Sandini Bib 388

14

Schemata, Definitionen und Synchronisation

CProviderTypes ist die Schema-Zeilenmengen-Klasse, die zum Navigieren in den Informationen zu den Provider-Typen dient. Für diese Klasse können die Informationen durch Angabe von zwei Restriktionen abgerufen werden: über den OLE DB-Typ und eine Angabe für die beste Übereinstimmung. CProviderTypes kann den Programmierer etwas in die Irre führen, weil die Restriktionen nicht die beiden ersten Klassenelemente sind und diese auch nicht vom Typ string sind. Daher bietet sich die Verwendung von CProviderTypes nur an, solange keine Restriktionen angegeben werden.

Über die Klasse CProviderTypesEx der OLE DB-Erweiterungen wird dieser Mangel behoben. Sie ist wie folgt definiert: class CProviderTypesEx: public CProviderTypes { public: HRESULT Open(const CSession& session, DBTYPE Type, bool bMatch = true) { USES_CONVERSION; CComVariant* pVariant; if (m_pvarRestrictions == NULL) return E_OUTOFMEMORY; *m_pvarRestrictions = Type; pVariant = m_pvarRestrictions + 1; *pVariant = match; return CSchemaRowset::Open(session, DBSCHEMA_PROVIDER_TYPES); } } ;

In der Open-Methode stellt Type den OLE DB-Typ dar, während über bMatch angegeben wird, ob die Abfrage auf die beste Übereinstimmung beschränkt werden soll. Wenn Sie beispielsweise den Datenspeichertyp abrufen möchten, der einer Ganzzahl mit Vorzeichen entspricht, schreiben Sie folgenden Code: CProviderTypesEx ProviderTypes; ProviderTypes.Open(Session, DBTYPE_I4, true); hr = ProviderTypes.MoveFirst(); // Der Name des Datenspeichertyps ist // in ProviderTypes.m_szTypeName enthalten.

Zum Abrufen aller Datenspeichertypen für den Typ string einschließlich derer, die keine direkte Übereinstimmung darstellen, programmieren Sie Folgendes: CProviderTypesEx ProviderTypes; ProviderTypes.Open(Session, DBTYPE_STR, false); hr = ProviderTypes.MoveFirst(); while (hr == S_OK)

Sandini Bib 14.3

Schema bearbeiten

389

{ // Der Name des Datenspeichertyps ist in // ProviderTypes.m_szTypeName enthalten. hr = ProviderTypes.MoveNext(); }

CProviderTypes stellt auch noch zusätzliche Informationen zur Verfügung. Hierzu gehört, ob der Typ auf null gesetzt werden kann und/oder vom Typ long ist (im Sinn

eines BLOBs) sowie die Angabe von Präfix und Suffix für Literale.

14.3 Schema bearbeiten OLE DB stellt zwei Möglichkeiten zum Ändern der Schema-Daten zur Verfügung. Einerseits können Sie einen in einer Datendefinitionssprache (DDL) geschriebenen Befehl ausführen lassen. In SQL könnte ein solcher Befehl den folgenden Text aufweisen: CREATE TABLE TableName( Column1 int)

Sie können auch mit spezialisierten OLE DB-Schnittstellen arbeiten. Diese werden zwar von den OLE DB Consumer Templates nicht unterstützt, lassen sich aber entweder direkt oder über die OLE DB-Erweiterungen verwenden. Das Programmieren mit einer Datendefinitionssprache und das Lesen der so erstellten Programme ist einfach. Außerdem können Sie auf diese Weise im Code und in den externen Tools dieselbe Sprache verwenden. Andererseits kann der C++-Compiler die Gültigkeit von DDL-Code nicht überprüfen. Wenn darin Syntaxfehler vorliegen, können Sie diese nur durch Testen des kompilierten Codes ermitteln. Beim Arbeiten mit OLE DB-Schnittstellen verwenden Sie nur eine Sprache und geben damit Syntaxfehlern keine Chance. Auf der anderen Seite ist das Arbeiten mit den OLE DB-Schnittstellen etwas komplizierter. Für den Fall, dass der zugrunde liegende Datenspeicher keine Datendefinitionssprache unterstützt, müssen Sie aber sowieso mit den OLE DB-Schnittstellen arbeiten. Die Behandlung der DDL-Befehle würde den Rahmen dieses Buches sprengen. Ein Hinweis ist an dieser Stelle aber trotzdem angebracht: Führen Sie DDL-Befehle wie alle anderen Befehle aus, die kein Ergebnis zurückliefern (siehe Kapitel 7). In diesem Abschnitt wollen wir uns jedoch auf die OLE DB-Erweiterungen konzentrieren. Die OLE DB-Erweiterungen stellen zwei Klassen zur Verfügung: 왘 CTableDefinition dient zum Erstellen, Löschen und Ändern von Tabellen und Spalten. 왘 CIndexDefinition dient zum Erstellen und Löschen von Indizes für bereits vorhandene Tabellen.

Sandini Bib 390

14

Schemata, Definitionen und Synchronisation

14.3.1 CTableDefinition Wie viele andere Objekte der OLE DB Consumer Templates muss die Klasse CTableDefinition zunächst unter Angabe einer Sitzung geöffnet werden. Anschließend definiert sie Operationen auf Tabellen- und Spaltenebene.

Tabellenoperationen Mit der Methode CreateTable wird eine Tabelle mit einem Namen, einer Menge von Spalten und ggf. einem Eigenschaften-Set erstellt: HRESULT CreateTable(LPCTSTR szTableName, DBCOLUMNDESC* rgColumnDesc = NULL, ULONG cColumnDesc = 0, DBPROPSET *pPropSet = NULL, ULONG cPropertySets = 0, DBID** out = NULL)

szTableName stellt dabei den Namen der zu erstellenden Tabelle dar, rgColumnDesc ist ein Array aus DBCOLUMNDESC-Strukturen, von denen jede eine hinzuzufügende Spalte beschreibt. cColumnDesc gibt die Anzahl der Spalten in der neuen Tabelle an. Die Spalten erscheinen in der Reihenfolge, in der sie in rgColumnDesc definiert sind.

Die Struktur DBCOLUMNDESC ist folgendermaßen definiert: typedef struct tagDBCOLUMNDESC { LPOLESTR pwszTypeName; ITypeInfo __RPC_FAR *pTypeInfo; /* [size_is] */ DBPROPSET __RPC_FAR *rgPropertySets; CLSID __RPC_FAR *pclsid; ULONG cPropertySets; ULONG ulColumnSize; DBID dbcid; DBTYPE wType; BYTE bPrecision; BYTE bScale; } DBCOLUMNDESC;

wobei 왘 dbcid den Namen der Spalte enthält. 왘 der Typ über wType (OLE DB-Typ) und pwszTypeName (Name des Datenspeichertyps) definiert ist. Wenn für pwszTypeName der Wert NULL gesetzt ist, wählt der Datenspeicher einen geeigneten Typnamen. 왘 bPrecision, bScale und ulColumnSize bei Bedarf die Genauigkeit, die Skalierung und die Größe der Spalte angeben.

Sandini Bib 14.3

Schema bearbeiten

391

왘 jede Spalte auch ein Eigenschaften-Set aufweisen kann. rgPropertySets stellt ein Array aus Eigenschaften-Sets dar und cPropertySets gibt die Anzahl der Elemente in rgPropertySets an. OLE DB definiert die in Tabelle 14.3 aufgeführten Standardeigenschaften für Spalten. Symbolischer Wert

Beschreibung

DBPROP_COL_AUTOINCREMENT

Der Wert in der Spalte wird automatisch inkrementiert.

DBPROP_COL_DEFAULT

Dies ist der Standardwert. Er muss mit dem Spaltentyp kompatibel sein.

DBPROP_COL_DESCRIPTION

Die Beschreibung der Spalte in Textform.

DBPROP_COL_FIXEDLENGTH

Die Spalte weist eine feste Länge auf.

DBPROP_COL_NULLABLE

Die Spalte darf den Wert NULL enthalten.

DBPROP_COL_PRIMARYKEY

Die Spalte wird als Primärschlüssel verwendet.

DBPROP_COL_UNIQUE

Die Spalte kann nur eindeutige Werte enthalten.

DBPROP_COL_ISLONG

Die Spalte enthält Daten vom Typ long (siehe Kapitel 11, BLOBs).

Tabelle 14.3: Spalteneigenschaften

Das folgende Beispiel zeigt, wie eine Tabelle mit zwei Spalten erzeugt wird. Die eine Spalte soll als Primärschlüssel verwendet werden und die andere darf keine Nullwerte enthalten: CTableDefinition TableDefinition; hr = TableDefinition.Open(Session); hr = TableDefinition.DropTable("TableName"); CDBColumnDesc Columns[2]; Columns[0].SetName("Column1"); Columns[0].wType = DBTYPE_I4; CDBPropSet Column1Properties (DBPROPSET_COLUMN); Column1Properties.AddProperty(DBPROP_COL_PRIMARYKEY, true); Columns[0].rgPropertySets = &Column1Properties; Columns[0].cPropertySets = 1; Columns[1].SetName("Column2"); Columns[1].wType = DBTYPE_STR; Columns[1].ulColumnSize = 10; CDBPropSet Column2Properties (DBPROPSET_COLUMN); Column1Properties.AddProperty(DBPROP_COL_NULLABLE, false); Columns[1].rgPropertySets = &Column1Properties; Columns[1].cPropertySets = 1; hr = TableDefinition.CreateTable("TableName", Columns, 2);

Das Löschen einer Tabelle gestaltet sich im Vergleich zum Erstellen einfacher, weil hierbei keine Spalten anzugeben sind. Der folgende Code zeigt, wie eine Tabelle mit dem Namen TableName gelöscht wird:

Sandini Bib 392

14

Schemata, Definitionen und Synchronisation

CSession Session; // Sitzung öffnen CTableDefinition TableDefinition; TableDefinition.Open(Session); TableDefinition.DropTable(TableName);

Es ist auch möglich, eine bereits erstellte Tabelle umzubenennen. Dies geht schneller und ist einfacher, als sie zu löschen und neu zu erstellen. Der folgende Code zeigt, wie eine Tabelle mit dem Namen oldname in newname umbenannt wird: CTableDefinition TableDefinition; TableDefinition.Open(Session); TableDefinition. AlterTable(_T("oldname"), _T("newname"));

Spaltenoperationen Nach dem Erstellen einer Tabelle ist es immer noch möglich, Spalten hinzuzufügen, zu löschen oder zu bearbeiten. Mit der Methode AddColumn der Klasse CTableDefinition wird eine Spalte hinzugefügt: HRESULT AddColumn(LPCTSTR szTableName, DBCOLUMNDESC* pColumnDesc, DBID** idColumn = NULL)

wobei 왘 szTableName den Namen der Tabelle darstellt. 왘 pColumnDesc die Beschreibung der hinzuzufügenden Spalte darstellt. Für pColumnDesc kann ein Exemplar der Klasse CDBColumnDesc verwendet werden. Das folgende Beispiel zeigt, wie die Methode AddColumn verwendet wird: CTableDefinition TableDefinition; CDBColumnDesc ColumnDesc; ColumnDesc.SetName(ColumnName1); ColumnDesc.wType = DBTYPE_I4; TableDefinition.AddColumn(TableName, &ColumnDesc);

Die Spaltenbeschreibung verhält sich wie bei Verwendung der Methode CreateTable. Sie können beispielsweise den Typ des Datenspeichers oder ein Eigenschaften-Set angeben, wie im folgenden Codebeispiel gezeigt: CTableDefinition TableDefinition; CDBColumnDesc ColumnDesc; ColumnDesc.SetName(ColumnName1); ColumnDesc.wType = DBTYPE_STR; ColumnDesc.pwszTypeName = T2W(_T("varchar")); CDBPropSet PropSet(DBPROPSET_COLUMN); PropSet.AddProperty(DBPROP_COL_NULLABLE, true); TableDefinition.AddColumn(TableName, &ColumnDesc);

Sandini Bib 14.3

Schema bearbeiten

393

Das Löschen einer Spalte geht einfacher als das Hinzufügen, weil in diesem Fall keine Eigenschaften anzugeben sind. Das folgende Beispiel zeigt, wie eine Spalte aus einer Tabelle gelöscht wird: CTableDefinition TableDefinition; TableDefinition.Open(Session); TableDefinition. DropColumn(TableName, ColumnName);

Schließlich können Sie eine Spalte noch umbenennen oder deren Eigenschaften ändern. Hierzu dient die Methode AlterColumn: HRESULT AlterColumn(LPCTSTR szTableName, LPCTSTR szColumnName, DBCOLUMNDESCFLAGS ColumnDescFlags, DBCOLUMNDESC* pColumnDesc)

wobei szTableName und szColumnName den Namen der Tabelle und der zu ändernden Spalte angeben; pColumnDesc enthält den neuen Spaltennamen bzw. die neuen Spalteneigenschaften, und ColumnDescFlags ist ein Flag, das angibt, welches der in pColumnDesc angegebenen Felder gültig ist.

14.3.2 CIndexDefinition Die Klasse CIndexDefinition ähnelt in gewisser Hinsicht der Klasse CTableDefinition: Bevor Sie diese Klasse nutzen können, müssen Sie sie für eine Sitzung öffnen. Nachdem die Indexdefinition geöffnet wurde, können Sie Indizes erzeugen und löschen. Zum Erzeugen eines Index müssen Sie einen Tabellennamen und eine Liste der Spaltennamen angeben. Es ist logisch, dass Sie die Tabelle erstellen müssen, bevor Sie den Index erzeugen können. Mit dem folgenden Beispielcode können Sie einen Index anhand von zwei Spalten erstellen: CIndexDefinition IndexDefinition; IndexDefinition.Open(Session); TCHAR ColumnNames [] = { _T("Column1"), _T("Column1")} ; IndexDefinition.CreateIndex(TableName, IndexName, ColumnNames, 2);

Optional können Sie die Indexreihenfolge für die einzelnen Spalten angeben. Per Voreinstellung wird der Index für jede Spalte in aufsteigender Reihenfolge erstellt. Es ist jedoch möglich, diese Voreinstellung außer Kraft zu setzen. Das folgende Beispiel zeigt, wie Sie hierbei vorgehen müssen. Hier wird ein Index für column1 mit aufsteigenden Werten und für column2 mit absteigenden Werten erstellt: CIndexDefinition IndexDefinition; IndexDefinition.Open(Session); TCHAR ColumnNames [] = { _T("Column1"), _T("Column1")} ; DBINDEX_COL_ORDER ColumnOrders [] = { DBINDEX_COL_ORDER_ASC , DBINDEX_COL_ORDER_DESC} ; IndexDefinition.CreateIndex(TableName, IndexName, ColumnNames, 2, ColumnOrder);

Sandini Bib 394

14

Schemata, Definitionen und Synchronisation

Schließlich können Sie einen Index mit einem Eigenschaften-Set erzeugen. In Tabelle 14.4 sind die von OLE DB definierten Indexeigenschaften aufgeführt. Symbolischer Wert

Beschreibung

DBPROP_INDEX_AUTOUPDATE

Gibt an, dass der Index automatisch aktualisiert wird.

DBPROP_INDEX_CLUSTERED

Gibt an, dass es sich um einen gruppenbildenden Index (Clustered Index) handelt.

DBPROP_INDEX_FILLFACTOR

Gibt den Füllfaktor an.

DBPROP_INDEX_INITIALSIZE

Gibt die Anfangsgröße des Index an.

DBPROP_INDEX_NULLCOLLATION

Gibt an, wie NULL-Werte einsortiert werden sollen.

DBPROP_INDEX_NULLS

Gibt an, ob NULL-Werte zulässig sind.

DBPROP_INDEX_PRIMARYKEY

Gibt an, dass der Index als Primärschlüssel verwendet wird.

DBPROP_INDEX_SORTBOOKMARKS

Gibt an, wie sich wiederholende Schlüssel behandelt werden sollen.

DBPROP_INDEX_TYPE

Gibt den Indextyp an.

DBPROP_INDEX_UNIQUE

Gibt an, dass der Index nur eindeutige Werte enthalten darf.

Tabelle 14.4: Indexeigenschaften

Der folgende Code zeigt, wie ein gruppenbildender Index erstellt wird: CIndexDefinition IndexDefinition; IndexDefinition.Open(Session); CDBPropSet IndexPropSet(DBPROPSET_INDEX); IndexPropSet.AddProperty(DBPROP_INDEX_CLUSTERED, true); TCHAR ColumnNames [] = { _T("Column1"), _T("Column1")} ; IndexDefinition.CreateIndex(TableName, IndexName, ColumnNames, 2, NULL, &IndexPropSet);

Das Löschen eines Index ist einfacher als das Erstellen, weil nur der Tabellen- und der Indexname angegeben werden müssen. Das folgende Beispiel zeigt, wie Sie hier vorgehen müssen: CIndexDefinition IndexDefinition; IndexDefinition.Open(Session); IndexDefinition.DropIndex(TableName, IndexName);

14.4 Schema mit Datenbank synchronisieren Einer der Vorteile einer Datenbank besteht darin, dass die Definition der Daten vom Code getrennt ist. Dies hat zur Folge, dass ohne vorherige Planung von vielen Anwendungen aus auf die Daten zugegriffen werden kann. Andererseits ergibt sich daraus aber auch ein Problem, weil die Daten und der Code synchronisiert werden müssen.

Sandini Bib 14.4

Schema mit Datenbank synchronisieren

395

Leider gibt es kein Patentverfahren um die Synchronisierung zu erzwingen, aber es ist möglich, durch entsprechende Programmierung die Verwaltung der Daten und ihre Synchronisierung mit dem Code zu erleichtern. In Kapitel 9 habe ich das Arbeiten mit dynamischen Zugriffsobjekten (Dynamic Accessors) beschrieben. Sie sind ideal geeignet, wenn die Struktur der Tabelle nicht bekannt ist, weil sie die Spalten erst zur Laufzeit ermitteln. Das Arbeiten mit dynamischen Zugriffsobjekten kann sich jedoch als recht aufwändig erweisen, weil der Code ständig die Spaltennamen überprüfen muss. In den folgenden Abschnitten werde ich erläutern, wie Sie das Risiko einer fehlenden Synchronisation zwischen statischen Zugriffsobjekten und Datenspeichertabellen verringern.

14.4.1 Prüfung der Bindungen zum Zeitpunkt der Kompilierung Eine der Regeln bei der Entwicklung von Software besagt, dass wichtige Prüfungen bereits zu einem frühen Zeitpunkt und danach häufig stattfinden müssen. Für die Synchronisierung von Datenbank und Code bedeutet dies, dass die Bindungen bei jeder Kompilierung geprüft werden müssen. Natürlich ist das keine Garantie dafür, dass die Synchronisation nach der Freigabe der Software-Komponente nicht verloren geht, aber das Risiko hierfür lässt sich auf diese Weise verringern. Eine der Möglichkeiten besteht darin, ein Token für die Befehlszeile zu definieren, das angibt, dass das Programm die Synchronisation mit dem Datenspeicher überprüfen soll. Zu diesem Zweck werden wir _CheckDB verwenden. Die Funktion WinMain prüft dann, ob dieses Token vorhanden ist und ruft eine spezielle Prüffunktion auf: LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens); while (lpszToken != NULL) { if (lstrcmpi(lpszToken, _T("UnregServer"))==0) { _Module.UpdateRegistryFromResource(IDR_Checkdb, FALSE); nRet = _Module.UnregisterServer(TRUE); bRun = FALSE; break; } if (lstrcmpi(lpszToken, _T("RegServer"))==0) { _Module.UpdateRegistryFromResource(IDR_Checkdb, TRUE); nRet = _Module.RegisterServer(TRUE); bRun = FALSE; break; } if (lstrcmpi(lpszToken, _T("CheckDB"))==0) { CheckDB(); break;

Sandini Bib 396

14

Schemata, Definitionen und Synchronisation

} lpszToken = FindOneOf(lpszToken, szTokens); }

Mit der Funktion CheckDB lassen sich verschiedene Prüfungen durchführen. Mit dem folgenden Code können Sie beispielsweise prüfen, ob die Klasse CdboMyTable erfolgreich mit der zugehörigen Tabelle gebunden wurde: #define CHECK(expr) if(FAILED(expr)){ printf (#expr); printf("failed\ n");} HRESULT CheckDB() { CdboMyTable MyTable; printf("-- start synchronisation check\ n"); CHECK(MyTable.Open()) printf("-- end synchronisation check\ n"); return S_OK; }

Der letzte Schritt besteht darin, die Prüfung zum Compiler hinzuzufügen, indem Sie CheckDB im Dialogfenster PROJECT SETTINGS in die Befehlszeile eintragen (siehe Abbildung 14.4). Wenn das Binden der Klasse mit der Tabelle scheitert, wird im entsprechenden Fenster von Visual C++ eine Meldung wie die unten gezeigte angezeigt.

Abbildung 14.4: Dialogfenster Project Settings mit Synchronisationsprüfung in Microsoft Visual C++ --------------------Configuration: checkdb – Win32 Debug-------------Compiling... checkdb.cpp Linking... Performing registration

Sandini Bib 14.4

Schema mit Datenbank synchronisieren

397

-- start synchronization check MyTable.Open()failed -- end synchronization check Server registration done!

Dieses Verfahren können Sie auch außerhalb der Entwicklung noch verwenden, nachdem die Software freigegeben wurde. So könnten Sie beispielsweise ein einfaches Diagnose-Tool für Datenbankadministratoren programmieren, das die Ergebnisse der Synchronisationsprüfungen anzeigt.

14.4.2 Spalten anhand von Namen binden Bisher wurden alle Bindungen anhand der Spaltennummern, d.h. über ihre Position in der Spaltenliste, durchgeführt und nicht anhand von Namen. Diese Vorgehensweise ist unter Umständen nicht ganz unproblematisch. Angenommen, unsere Tabelle besteht aus den beiden Spalten Column1 und Column2. Die zugehörige Zugriffsobjekt-Klasse definiert die folgende Bindung: BEGIN_COLUMN_MAP(CMyAccessor) COLUMN_ENTRY(1, m_Column1) COLUMN_ENTRY(2, m_Column2) END_COLUMN_MAP()

SpaltenNr.1

SpaltenNr.2

SpaltenNr.1

SpaltenNr.2

Spalte 1

Spalte 2

Spalte 2

Spalte 1

Abbildung 14.5: Unterschiedliche Reihenfolge der Spalten in der Tabelle

Es besteht immer die Möglichkeit, dass eine solche Tabelle neu erstellt wird, wobei die Spalten in einer anderen Reihenfolge angeordnet werden. Abbildung 14.5 zeigt die Tabelle vor und nach dem Vertauschen der Spalten. Sie enthält in beiden Fällen dieselben Spalten, aber jeweils in anderer Reihenfolge. In vielen Fällen hat das Vertauschen der Spalten keinerlei Auswirkungen. Wenn Sie einen SQL-Befehl wie SELECT Column1, Column2 FROM TableName

verwenden, wird die Zeilenmenge in der Reihenfolge zurückgeliefert, die im Befehl angegeben ist, und nicht in der Reihenfolge der Spalten der zugrunde liegenden Tabelle.

Sandini Bib 398

14

Schemata, Definitionen und Synchronisation

Nicht in allen Fällen wird jedoch der Benutzer die Reihenfolge der Spalten angeben. Wenn beispielsweise die Klasse CTable oder ein Befehl wie der Folgende verwendet wird, SELECT * FROM TableName

so entspricht die Spaltenreihenfolge der zurückgelieferten Zeilenmenge der Reihenfolge in der zugrunde liegenden Tabelle. In solchen Fällen ist das Binden der Spalten anhand ihrer Positionsnummern gefährlich, weil diese von der Reihenfolge der Spalten in der Tabelle abhängen. Eine Lösung hierfür ist das Binden der Spalten anhand von Namen. Bei diesem Ansatz wird die Bindung aufgehoben, wenn sich ein Spaltenname ändert, nicht aber, wenn die Spalte innerhalb der Tabelle an eine andere Position verschoben wird. Die OLE DBErweiterungen stellen ein neues Zugriffsobjekt zur Verfügung, das die Spalten anhand von Namen bindet (CAccessorByName). Sie sollten dieses anstelle von CAccessor verwenden. So könnten Sie beispielsweise eine Variable wie die folgende deklarieren: CTable var;

CAccessorByName funktioniert zusammen mit einer neuen Gruppe von Makros. Grund-

sätzlich entsprechen alle Namen der neuen Makros denen aus vorhandenen OLE DB Consumer Templates, wobei jeweils COLUMN durch NAMED_COLUMN ersetzt wurde. Tabelle 14.5 zeigt eine Übersicht über die neuen Makros. Diese neue Art der Bindung ist wie folgt definiert: BEGIN_NAMED_COLUMN_MAP(CMyAccessor) NAMED_COLUMN_ENTRY(1, L"Column1", m_Column1) NAMED_COLUMN_ENTRY(2, L"Column2", m_Column2) END_COLUMN_MAP()

Makro

Beschreibung

BEGIN_NAMED_ACCESSOR_MAP

Entspricht BEGIN_ACCESSOR_MAP und unterstützt Spaltennamen.

BEGIN_NAMED_COLUMN_MAP

Entspricht BEGIN_COLUMN_MAP und unterstützt Spaltennamen.

_NAMED_COLUMN_ENTRY_CODE

Implementiert den Code für eine benannte Spalte.

NAMED_COLUMN_ENTRY_EX

Entspricht COLUMN_ENTRY_EX; der zweite Parameter gibt den Spaltennamen an.

NAMED_COLUMN_ENTRY_TYPE

Entspricht COLUMN_ENTRY_TYPE; der zweite Parameter gibt den Spaltennamen an.

NAMED_COLUMN_ENTRY_TYPE_SIZE

Entspricht COLUMN_ENTRY_TYPE_ SIZE; der zweite Parameter gibt den Spaltennamen an.

NAMED_COLUMN_ENTRY

Entspricht COLUMN_ENTRY; der zweite Parameter gibt den Spaltennamen an.

Tabelle 14.5: Binde-Makros mit Unterstützung von Spaltennamen 402

Sandini Bib 14.4

Schema mit Datenbank synchronisieren

Makro

Beschreibung

NAMED_COLUMN_ENTRY_LENGTH

Entspricht COLUMN_ENTRY_LENGTH; der zweite Parameter gibt den Spaltennamen an.

NAMED_COLUMN_ENTRY_STATUS

Entspricht COLUMN_ENTRY_STATUS; der zweite Parameter gibt den Spaltennamen an.

NAMED_COLUMN_ENTRY_LENGTH_STATUS

Entspricht COLUMN_ENTRY_LENGTH_STATUS; der zweite Parameter gibt den Spaltennamen an.

399

Tabelle 14.5: Binde-Makros mit Unterstützung von Spaltennamen 402 (Fortsetzung)

Zusätzlich zu den Spaltennamen enthalten die Makros vom Typ NAMED_COLUMN alle Informationen, die auch in den normalen Makros der OLE DB Consumer Templates enthalten sind. Daher sind sie vollständig kompatibel mit diesen. Wenn Sie beispielsweise die folgende Variable deklarieren: CTable MyTable;

nimmt CAccessor die Bindungen anhand der Spaltennummern vor und berücksichtigt die Spaltennamen nicht. Deklarieren Sie andererseits die folgende Variable: CTableMyTable;

nimmt CAccessorByName die Bindungen anhand der Namen vor und berücksichtigt die Spaltennummern nicht.

14.4.3 Tabellen über den Programmcode erstellen Das Software-Programm kann die vollständige Steuerung der Daten übernehmen. Insbesondere lässt es sich zum Erstellen aller Tabellen in der Datenbank einsetzen. Dazu gibt es zwei Möglichkeiten: Entweder wird dies explizit vom Installationsprogramm übernommen, was bedeutet, dass es die Liste der zu erstellenden Tabellen, ihre Spalten und Eigenschaften kennen muss. Es ist aber auch möglich, die Tabellen nach der Installation des Software-Programms entweder sofort oder auf Anforderung erstellen zu lassen. Bei der Erstellung auf Anforderung kann jede Komponente die zugrunde liegende Tabelle erstellen. Wenn die Komponente auf die Datenbank zugreift, wird geprüft, ob die zugrunde liegende Tabelle vorhanden ist. Ist dies nicht der Fall, wird sie spontan erstellt. Das zweite Verfahren hat drei Vorteile: Erstens erleichtert es den Installationsvorgang, weil bei diesem keine Tabellen erstellt zu werden brauchen. Zweitens werden nur die wirklich benötigten Tabellen erstellt. Wenn eine Komponente nie verwendet wird, erstellt das Programm die entsprechende Tabelle auch nicht. Drittens verfügt diese Software über »Selbstheilungskräfte«, weil sie in der Lage ist, eine versehentlich gelöschte Tabelle im Hintergrund erneut zu erstellen, ohne dass hierzu die Software

Sandini Bib 400

14

Schemata, Definitionen und Synchronisation

neu installiert werden muss. Hierzu sei angemerkt, dass es sinnvoll ist, den Benutzer oder den Datenbankadministrator über das Erstellen neuer Tabellen zu informieren. Es gibt für Komponenten zwei Möglichkeiten, die zugrunde liegenden Tabelle zu erstellen: entweder über einen DDL-Befehl oder über die Klasse CTableDefinition. Wenn die Zugriffsobjekt-Klassen über Spaltennamen gebunden werden, werden Name und Typ der einzelnen Spalten bereits mit definiert. Daher können Sie die Klasse CTableDefinition direkt für die Zugriffsobjekt-Klasse verwenden, ohne die Spaltendefinitionen nochmals mit CreateTableByAccessor durchführen zu müssen. CreateTableByAccessor erhält die Bindungen vom Zugriffsobjekt, wandelt sie in Spal-

teninformationen um und erstellt die Tabelle anhand dieser Informationen: template HRESULT CreateTableByAccessor (LPCTSTR szTableName, T & Accessor, int nAccessor = 0, DBPROPSET *pPropSet = NULL, ULONG cPropertySets = 0, DBID** out = NULL) { DBBINDING* pBindings = NULL; ULONG nColumns; bool bAuto; HRESULT hr; // Beim ersten Mal wird nur die Anzahl der Einträge // abgerufen. // Hierzu wird &nColumns übergeben. T::_GetBindEntries(&nColumns, NULL, nAccessor, NULL); // Jetzt werden die Bindungs-Strukturen zugewiesen ATLTRY(pBindings = new DBBINDING[nColumns]); if (pBindings == NULL) return E_OUTOFMEMORY; // Jetzt werden die Bindungs-Einträge abgerufen StructInit * pColumnDesc = NULL; OLECHAR** pNames = NULL; ATLTRY(pColumnDesc = new StructInit[ nColumns]); ATLTRY(pNames = new OLECHAR*[nColumns]); hr = T::_GetBindEntries(&nColumns, pBindings, nAccessor, &bAuto, NULL, pNames); for (ULONG i = 0; i < nColumns; i++) { DBTYPE wType = pBindings[i].wType; pColumnDesc[i].wType = wType; pColumnDesc[i].dbcid.eKind = DBKIND_NAME; pColumnDesc[i].dbcid.uName.pwszName = pNames[i]; pColumnDesc[i].ulColumnSize = pBindings[i].cbMaxLen; if (wType == DBTYPE_STR || wType == DBTYPE_WSTR)

Sandini Bib 14.4

Schema mit Datenbank synchronisieren

401

pColumnDesc[i].ulColumnSize--; pColumnDesc[i].bPrecision = pBindings[i].bPrecision; pColumnDesc[i].bScale = pBindings[i].bScale; } hr = CreateTable(szTableName, pColumnDesc , nColumns , pPropSet , cPropertySets , out ); delete [] pColumnDesc; delete [] pBindings; delete [] pNames; return hr; }

Wenn Sie beispielsweise die folgende Zugriffsobjekt-Klasse deklarieren: class CdbomytableAccessor { public: LONG m_a; TCHAR m_b[11]; DBTIMESTAMP m_c; BEGIN_NAMED_COLUMN_MAP(CdbomytableAccessor) NAMED_COLUMN_ENTRY(0, L"a", m_a) NAMED_COLUMN_ENTRY(0, L"b",m_b) NAMED_COLUMN_ENTRY(0, L"c",m_c) END_COLUMN_MAP() } ;

brauchen Sie nur den folgenden Code zu schreiben um die zugehörige Tabelle zu erstellen: CdbomytableAccessor Accessor; // Tabelle löschen, wenn diese bereits vorhanden ist. TableDefinition.DropTable("my_table"); // Tabelle anhand der Zugriffsobjekt-Informationen erstellen. hr = TableDefinition.CreateTableByAccessor("my_table", Accessor);

Obwohl dieses Verfahren sehr praktisch ist, hat es auch Nachteile: 왘 Das Zugriffsobjekt muss alle Spalten der Tabelle enthalten. 왘 Es sind keine Typennamen vorhanden. Daher wählt der Datenspeicher den am besten passenden Typ aus. 왘 Es werden keine Spalteneigenschaften angegeben. Der Datenspeicher verwendet somit die Standard-Spalteneigenschaften.

Sandini Bib 402

14

Schemata, Definitionen und Synchronisation

14.5 Zusammenfassung In diesem Kapitel wurde beschrieben, wie Sie über Klassen der OLE DB Consumer Templates auf die Schema-Daten zu einer Datenbank zugreifen können, wie Sie das Schema über die OLE DB-Erweiterungen bearbeiten und die verschiedenen Komponenten mit der Datenbank synchronisieren können. Abschließend wurde erläutert, wie Sie OLE DB für relationale Provider verwenden. In den beiden folgenden Kapiteln werden wir uns zwei anderen Provider-Typen zuwenden: mehrdimensionalen und hierarchischen Providern.

Sandini Bib

15 OLAP In den vorherigen Kapiteln sind wir immer davon ausgegangen, dass die Daten in einer Reihe von Tabellen vorliegen, auf die über ein Zeilenmengen-Objekt (Rowset) zugegriffen wird. OLE DB unterstützt allerdings nicht nur Tabellen-Daten, sondern auch mehrdimensionale Daten. Diese Art der Verarbeitung wird als OLAP (OnLine Analytical Processing, analytische Online-Verarbeitung) bezeichnet. Die Unterstützung für OLAP, die als OLE DB für OLAP bezeichnet wird, wurde mit OLE DB 2.0 eingeführt. Bevor wir uns näher mit den mehrdimensionalen Daten beschäftigen, müssen Sie wissen, dass die OLE DB Consumer Templates diese Daten nicht unterstützen. Aus diesem Grund müssen Sie entweder die OLE DB-Erweiterungen verwenden, die zusammen mit diesem Buch zur Verfügung gestellt werden, oder die unspezifischen OLE DB-Schnittstellen (Raw Interfaces). In diesem Kapitel wird davon ausgegangen, dass Sie die OLE DB-Erweiterungen verwenden. Die Erweiterungen, die zur Unterstützung mehrdimensionaler Daten benötigt werden, sind überraschend klein und ihre Umwandlung in unspezifischen OLE DB-Code ist unkompliziert. OLAP selbst ist eine komplexe Technologie, die in einem Kapitel nicht auch nur annähernd abgedeckt werden kann. Die einfache Einführung, die ich in diesem Kapitel geben möchte, beschäftigt sich hauptsächlich damit, wie OLE DB für OLAP auf die Daten zugreift. Wenn Sie noch keine Erfahrungen mit OLAP haben, ist die folgende Einführung wahrscheinlich zu summarisch. In diesem Fall müssen Sie sich noch weiter zu diesem Thema informieren.

15.1 Tabellendaten und mehrdimensionale Daten Bei herkömmlichen Tabellendaten (relationalen Daten) verwaltet der Server eine Reihe von Tabellen. Auf der Consumer-Seite hat der Programmierer mit Zeilenmengen zu tun. Vereinfacht gesagt, handelt es sich bei beiden Objekten um zweidimensionale Arrays, in denen die Zeilen die erste Dimension darstellen und die Spalten die zweite. Abbildung 15.1 zeigt eine Tabelle (Zeilenmenge) mit drei Spalten und vier Zeilen.

Sandini Bib 404

15

Spalte

Spalte

Spalte

A

B

C

OLAP

Zeile 1 Zeile 2 Zeile 3 Zeile 4 Abbildung 15.1: Zweidimensionale Tabelle (Zeilenmenge)

In der Praxis gestaltet sich dieses Konzept ein wenig komplizierter, weil Zeilen und Spalten nicht dieselbe Funktion haben: Spalten haben Namen, und es ist möglich, direkt auf diese zuzugreifen, während Zeilen »anonym bleiben«. Daher werden Tabellen und Zeilenmengen nicht wie normale zweidimensionale Anordnungen aus Zeilen und Spalten verwendet. Als Beispiel soll uns im Folgenden eine Datenbank dienen, mit der Vertriebsdaten verwaltet werden. Jeder Vertriebsmitarbeiter (Salesperson) schließt in einem bestimmten Zeitraum eine gewisse Anzahl von Verträgen ab. Tabelle 15.1 zeigt, wie dieser Sachverhalt in einer typischen Implementierung als Tabelle einer relationalen Datenbank dargestellt würde. Eine solche Tabelle ist jedoch nicht sehr übersichtlich. Experten für relationale Datenbanken kommen wahrscheinlich mit dieser Art der Darstellung gut zurecht, während ein Laie die natürlichere Darstellung wie in Tabelle 15.2 bevorzugen würde. Salesperson (Mitarbeiter)

Date (Jahr)

Amount (Anzahl)

Albert

1998

4

Albert

1999

5

Bob

1998

6

Bob

1999

2

Charlie

1998

5

Charlie

1999

8

Tabelle 15.1: Vertriebstabelle – Datenbankversion

Date (Jahr) Salesperson (Mitarbeiter)

1998

1999

Albert

4

5

Bob

6

2

Charlie

5

8

Tabelle 15.2: Vertriebstabelle – Für Laien übersichtlichere Version

Sandini Bib 15.1

Tabellendaten und mehrdimensionale Daten

405

In der Praxis können die Daten schnell noch sehr viel komplexer werden. Unsere ursprüngliche Tabelle könnte beispielsweise eine weitere Spalte aufweisen, in der das Geschlecht des jeweiligen Kunden angegeben ist (Gender). In diesem Fall könnte man sich die Daten als dreidimensionales Objekt vorstellen, wobei die erste Dimension den Mitarbeiter darstellt, die zweite das Jahr und die dritte das Geschlecht der Kunden. Da dieses Objekt drei Dimensionen aufweist, wird es über einen Würfel dargestellt (siehe Abbildung 15.2). Natürlich gibt es keinen Grund, bei der dritten Dimension aufzuhören. Es sind genauso gut auch Objekte mit vier, fünf oder mehr Dimensionen vorstellbar. Da wir jedoch in einer dreidimensionalen Welt leben, können wir Objekte graphisch nur mit bis zu drei Dimensionen darstellen. Dennoch hat sich der Name »Würfel« für solche mehrdimensionalen Objekte durchgesetzt, auch wenn sie mehr als drei Dimensionen aufweisen. Ein Tabellen-Provider verarbeitet also eine Reihe von Tabellen, ein mehrdimensionaler Provider eine Reihe von Würfeln. Würfel können über eine Reihe von relationalen Tabellen in einem sternförmigen Schema implementiert werden. Ein weiterer Ansatz wäre, den Würfel als natives Element zu implementieren. Während Tabellen eine Reihe von Spalten aufweisen, verfügen Würfel für jede Dimension über eine Achse. Abbildung 15.2 zeigt die drei Achsen für den Würfel unserer Vertriebsdatenbank: Salesperson, Date und Gender. OLE DB-Zeilenmengen-Objekte sind für Tabellen-Daten gut geeignet, für mehrdimensionale Provider wird allerdings ein neues COM-Objekt benötigt, das den Aspekt der Mehrdimensionalität berücksichtigt: das Datenmengen-Objekt (Dataset). Dies ist das einzige neue COM-Objekt, das von OLE DB für OLAP eingeführt wird, und gleichzeitig das zentrale Objekt in OLE DB für OLAP. Datenmengen brauchen nicht dieselbe Anzahl von Dimensionen aufzuweisen wie der zugrunde liegende Würfel, weil es sich bei ihnen um das Ergebnis einer oder mehrerer Projektionen handeln kann. So wäre es beispielsweise denkbar, dass eine Datenmenge die Vertragsabschlüsse für Kunden eines bestimmten Geschlechts angibt, so dass hier die Achse Gender gar nicht benötigt würde. Der Hauptunterschied zwischen Datenmengen und Zeilenmengen besteht darin, dass Datenmengen keine Zeilen enthalten, weil dies in einem mehrdimensionalen Konzept nicht sinnvoll wäre. Statt dessen weisen Datenmengen Zellen auf, die zur Darstellung von Datenelementen der Datenmenge dienen (siehe Abbildung 15.2). Im Gegensatz zu Zeilen weisen Zellen keine Spalten auf, sondern entsprechen jeweils einem einzigen Datenpunkt. SQL hat sich als Standard für relationale Daten durchgesetzt, kann aber für mehrdimensionale Provider nicht verwendet werden. Hier kommt eine neue Sprache ins Spiel. Microsoft versucht mit MDX (Multidimensional Expression) einen solchen Stan-

Sandini Bib 406

15

OLAP

SalespersonAchse (Mitarbeiter)

Date-Achse (Jahr) 1998

M

F

dard zu etablieren. Provider, die OLE DB für OLAP nutzen, sind jedoch nicht gezwungen, MDX zu nutzen, sondern können eine eigene, angepasste Sprache einsetzen. MDX ist sehr leistungsfähig und weist viele Funktionen auf, die aber nicht Gegenstand dieses Buches sein sollen. Wenn Sie weitere Informationen zu diesem Thema benötigen, lesen Sie die entsprechende Dokumentation von Microsoft.

1999

Albert Bob Charlie Zelle

e hs Ac ht) r c e nd hle Ge esc (G

Abbildung 15.2: Dreidimensionaler Würfel

Abbildung 15.3 zeigt, wie MDX für mehrdimensionale Provider verwendet wird. In Tabelle 15.3 sind die Unterschiede zwischen Tabellen-Providern und mehrdimensionalen Providern im Überblick zusammengestellt. OLE DB für OLAP verwendet normale OLE DB-Objekte ständig wieder. Datenquellen-, Sitzungs-, Fehler- und Befehlsobjekte verhalten sich bei beiden Arten von Providern gleich. OLE DB für OLAP führt einige zusätzliche Eigenschaften ein, die jedoch wie alle übrigen OLE DB-Eigenschaften verwendet werden. Der folgende Abschnitt erläutert, wie ein mehrdimensionaler Provider mit OLE DB für OLAP verwendet wird. Da der Code in vieler Hinsicht dem normalen OLE DB-Code entspricht, will ich mich in diesem Kapitel auf die Bereiche konzentrieren, die OLAP-spezifisch sind.

Tabelle

Würfel

SQL

MDX

Zeilenmenge

Datenmenge

Abbildung 15.3: MDX wandelt Würfel in Datenmengen um

Sandini Bib 15.2

Mehrdimensionale Sitzungen und Befehle

407

Tabellen-Provider (Relationale Datenbank)

Mehrdimensionaler Provider

Tabelle

Würfel

Zeilenmenge

Datenmenge

SQL

MDX

Spalte

Achse

Zeile

Zelle

Dimension = 2

Beliebig viele Dimensionen

Tabelle 15.3: Vergleich zwischen Tabellen-Provider und mehrdimensionalem Provider

15.2 Mehrdimensionale Sitzungen und Befehle In einigen Fällen ist CSession nicht zum Arbeiten mit mehrdimensionalen Sitzungen geeignet. Der Sitzungstyp für Tabellen-Provider ist anders definiert als der für mehrdimensionale Provider: Während IOpenRowset für Sitzungen von Ersteren obligatorisch zu verwenden ist, hat diese Schnittstelle für Sitzungen von letzteren nur optionalen Charakter. CSession wurde geschrieben, bevor es OLE DB 2.0 gab, und nimmt daher an, dass die Sitzung IOpenRowset unterstützt. Aus diesem Grund enthält CSession eine Referenz auf eine IOpenRowset-Schnittstelle: CComPtr m_spOpenRowset;

Einige mehrdimensionale Provider unterstützen diese Schnittstelle nicht, so dass wir eine neue Sitzungsklasse benötigen. Die Lösung besteht darin, eine Referenz auf die immer obligatorische Schnittstelle IGetDataSource zu verwenden. Die in den OLE DBErweiterungen enthaltene Sitzungsklasse CMDSession entspricht, abgesehen von ihrem Klassenelement, der Klasse CSession: CComPtr m_spGetDataSource;

Außerdem wird CCommand immer mit einem Exemplar von CSession geöffnet. Deshalb benötigen wir eine neue Befehlsklasse, die statt dessen ein Exemplar von CMDSession akzeptiert. Diese neue Befehlsklasse aus den OLE DB-Erweiterungen heißt CMDCommand und entspricht CCommand, abgesehen davon, dass ihre Open-Methode ein Exemplar von CMDSession akzeptiert. // Befehl für die Sitzung erstellen und ausführen HRESULT Open(const CMDSession& session, LPCTSTR szCommand =NULL, DBPROPSET *pPropSet = NULL, LONG* pRowsAffected = NULL, REFGUID guidCommand = DBGUID_DEFAULT, bool bBind = true)

Sie können CSession und CCommand auch weiterhin mit mehrdimensionalen Providern wie MS OLAP verwenden, welche die Schnittstelle IOpenRowset unterstützen. Andersherum ist auch die Verwendung von CMDSession und CMDCommand für Tabellen-Provider möglich, da diese IGetDataSource immer unterstützen.

Sandini Bib 408

15

OLAP

15.3 Datenmengen Der Cube Browser, der zusammen mit Microsoft OLAP Manager ausgeliefert wird, ermöglicht ein intuitives Verständnis der Datenmengen. Andere mehrdimensionale Provider stellen unter Umständen andere Tools zur Verfügung. Da Würfelobjekte, wie bereits erwähnt, auch mehr als drei Dimensionen aufweisen können, lassen sie sich schlecht darstellen. Eine übliche Darstellungsweise besteht darin, die Daten so zu projizieren, dass sie in zwei Dimensionen sichtbar sind. Wie Abbildung 15.4 zeigt, weist der Cube Browser zwei Bereiche auf: Im oberen Teil werden alle Dimensionen gezeigt, im unteren Bereich die Projektion auf zwei Achsen (hier Year und Gender). Mit dem Cube Browser können Sie die Projektion ändern, indem Sie die Dimensionen mit Hilfe der Maus an die gewünschte Stelle ziehen.

Abbildung 15.4: Der Cube Browser

Das Datenmengen-Objekt ist das einzige COM-Objekt, das von OLE DB für OLAP neu eingeführt wird. Im weiteren Verlauf dieses Abschnitts werde ich dieses COM-Objekt und die zugehörigen C++-Klassen erläutern.

15.3.1 Das Datenmengen-Objekt Wie Abbildung 15.5 zeigt, unterstützt das Datenmengen-Objekt die folgenden drei Schnittstellen: 왘 IMDDataset 왘 IMDRangeRowset 왘 IMDFind

Sandini Bib 15.3

Datenmengen

409

IMDDataset ist die Hauptschnittstelle. Sie dient zum Abrufen von Achseninformationen und Zellendaten. IMDDataset ist die einzige obligatorische Schnittstelle. Über IMDRangeRowset wird ein praktisches Verfahren zum Zugriff auf eine Zellenmenge über herkömmliche Zeilenmengen zur Verfügung gestellt. Die Schnittstelle IMDFind wird in

diesem Buch nicht behandelt.

15.3.2 CDataset Die OLE DB-Erweiterungen stellen die Klasse CDataset zur Verfügung, die das OLE DB-Datenmengen-Objekt kapselt. Diese Klasse wird genauso verwendet wie die Klasse CRowset. Insbesondere kann CDataset zusammen mit einer Zugriffsobjekt-Klasse verwendet werden. So könnten Sie eine Datenmengen-Variable wie folgt deklarieren: CCommand MyDataSet; // oder CMDCommand MyDataSet;

DatenmengenObjekt

IMDDataset IMDRangeRowset IMDFind

Abbildung 15.5: Das Datenmengen-Objekt

CDataset ist so aufgebaut, dass diese Klasse an den Stellen verwendet werden kann, an

denen eine Zeilenmenge erwartet wird. Dies bedeutet, dass diese Klasse alle Methoden implementiert, die auch in CNoRowset deklariert sind. Trotzdem enthält CDataset eine Referenz auf einen IMDDataset-Schnittstellenzeiger und nicht auf ein ZeilenmengenObjekt. Daher gibt GetIID den Wert IID_IMDDataset zurück. Obwohl CDataset eine Referenz auf eine IMDDataset-Schnittstelle enthält, müssen für diese Klasse die Methoden GetInterface und GetInterfacePtr vorhanden sein, die denselben Rücklieferungstyp aufweisen wie bei den Versionen für Zeilenmengen. Hierzu muss reinterpret_cast verwendet werden: IRowset* GetInterface() const { return reinterpret_cast (m_spDataset.p); } IRowset** GetInterfacePtr() { return reinterpret_cast (&m_spDataset); }

CDataset weist das Klassenelement m_pAccessor auf, das genauso auf das Zugriffsobjekt verweist, wie dies bei CRowset der Fall ist. Das Zugriffsobjekt kann über die Methode SetAccessor festgelegt werden.

Sandini Bib 410

15

OLAP

15.3.3 Verwendung von Datenmengen Bei der Verwendung von Datenmengen besteht der erste Schritt darin, die gesamten Metadaten abzurufen. Hierzu gehören die Anzahl der Achsen sowie Informationen zu den einzelnen Achsen. Im zweiten Schritt werden die Daten der Datenmenge (Zellenwerte und -bereiche) abgerufen. Diese Schritte werden in den folgenden Abschnitten erläutert.

15.4 Metadaten: Informationen zu den Achsen Das Klassenelement m_cAxis enthält die Anzahl der Achsen, die OLE DB zurückliefert. Beachten Sie dabei, dass OLE DB eine Achse hinzufügt, welche die für die Projektion verwendeten Dimensionen enthält, d.h. alle Dimensionen des Würfels, die nicht in einer anderen Achse dargestellt werden. Diese Zusatzachse wird auch als Slicer bezeichnet. Die in Tabelle 15.2 dargestellte Datenmenge verfügt über zwei Achsen. Trotzdem ist der Wert im m_cAxis-Klassenelement 3:2 (»echte« Achsen plus 1 für die Zusatzachse). Das Klassenelement m_AxisInfo enthält Achseninformationen zu allen Achsen einschließlich der Zusatzachse. Es handelt sich hier um ein Array aus MDAXISINFO-Strukturen, die folgendermaßen definiert sind: typedef struct { ULONG ULONG ULONG ULONG ULONG * LPOLESTR * } MDAXISINFO

MDAXISINFO cbSize; iAxis; cDimensions; cCoordinates; rgcColumns; rgpwszDimensionNames;

cbSize gibt die Größe der MDAXISINFO-Struktur in Bytes an, iAxis den Achsenbezeichner (MDAXIS_COLUMNS für die erste Achse, MDAXIS_ROWS für die zweite Achse usw., die Zusatzachse (Slicer) wird mit MDAXIS_SLICERS bezeichnet).

Die in Tabelle 15.2 dargestellte Datenmenge weist drei MDAXISINFO-Strukturen auf. Die Werte für iAxis sind MDAXIS_COLUMNS, MDAXIS_ROWS und MDAXIS_SLICERS. cCoordinates gibt die Anzahl der Achsenelemente an. Für die Spaltenachse der in Tabelle 15.2 dargestellten Tabelle beispielsweise gilt cCoordinates = 2, für die Zeilenachse cCoordinates = 3. rgcColumns dient zur Angabe der Anzahl von Spalten für die einzelnen Dimensionen (weitere Informationen zu diesem Thema finden Sie im Abschnitt über Achsen-Zeilenmengen).

Sandini Bib 15.4

Metadaten: Informationen zu den Achsen

411

cDimensions und rgpwszDimensionNames werden für mehrdimensionale Achsen verwendet. Näheres hierzu können Sie im Abschnitt über Achsen mit mehreren Dimensionen nachlesen. Es ist nicht notwendig, m_cAxis und m_AxisInfo explizit zu setzen, weil CDataset dies automatisch tut. Da CDataset als Teil der Klasse CAccessorRowset verwendet wird, ruft CAccessorRowset die Methode BindFinished auf, nachdem das COMObjekt erfolgreich geöffnet wurde. Die jeweilige Version von CDataset::BindFinished ruft die Achseninformationen ab: HRESULT BindFinished() { return GetAxisInfo(&m_cAxis, &m_AxisInfo); }

GetAxisInfo wiederum ruft die OLE DB-Methode auf, welche die Achseninformatio-

nen abruft. Die Methode Close gibt die Achseninformationen automatisch wieder frei: void Close() { if (m_spDataset) { if (m_AxisInfo) { m_spDataset->FreeAxisInfo(m_cAxis, m_AxisInfo); m_AxisInfo = NULL; } m_spDataset.Release(); } }

15.4.1 Durch die Achseninformationen navigieren Eine Achse ist nichts anderes als eine Zeilenmenge, bei der jede Zeile eine Koordinate darstellt. Diese Achsenkoordinaten werden auch als Tupel bezeichnet. Jede Zeile (bzw. jede Koordinate oder jedes Tupel) enthält die folgenden Spalten: 왘 Die Nummer des Tupels gibt die Positionsnummer des Tupels auf der Achse an. 왘 Die Elementüberschrift gibt den Namen der Koordinate an. 왘 Weitere Informationen. Die in Tabelle 15.2 gezeigte Tabelle beispielsweise weist zwei Achsen auf: Jahr (Date) und Mitarbeiter (Salesperson). Tabelle 15.4 enthält eine vereinfachte Version der Zeilen der ersten Achsen-Zeilenmenge, Tabelle 15.5 zeigt die Achse für die Mitarbeiter (Salesperson).

Sandini Bib 412

15

OLAP

Hierarchien In vielen Fällen sind Dimensionen hierarchisch organisiert. Dabei entsteht eine Baumstruktur, in der die Dimensionselemente die Blätter darstellen. Ein Beispiel hierfür ist in Abbildung 15.6 zu sehen. Nummer des Tupels

Elementüberschrift

0

1998

1

1999

Tabelle 15.4: Achsen-Zeilenmenge für die Jahresachse (Date)

Nummer des Tupels

Elementüberschrift

0

Albert

1

Bob

2

Charlie

Tabelle 15.5: Achsen-Zeilenmenge für die Mitarbeiterachse (Salesperson)

United States

Alabama

...

...

Massachusetts

Ebene (0) (Country)

...

Boston

Albert

New York

Albany

Bob

Ebene (1) (State)

Ebene (2) (City) Ebene (3) (Salesperson)

Abbildung 15.6: Beispiel für eine hierarchische Struktur

Wenn der Cube Browser eine Hierarchie findet, wird neben den Elementen, zu denen untergeordnete Elemente vorhanden sind, ein Pluszeichen (+) angezeigt. In Abbildung 15.7 ist der Cube Browser mit der erweiterten Dimension für die Mitarbeiter gezeigt. Obwohl das Konzept der Hierarchie in OLAP von großer Bedeutung ist, hat es keine großen Auswirkungen auf OLE DB, weil es sich dabei um statische Informationen handelt. Datenmengen hängen nicht von Hierarchien ab, sondern nur Achsenkoordinaten. In der Achsen-Zeilenmenge gibt das Klassenelement m_LevelUniqueName den Namen der Koordinatenebene in der Hierarchie an. Wenn es sich dabei um die Stadt handelt, lautet der Name City. Das Klassenelement m_LevelNumber stellt die Tiefe der Koordinatenebene dar, bei City ist dies Ebene 2.

Sandini Bib 15.4

Metadaten: Informationen zu den Achsen

413

Abbildung 15.7: Darstellung der Hierarchie im Cube Browser

Verwendung von Achsen-Zeilenmengen Die OLE DB Consumer Templates stellen zwei Klassen zum Navigieren durch die Achseninformationen zur Verfügung: 왘 CAxisRowset ist die Zeilenmengen-Klasse. Sie wird wie eine Tabelle oder ein Befehl verwendet. 왘 CAxisAccessor ist die Zugriffsobjekt-Klasse. Sie weist einen Template-Parameter für die Dimension auf (in der Voreinstellung ist dies der Wert 1). Sie könnten beispielsweise die folgende Variable deklarieren: CAxisRowset AxisRowset;

CAccessorRowset definiert nur die zusätzliche Open-Methode: HRESULT Open(const CDataset& DataSet, DBCOUNTITEM iAxis, DBPROPSET* PropSets = NULL, ULONG cPropertySets = 1)

DataSet ist die Datenmenge, in der die Achse enthalten ist, iAxis ist die Nummer der

Achse.

Sandini Bib 414

15

OLAP

Der folgende Code zeigt die Verwendung von CAxisRowset und CAxisAccessor: CAxisRowset AxisRowset; // Spaltenachse öffnen hr = AxisRowset.Open(aDateset, MDAXIS_COLUMNS); // AxisRowset wie eine normale Zeilenmenge verwenden hr = AxisRowset.MoveFirst(); // usw.

15.4.2 Achsen mit mehreren Dimensionen Bisher sind wir implizit davon ausgegangen, dass jede Achse einer Dimension entspricht. In unserem Beispiel war die Spaltenachse für die zeitliche Dimension (Date) zuständig, die Zeilenachse für die Mitarbeiter (Salesperson). Es ist allerdings auch möglich, mehrere Dimensionen einer Achse zuzuordnen. In diesem Fall enthält die jeweilige Achse das kartesische Produkt der Dimensionselemente. Eine solche Konfiguration wird auch als Kreuzverknüpfung (Cross-Join) bezeichnet. Wenn die Werte der ersten Dimension {a, b, c} sind und die Werte der zweiten Dimension {A, B}, so sind die Werte der Kreuzverknüpfungsachse {(a, A), (a, B), (b, A), (b, B), (c, A), (c, B)}. Abbildung 15.8 zeigt, wie Kreuzverknüpfungen im Cube Browser behandelt werden. Die Zeilenachse weist zwei Dimensionen auf: die für das Jahr (Year) und die für das Geschlecht der Kunden (Gender).

Abbildung 15.8: Die Zeilenachse als Kreuzverknüpfung zwischen den Dimensionen für das Geschlecht (Gender) und für das Jahr (Year) auf der Länderebene (Country)

Sandini Bib 15.4

Metadaten: Informationen zu den Achsen

415

Die Tatsache, dass mehrere Dimensionen einer Achse zugeordnet sind, hat keine Auswirkungen auf die Werte in den Zellen. Diese Art der Darstellung ist nur eine praktische Form der Projektion, weil es damit beispielsweise möglich ist, in einer zweidimensionalen Ansicht mehr als zwei Achsen darzustellen.

15.4.3 Achseninformationen für Datenmengen mit mehreren Dimensionen Jetzt wollen wir uns wieder der Struktur MDAXISINFO zuwenden um die übrigen Felder unter die Lupe zu nehmen. cDimensions gibt die Anzahl der Dimensionen an. Bei einer einzigen Dimension ist dies logischerweise der Wert 1. rgpwszDimensionNames ist ein Array, das die Namen aller Dimensionen enthält. Die Achseninformationen zu der in Abbildung 15.8 dargestellten Zeilenachse der Datenmenge lauten folgendermaßen: cDimensions = 2 und rgpwsz DimensionNames = { "Year", "Gender"}.

15.4.4 Verwendung von CAxisAccessor für Datenmengen mit mehreren Dimensionen Der Template-Parameter von CAxisAccessor gibt die Anzahl der Dimensionen der Achse an. Dieser Wert muss kleiner oder gleich der tatsächlichen Anzahl sein. Da den meisten Achsen nur eine Dimension zugeordnet ist, ist der vorgegebene Wert hierfür 1. Wir wollen CAxisAccessor einmal näher untersuchen um zu sehen, wie dieses Zugriffsobjekt funktioniert: template class CAxisAccessor { public: ULONG m_TupleOrdinal; BEGIN_COLUMN_MAP(CAxisAccessor) COLUMN_ENTRY(1, m_TupleOrdinal) for (ULONG iAxis = 0; iAxis < AxisDimension; iAxis++) { COLUMN_ENTRY(2+ iAxis * 5, m_Columns[iAxis].m_MemberUniqueName) COLUMN_ENTRY(3+ iAxis * 5, m_Columns[iAxis].m_MemberCaption) COLUMN_ENTRY(4+ iAxis * 5, m_Columns[iAxis].m_LevelUniqueName) COLUMN_ENTRY(5+ iAxis * 5, m_Columns[iAxis].m_LevelNumber) COLUMN_ENTRY(6+ iAxis * 5, m_Columns[iAxis].m_DisplayInfo) } END_COLUMN_MAP() CAxisColumns m_Columns[AxisDimension]; } ;

Die hier wichtigen Daten befinden sich im m_Columns-Array. Es enthält eine Menge von Elementen der Klasse CAxisColumns, welche die eigentlichen Daten enthält:

Sandini Bib 416

15

OLAP

class CAxisColumns { public: TCHAR m_MemberUniqueName [512]; TCHAR m_MemberCaption [512]; TCHAR m_LevelUniqueName [512]; LONG m_LevelNumber; ULONG m_DisplayInfo; } ;

Die Größe von m_Columns wird über den Template-Parameter AxisDimension angegeben. Abbildung 15.9 zeigt die Struktur eines Exemplars von CAxisAccessor. Beachten Sie, dass alle Klassenelemente im Zugriffsobjekt gebunden sind. Daher enthält CAxisAccessor die Informationen für alle Achsendimensionen und nimmt die entsprechenden Bindungen vor. Das folgende Beispiel zeigt CAxisAccessor mit mehreren Dimensionen: CAxisRowset AxisRowset; // Spaltenachse öffnen hr = AxisRowset.Open(aDateset, MDAXIS_ROWS); // AxisRowset wie eine normale Zeilenmenge verwenden hr = AxisRowset.MoveFirst(); // Die Elementnummer ist AxisRowset.m_TupleOrdinal // AxisRowset.m_Columns[0] enthält die Werte der // Dimension für die Mitarbeiter(salesperson) // AxisRowset.m_Columns[1] enthält die Werte der // Dimension für das Geschlecht (gender)

m_TupleOrdinal m_MemberUniqueName m_MemberCaption m_LevelUniqueName

m_Columns[0]

m_LevelNumber m_DisplayInfo m_MemberUniqueName m_MemberCaption m_LevelUniqueName

m_Columns[1]

m_LevelNumber m_DisplayInfo

Abbildung 15.9: Struktur von CAxisAccessor

Sandini Bib 15.5

Zellen

417

15.5 Zellen Zellen in einer Datenmenge unterscheiden sich von Zeilen in einer Zeilenmenge. Da Datenmengen mehrere Dimensionen aufweisen, gibt es kein Navigationskonzept, keine erste oder nächste Zeile. Daher ist es möglich, von einer Zelle zu einer anderen zu springen. Hierzu müssen die einzelnen Zellen eindeutig identifizierbar sein. Zellen weisen keine Spalten auf, sondern beziehen sich immer nur auf einen einzigen Wert. Daher wird für sie ein besonderes Zugriffsobjekt benötigt.

15.5.1 Wie werden Zellen identifiziert? Zellennummern dienen zur eindeutigen Identifikation von Zellen innerhalb der Datenmenge. Die Zählung beginnt bei null für diejenige Zelle, die dem ersten Tupel der jeweiligen Achse entspricht. Die Nummern werden anfangs entlang der ersten Achse hochgezählt. Am Ende der ersten Achse beginnt die Inkrementierung entlang der zweiten Achse, wobei wieder in derselben Richtung wie bei der ersten Achse vorgegangen wird. Danach erfolgt dieselbe Art der Inkrementierung für die übrigen Achsen. Tabelle 15.6 zeigt die Zellennummern für eine zweidimensionale Datenmenge, Abbildung 15.10 die Zellennummern für eine dreidimensionale Datenmenge. Jahr (Date) Mitarbeiter (Salesperson)

1998

1999

Albert

0

1

Bob

2

3

Charlie

4

5

Tabelle 15.6: Zellennummern in einer zweidimensionalen Datenmenge

M

F

Date-Achse (Jahr) 1998

1999

SalespersonAchse (Mitarbeiter)

6

7

Albert

0

8

1

9

Bob

2

10

3

11

Charlie

4

5

ernd e t) e G chs ch A hle c es (G

Abbildung 15.10: Zellennummern in einer dreidimensionalen Datenmenge

Sandini Bib 418

15

OLAP

OLE DB stellt keine Funktion zum Abrufen der Zellennummer zur Verfügung. Dies ist allerdings relativ einfach, sobald die Achse bekannt ist, zu der die Zelle gehört. CDataset enthält einen Verweis auf die Achseninformationen, so dass dadurch die Zellennummer abgerufen werden kann. Außerdem gibt die Methode GetCellOrdinal die Zellennummer für eine Menge von Achsenkoordinaten zurück. GetCellOrdinal geht davon aus, dass das Array aus Achsenkoordinaten der Anzahl von Achsen entspricht und dass die Koordinaten in geordneter Form vorliegen. Die Implementierung sieht folgendermaßen aus: ULONG GetCellOrdinal(ULONG pAxisOrdinal[]) { ATLASSERT(m_AxisInfo); ULONG Result = 0; ULONG Temp = 1; for (ULONG i = 0; i < m_cAxis; i++) { if (AxisInfo(i).iAxis != MDAXIS_SLICERS) { ATLASSERT(pAxisOrdinal[i] < AxisInfo(i).cCoordinates); Result += pAxisOrdinal[i] * Temp; Temp *= AxisInfo(i).cCoordinates; } } return Result; }

Bei einer dreidimensionalen Datenmenge würde der Aufruf wie folgt lauten: ULONG Coordinates [] = { ColumnCoordinate, RowCoordinate, PageCoordinate} ; CellOrdinal = DataSet.GetCellOrdinal(Coordinates);

Der folgende Code darf jedoch nicht aufgerufen werden, weil die Anzahl der Koordinaten nicht stimmt (hier fehlt eine Koordinate): ULONG Coordinates [] = { ColumnCoordinate, RowCoordinate} ; CellOrdinal = dataset.GetCellOrdinal(Coordinates);

Auch der folgende Code darf nicht verwendet werden, weil die Koordinaten nicht in der richtigen Reihenfolge angegeben wurden: ULONG Coordinates [] = { RowCoordinate, ColumnCoordinate, PageCoordinate} ; CellOrdinal = dataset.GetCellOrdinal(Coordinates);

Sandini Bib 15.5

Zellen

419

15.5.2 Zugriffsobjekte für Zellen Datenmengen unterscheiden sich auch insofern von Zeilenmengen, weil die kleinste Einheit hier die Zelle ist und keine Zeile. Zum Abrufen von Daten wird allerdings in beiden Fällen der gleiche Zugriffsobjekt-Mechanismus verwendet. Zu jeder Zelle gehören drei verschiedene Informationselemente. Für das Zugriffsobjekt handelt es sich bei allen drei Elementen um »Spalten«, wobei aber alle drei Spalten zu derselben Zelle gehören. In diesem Zusammenhang sei darauf hingewiesen, dass die Spalten des Zugriffsobjekts nichts mit der Spalte der Zelle selbst zu tun haben. Dies möchte ich an einem Beispiel verdeutlichen: Tabelle 15.7 zeigt eine Datenmenge mit den Spalten 1 und 2. Innerhalb dieser Spalten gilt Folgendes: 왘 Der Wert stellt den tatsächlichen Wert der Zelle dar. Er kann typisiert sein. 왘 Der formatierte Wert ist eine String-Darstellung des Wertes, der zur Anzeige dient. 왘 Die Nummer ist die Positionsnummer der Zelle innerhalb der Datenmenge. Die Spalten des Zugriffsobjekts lassen sich nicht ändern. Da man davon ausgehen kann, dass Sie immer dasselbe Zugriffsobjekt verwenden werden, stellen die OLE DBErweiterungen mit CCellAccessor eine fertige Zugriffsobjekt-Klasse zur Verfügung. Spalte 1

Spalte 2

Zeile 1

Wert

Formatierter Wert

Zellennummer

Wert

Formatierter Wert

Zellennummer

Zeile 2

Wert

Formatierter Wert

Zellennummer

Wert

Formatierter Wert

Zellennummer

Tabelle 15.7: Spalten des Zugriffsobjekts für eine Datenmenge template class CCellAccessor { public: TValueType m_Value; TCHAR m_FormattedValue [512]; LONG m_CellOrdinal; BEGIN_COLUMN_MAP(CCellAccessor) COLUMN_ENTRY(1, m_Value) COLUMN_ENTRY(2, m_FormattedValue) COLUMN_ENTRY(3, m_CellOrdinal) END_COLUMN_MAP() } ;

CCellAccessor dient zum Binden aller drei Spalten. Der Template-Parameter TValueType stellt den Wertetyp für die jeweilige Zelle dar. Wenn beispielsweise bekannt ist,

Sandini Bib 420

15

OLAP

dass die Zelle Doppelbyte-Werte enthält, verwenden Sie CCellAccessor. Ist der Typ der Zelle dagegen nicht bekannt, so ist es sinnvoll, CCellAccessor oder CCellAccessor zu verwenden. Die Verwendung von CCellAccessor ist nur eine der Möglichkeiten für ZellenZugriffsobjekte. Wenn das Binden aller drei Spalten zu aufwändig ist, können Sie eine andere Zugriffsobjekt-Klasse erstellen, die weniger Spalten bindet. Es ist aber auch möglich, noch mehr Informationen zu binden, wie beispielsweise den Status des Zellenwertes. Da die Zugriffsobjekte hier genauso funktionieren wie bei den Zeilenmengen, gelten die entsprechenden Ausführungen aus dem Kapitel über die Zugriffsobjekte auch hier.

15.5.3 Zellendaten abrufen Zellendaten können anhand der Zellenpositionsnummer oder über die Koordinaten abgerufen werden. Die erste GetCellData-Methode ruft die Daten an einer bestimmten Positionsnummer ab: HRESULT GetCellData(DBORDINAL ulCell, int nAccessor = 0)

ulCell stellt die Zellennummer dar, nAccessor steht für den Index des Zugriffsobjekts.

In vielen Fällen gibt es pro Zelle nur ein Zugriffsobjekt, so dass die Verwendung des Parameters nAccessor = 0 zulässig ist. Die zweite Methode ruft Zellendaten über Koordinaten ab: HRESULT GetCellData (ULONG pAxisOrdinal[], int nAccessor = 0)

Für die Koordinaten gelten dieselben Zwangsbedingungen wie für die Methode GetCellOrdinal. Das folgende Beispiel zeigt, wie Zellendaten mit den beiden Methoden abgerufen werden: // Zellendaten für Zellennummer 2 abrufen hr = Command.GetCellData (2); // Zellendaten für die Koordinaten (2, 2) abrufen ULONG Coordinates [] = { 2, 2} ; hr = Command.GetCellData(Coordinates);

Die bei Zeilenmengen geltenden Regeln für die Speicherverwaltung sind auch bei Datenmengen gültig. Wenn das Zugriffsobjekt eine Spalte über eine Referenz bindet und angibt, dass der zugehörige Speicher vom Client verwaltet wird, muss der Consumer die Methode FreeRecordMemory aufrufen, wenn die Daten nicht mehr benötigt werden.

Sandini Bib 15.5

Zellen

421

15.5.4 Zellenbereiche Im vorigen Abschnitt wurde erläutert, wie Daten für jeweils eine Zelle abgerufen werden. Diese Vorgehensweise kann natürlich die Ausführungsgeschwindigkeit der Anwendung beeinträchtigen. Eine Lösung hierfür besteht darin, mehrere Zellen gleichzeitig in Form eines Zellenbereichs abzurufen. Ein Zellenbereich enthält alle Zellen in der größten Unter-Datenmenge zwischen einer Anfangs- und einer Endpositionsnummer. Abbildung 15.11 zeigt eine Datenmenge mit 25 Zellen, in der die Zellen zwischen der Nummer 6 und der Nummer 18 durch Fettschrift gekennzeichnet sind. Dabei stellt der grau markierte Bereich das größtmögliche Rechteck in diesem Intervall dar. Dies ist der durch start = 6 und end = 18 definierte Bereich. OLE DB bietet zwei Möglichkeiten zum Abrufen eines Zellenbereichs: Einerseits erlaubt die OLE DB-Methode GetCellData das gleichzeitige Abrufen mehrerer Zellen. Durch direkten Aufruf dieser Methode lässt sich das Abrufen von Daten beschleunigen. Andererseits kann das OLE DB-Objekt für Bereichs-Zeilenmengen verwendet werden. Beide Verfahren werden in den folgenden Abschnitten erläutert.

Abbildung 15.11: Zellenbereich

Bereichs-Zeilenmengen Eine Bereichs-Zeilenmenge enthält eine Menge von Zeilen, von denen jede einer Zelle in einem bestimmten Bereich entspricht. So zeigt Tabelle 15.8 die Bereichs-Zeilenmenge für den Bereich, der in Abbildung 15.11 dargestellt ist. Das Zugriffsobjekt für die Bereichs-Zeilenmenge entspricht dem für die Zellen.

Sandini Bib 422

Zellenwert

15

Formatierter Zellenwert

OLAP

Zellennummer

Wert

Wert

6

Wert

Wert

7

Wert

Wert

8

Wert

Wert

11

Wert

Wert

12

Wert

Wert

13

Wert

Wert

16

Wert

Wert

17

Wert

Wert

18

Tabelle 15.8: Bereichs-Zeilenmenge für den Bereich aus Abbildung 15.11

Die Klasse CRangeRowset implementiert Bereichs-Zeilenmengen. Dabei handelt es sich einfach um einen weiteren Nachkommen der Klasse CAccessorRowset: template class CRangeRowset: public CAccessorRowset

Die für CRangeRowset voreingestellten Parameter für das Zugriffsobjekt und die Zeilenmenge sind für die meisten Fälle gut geeignet. Für diese Klasse ist mit Open() nur eine weitere Methode definiert. HRESULT Open(CDataset DSet, ULONG ulStartCell = 0, ULONG ulEndCell = -1, DBPROPSET* PropSets = NULL, ULONG cPropertySets = 1)

Es ist möglich, ein optionales Eigenschaften-Set anzugeben, das die Eigenschaften für die zu öffnende Bereichs-Zeilenmenge enthält. Nach dem Öffnen verhält sich die Bereichs-Zeilenmenge wie jede andere Zeilenmenge. Das folgende Beispiel zeigt, wie CRangeRowset verwendet wird: CRangeRowset RangeRowset; // Die voreingestellten Template-Parameter sind gut geeignet hr = RangeRowset.Open (DataSet, 6, 18); // Bereich zwischen den Zellen mit den Nummern 6 und 18 öffnen. hr = RangeRowset.MoveFirst(); // Navigation in der Bereichs-Zeilenmenge wie in einer normalen // Zeilenmenge while (hr == S_OK) { hr = RangeRowset.MoveNext(); }

Bereichs-Zeilenmengen unterstützen insbesondere das Setzen von Daten sowie verzögerte Aktualisierungen. In dieser Hinsicht verhalten sie sich wie normale Zeilen-

Sandini Bib 15.5

Zellen

423

mengen. So müssen Sie zum Öffnen einer Bereichs-Zeilenmenge angeben, welche Zeilenmengen-Schnittstelle verwendet werden soll. Allerdings können bei Datenmengen, im Gegensatz zu Zeilenmengen, keine Zeilen eingefügt oder gelöscht werden, da dies der Achsenstruktur entgegensteht. Daher lassen sich auch bei Bereichs-Zeilenmengen keine Zeilen einfügen oder löschen. Die in Kapitel 8 bezüglich der Zeilenmengen beschriebenen Optimierungsmöglichkeiten gelten auch für Bereichs-Zeilenmengen. So können Sie auch hier die Klassen CBulkRowset oder CArrayRowset verwenden. Nachdem die Bereichs-Zeilenmenge abgerufen wurde, müssen Sie allerdings wiederholt MoveNext aufrufen. Falls dies die Anwendungsausführung zu sehr verlangsamt, bietet OLE DB noch ein anderes Verfahren.

Zellenbereich abrufen Die OLE DB-Methode GetCellData ist leistungsfähiger, als bisher gezeigt wurde. Mit ihr ist es möglich, den ganzen Datenbereich genauso abzurufen wie eine Bereichs-Zeilenmenge. Es werden jedoch alle Daten sofort abgerufen, was das wiederholte Aufrufen von MoveNext und GetData vermeidet. Die Signatur von GetCellData lautet wie folgt: HRESULT GetCellData( HACCESSOR hAccessor, ULONG ulStartCell, ULONG ulEndCell, VOID * pData);

wobei 왘 hAccessor das Zellen-Zugriffsobjekt ist. 왘 ulStartCell und ulEndCell die Anfangs- bzw. Endzelle angeben. 왘 pData auf den Puffer verweist, der die Daten empfängt. Es ist besonders wichtig, dass der Puffer groß genug ist um alle abgerufenen Daten aufnehmen zu können. Das folgende Beispiel zeigt, wie ein Zellenbereich direkt abgerufen werden kann. CCommand Dataset; hr = Command.Open(Session, SomeText); CCellAccessor Values [13]; hr = Dataset.GetCellData(Dataset.m_pAccessor->GetHAccessor(0), 6, 18, Values);

Insgesamt bieten die OLE DB-Erweiterungen also drei Möglichkeiten des Zugriffs: 왘 Zugriff auf jeweils eine einzelne Zelle 왘 Zugriff über eine Bereichs-Zeilenmenge 왘 Zugriff über einen Zellenbereich

Sandini Bib 424

15

OLAP

Jedes dieser Verfahren hat seine Vor- und Nachteile. Welches der Programmierer letztendlich verwendet, hängt davon ab, welcher der Faktoren Leistungseinbuße, einfache Verwendung und Aktualisierungsunterstützung ihm am wichtigsten ist.

15.6 Über Zeilenmengen auf mehrdimensionale Daten zugreifen Obwohl Zeilenmengen-Objekte vielleicht nicht am besten für den Zugriff auf Datenmengen geeignet sind, empfiehlt sich ihre Verwendung dennoch in den Fällen, in denen andere Teile des Systems eine Zeilenmenge erwarten. In diesem Fall wird die Datenmenge durch die OLAP-Engine zu einer Zeilenmenge »plattgedrückt«, d.h. auf zwei Dimensionen reduziert. Der hierzu verwendete Algorithmus ist in der OLE DB-Dokumentation gut dokumentiert und kann nicht geändert werden. Er ist einfach zu verstehen, aber bei der Implementierung sind zahlreiche Einzelheiten zu beachten. Bei einer zweidimensionalen Datenmenge ist dieser Vorgang einfach nachvollziehbar: Die Spalten der Zeilenmenge enthalten die Beschriftungen für die Zeilenachse und die Werte der Spaltenachse. Tabelle 15.9 zeigt die Zeilenmenge, die der Datenmenge aus Tabelle 15.2 entspricht. Wenn die Datenmenge mehr als zwei Dimensionen aufweist, werden die übrigen Achsen auf die Spaltenachse abgebildet. Wenn man sich die dritte Dimension als »Tiefendimension« vorstellt, so wird diese bei der Reduzierung auf zwei Dimensionen so gedreht, dass sie anschließend in die Spaltendimension eingefügt werden kann. Tabelle 15.10 zeigt beispielsweise eine als Zeilenmenge dargestellte dreidimensionale Datenmenge. Zur Verwendung einer Datenmenge als Zeilenmenge sind zwei Schritte durchzuführen: Zunächst muss CDataset durch die geeignete Zeilenmengen-Klasse ersetzt werden. In den meisten Fällen wird dies CRowset sein. Außerdem müssen Sie eine Zugriffsobjekt-Klasse verwenden, die genügend Spalten aufnehmen kann. Ein ZellenZugriffsobjekt ist hierfür nicht geeignet, weil die Zeilenmenge auf eine ganze Zeile zugreift und nicht nur auf eine Zelle. Es empfiehlt sich die Verwendung von CDynamicAccessor, weil diese Klasse keine Informationen zu den Spalten benötigt. [Salesperson]

[Date].[1998]

[Date].[1999]

Albert

4

5

Bob

6

2

Charlie

5

8

Tabelle 15.9: Darstellung einer zweidimensionalen Datenmenge als Zeilenmenge

Sandini Bib 15.7

OLAP-Schema-Daten

425

[Salesperson] [1998].[Albany]

[1998].[Boston]

[1999].[Albany]

[1999].[Boston]

Albert

2

2

3

2

Bob

4

2

0

2

Charlie

3

2

1

7

Tabelle 15.10: Darstellung einer dreidimensionalen Datenmenge als Zeilenmenge

Wenn für die Datenmenge der folgende Code definiert wurde, CCommand Dataset; hr = Command.Open(Session, SomeText);

kann dieser durch den folgenden Code ersetzt werden: CCommand Rowset; hr = Rowset.Open(Session, CommandText); // der Rest wie gehabt

15.7 OLAP-Schema-Daten Im vorherigen Kapitel habe ich erklärt, wie Schema-Daten zu Tabellen-Providern abgerufen werden. So ist es z.B. möglich, die Tabellen einer bestimmten Datenbank und die Spalten einer bestimmten Tabelle aufzuzählen. Da es bei mehrdimensionalen Providern jedoch keine Tabellen und Spalten gibt, greift hier das übliche Schema nicht. Statt dessen definieren diese ihre eigenen Schema-Objekte wie Würfel, Dimensionen, Hierarchien oder Ebenen. Die OLE DB-Erweiterungen stellen eine Reihe von Klassen zur Verfügung, die diese Schema-Objekte genauso kapseln, wie dies bei herkömmlichen Schema-Daten der Fall ist.

15.8 Zusammenfassung Dieses Kapitel gab eine kurze Übersicht über die Mehrdimensionalität von Daten und die zugehörigen OLE DB-Erweiterungsklassen. OLE DB für OLAP ist eine faszinierende Technologie, weil sie auf einfache, intuitive Weise den Zugriff auf einen komplexen Daten-Server ermöglicht. Das einzige wirklich neue Konzept, das in diesem Kapitel eingeführt wurde, ist die Datenmenge.

Sandini Bib

Sandini Bib

16 Hierarchische Daten Eines der Ziele von OLE DB besteht darin, auf so viele relevante Daten zuzugreifen wie möglich. Nun verwalten die wenigsten Leute ihre Daten in Tabellen oder mehrdimensionalen Datenspeichern, sondern verwenden dafür eine Reihe von Dateien, Internet-Ordnern, E-Mail-Nachrichten, Notizen, Kontakt- und Termindaten. Es kommt gar nicht darauf an, ob eine solche Datei auf einem lokalen Laufwerk, in einem LAN oder im Internet gespeichert ist. Beim Format des Dokuments kann es sich um HTML oder ein anderes Format wie Word oder Excel handelt. Viele Programme wie Microsoft Outlook und Exchange verwalten E-Mail- und Faxnachrichten, Kontakte, Termine und Notizen. Gleichgültig, welches Format hierbei verwendet wird: Diese Daten unterscheiden sich grundlegend von Tabellendaten oder mehrdimensionalen Daten. Zunächst einmal sind sie von sich aus hierarchisch strukturiert: Jedes Datenelement ist Bestandteil einer Baumstruktur. Die Dateien kann man sich als untergeordnete Elemente (Kindknoten) eines Dateisystems vorstellen, während die Verzeichnisse die übergeordneten Elemente (Elternknoten) sind. Dies gilt sowohl in einem lokalen System als auch im Internet. Auf die gleiche Weise sind E-Mail-Nachrichten, Kontakte und andere Daten in Ordnern von Microsoft Exchange organisiert. Außerdem entsprechen hierarchische Ordner in gewisser Weise Zeilenmengen, weil sie eine Reihe von Zeilen darstellen. Andererseits unterscheiden sie sich von diesen, weil sie Elemente ganz unterschiedlicher Typen enthalten können und für jeden Typ eine eigene Gruppen von Spalten definiert sein kann. Mit anderen Worten: Hierarchisch strukturierte Ordner weisen keine homogene Struktur auf. So kann ein Verzeichnis sowohl Dateien als auch Unterverzeichnisse enthalten. Während Dateien eine Größe aufweisen (für den Dateityp ist die Spalte »Größe« definiert), gilt dies nicht für Verzeichnisse. Drittens werden hierarchische Daten direkt gebunden. In den vorherigen Kapiteln haben wir die folgenden Schritte untersucht, die zum Abrufen von Daten erforderlich sind: 왘 Eine Datenquelle öffnen. 왘 Eine Sitzung für die Datenquelle öffnen.

Sandini Bib 428

16

Hierarchische Daten

왘 Einen Befehl für die Sitzung öffnen. 왘 Den Befehl ausführen und die Zeilenmenge durchgehen. Diese Schritte mögen ja für einen Datenbankprogrammierer akzeptabel sein, sind aber für den durchschnittlichen Benutzer viel zu kompliziert. Zum Abrufen einer Webseite oder einer lokal gespeicherten Datei brauchen Sie nur eine Adresse (URL) einzugeben und die Eingabetaste zu drücken. Hierarchische Objekte verhalten sich genauso, denn sie können direkt anhand ihres Namens ohne Verwendung von Datenquellen, Sitzungen oder Befehlen geöffnet werden. Für den Benutzer bedeutet das direkte Binden, dass es möglich ist, durch Klicken auf einen URL das zugrunde liegende Objekt abzurufen. Schließlich sind hierarchische Objekte oft mit einem Datenstrom (Stream) verknüpft. Für jede Datei sind Spaltenwerte wie Name, URL und Größe definiert, aber die Hauptinformation ist ihr Inhalt. Bei E-Mail-Nachrichten verhält es sich genauso: Absender und Empfängerliste sind definiert, aber die wichtigste Information ist ihr Text. Der Inhalt einer Zeile kann umfangreich sein, aber der Datenstrom ermöglicht stückweises Lesen. OLE DB 2.5 unterstützt hierarchische Provider. Zum Funktionsumfang gehört ein Provider für Internet-Veröffentlichungen (Internet Publishing Provider), der das Navigieren in Dateien im Internet genauso ermöglicht, als wären diese auf einem lokalen System gespeichert. Sie können die Dateien eines Verzeichnisses auflisten, Dateien schreiben usw. Während Webseiten normalerweise nur gelesen werden können, ermöglicht der Internet Publishing Provider Lese- und Schreiboperationen. Da er fest zu OLE DB 2.5 gehört, wird er für die Beispiele in diesem Kapitel verwendet. Abbildung 16.1 zeigt das Dateisystem mit den vier Hauptmerkmalen eines hierarchischen Providers. Der Internet Publishing Provider stellt zwar kein lokales Laufwerk zur Verfügung, aber die Mechanismen sind identisch. Bevor wir weiter ins Detail gehen, müssen Sie wissen, dass herkömmliche relationale Provider die Funktionalität von hierarchischen Providern emulieren könnten. So wäre es beispielsweise möglich, hierarchische Strukturen mit einer Eltern/Kind-Tabelle zu implementieren oder variable Spaltenanzahl durch Verwendung von NULL-Spalten zu simulieren. Da diese Art der Implementierung von Hause aus schwierig und wenig effektiv ist, empfiehlt sich jedoch die Verwendung eines echten hierarchischen Providers. Hierarchische Provider unterscheiden sich von den in Kapitel 13 beschriebenen hierarchischen Zeilenmengen. Letztere verwenden Kapitelmarken zum Simulieren einer Eltern/KindBeziehung, die Daten liegen aber trotzdem immer in Form von Tabellen vor. OLE DB 2.5 wird von den OLE DB Consumer Templates nicht unterstützt. Aus diesem Grund werden wir in diesem Kapitel die OLE DB-Erweiterungsklassen verwenden. Wie üblich können Sie den C++-Code durcharbeiten um die verwendeten OLE DBSchnittstellen besser zu verstehen.

Sandini Bib 16.1

Übersicht über die verwendeten Objekte

Hierarchische Struktur

429

Direktes Binden

Datenstrom

Inhomogene Zeilen

Abbildung 16.1: Dateisystem als OLE DB 2.5-Provider

16.1 Übersicht über die verwendeten Objekte Die OLE DB-Erweiterungen stellen drei Klassen zur Verfügung, die in den folgenden Abschnitten erklärt sind: CRow, CRowFolder und CStream. CRow implementiert den Zugriff auf das Zeilenobjekt, das den Kern des hierarchischen

Providers darstellt. Diese Klasse weist eine Reihe von Schreib-/Lese-Spalten auf und definiert Operationen wie das Kopieren, Verschieben und Löschen. Zeilen an sich nützen nicht viel, werden aber in der Regel zusammen mit anderen Objekten verwendet. CRowFolder implementiert den Zugriff auf das Zeilenordner-Objekt. Dieses stellt eine

Sammlung von Kind-Zeilen dar. Microsoft verwendet den Begriff des Zeilenordners nicht, weil es sich hierbei um eine normale OLE DB-Zeilenmenge handelt. Ich verwende diesen Ausdruck aber dennoch, weil er angibt, worum es sich bei diesem Objekt handelt und in welchem Bereich es verwendet wird. Der Hauptzweck des Zeilenordners besteht darin, die ihm untergeordneten Elemente aufzuzählen. Zeilenordner-Objekte sind ebenfalls Zeilenobjekte, so dass alles für Zeilen Gesagte auch für Ordner gilt (z.B. kopieren, verschieben oder löschen über CRow).

Sandini Bib 430

16

Hierarchische Daten

CStream implementiert den Zugriff auf das Datenstrom-Objekt, dessen Hauptzweck im

Lesen und Schreiben einer Byte-Folge besteht. Bei diesen Objekten handelt es sich ebenfalls um Zeilenobjekte, sie können über CRow kopiert, verschoben und gelöscht werden. Es ist wichtig zu wissen, dass diese drei Klassen nur eine Möglichkeit darstellen um auf ein Objekt des zugrunde liegenden Providers zuzugreifen. So definieren Dateisystem und Internet zwei »reale« Objekte, nämlich Verzeichnisse und Dateien. Es ist möglich, über eine Zeile auf ein Verzeichnis zuzugreifen und die Spaltenwerte abzufragen oder Kopier-, Verschiebe- und Löschoperationen durchzuführen. Sie können auf ein Verzeichnis auch über den zugehörigen Zeilenordner zugreifen und durch dessen untergeordnete Objekte navigieren. Gleichfalls ist es möglich, auf eine Datei über die zugehörige Zeile zuzugreifen und die Spaltenwerte abzufragen oder Kopier-, Verschiebe- und Löschoperationen durchzuführen. Sie können auf eine Datei auch über den Datenstrom zugreifen und deren Inhalt lesen bzw. schreiben.

16.1.1 Direktes Binden Das direkte Binden ermöglicht es Ihnen, ein Objekt direkt zu öffnen, ohne hierzu eine Datenquelle, eine Sitzung oder einen Befehl zu verwenden. Diese Art der Bindung ermöglicht es, dass Objekte sich selbst direkt über einen URL öffnen. Die drei OLE DBKlassen aus Version 2.5 unterstützen das direkte Binden. Sie verfügen jeweils über eine Open-Methode, die wie folgt deklariert ist: HRESULT Open(LPTSTR szURL, DBBINDURLFLAG Flag = ... etc);

wobei 왘 szURL der URL des zu öffnenden Objekts ist und die für URLs definierten Konventionen einhält. 왘 Flag ein URL-Binde-Flag ist, das den Zugriff auf das Objekt beschreibt. Die OLE DB-Erweiterungen stellen Voreinstellungen zur Verfügung, die Sie aber durch andere Angaben außer Kraft setzen können. Für Flag sind viele verschiedene Werte möglich, die ich nach und nach erläutern werde, wenn wir auf sie stoßen. Tabelle 16.1 enthält eine Übersicht über die am häufigsten verwendeten.

16.1.2 Mit direkter Bindung arbeiten Wenn Sie ein Objekt über einen URL öffnen, wird als Erstes ein Basis-Bindungsobjekt (Root Binder) erzeugt. Dabei handelt es sich um ein bekanntes Objekt, das zum Funktionsumfang von MDAC gehört. Das Objekt ruft dann die Funktion Bind für das BasisBindungsobjekt auf.

Sandini Bib 16.1

Übersicht über die verwendeten Objekte

Symbolischer Wert

Beschreibung

DBBINDURLFLAG_READ

Das Objekt wird im Nur-Lese-Modus geöffnet.

DBBINDURLFLAG_WRITE

Das Objekt wird im Nur-Schreib-Modus geöffnet.

DBBINDURLFLAG_READWRITE

Das Objekt wird im Schreib/Lese-Modus geöffnet.

DBBINDURLFLAG_SHARE_DENY_READ

Anderen wird der Lesezugriff verweigert.

DBBINDURLFLAG_SHARE_DENY_WRITE

Anderen wird der Schreibzugriff verweigert.

DBBINDURLFLAG_SHARE_EXCLUSIVE

Anderen wird jeglicher Zugriff verweigert.

DBBINDURLFLAG_SHARE_DENY_NONE

Anderen wird kein Zugriff verweigert.

DBBINDURLFLAG_ASYNCHRONOUS

Das Objekt wird im asynchronen Modus geöffnet.

431

Tabelle 16.1: Häufig verwendete URL-Binde-Flags

Basis-Bindeobjekt http://myserver/myfile Bindeobjekt für HTTP (=Provider-Bindeobjekt) http://myserver/myfile Objekt Abbildung 16.2: Architektur für direktes Binden { CComPtr RootBinder; HRESULT hr = RootBinder.CoCreateInstance(CLSID_RootBinder); //... hr = RootBinder->Bind(NULL, T2W(szName), Flag, // weitere Parameter); //... }

Wenn das Basis-Bindeobjekt den URL abruft, wird dieser so weit analysiert, dass der Protokolltyp erkannt wird. Wenn Sie beispielsweise http://myserver/myfile übergeben, benötigt das Basis-Bindeobjekt nur die Angabe http:// um festzustellen, dass hier das Protokoll HTTP verwendet wird. Daraufhin wird ein Bindeobjekt für dieses Protokoll erstellt, das als Provider-Bindeobjekt bezeichnet wird, und der Aufruf wird an den entsprechenden Provider übertragen. Wenn das Provider-Bindeobjekt den URL erhält, decodiert es diesen auf Provider-spezifische Weise und erstellt das zugehörige Exemplar. Abbildung 16.2 zeigt eine Übersicht über diese Architektur.

Sandini Bib 432

16

Hierarchische Daten

16.2 Das Zeilenobjekt Das Zeilenobjekt ist der Kern eines hierarchischen Providers. Die Klasse CRow implementiert die Unterstützung für Zeilen und ähnelt in gewisser Hinsicht CAccessorRowset. Da Zeilen zum Abrufen von Spaltenwerten dienen können, arbeitet CRow immer mit einer Zugriffsobjekt-Klasse zusammen. CRow ist wie folgt deklariert: template class CRow: public TRowAccessor

Wie wir später noch sehen werden, wird für Zeilen ein spezielles Zugriffsobjekt benötigt. Zunächst einmal werden wir CDynamicRowAccessor als Zugriffsobjekt-Klasse verwenden.

16.2.1 Zeile öffnen CRow implementiert zwei Open-Methoden. Die erste ist eine klassische, direkt bindende Open-Methode: HRESULT Open(LPTSTR szName, DBBINDURLFLAG Flag = DBBINDURLFLAG_READ, DBPROPSET* pPropSet = NULL, ULONG nPropertySets = 1,bool bGetColumns = true)

Sie könnten beispielsweise den folgenden Code programmieren: CRow Row; hr = Row.Open("http://myserver/myfile"); //oder hr = Row.Open(someotherURL, DBBINDURLFLAG_READWRITE, &propset);

Der Parameter bGetColumns gibt an, ob die Spaltenwerte beim Öffnen des Objekts abgerufen werden sollen. Dieses Thema habe ich in Kapitel 10, Datenaustausch, eingehend behandelt. Man könnte sagen, dass der URL dem absoluten Pfad des Zeilenobjekts entspricht. CRow stellt noch eine zweite Open-Methode zur Verfügung. Diese akzeptiert eine relative Pfadangabe: HRESULT Open(CRow& Row, LPTSTR szName, DBBINDURLFLAG Flag = DBBINDURLFLAG_READ, bool bGetColumns = true)

Der Parameter Row stellt die Eltern-Zeile dar, szName gibt den relativen Pfad an. Für diese Methode können keine Eigenschaften angegeben werden, weil die neue Zeile ihre Eigenschaften von der Eltern-Zeile erbt. Das folgende Beispiel zeigt, wie Sie eine Zeile mit einer relativen Pfadangabe öffnen: CRow Row; CRow ChildRow; hr = Row.Open ("http://myserver/dir");

Sandini Bib 16.2

Das Zeilenobjekt

433

hr = ChildRow.Open (Row, "subrow"); // entspricht: hr = ChildRow.Open("http://myserver/dir/subrow");

16.2.2 Zeile erstellen Es ist auch möglich, eine Zeile auf die gleiche Weise zu erstellen wie Dateien. CRow verfügt über zwei Create-Methoden, die wie die Open-Methoden funktionieren: Die eine akzeptiert einen URL, die andere eine relative Pfadangabe. Die erste Create-Methode ist wie folgt deklariert: HRESULT Create(LPTSTR szName, DBBINDURLFLAG Flag = DBBINDURLFLAG_OPENIFEXISTS|DBBINDURLFLAG_READWRITE, DBPROPSET* pPropSet = NULL, ULONG nPropertySets = 1)

Zum Erstellen einer Zeile könnten Sie beispielsweise den folgenden Aufruf programmieren: CRow Row; hr = Row.Create("http://myserver/myfile");

Flag stellt das URL-Binde-Flag dar. Dabei handelt es sich um eine Kombination der üblichen Flags (siehe Tabelle 16.1) und spezieller, in Tabelle 16.2 aufgeführter, Flags. Die zweite Create-Methode ist folgendermaßen deklariert: HRESULT Create(CRow& Row, LPTSTR szName, DBBINDURLFLAG dwFlags = DBBINDURLFLAG_READWRITE)

Zum Erstellen einer Zeile könnten Sie beispielsweise den folgenden Aufruf programmieren: CRow Row; CRow ChildRow; hr = Row.Open ("http://myserver/dir"); hr = ChildRow.Create (Row, "subrow");

16.2.3 Spaltenwerte abrufen und setzen Wie Zeilenmengen weisen auch Zeilen Spalten mit Werten auf. Stellen Sie sich einen Mechanismus zum Abrufen von Zeilendaten vor, der analog zu dem für Zeilenmengen funktioniert: Erstellen Sie ein Zugriffsobjekt ausgehend von einer Reihe von Spaltenbindungen. Rufen Sie anschließend die Daten ab und setzen Sie Daten. Für eine Zeilenmenge ist die Verwendung eines Mechanismus mit Zugriffsobjekten sinnvoll, weil Sie das Zugriffsobjekt einmal definieren und dann mehrfach verwenden können (einmal pro Zeile). Bei Zeilen ist dies jedoch anders, weil jede Zeile nur für sich allein steht, so dass das Programmieren eines Zugriffsobjekts für jede einzelne Zeile sehr mühsam wäre. Deshalb wird zum Abrufen von Daten aus Zeilen ein einfacheres Verfahren verwendet.

Sandini Bib 434

16

Hierarchische Daten

Rufen Sie zum Abrufen der Spaltenwerte einfach IRow::GetColumns auf: HRESULT STDMETHODCALLTYPE GetColumns( DBORDINAL cColumns, DBCOLUMNACCESS __RPC_FAR rgColumns[ ])

Symbolischer Wert

Beschreibung

DBBINDURLFLAG_OVERWRITE

Wenn sich an der angegebenen Position eine Zeile befindet, soll diese überschrieben werden.

DBBINDURLFLAG_OPENIFEXISTS

Wenn sich an der angegebenen Position eine Zeile befindet, soll diese geöffnet und keine neue erstellt werden.

DBBINDURLFLAG_ISSTRUCTUREDDOCUMENT

Es soll ein strukturiertes Dokument erstellt werden.

Tabelle 16.2: Flags zum Erstellen von Zeilen

cColumns gibt die Anzahl der abzurufenden Spalten an, rgColumns enthält ein Array aus DBCOLUMNACCESS-Strukturen für die betreffenden Spalten. DBCOLUMNACCESS enthält jeweils

die Informationen für eine Spalte und ist folgendermaßen definiert: typedef struct tagDBCOLUMNACCESS { void * pData; // Zeiger auf den Puffer, der die Daten // empfängt DBID columnid; // Spalten-ID DBLENGTH cbDataLen; // Länge der zurückgelieferten Daten DBSTATUS dwStatus; // Status der zurückgelieferten Daten DBLENGTH cbMaxLen; // Länge des Puffers DB_DWRESERVE dwReserved; // Reserviert DBTYPE wType; // Datentyp BYTE bPrecision; // Genauigkeit BYTE bScale; // Skalierung } DBCOLUMNACCESS;

Dieses Verfahren ist zwar sehr einfach, unterscheidet sich aber grundlegend vom Modell des Bindens von Zeilenmengen. Zum Glück ermöglicht CRow die gemeinsame Verwendung von Code durch Zeilenmengen- und Zeilenbindungen. Einer der wichtigsten Unterschiede zwischen beiden besteht darin, dass die Bindung der Zeilenmengen über die Positionsnummer der Spalte erfolgt, während die Bindung der Zeilen über die Spaltennamen durchgeführt wird. Wenn Sie ein Zugriffsobjekt definieren, das die Bindung sowohl über die Spaltennamen als auch über die Spaltennummern vornimmt, kann dieses sowohl für Zeilenmengen als auch für Zeilen verwendet werden. Die OLE DB-Erweiterungen führen das Konzept des Zeilen-Zugriffsobjekts ein, das zusätzlich zu den üblichen Methoden für Zugriffsobjekte die folgende Methode implementiert:

Sandini Bib 16.2

Das Zeilenobjekt

435

HRESULT GetColumnAccess(ULONG* pColumns, int nAccessor, DBCOLUMNACCESS *pColumnAccess , DBCOLUMNACCESS_COMPLEMENT *pColumnAccess2)

Wie herkömmliche Zugriffsobjekte unterstützen auch Zeilen-Zugriffsobjekte die Verwendung mehrerer Zugriffsobjekte. Für das Zugriffsobjekt bei Index nAccessor empfängt pColumns die Anzahl der gebundenen Spalten. Wenn der Wert für pColumnAccess nicht NULL ist, müsste darin ein pColumns entsprechendes Array aus DBCOLUMNACCESSStrukturen enthalten sein. GetColumnAccess ist dafür zuständig, diese Strukturen mit Daten zu füllen. Da die Bindemodelle für Zeilenmengen und Zeilen unterschiedlich sind, benötigen wir noch einige Zusatzinformationen, die aus DBCOLUMNACCESS_COMPLEMENT abgerufen werden können. typedef struct tagDBCOLUMNACCESS_COMPLEMENT { DBPART dwPart; DBLENGTH *pLength; DBSTATUS *pStatus; } DBCOLUMNACCESS_COMPLEMENT;

dwPart enthält die gebundenen Datenelemente. Wenn beispielsweise das Zugriffsobjekt nur den Wert und die Länge bindet, sind dies DBPART_VALUE|DBPART_LENGTH. pLength und pStatus stellen einen Zeiger auf die Länge bzw. den Status dar. Sie sind nützlich, wenn

diese Werte bei Bedarf in das Zeilenobjekt zurückgeschrieben werden müssen. Bei CRowAccessor handelt es sich um eine Anpassung von CAccessor, mit der die Operationen für Zeilen-Zugriffsobjekte implementiert werden. So wird beispielsweise CTable MyTable;

durch den folgenden Code ersetzt: CRow MyRow;

Die einzige Bedingung für CRowAccessor besteht darin, dass die zugehörige Zugriffsobjekt-Klasse (z.B. CMyAccessor) die Spalten anhand der Namen binden muss. Weitere Informationen zu diesem Thema finden Sie in Kapitel 14. Bei CDynamicRowAccessor handelt es sich um eine Anpassung von CDynamicAccessor, mit der die Operationen für Zeilen-Zugriffsobjekte implementiert werden. Diese Klasse dient zum dynamischen Abrufen von Spaltennamen. Mit GetColumns können Sie die Spaltenwerte abrufen: HRESULT GetColumns (int nAccessor = 0)

Beispiel: CRow Row; //Zeile abrufen hr = Row.GetColumns();

Sandini Bib 436

16

Hierarchische Daten

Da jede Zeile nur einer einzigen Menge von Spaltenwerten entspricht, ist es sinnvoll, diese Werte beim Öffnen einer Zeile gleich abzurufen. Über den Parameter bGetColumns der Methode Open wird festgelegt, ob die Spaltenwerte beim Öffnen einer Zeile abgerufen werden sollen. Per Voreinstellung ist der Wert auf true gesetzt, wie im folgenden Beispiel gezeigt wird: CRow Row; hr = Row.Open(szURL); // bewirkt dasselbe wie hr = Row.Open(szURL, DBBINDURLFLAG_READ, NULL, 0, true); // bewirkt dasselbe wie hr = Row.Open(szURL, DBBINDURLFLAG_READ, NULL, 0, false); if (SUCCEEDED (hr)) hr = Row.GetColumn();

Beim Öffnen einer Zeile ruft der Provider die Spaltenwerte immer ab und speichert sie im Cache, unabhängig davon, ob Sie GetColumns aufrufen oder nicht. Dieses Verhalten lässt sich über das URL-Binde-Flag DBBINDURLFLAG_DELAYFETCHCOLUMNS außer Kraft setzen. Wenn es gesetzt ist, ruft der Provider die Spaltenwerte nur ab, wenn er explizit dazu aufgefordert wird.

16.2.4 Operationen mit definiertem Bereich In OLE DB 2.5 sind drei Arten von Operationen mit definiertem Bereich (Scoped Operations) für Zeilen definiert: 왘 Kopieren 왘 Verschieben 왘 Löschen Sie heißen »Operationen mit definiertem Bereich«, weil sie sich auf die Zeile sowie die ihr untergeordneten Elemente (Kind-Elemente) beziehen. So bewirkt das Löschen einer Zeile, dass die zugehörigen Kind-Elemente gleichfalls gelöscht werden. Der Provider kann auch kaum anders vorgehen. Solche Operationen kennen Sie vom Windows Explorer her. Wenn Sie dort Dateien und Verzeichnisse (Ordner) kopieren, verschieben und löschen, so finden Sie dasselbe Verhalten vor, das auch hierarchische Provider bei diesen Operationen zeigen.

Zeile kopieren CRow definiert eine Copy-Methode, die zum Kopieren der betreffenden Zeile dient: HRESULT Copy(LPCTSTR szDestURL, DBCOPYFLAGS dwCopyFlags = 0, LPCTSTR szSourceURL = _T(""), IAuthenticate * Authenticate = NULL)

Sandini Bib 16.2

Das Zeilenobjekt

437

Beim Kopieren müssen Sie die Zielposition der Zeile angeben. Angenommen, Sie wollen den in Abbildung 16.3 gezeigten Kopiervorgang durchführen.

Server-Basiselement Zeile 1

Kind-Zeile

Server-Basiselement Zeile 1

Kind-Zeile Zeile 2

Kind-Zeile Abbildung 16.3: Eine Zeile kopieren

Die Zielposition ist http://myserver/row2; der zugehörige Code lautet: hr = Row.Open("http://myserver/row1"); hr = Row.Copy("http://myserver/row2");

Beachten Sie hierbei, dass die Zielposition sich nicht auf demselben Server oder sogar Provider zu befinden braucht. Für die Methode Copy können optional einige zusätzliche Parameter angegeben werden: 왘 dwCopyFlags enthält die Kopier-Flags, wobei die in Tabelle 16.3 gezeigten Werte verwendet werden können. 왘 szSourceURL ist der Name der zu kopierenden Kind-Zeile. Per Voreinstellung ist hier der Wert "" enthalten. Dies bedeutet, dass nur die aktuelle Zeile kopiert wird. Wenn Sie einen anderen String angeben, wird dieser als Kind-Zeile interpretiert. Wenn Sie beispielsweise eine Zeile mit der Bezeichnung http://myserver/row/childrow kopieren möchten, wird mit der ersten Methode die Kind-Zeile (childrow) direkt geöffnet: hr = Row.Open("http://myserver/row/childrow"); hr = Row.Copy("somewhereelse");

Sandini Bib 438

16

Hierarchische Daten

Mit der zweiten Methode wird zunächst nur die Eltern-Zeile geöffnet und die KindZeile wird im Ausgangs-URL (szSourceURL) angegeben: hr = Row.Open("http://myserver/row"); hr = Row.Copy("somewhereelse", 0, "childrow");

Symbolischer Wert

Beschreibung

DBCOPY_ALLOW_EMULATION

Der Provider kann die Kopieroperation bei Bedarf durch einen Download oder Upload emulieren.

DBCOPY_ASYNC

Der Kopiervorgang läuft asynchron ab.

DBCOPY_ATOMIC

Die Kopiervorgang unterliegt, analog zu einer Transaktion, dem Alles-oder-nichts-Prinzip (Atomicity): Entweder werden alle Zeilen erfolgreich kopiert oder gar keine.

DBCOPY_NON_RECURSIVE

Es wird die aktuelle Zeile kopiert, die ihr untergeordneten Zeilen jedoch nicht.

DBCOPY_REPLACE_EXISTING

Wenn sich an der Zielposition eine Zeile befindet, wird sie durch die Ausgangszeile ersetzt.

Tabelle 16.3: Flags für das Kopieren von Zeilen

Zeile verschieben CRow deklariert die Methode Move folgendermaßen: HRESULT Move(LPCTSTR szDestURL, DBMOVEFLAGS dwMoveFlags = 0, LPCTSTR szSourceURL = _T(""), IAuthenticate * Authenticate = NULL)

Das Verschieben einer Zeile entspricht annähernd dem Kopieren. Der einzige Unterschied besteht darin, dass die Ausgangszeile am Ende der Operation gelöscht wird. Abbildung 16.4 zeigt diesen Vorgang. Der zugehörige Code lautet: hr = Row.Open("http://myserver/row1"); hr = Row.Move("http://myserver/row2");

dwMoveFlags enthält das Flag zum Verschieben von Zeilen, wobei die in Tabelle 16.4 aufgeführten Werte verwendet werden können. szSourceURL ist der Name der zu verschiebenden Kind-Zeile. Wie bei der Methode Copy ist hier die Voreinstellung "", was

bedeutet, dass nur die aktuelle Zeile verschoben wird. Wenn Sie hier einen anderen String angeben, so wird dieser als Kind-Zeile interpretiert. Somit hat der Code hr = Row.Open("http://myserver/row/childrow"); hr = Row.Move("somewhereelse");

dieselbe Auswirkung wie hr = Row.Open("http://myserver/row"); hr = Row.Move("somewhereelse", 0, "childrow");

Sandini Bib 16.2

Das Zeilenobjekt

439

Server-Basiselement Zeile 1

Kind-Zeile Server-Basiselement

Zeile 2

Kind-Zeile Abbildung 16.4: Eine Zeile verschieben Symbolischer Wert

Beschreibung

DBMOVE_ALLOW_EMULATION

Der Provider kann die Operation durch einen Upload oder Download emulieren.

DBMOVE_ASYNC

Die Verschiebeoperation wird asynchron durchgeführt.

DBMOVE_ATOMIC

Die Verschiebeoperation erfolgt nach dem Alles-oder-nichtsPrinzip (Atomicity).

DBMOVE_DONT_UPDATE_LINKS

Der Server darf die Links nicht aktualisieren.

DBMOVE_REPLACE_EXISTING

Wenn sich an der Zielposition eine Zeile befindet, wird sie durch die Ausgangszeile ersetzt.

Tabelle 16.4: Flags für das Verschieben von Zeilen

Symbolischer Wert

Beschreibung

DBDELETE_ASYNC

Das Löschen der Zeile erfolgt asynchron.

DBDELETE_ATOMIC

Die Löschoperation erfolgt nach dem Alles-oder-nichts-Prinzip (Atomicity).

Tabelle 16.5: Flags für das Löschen von Zeilen

Zeile löschen Programmieren Sie zum Löschen einer Zeile den folgenden Aufruf: HRESULT Delete (LPCTSTR szURL = _T(""), DBDELETEFLAGS dwDeleteFlags = 0)

Das folgende Beispiel zeigt, wie eine Zeile mit einem bestimmten URL gelöscht wird: hr = Row.Open(URL); hr = Row.Delete();

Sandini Bib 440

16

Hierarchische Daten

Optional können Sie eines der Lösch-Flags setzen. Für dwDeleteFlags kann eine Kombination der in Tabelle 16.5 aufgeführten Werte angegeben werden. Wenn Sie eine Zeile beispielsweise asynchron löschen möchten, schreiben Sie folgenden Code: hr = Row.Open(URL); hr = Row.Delete(_T(""),DBDELETE_ASYNC);

Die Methode Delete akzeptiert eine relative Pfadangabe für die zu löschende Zeile. Per Voreinstellung lautet die Angabe hierfür "", d.h. die aktuelle Zeile wird gelöscht. Sie können auch den Pfadnamen angeben. Mit dem folgenden Beispielcode wird die Zeile bei http://myserver/row/childrow gelöscht: hr hr // hr hr

= Row.Open("http://myserver/row"); = Row.Delete("childrow"); bewirkt dasselbe wie = Row.Open("http://myserver/row/childrow"); = Row.Delete();

16.3 Dokumente In einigen Fällen stehen die Zeilen einer Datenstruktur für ein Dokument. Beim Internet Publishing Provider ist jede HTML-Datei ein Dokument. Auch Dokumente aus Microsoft Office gehören dazu, und noch viele andere. Provider, die Dokumente unterstützen, stellen Zeilen über eine Reihe von Spalten dar, die von OLE DB vordefiniert sind, wobei jede eine genau definierte DBID aufweist. Die Klasse CResourceAccessor bindet diese Spalten sowohl bei einer einzelnen Zeile als auch bei einer Zeilenmenge. Dadurch ist ein bequemer Zugriff auf die Zeilen von Dokumenten möglich. Ein Zeilentyp könnte beispielsweise folgendermaßen deklariert werden: CRow MyRow;

CResourceAccessor ist dabei wie folgt definiert: class CResourceAccessor { public: TCHAR m_ParseName [512]; TCHAR m_ParentName [512]; TCHAR m_AbsoluteParseName [512]; BOOL m_IsHidden; BOOL m_IsReadOnly ; TCHAR m_ContentType [512]; TCHAR m_ContentClass[512]; TCHAR m_ContentLanguage [512]; DBTIMESTAMP m_CreationTime; DBTIMESTAMP m_LastAccessTime; DBTIMESTAMP m_LastWriteTime;

Sandini Bib 16.3

Dokumente

441

ULARGE_INTEGER m_StreamSize; BOOL m_IsCollection; BOOL m_IsStructuredDocument; TCHAR m_DefaultDocument [512]; TCHAR m_DisplayName [512]; BOOL m_IsRoot; BEGIN_NAMED_COLUMN_MAP (CResourceAccessor) DBID_COLUMN_ENTRY(1, DBROWCOL_PARSENAME, m_ParseName) DBID_COLUMN_ENTRY(2, DBROWCOL_PARENTNAME, m_ParentName) DBID_COLUMN_ENTRY(3, DBROWCOL_ABSOLUTEPARSENAME, m_AbsoluteParseName) DBID_COLUMN_ENTRY(4, DBROWCOL_ISHIDDEN, m_IsHidden) DBID_COLUMN_ENTRY(5, DBROWCOL_ISREADONLY, m_IsReadOnly) DBID_COLUMN_ENTRY(6, DBROWCOL_CONTENTTYPE, m_ContentType) DBID_COLUMN_ENTRY(7, DBROWCOL_CONTENTCLASS, m_ContentClass) DBID_COLUMN_ENTRY(8, DBROWCOL_CONTENTLANGUAGE, m_ContentLanguage) DBID_COLUMN_ENTRY(9, DBROWCOL_CREATIONTIME, m_CreationTime) DBID_COLUMN_ENTRY(10, DBROWCOL_LASTACCESSTIME, m_LastAccessTime) DBID_COLUMN_ENTRY(11, DBROWCOL_LASTWRITETIME, m_LastWriteTime) DBID_COLUMN_ENTRY(12, DBROWCOL_STREAMSIZE, m_StreamSize) DBID_COLUMN_ENTRY(13, DBROWCOL_ISCOLLECTION, m_IsCollection) DBID_COLUMN_ENTRY(14, DBROWCOL_ISSTRUCTUREDDOCUMENT, m_IsStructuredDocument) DBID_COLUMN_ENTRY(15, DBROWCOL_DEFAULTDOCUMENT, m_DefaultDocument) DBID_COLUMN_ENTRY(16, DBROWCOL_DISPLAYNAME, m_DisplayName) DBID_COLUMN_ENTRY(17, DBROWCOL_ISROOT, m_IsRoot) END_COLUMN_MAP() } ;

m_ParseName gibt den relativen Namen des Dokuments an, während m_ParentName der absolute Name des Eltern-Elements ist. Zusammen bilden beide Angaben den absoluten Dokumentnamen (m_AbsoluteParseName), wie im folgenden Beispiel gezeigt:

왘 m_ParseName= »myfile.htm« 왘 m_ParentName= »http://myserver/« 왘 m_AbsoluteParseName= »http://myserver/myfile.htm« Für jedes Dokument sind Attribute festgelegt: m_IsHidden gibt an, ob das Dokument verborgen ist (dies ist beispielsweise oft bei Systemdateien der Fall), und m_IsReadOnly gibt an, ob es sich um ein Nur-Lese-Dokument handelt. Dieses Attribut ist hilfreich, wenn zum Öffnen des Dokuments das geeignete URL-Binde-Flag ausgewählt werden soll. Für Dokumente können als weitere Attribute ein Typ, eine Klasse und eine Sprache festgelegt sein (m_ContentType, m_ContentClass und m_ContentLanguage). Ferner sind drei zugehörige Zeitangaben festgelegt: m_CreationTime gibt Datum und Uhrzeit bei Erstel-

Sandini Bib 442

16

Hierarchische Daten

lung des Dokuments an, m_LastWriteTime gibt an, wann zum letzten Mal in das Dokument geschrieben wurde, und m_LastAccessTime den Zeitpunkt des letzten Zugriffs auf das Dokument. Beachten Sie in diesem Zusammenhang, dass es möglich ist, auf ein Dokument zuzugreifen, ohne darin zu schreiben. Dadurch ergibt sich folgende zeitliche Abfolge: m_CreationTime Row; hr = Row.Open(_T("http://myserver/directory"); CRowFolder RowFolder; //http://myserver/directory/subdirectory erstellen: hr = RowFolder.Create(Row, _T("subdirectory"));

16.4.2 Untergeordnete Zeilen (Kind-Zeilen) aufzählen Jedes Zeilen-Handle in der Zeilenmenge für den Zeilenordner stellt eine untergeordnete Zeile (Kind-Zeile) dar. Innerhalb dieser Zeilen können Sie wie in einer normalen Zeilenmenge navigieren. Wenn eine Zeile zwei Kind-Zeilen a und b aufweist, so sind im Zeilenordner zwei Zeilen enthalten, eine für Zeile a und eine für Zeile b. Weist der Zeilenordner eine aktuelle Zeile auf (beispielsweise nach dem Aufruf von MoveFirst), so können Sie das zugehörige Zeilenobjekt öffnen. Die hierzu verwendete CRowMethode ist wie folgt deklariert: HRESULT Open(const CRowset& Rowset, bool bGetColumns = true)

Abbildung 16.6 zeigt die Architektur mit Zeilen und Zeilenordnern. Im folgenden Beispiel demonstriere ich eine Möglichkeit, die zu einer Zeile gehörenden Kind-Zeilen aufzuzählen: CRow ParentRow; hr = ParentRow.Open(..); CRowFolder RowFolder; hr = RowFolder.Open(ParentRow); hr = RowFolder.MoveFirst(); CRow RowA; hr = RowA.Open(RowFolder); CRow RowB; hr = RowFolder.MoveNext(); hr = RowB.Open(RowFolder);

Eine andere Möglichkeit bestünde darin, den URL der Kind-Zeile abzurufen und die Zeile dann über diesen zu öffnen. Der Vorteil dabei ist, dass Sie beim Öffnen der KindZeile die gewünschten URL-Binde-Flags und Eigenschaften angeben können. Das vorherige Beispiel würde dann so aussehen: CRow ParentRow; hr = ParentRow.Open(..); CRowFolder RowFolder; hr = RowFolder.Open(ParentRow); hr = RowFolder.MoveFirst();

Sandini Bib 446

16

Hierarchische Daten

CRow RowA; hr = RowA.Open(RowFolder.m_AbsoluteParseName); CRow RowB; hr = RowFolder.MoveNext(); hr = RowB.Open(RowFolder.m_AbsoluteParseName);

Es ist klar, dass dieses Verfahren nur funktionieren kann, wenn der jeweilige Zeilenordner die absoluten Namen binden kann.

Eltern-Zeile CRowFolder::Open MoveFirst

Zeilenordner

CRow::Open

Zeilen-Handle a

Zeile a

Zeilen-Handle b Abbildung 16.6: Architektur mit Zeilen und Zeilenordnern

16.5 Datenströme Viele Zeilenobjekte sind mit einem Datenstrom-Objekt verknüpft. So ist für ein Dateiobjekt ein Datenstrom vorhanden, der die eigentlichen Daten der Datei enthält. Analog ist zu einer E-Mail-Nachricht ein Datenstrom vorhanden, der den eigentlichen Text enthält. Die Datenströme in OLE DB 2.5 haben große Ähnlichkeit mit BLOBs, weil sie große Datenmengen enthalten können und der Zugriff bevorzugt über die Schnittstelle ISequentialStream erfolgt. Wenn Ihnen das Arbeiten mit BLOBs geläufig ist, werden Sie bei der Verwendung von Datenströmen aus OLE DB 2.5 keine Probleme haben. Das Datenstrom-Objekt wird über die Klasse CStream implementiert. Die Verwendung eines Datenstroms erfolgt in zwei Schritten: Zunächst wird der Strom geöffnet, anschließend werden die darin enthaltenen Daten gelesen, oder es wird in den Strom geschrieben.

16.5.1 Datenstrom öffnen Es gibt zwei Möglichkeiten um einen Datenstrom zu öffnen. Bei der ersten wird die zugehörige Zeile geöffnet und damit der entsprechende Datenstrom. Dabei ist es unerheblich, auf welche Weise die Zeile geöffnet wurde: Sie können einen absoluten oder einen relativen Pfad angeben und Sie können die Zeile über einen Zeilenordner öffnen. Das folgende Beispiel zeigt diese Technik: CRow Row; hr = Row.Open(szURL); CStream Stream hr = Stream.Open(Row);

Sandini Bib 16.5

Datenströme

447

Per Voreinstellung wird der Inhalt des Datenstroms beim Öffnen der Zeile heruntergeladen. Sie können dieses Verhalten allerdings außer Kraft setzen, indem Sie das URLBinde-Flag DBBINDURLFLAG_DELAYFETCHSTREAM angeben. Beim zweiten Verfahren wird der Datenstrom direkt über einen URL geöffnet. Die Open-Methode hierfür weist die folgende Signatur auf: HRESULT Open(LPTSTR szName, DBBINDURLFLAG Flag = DBBINDURLFLAG_READWRITE, DBPROPSET* pPropSet = NULL, ULONG nPropertySets = 1)

Das vorherige Beispiel würde damit wie folgt aussehen: CStream Stream; hr = Stream.Open(szURL);

Dies ist einfacher, aber es besteht auf diese Weise kein Zugriff auf die Zeilenmethoden. So ist es beispielsweise nicht möglich, einen Datenstrom direkt zu verschieben oder zu kopieren. Zu diesem Zweck muss zunächst das zugehörige Zeilenobjekt abgerufen werden. In jedem Fall können Sie eine Zeile für einen Datenstrom über die folgende Methode öffnen: HRESULT Open(const CStream& Stream, bool bGetColumns = true)

Beispiel: CStream Stream; hr = Stream.Open(szURL); CRow Row; hr = Row.Open(Stream);

Zum Erzeugen eines Datenstroms rufen Sie die Methode Create auf: HRESULT Create(LPTSTR szName, DBBINDURLFLAG Flag = DBBINDURLFLAG_OPENIFEXISTS|DBBINDURLFLAG_READWRITE, DBPROPSET* pPropSet = NULL, ULONG nPropertySets=1)

Das folgende Beispiel zeigt, wie ein Datenstrom über einen URL erzeugt und geöffnet wird: CStream Stream; hr = Stream.Create(URL);

In jedem Fall wird der Standard-Datenstrom der Zeile geöffnet. CStream bietet keine Methoden zum Öffnen von Datenströmen, die nicht dem Standard entsprechen.

Sandini Bib 448

16

Hierarchische Daten

16.5.2 Daten aus einem Datenstrom lesen und in einen Datenstrom schreiben CStream stellt Read- und Write-Methoden zur Verfügung, die das Lesen der Daten eines

Datenstroms und das Schreiben in einen Datenstrom ermöglichen. Es handelt sich bei diesen Methoden um Kapselungen der entsprechenden ISequentialStream-Methoden (weitere Informationen hierzu finden Sie in Kapitel 11). HRESULT Read(void *pv, ULONG cb, ULONG *pcbRead)

und HRESULT Write(const void *pv, ULONG cb, ULONG *pcbWritten)

Das folgende Beispiel zeigt, wie Daten aus einem Datenstrom-Objekt gelesen werden: char text [1234]; ULONG ActuallyRead; hr = Stream.Read(text, sizeof(text), & ActuallyRead);

Wenn Sie den Datenstrom nicht mehr benötigen, können Sie entweder durch einen Aufruf von Close das zugrunde liegende Objekt freigeben oder darauf warten, dass der Destruktor von CStream dies für Sie tut.

16.6 Zusammenfassung In diesem Kapitel habe ich erläutert, wie Sie über OLE DB auf hierarchische Provider zugreifen können. In den OLE DB-Erweiterungen stehen drei Klassen zur Verfügung, die den Zugriff auf die zugrunde liegenden Objekte ermöglichen: CRow, CRowFolder und CStream.

16.7 Nachwort In diesem Buch habe ich OLE DB und die OLE DB Consumer Templates beschrieben und Sie haben die Vorzüge von OLE DB kennen gelernt: Diese Technologie bietet schnellen Zugriff auf eine Vielzahl von Datenquellen. Mit den OLE DB Consumer Templates steht den C++-Programmierern etwas zur Verfügung, wonach sie sich schon immer gesehnt haben: unbegrenzte Steuerungsmöglichkeiten und hohe Flexibilität. Aber dabei darf man nicht vergessen, dass sich OLE DB weiterentwickelt. Wo immer man mit Daten zu tun hat, wird man auf OLE DB stoßen. Zum Zeitpunkt der Fertigstellung dieses Buches bringt Microsoft OLE DB 2.6 mit XML-Unterstützung auf den Markt. Beobachten Sie die Entwicklung in diesem Bereich. Außerdem müsste Visual Studio 7 einige Verbesserungen der Templates mit sich bringen. Die Vorveröffentli-

Sandini Bib 16.7

Nachwort

449

chung von Version 7 zeigt bereits, wie C++-Attribute die Entwicklung des ConsumerCodes erleichtern, es ist aber möglich, dass dieses Release noch weitere Verbesserungen bringt. Schließlich führt die neue MSIL-Architektur einige interessante Konzepte ein, wie beispielsweise eine sprachenübergreifende Entwicklung und die Entfernung nicht mehr benötigter Daten aus dem Cache (Garbage Collection), aber OLE DB wird vermutlich das COM-Konzept beibehalten. Mit Visual Studio 7 müsste ferner auch eine neue Bibliothek (ATL Server) geliefert werden, welche die Entwicklung von ISAPI-Lösungen erleichtert. Zusammen mit den OLE DB Consumer Templates sollten dem Programmierer damit die besten Instrumente zur Verfügung stehen um leistungsfähige Websites und Dienste zu entwickeln.

Sandini Bib

Sandini Bib

A Erweiterte C++-Techniken Die OLE DB Consumer Templates nutzen einige »erweiterte« Funktionen von C++ einschließlich Mehrfachvererbung, Templates und lokalen Typen. Während es für erfahrene Programmierer ein Vergnügen darstellt, mit diesen Techniken zu arbeiten, kann so mancher Neuling zunächst ein wenig ratlos davor stehen. Daher ist es sicher nicht schlecht, sich vor dem Vorstoß zum Kern der C++-Bibliothek diese erweiterten Techniken einmal genauer anzusehen.

A.1

Vererbung ausgehend von einem Template-Parameter

Um eine Klasse zu erzeugen, die von einem Template-Parameter erbt, können Sie einen Code wie den Folgenden schreiben: template class CMyClass : public T { void MyMethod(); int m_MyClassMember; };

In diesem Beispiel erbt CMyClass alle Klassenelemente und Methoden von T. Ausgangspunkt für die folgenden Überlegungen sind zwei Klassen A und B: Class A { void AMethod(); int m_AClassMember; }; Class B { void BMethod(); int m_BClassMember; };

Wenn Sie eine Klasse vom Typ CMyClass deklarieren, entspricht diese einer gedachten Klasse CMyClass_A, die folgendermaßen definiert ist:

Sandini Bib 452

A

Erweiterte C++-Techniken

Class CMyClass_A: public A { void MyMethod(); int m_MyClassMember; }

Daraus ergibt sich, dass CMyClass folgende Klassenelemente und Methoden aufweist: void AMethod(); int m_AClassMember; void MyMethod(); int m_MyClassMember;

CMyClass weist folgende Klassenelemente und Methoden auf: void BMethod(); int m_BClassMember; void MyMethod(); int m_MyClassMember;

Die Vererbung von Template-Parametern ist in Abbildung A.1 dargestellt.

A.2

Template-Standardparameter

Wenn Template-Standardparameter verwendet werden, ist eine Klasse folgendermaßen definiert: template class CMyClass { };

CMyClass

CMyClass

Klassenelemente

Klassenelemente

Klassenmethoden

Klassenmethoden

A

Klassenelemente

Klassenelemente

Klassenmethoden

Klassenmethoden Abbildung A.1: Vererbung ausgehend von einem Template-Parameter

Sandini Bib 453

Somit ist CMyClass

gleich CMyClass< CDefaultParameter >

Diese Definition ist nicht schwer zu verstehen, aber wenn die Vererbung ausgehend von einem Template-Parameter hinzukommt, wird es schon schwieriger. Angenommen, es ist eine folgendermaßen definierte Klasse vorhanden: template class CMyClass : public T { };

In diesem Fall erbt ein Exemplar von CMyClass seine Klassenelemente und Methoden »heimlich« von CDefaultParameter.

A.3

Überladene Typendefinitionen

Die Möglichkeit, nicht nur Klassen, sondern auch Typen deklarieren zu können, ist eine meiner Lieblingsfunktionen von C++, da der Programmierer damit schöne, flexible Architekturen konstruieren kann. Es ist auch möglich, eine globale Typendefinition durch eine lokal für eine Klasse geltende Typendefinition außer Kraft zu setzen. Das folgende Beispiel veranschaulicht diesen Punkt. Angenommen, es steht die folgende Klasse zur Verfügung: class class1 { static int f() { return 1; } }

Diese kann in einer globalen Typendefinition (typedef) folgendermaßen verwendet werden: typedef class1 mytypedef;

Somit kann jede nach dieser Typendefinition erzeugt Klasse mytypedef verwenden. Eine solche Klasse könnte beispielsweise wie folgt definiert sein: Class CTest1 { int test1() { return mytypedef::f(); } };

Sandini Bib 454

A

Erweiterte C++-Techniken

In diesem Kontext liefert ein Aufruf von test1 den Wert 1 zurück. Zusätzlich kann eine Klasse eine lokale Typendefinition deklarieren, d.h. eine Typendefinition, deren Gültigkeitsbereich lokal auf die betreffende Klasse begrenzt ist. Eine solche Klasse könnte so aussehen: class CTest2 { typedef class2 mytypedef; int test2() { return mytypedef::f(); } };

wobei class2 folgendermaßen definiert ist: class class2 { static int f() { return 2; } }

In diesem Fall setzt die lokale Typendefinition die globale typedef außer Kraft. Dies führt dazu, dass ein Aufruf von test2 an class2 weitergeleitet wird. Mit anderen Worten: test2() liefert den Wert 2 zurück. Dies gilt auch, wenn die typedef vererbt wurde. Wenn wir alle Techniken zusammen anwenden, ergibt sich folgendes Szenario: template class CMyClass : public T { int test() { return mytypedef::f(); } };

Welchen Wert liefert CMyClass::test() zurück? Die ersten beiden der behandelten Techniken bewirken, dass CMyClass von CTest2 erbt. Da CTest2 die Klasse mytypedef in class2 umdefiniert, verwendet CMyClass ebenfalls die class2-Version von f. Ergebnis: CMyClass::test() liefert 2 zurück.

Sandini Bib

B OLE DB-Typen und Konvertierungen Die folgenden OLE DB-Datentypen geben an, zu welchem Typ die Daten in der Spalte einer Zeilenmenge gehören: DBTYPE_I1: eine 1 Byte große Ganzzahl (integer) mit Vorzeichen. Der entsprechende C-Typ ist signed char. DBTYPE_UI1: eine 1 Byte große Ganzzahl (integer) ohne Vorzeichen. Der entsprechende C-Typ ist BYTE. DBTYPE_I2: eine 2 Byte große Ganzzahl (integer) mit Vorzeichen. Der entsprechende C-Typ ist SHORT. DBTYPE_UI2: eine 2 Byte große Ganzzahl (integer) ohne Vorzeichen. Der entsprechende C-Typ ist unsigned short. DBTYPE_I4: eine 4 Byte große Ganzzahl (integer) mit Vorzeichen. Der entsprechende C-Typ ist LONG. DBTYPE_UI4: eine 4 Byte große Ganzzahl (integer) ohne Vorzeichen. Der entsprechende C-Typ ist unsigned long. DBTYPE_I8: eine 8 Byte große Ganzzahl (integer) mit Vorzeichen. Der entsprechende C-Typ ist LARGE_INTEGER. DBTYPE_UI8: eine 8 Byte große Ganzzahl (integer) ohne Vorzeichen. Der entsprechende C-Typ ist ULARGE_INTEGER. DBTYPE_R4: ein Gleitkommawert mit einfacher Genauigkeit. Der entsprechende C-Typ ist float. DBTYPE_R8: ein Gleitkommawert mit doppelter Genauigkeit. Der entsprechende C-Typ ist double. DBTYPE_BOOL: ein boolescher Wert, der im Typ VARIANT_BOOL gespeichert wird. DBTYPE_ERROR: ein Fehlercode. Der entsprechende C-Typ ist SCODE. DBTYPE_STR: ein Einzelbyte-String. Der entsprechende C-Typ ist CHAR[]. DBTYPE_WSTR: ein Doppelbyte-String. Der entsprechende C-Typ ist WCHAR[].

Sandini Bib 456

B

OLE DB-Typen und Konvertierungen

DBTYPE_BSTR: ein Doppelbyte-String mit intern gespeicherter Längenangabe. Der entsprechende C-Typ ist BSTR. DBTYPE_BYTES: ein Byte-Array. Der entsprechende C-Typ ist BYTE[]. DBTYPE_DATE: eine Datumsangabe, die im Typ DATE gespeichert wird. DBTYPE_DBDATE: ein Datumswert, der im Typ DBDATE gespeichert wird. DBTYPE_DBTIME: eine Uhrzeitangabe, die im Typ DBTIME gespeichert wird. DBTYPE_DBTIMESTAMP: eine Datums- und Uhrzeitangabe, die im Typ DBTIMESTAMP gespei-

chert wird. DBTYPE_FILETIME: eine Datums- und Uhrzeitangabe, die im Typ FILETIME gespeichert

wird. DBTYPE_GUID: ein 128 Bit großer Bezeichner (ID). Der entsprechende C-Typ ist GUID. DBTYPE_CY: eine Währungsangabe. Der entsprechende C-Typ ist LARGE_INTEGER. DBTYPE_DECIMAL: ein Dezimalwert. Der entsprechende C-Typ ist DECIMAL. DBTYPE_NUMERIC: ein numerischer Wert. Der entsprechende C-Typ ist DB_NUMERIC. DBTYPE_VARNUMERIC: ein numerischer Wert mit variabler Länge. Der entsprechende C-Typ ist DB_VARNUMERIC. DBTYPE_VARIANT: ein OLE-Automatisierungswert vom Typ VARIANT. DBTYPE_PROPVARIANT: ein OLE-Automatisierungswert vom Typ PROPVARIANT. DBTYPE_IDISPATCH: ein Zeiger auf eine IDispatch-Schnittstelle. DBTYPE_IUNKNOWN: ein Zeiger auf eine IUnknown-Schnittstelle.

OLE DB verfügt über eine Datenkonvertierungsbibliothek. Die verschiedenen Provider müssen diese zwar nicht verwenden, aber die meisten von ihnen tun es, da es nicht sinnvoll wäre, den mit der Erstellung einer solchen Bibliothek verbundenen Aufwand zu wiederholen. Aus diesem Grund kann man davon ausgehen, dass die einzelnen Provider jeweils dieselbe Art der Konvertierung bieten.

B.1 Konvertierungstabelle Die folgende Tabelle zeigt eine Übersicht über die einzelnen Konvertierungsmöglichkeiten. Die erste Spalte enthält die Quelltypen, die erste Zeile die Zieltypen.

Sandini Bib 457

Von: | Nach:

I1

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

UI1

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

I2

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

UI2

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

I4

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

UI4

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

I8

J

J

J

J

J

J

J

J

J

J

J

J

J

J

UI8

J

J

J

J

J

J

J

J

J

J

J

J

J

J

R4

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

R8

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

BOOL

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

ERROR

J

J

J

STR

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

WSTR

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

BSTR

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

BYTES DATE

J

J

J

J

J

J

J

J

J

J

J

J

DBDATE

J

J

J

J

J

J

J

J

DBTIME

J

J

J

J

J

J

J

J

DBTIMESTAMP

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

FILETIME

J

J

GUID

J

J

CY

J

J

J

J

J

J

DECIMAL

J

J

J

J

J

J

NUMERIC

J

J

J

J

J

VARNUMERIC

J

J

J

J

J

VARIANT

J

J

J

J

J

J

J

J

J

J

J

J

J

PROPVARIANT

J

J

J

J

J

J

J

J

J

J

J

J

J

IDispatch

J

J

J

J

J

J

J

J

J

J

J

J

J

IUnknown

IUnknown

Konvertierungstabelle

I1, I2, UI1, UI2 I4, UI4 I9, UI8 R4 R8 BOOL ERROR STR BSTR, WSTR BYTES DATE DBDATE DBTIME DBTIMESTAMP FILETIME GUID CY DECIMAL NUMERIC VARNUMERIC VARIANT PPOPVARIANT IDispatch

B.1

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

J

Sandini Bib

Sandini Bib

Index

A Abbrechen, Transaktionen 310 Ablaufverfolgung benutzerdefinierte Fehlerinformationen 106 für Eigenschaften 126 für Fehler 100 Abort 312 Abrufen CLSID 163 Daten 257 Datenbestandteile 251 Eigenschaften 127, 130 Eigenschafteninformationen 133 Eigenschaftenstrukturen 128 Positionsmarken 249 Spaltenlänge 252 Spaltenstatus 252 Spaltenwerte 252 Zeilen mit Snapshot 215 Zeilenanzahl in Zeilenmenge 197 Zeilenmengen-Eigenschaften 209 Zeilenmengen-Sammelpackungen 210 Absoluter Dokumentname 441 Accessor Siehe Zugriffsobjekt (Typ) Accessor-Klassen Siehe Zugriffsobjekt-Klassen Achsen, mit mehreren Dimensionen 414 Achseninformationen 411 mehrere Dimensionen 415 Achsenkoordinaten 411 Achsen-Zeilenmengen 411 Koordinatenebenen 413 ACID 311 Atomicity 311 Consistency 311 Durability 311 Isolation 311 Active Server Pages Siehe ASP ActiveX Data Objects Siehe ADO ActiveX Template Library Siehe ATL Add 121 AddColumn 392

AddErrorRecord 103 AddProperty 120, 123 AddRef 33 AddRefAccessor 231 ADO 26, 52, 63 Architektur 63 ADO-Objekttypen Befehlsobjekt (Command) 63 Datensatzmengenobjekt (Recordset) 64 Fehlerobjekt (Error) 64 Feldobjekt (Field) 64 Parameterobjekt (Parameter) 64 Verbindungsobjekt (Connection) 63 Advise 277 Änderungsstatus bei verzögerter Aktualisierung 267 Aggregation Siehe Teile/Ganzes-Beziehung Aktive Zeilen 197 Referenzzähler 197 Aktualisierungen, verzögerte 266 Aktuelle Zeile 197 AlterColumn 393 Anbieter Siehe Provider Architektur für verteilte Internet-Anwendungen Siehe DNA Arrays, aus Zeilenmengen 214 ASP 28 Asynchrone Transaktionen 329 ATL 43, 74 AtlAdvise 277 ATL-Projekte erstellen 74 AtlTraceErrorRecords 101, 106 AtlTraceErrorRecordsEx 107 AtlTracePropertySets 126 AtlUnadvise 278 Aufzählen Eigenschaften 131 Fehler 132 Kind-Zeilen 445 untergeordnete Zeilen 445 Aufzählungs-GUIDs 132

Sandini Bib 460 Ausführungsplan 179 Ausgabe-Zugriffsobjekte 225 Definition 225 Ausnahmen 97 Nachteile 97 Authentifizierung, beim Öffnen von Datenquellen 145 Auto Accessor Siehe Automatische Zugriffsobjekte Automatische Zugriffsobjekte 232, 241, 258 Automatisches Festschreiben, Isolationsstufen 328

B Basis-Bindungsobjekt 430 Basisobjekt, MTS 341 bAutoAccessor 232 Befehl (Typ) 57 Befehle Definition 169 erzeugen 171, 179 in OLE DB 91 mehrdimensionale 407 öffnen 84, 171, 179 Parameter-Zugriffsobjekte 187 schließen 177 zum Zugriff auf Daten 165 Befehle mit mehreren Parameter-Sets 191 Befehle mit Mehrfachergebnis 183 Befehle mit Parametern 182 Befehle vorbereiten 179 Ausführungsplan 180 mehrmalige Ausführung 181 Befehlsausführung, Ausführungsplan 179 Befehlsobjekte (ADO) 63 Befehlsobjekte freigeben 177 Befehlssprachendialekte 174 Befehlstext angeben 173 BeginTransaction 334 Beibehaltung von Zeilenmengen 314 Eigenschaften 314 Benachrichtigungen 276 konfigurieren 282 Benachrichtigungsobjekte 280 Benutzerdefinierte Eigenschaften-Klassen 125 Benutzerdefinierte Fehlerinformationen, Ablaufverfolgung 106 Benutzerdefinierte Fehlerobjekte abrufen 106 Benutzernamen, Datenquelle öffnen 140 Berechtigungen 386 Berechtigungsinformationen 387 Bereichs-Zeilenmengen 421 Vergleich mit Zeilenmengen 422 Binary Large Objects Siehe BLOBs Bind 379, 430

Index BindColumns 233 Binden direkt 242 über Referenz 242 Binder Siehe Bindungsobjekt (Typ) BindFinished 411 BindParameters 233 Bindungen Datenbestandteile 227 Definition 225 Kategorien von Datentypen 225 Makros 239 Offset-Werte 228 prüfen 395 Zugriffsobjekte 224 Bindungsmakros für Spaltentyp und Größe 240 Unterstützung von Genauigkeit und Skalierung 240 Bindungsobjekt (Typ) 57 Bindungsverfahren, Zusammenfassung 246 Blättern, in Zeilenmengen 198 BLOB_ENTRY 292 BLOB_ENTRY_CODE_EX 295 BLOB_ENTRY_STATUS 292 BLOBs 285 als COM-Objekt abrufen 306 als persistente COM-Objekte 305 Daten im Speicher 287 Daten im Speicher mit Pufferreferenz 288 Daten in Speicherobjekt speichern 296 Daten über Speicherobjekt abrufen 291 Datenlänge 295 Eigenschaften 298 im Speicherobjekt 289 in Consumer-Speicherobjekt schreiben 297 in Provider-Speicherobjekt schreiben 296 Inhalt 285 kurze BLOBs 286 lange BLOBs 286 mit fester Größe 287 mit mehreren arbeiten 299 mit variabler Größe 287 Schnittstellen für Speicherobjekte 289 Speichermodus 292 Typen 285 Zugriffsverfahren 286 BLOB-Typen 285 Zugriffsverfahren 287 Bookmark Siehe Positionsmarken BSTR 245 Bulk Rowsets Siehe ZeilenmengenSammelpackungen

Sandini Bib Index C C++ erweiterte Techniken 451 Vererbung 451 C++-Typen und Variantentypen 120 Vergleich mit OLE DB-Typen 236 CAccessor 82, 398 CAccessor 92 CAccessorBase 232 CAccessorByName 398 CAccessorRowset 92ff., 177, 379, 411, 422 CArrayRowset 91, 214ff., 299, 423 CArrayRowset 195 CAxisAccessor 413, 415f. CAxisColumns 415 CAxisRowset 413 CBookmark 204 CBookmarkBase 204 CBulkRowset 91, 195, 211f., 423 CCatalogs 380 CCellAccessor 419 CChildRowset 363 CColumnPrivileges 386 CColumns 382 CCommand 84, 91, 170, 195, 407 CCommandBase 169 CCommandEx 190 CComPtr 43 Klasse 43 CComVariant 131 CDataset 409 CDataSource 90, 133, 137, 160 CDataSourceEx 133, 161, 163 CDBErrorInfo 104 CDBPropertyInfoSet 136 CDBPropIDSet 129 CDBPropSet 90, 120, 123 CDBPropSetEx 123 CDynamicAccessor 92, 232, 247, 286, 424 CDynamicParameterAccessor 92, 232, 247, 253 CDynamicParameterAccessorEx 254 CDynamicRowAccessor 432, 435 CEnumerator 158 CEnumeratorAccessor 158 CForeignKeys 385 Chapter Siehe Kapitelmarken CHasChapter 357 CHECK_RESULT 100 CheckDB 395 CIndex 346 Klassenelemente 348 CIndexDefinition 389, 393 CIndexes 384 Class Member Siehe Klassenelemente Client/Server-Architektur 28

461 CLIENT_OWNED 244 Client-Cursor 59, 153, 217 Client-Cursor-Engine 220 Client-Speicher 244 Close 95, 177, 234, 411 CLSID 102, 139 abrufen 163 in ProgID konvertieren 140 Clustered Indexes Siehe Gruppenbildende Indizes CManualAccessor 92, 233 CMDCommand 407 CMDSession 407 CMultipleResults 91, 185 CNoAccessor 92, 167, 233 CNoChapter 357 CNoMultipleResults 91, 185 CNoRowset 91, 195, 409 CoCreateInstance 33, 152, 154 Codegenerierung, mit dem OLE DB-Assistenten 80 Column Accessor Siehe Spalten-Zugriffsobjekte COLUMN_ENTRY 235 COLUMN_ENTRY_LENGTH 238 COLUMN_ENTRY_LENGTH_STATUS 238 COLUMN_ENTRY_STATUS 238 COLUMN_MAP 237 COM 31 Mechanismen zur Fehlerbehandlung 97 Vorteile 35 COM+ 34 COM-Komponenten, Kapselung 44 Command Siehe Befehl (Typ) Command Siehe Befehlsobjekte (ADO) Commit 312 Commit Siehe Festschreiben COM-Objekte abrufen 306 Konstruktions-Strings 150 mehrere Schnittstellen verwenden 58 persistente 305 Compare 206 Component Object Model Siehe COM COM-Schnittstellen Aktualisierung 36 Leistung 35 Sprachenunabhängigkeit 37 Transparenz des Ausführungsortes 37 COM-Speicherallokator 269 Connection Points Siehe Verbindungspunkte Connection Siehe Verbindungsobjekte (ADO) Consumer 54 Consumer Templates, Erweiterungen 71 Consumer-Speicher Siehe Client-Speicher Copy 436 CPrimaryKeys 385 CProcedureColumns 384

Sandini Bib 462 CProcedureParameters 384 CProviderTypes 388 CProviderTypesEx 388 CRangeRowset 422 Create 180, 433 CreateAccessor 231 CreateDBInstance 154 CreateTable 390, 392 CreateTableByAccessor 400 CResourceAccessor 440 CRestrictions 378 Cross-Join Siehe Kreuzverknüpfung CRow 429, 432 CRowAccessor 435 CRowFolder 429, 442 CRowset 82, 91, 195, 257, 409 Klassenelemente 348 Methoden für Kapitelmarken 355 CRowsetEx 196, 209, 269, 272, 274, 356 CRowsetFind 196, 219 CSchemaRowset 379 CSession 90, 163, 312, 334, 407 CSessionEx 334f. CStoreRestrictions 379 CStream 429, 446 CTable 82, 91, 165f., 170, 195, 398 CTableDefinition 389, 392, 400 CTablePrivileges 386 CTables 381 CTransactionManager 334 Cube Browser 408 hierarchische Dimensionen 413 Cursorbewegung Anforderungen 202 in Zeilenmengen 198 CView 372

D Data Definition Language Siehe DDL Data Link Siehe Datenverknüpfungen Data Shape-Dienst 59, 361 Data Shape-Sprache 60, 361 Data Source Siehe Datenquelle (Typ) Database Management Systems Siehe Datenbankverwaltungssysteme Dataset Siehe Datenmenge (Typ) Dateien, konvertieren 302 Daten abrufen 257 mit Zugriffsobjekt-Index 257 ohne Zugriffsobjekt-Index 258 Daten ändern 263 verzögerte Aktualisierung 266 Daten setzen 263 Datenaustausch 257

Index Datenbanken Inhalte 49 proprietäre 138 Datenbankverwaltungssysteme 49 Datenbestandteile 227 abrufen 251 Flags 227 setzen 251 Datenlinks Siehe Datenverknüpfungen Datenmenge (Typ) 57 Datenmengen 408 CDataset 409 Darstellung als Zeilenmengen 424f. Metadaten 410 mit mehreren Dimensionen 415 Speicherverwaltung 420 Unterschied zu Zeilenmengen 405 Zellen 405, 417 Zellenbereiche 421 Zellenbereiche abrufen 423 Zusatzachse 410 Datenmengen-Objekte 405, 408 Datenquelle (Typ) 137 Datenquelle auswählen, im Assistenten 77 Datenquellen 56 aufzählen mit Enumeratoren 157 in OLE DB 137 interaktiv bearbeiten 162 ODBC 138 OLE DB-Erweiterungen 161 schließen 157 Datenquellen öffnen Authentifizierung 145 Datenverknüpfungen 147 grundlegende Initialisierungseigenschaften 143 Initialisierungsmodi 141 Initialisierungs-Strings 147 interaktiv 150 mit Benutzername und Kennwort 140 mit Dienstkomponenten 153 mit Enumerator und Eigenschaften-Set 160 mit unvollständigen Informationen 144 Schutz 146 Sicherheit 145 über die Programmierung 139 über Eigenschaften-Sets 143 über eine Datenverknüpfung 149 Verschlüsselung 146 zusätzliche Initialisierungseigenschaften 144 Datensatzmengenobjekte (ADO) 64 Datenschicht 24, 26 Datenspeicher Metadaten 377

Sandini Bib Index Schemata 377 Sichten 383 Datenströme 428, 430, 446 Daten lesen 448 Daten schreiben 448 erzeugen 447 öffnen 446 Datenstrom (Typ) 57 Datenstrom-Objekte 446 Implementierung 430 Datentypen besonders große 226 Kategorien für Bindungen 225 Konvertierungstabelle 456 mit fester Länge 226 mit variabler Länge 226 von Eigenschaften 114 Datenverknüpfungen 147 Definition 147 Datenzugriff mit OLE DB Befehle 165 Nachteile von Tabellen 165 Tabellen 165 Vorteile von Tabellen 165 Datenzugriffskomponenten von Windows 60 DBID 124 DBMS Siehe Datenbankverwaltungssysteme DBPROP 117 DBPROP_IRowsetChange 85 DBPROP_UPDATABILITY 85 DBPROPIDSET 127, 131 DBPROPINFO 134 DBPROPINFOSET 134 DBPROPSTATUS 125 DBPROPVAL_UP_CHANGE 85 DBPROPVAL_UP_DELETE 85 DBPROPVAL_UP_INSERT 85 DBRPROPSET 117 DDL 400 Delegierende Schnittstelle 39 DeleteRows 262 Demoprogramme Preservation Demo 315 Transaktionsisolation 318 Dialekte, von Befehlssprachen 174 Dienstkomponenten 59, 152 Client-Cursor 153, 217 Ressourcen-Pooling 153 Transaktionseinbindung 153 zum Öffnen von Datenquellen 153 Dimensionen, Hierarchien 412 Direktes Binden 242, 430 Architektur 431 Basis-Bindungsobjekt 430 DisableCommit 340

463 Distributed Internet Applications Architecture Siehe DNA DNA 23 DNA-Architektur 24 Dokumente 440 Attribute 441 Kind-Zeilen aufzählen 445 untergeordnete Zeilen aufzählen 445 Domains Siehe Ereignisbereiche dsedit, Online-Beispiel 142, 149, 152, 161, 162 Dynamische Zugriffsobjekte 188, 247 Erweiterungen 254

E Eigenschaften Ablaufverfolgung 126 abrufen 127, 130 aufzählen 131 Datentypen 114 Definition 113 Eigenschaften-Sets 114 erforderliche 123 optionale 123 von Zeilenmengen 209 Eigenschaften abrufen, Beispielcode 135 Eigenschaften setzen 122 Fehler aufzählen 132 Eigenschaften-Flags, in OLE DB 134 Eigenschaftengruppen Definition 115 in OLE DB 116 Unterschiede zu Eigenschaften-Sets 116 Eigenschafteninformationen abrufen 133 Eigenschaften-Klassen, benutzerdefinierte 125 Eigenschaften-Sets Definition 114 GUIDs 115 in OLE DB 125 OLE DB-Erweiterungsklassen 125 Speicherverwaltung 129 zum Öffnen von Datenquellen 143 Eigenschaftenstatus prüfen 125 Eigenschaftenstrukturen abrufen 128 Eigenschaftsbeschreibung, in OLE DB 134 Eigenschaftsbezogene Operationen 116 Eigenschaften abrufen 127 Eigenschaften aufzählen 131 Eigenschaften setzen 117, 122 Eigenschafteninformationen abrufen 133 Eigenschaftsoption 123 Eigenschaftstypen, inOLE DB 134 Einfache Transaktionen 312 Eingabe-Zugriffsobjekte 225 Definition 225 Elternknoten 427 Eltern-Zeilenmengen 363

Sandini Bib 464 EnableCommit 340 Enumeratoren 157 Cursorbewegung 159 Provider-Enumerator 157 Stamm-Enumeratoren 157 Ereignisbereiche 278 Ereignisse Ereignisbereiche 278 Gründe für 279 Phasen 280 Erforderliche Eigenschaften 123 Erneuerung von Transaktionen 313 Error Siehe Fehler (Typ) Error Siehe Fehlerobjekte (ADO) ERRORINFO 105 ErrorInfo, Unterstützung 109 Erweiterungsbibliothek, OLE DB Consumer Templates 71 Erzeugen, Befehle 171, 179 Exceptions Siehe Ausnahmen Execute 169, 180 Exemplar, eines Objekts 33 Extensible Markup Language Siehe XML

F Fehler (Typ) 57 Fehler aufzählen 132 Fehlerbehandlung, Mechanismen in COM 97 Fehlerdatensätze, IErrorRecords 102 Fehlerdatensatz-Objekte 100, 102 Fehlerinfo-Objekte 100, 102 Fehlerinformationen abrufen 104 IErrorInfo 102 Fehlerobjekte (ADO) 64 Fehlerobjekte abrufen, benutzerdefinierte 106 Fehler-Trace Siehe Ablaufverfolgung für Fehler Felder, in Zeilenmengen 193 Feldobjekte (ADO) 64 Festschreiben automatisches 328 Transaktionen 309 Fetching Siehe Holen Field Siehe Feldobjekte (ADO) FindConnectionPoint 277 FindNextRow 220 Flags für Datenbestandteile 227 Flags für Parametertyp 247 Flags für Parameter-Zugriffsobjekte 230 FormatMessage 98 FREE_MEMORY 244 FreePropertySets 130 FreeRecordMemory 244, 294, 420 Freigeben, Befehlsobjekte 177 Fremdschlüssel 385 Funktionsaufruf, indirekter 35

Index G Genauigkeit, Unterstützung in Bindungsmakros 240 Geschachtelte Transaktionen Siehe Verschachtelte Transaktionen Geschäftslogikschicht 24f. GetAllErrorInfo 104 GetApproximatePosition 208 GetAxisInfo 411 GetBasicErrorInfo 103 GetBindings 231 GetBuffer 232 GetCellOrdinal 418 GetColumnAccess 435 GetColumnFlags 250 GetColumnName 250 GetColumns 434f. GetColumnType 249 GetCustomErrorObject 103, 106 GetData 257f. GetDataHere 260 GetDescription 102 GetErrorInfo 101, 103 GetErrorParameters 103 GetErrorRecords 104 GetGUID 102 GetHChapter 356f. GetHelpContext 102 GetHelpFile 102 GetIID 102 GetInterface 95, 409 GetInterfacePtr 168, 409 GetLastVisibleData 274 GetLength 252 GetNextResult 184 GetObjectContext 339 GetOriginalData 274 GetPendingRows 269 GetProperties 210 GetProperty 130, 210 GetRecordCount 103 GetSortOrder 372 GetStatus 252 GetValue, Versionen 252 Global Unique Identifier Siehe GUID Globale typdef-Deklaration, durch lokale überschreiben 174 Globaler eindeutiger Bezeichner Siehe GUID Granularität fein granulierte Komponenten 336 grob granulierte Komponenten 337 Komponenten 336 Granularity Siehe Granularität Größe, Bindungsmakro 240 Grundlegende Fehlerinformationen, Fehlerdatensätze 105

Sandini Bib Index Gruppenbildender Index 385 GUIDs 34 von Eigenschaften-Sets 115

H hAccessor 232 Hierarchische Daten Datenformate 427 Datenströme 430, 446 definierte Bereiche 436 direktes Binden 427, 430 Dokumente 440 Elternknoten 427 Hauptmerkmale 428 Kindknoten 427 öffnen 428 Spaltenwerte abrufen 433 Spaltenwerte setzen 433 verwendete Objekte 429 Zeilen erstellen 433 Zeilen kopieren 436 Zeilen löschen 439 Zeilen öffnen 432 Zeilen verschieben 438 Zeilenmengen binden 434 Zeilenobjekte 429, 432 Zeilenordner 429 Zeilenordner-Objekt 442 Hierarchische Provider 56 Hierarchische Zeilenmengen 358 Cursorbewegung 363 Holen, Zeilen in Zeilenmengen 198 HRESULT 97 Code-Teil 98 Facility-Teil 98 Inhalt 98 Interpretation 98 Severity-Teil 98

I IAccessor 169, 231 ICommand 58, 169 ICommandPrepare 169 ICommandProperties 169 ICommandText 169 ICommandWithParameters 58, 169, 182 IConnectionPointContainer 277 IDataInitialize 154 IDL 32 IErrorInfo 101f. IErrorRecords 103f. IGetDataSource 407 IID 34 IInner 41

465 ILockBytes 291, 303 IMDDataset 409 IMDRangeRowset 409 Implementierung, zwei IUnknown-Schnittstellen 38 Index (Typ) 346 Indexeigenschaften 394 Indizes 345, 384 Arten 346 aufzählen 385 Bereich beschränken 351 Bereichs-Flags 352 Bindungen 347 Cursor bewegen 349 Definition 345 Eigenschaften 394 gruppenbildende 385 integrierte 346 navigieren in 349 öffnen 348 separate 346 Suchoptionen 350 unterstützte Schnittstellen 347 Verwendung 346 Zeile suchen 349 Zugriffsobjekte 347 Initialisierungseigenschaften, beim Öffnen von Datenquellen 143, 144 Initialisierungs-Strings 147 Input Accessor Siehe Eingabe-Zugriffsobjekte Insert 264 InsertRow 262 Instance Siehe Exemplar Instanz, eines Objekts Siehe Exemplar Instanziierung, von Objekten 38 Interaktives Bearbeiten von Datenquellen 162 Interaktives Öffnen von Datenquellen 150 Interface Definition Language Siehe IDL Interfaces Siehe Schnittstellen InterfaceSupportsErrorInfo 109 Interne IUnknown-Schnittstelle 39 Internet Publishing Provider 428, 440 IObjectContext 339 Methoden 340 Methoden und Transaktionen 340 IOpenRowset 407 IOuter 41 IOuter::QueryInterface 41 IPersistStorage 305 IPersistStream 305 IPersistStreamInit 305 IRowset 196 IRowsetChange 262, 265 IRowsetFind 217, 220 IRowsetIndex 346

Sandini Bib 466 IRowsetLocate 209 IRowsetNotify 277, 280 IRowsetUpdate 267, 272 IRowsetView 371 ISequentialStream 290, 446, 448 Isolationsphänomene 318 Isolationsstufen 316, 318 Auswahl 326 Festgeschriebene Daten lesen 316, 322 Nicht festgeschriebene Daten lesen 316, 320 Serialisierbar 316, 325 und Sperrungen 325 Wiederholbares Lesen 316, 323 ISQLErrorInfo 106 IStorage 291, 304 IStream 291, 302 ISupportErrorInfo 109 ITransactionOutcomeEvents 329 Methoden 330 IUnknown 38 IUnknown-Schnittstelle, interne 39 IViewChapter 370 IViewFilter 370 IViewRowset 370 IViewSort 370

K Kapitelmarken 353 für Sichten 373 Handles 354 in OLE DB 196 Methoden 355 Vergleich mit Positionsmarken 353 Verwendung 354 Zweck 353 Kapitelmarken-Handles 361 Kapselung, COM-Komponenten 44 Katalog/Schema-Modell 380 Kataloge 380 Schemata 380 Kataloge 380 Katalogliste, navigieren in 381 Kennwörter, Datenquellen öffnen 140 Kindknoten 427 Kind-Zeilen aufzählen 445 Kind-Zeilenmengen 363 öffnen 363 Schnittstellenzeiger abrufen 364 Klassen der OLE DB Consumer Templates 89 Klassenbezeichner Siehe CLSID Klassenelemente 45 Kompilieren, Bindungen prüfen 395 Komponenten Granularität 336 und Transaktionen 336 Wiederverwendung 30, 336

Index Komponentendienste, Verwaltung 335 Komponentenobjektmodell Siehe COM Konstruktions-Strings für COM-Objekte 150 Konvertieren CLSID in ProgID 140 ProgID in CLSID 140 Konvertierungen 455 Konvertierungstabelle 456 Kreuzverknüpfungen 414

L Längenbindung, Makros 238 Listener, für Zeilenmengen Siehe ZeilenmengenListener Lokale Klassen 188 über Makros definieren 190 Lokale typdef-Deklaration, globale überschreiben 174

M Makros für Bindungen, Übersicht 239 Makros zur Längenbindung 238 Makros zur Spaltenbindung 235 Makros zur Statusbindung 238 MDAC 430 MDAC Siehe Datenzugriffskomponenten von Windows MDAXISINFO 415 MDX 405 Mehrdimensionale Daten 403 Datenmengen-Objekt 405 Dimensionen 411f. MDX 405 Würfel 405 Zugriff über Zeilenmengen 424 Mehrdimensionale Provider 56, 405 Vergleich zu Tabellen-Providern 407 Mehrdimensionale Sitzungen 407 Mehrere Parameter-Sets, Verwendung bei Befehlen 191 Mehrere Zugriffsobjekte 241 Mehrere Zugriffsobjekt-Klassen, Verwendung vermeiden 188 Mehrfachergebnis-Objekt, Definition 185 Mehrfachergebnisse mehrere Zeilenmengen 183 von Befehlsausführungen 183 Mehrmalige Ausführung eines Befehls 181 Member Siehe Klassenelemente Metadaten 410 Achsenkoordinaten 411 Datenspeicher 377 Metadaten Siehe auch Schemata Methodenaufrufe, virtuelle 35 MFC-Assistent 89

Sandini Bib Index Microsoft Data Access Components Siehe Datenzugriffskomponenten von Windows Microsoft Data Engine 49 Microsoft Distributed Transaction Coordinator 333 Microsoft OLAP Manager 408 Microsoft Transaction Server 338 Microsoft Transaktion Server, Basisobjekt 341 Move 438 MoveFirst 200 MoveNext 199, 202 MovePrev 202 MoveToBookmark 213 MoveToRatio 213 MS DTC Siehe Microsoft Distributed Transaction Coordinator MSDE Siehe Microsoft Data Engine MTS Siehe Microsoft Transaction Server Multidimensional Expression Siehe MDX Multidimensionale Daten Siehe Mehrdimensionale Daten

N Namensbereiche 34 Namespace Siehe Namensbereiche Numerische Positionsmarken 205 Nutzer Siehe Consumer

O Objekte Implementierung 32 Schnittstellen 32 Objektexemplare 33 Objektinstanz Siehe Objektexemplare Objektkontext 339 Methoden 340 ODBC 26, 52f., 138 Architektur 53 Nachteile 54 Vergleich mit OLE DB 61 ODBC-Datenquellen-Administrator 138 Öffnen Befehle 84, 171, 179 Datenquellen 139, 150 Tabellen 81, 167 OLAP mehrdimensionale Daten 403 Tabellendaten 403 OLAP-Provider Siehe Mehrdimensionale Provider OLAP-Schema-Daten 425 OLE DB 26, 52, 54 Befehle 91 Consumer Templates verwenden 70

467 Dienstkomponenten 59 direkt verwenden 66 Fehlermechanismen 97 Tabellen 90 Unterschiede zu ODBC 54, 61 Versionen 61 OLE DB Consumer Templates 65 automatische Zugriffsobjekte 241 Klassen 89 Zeilenmengen-Klassen 195 Zugriffsobjekt-Klassen 231 OLE DB Extensions Siehe OLE DB-Erweiterungen OLE DB für OLAP 403, 405 OLE DB Provider Templates 65 OLE DB Software Development Kit Siehe OLE DB-SDK OLE DB Templates, Definition 65 OLE DB Wizard Siehe OLE DB-Assistent OLE DB-Assistent 73 generierter Code 80 Verbesserungen am generierten Code 88 Verwendung 74 OLE DB-Datenquellen 137 OLE DB-Dienstkomponenten Client-Cursor 59, 369 Ressourcen-Pooling 59 Transaktionseinbindung 59 OLE DB-Eigenschaften 113 setzen 118 OLE DB-Erweiterungen 71 alle Änderungen übertragen 272 dynamische Zugriffsobjekte 254 für Befehle 186 für Datenquellen 161 Klasse für Kapitelmarken 356 Klasse für Zeilen-Suchfunktion 219 Makro zum Binden der Datenlänge 295 Makros für lokale Klassen 190 Zeilen mit Änderungsstatus abrufen 269 Zeilen-Zugriffsobjekte 434 zusätzliche Zeilenmengen-Klassen 195 Zweck 72 OLE DB-Erweiterungsklassen 125 OLE DB-Klassen 104 OLE DB-Provider 60 Typen 54 OLE DB-SDK 98 OLE DB-Sitzungen 137, 163 OLE DB-Typen 56, 455 Befehl (Command) 57 Bindungsobjekt (Binder) 57 Datenmenge (Dataset) 57 Datenquelle (Data Source) 56, 137 Datenstrom (Stream) 57 Fehler (Error) 57

Sandini Bib 468 Index 346 Sicht (View) 57, 368 Sitzung (Session) 57, 137 Transaktion (Transaction) 57 Vergleich mit C++-Typen 236 Zeile (Row) 57 Zeilenmenge (Rowset) 57, 193 OLE for Data Bases Siehe OLE DB OnFieldChange 278 OnLine Analytical Processing Siehe OLAP OnRowChange 278 OnRowsetChange 278 Open 90, 141, 172, 432 Vergleich mit OpenWithServiceComponents 154 Open DataBase Connectivity Siehe ODBC OpenDataSource 82, 90 OpenEx 190 OpenFromInitializationString 148 OpenRowset 83 OpenWithServiceComponents 154 Vergleich mit Open 154 Operationen für Eigenschaften 116 mit definiertem Bereich 436 Optionale Eigenschaften 123 Output Accessor Siehe Ausgabe-Zugriffsobjekte

Index Position 208 Typen 205 Vergleich mit Kapitelmarken 353 vergleichen 206 Positionsmarkentypen numerische Positionsmarken 205 Schlüsselwert-Positionsmarken 205 Präsentationsschicht 24f. Prepare 180 Primärschlüssel 385 ProgID 139 in CLSID konvertieren 140 Programmgestütztes Öffnen von Datenquellen 139 Proprietäre Datenbanken 138 Provider 54 hierarchische 56 mehrdimensionale Provider 405 Tabellen-Provider 405 Provider-Enumerator 160 Provider-Speicher 243 Nachteile 243 Provider-Typen 387 Prozeduren 383 Werte 384 Prüfen, Eigenschaftenstatus 125 Puffer 257 Inhalt synchronisieren 258

P PARAM_MAP 87 Parameter, für Befehle 182 Parameter Accessor Siehe ParameterZugriffsobjekte Parameter Siehe Parameterobjekte (ADO) Parameterbindung 246 Flags für Parametertyp 247 Verfahren 246 Parametermethoden, Vergleich mit Spaltenmethoden 253 Parameterobjekte (ADO) 64 Parameter-Sets, für Befehle 191 Parametertyp, Flags 247 Parameter-Zugriffsobjekte 224 Definition 224 Flags 230 für Befehle 187 Pending Status Siehe Änderungsstatus bei verzögerter Aktualisierung Pfadangabe absolute 432 relative 432 Positionsmarken 203 abrufen 249 Definition 203 Klassen 204

Q QueryInterface 33, 40

R Readcommitted 316 Readuncommitted 316 Recordset Siehe Datensatzmengenobjekte (ADO) Referenzgestütztes Binden 242 Referenzzähler, für Zeilen 196 Release 33 ReleaseAccessor 231 ReleaseAccessors 234 ReleaseCommand 177 Repeatableread 316 Resource Pooling Siehe Ressourcen-Pooling Ressourcen-Manager 342 Ressourcen-Pooling 59, 153, 156 Ressourcen-Verteiler 342 Restrictions Siehe Restriktionen Restriktionen, Definition 378 Rollback, Transaktionen 310 Root Binder Siehe Basis-Bindungsobjekt Row Siehe Zeile (Typ) Rowset Siehe Zeilenmenge (Typ) Rowset-Klassen Siehe Zeilenmengen-Klassen

Sandini Bib Index S Schema-Daten, OLAP 425 Schema-Klassen 377 Schemata abfragen 378 Berechtigungen 386 Daten abfragen 377 Datenspeicher 377 Indizes 384 Kataloge 380 mit Datenbank synchronisieren 394 navigieren in 377 Provider-Typen 387 Prozeduren 383 Schlüssel 385 Sichten 383 Spalten 382 Spalteneigenschaften 391 Spaltenoperationen 392 Tabellen 381 Schemata bearbeiten 389 Tabellenoperationen 390 Schema-Zeilenmengen 378 Schließen Befehle 177 Datenquellen 157 Schlüssel 385 Fremdschlüssel 385 Primärschlüssel 385 Schlüsselwert- Positionsmarken 205 Schnittstellen delegierende 39 konvertieren 302 Schnittstellenbezeichner Siehe IID Schnittstellendefinitionssprache Siehe IDL Schutz, beim Öffnen von Datenquellen 146 Scoped Operations Siehe Operationen mit definiertem Bereich Scrolling Siehe Blättern Security Service Provider Siehe SSP-Dienste Seek 349 Optionen 350 Serializable 316 Session Siehe Sitzung (Typ) SetAbort 340 SetAccessor 94, 409 SetBuffer 232 SetChapter 374 SetComplete 339f. SetData 262f. SetErrorInfo 101 SetGUID 120 SetLength 253 SetProperty 119, 122

469 SetRange 351 SetSortOrder 372 SetStatus 253 SetupOptionalRowsetInterfaces 349 SetValue, Versionen 253 SetViewChapter 374 Setzen Datenbestandteile 251 Eigenschaften 117 Spaltenlänge 253 Spaltenstatus 253 Spaltenwerte 253 Setzen von Eigenschaften, mit unspezifischen OLE DB-Strukturen 118 Sicherheit, beim Öffnen von Datenquellen 145 Sicht (Typ) 57, 368, 370 Sichten 368, 383 filtern 372 Kapitelmarken 373 öffnen 369 Schnittstellen 370 sortieren 372 stapeln 374 Verwendung 369 Sichtobjekt 166 Simple Object Access Protocol Siehe SOAP Sitzung (Typ) 57, 137 Sitzungen in OLE DB 137, 163 ohne Transaktionen 328 Sitzungsobjekte 163 Skalierung, Unterstützung in Bindungsmakros 240 Slicer Siehe Zusatzachse Snapshot 215 SOAP 27 Sofortige Aktualisierungen 266 Unterschied zu verzögerten Aktualisierungen 266 Spalten 382 Anzahl ermitteln 249 Datentypen 249 Flag abrufen 251 Flags 249 in Zeilenmengen 193 Name abrufen 251 Namen 249 Typ abrufen 251 über Makros binden 398 über Namen binden 397 Spaltenanzahl ermitteln 249 Spaltenbezeichner 124 Spaltenbindung, Makros 235 Spalteneigenschaften 391 Spalten-Flags 250

Sandini Bib 470 Spaltenlänge abrufen 252 Spaltenlänge setzen 253 Spaltenmethoden, Vergleich mit Parametermethoden 253 Spaltenoperationen 392 Spaltenstatus abrufen 252 Spaltenstatus setzen 253 Spaltentyp, Bindungsmakro 240 Spaltenwerte abrufen 252, 433, 435 Spaltenwerte setzen 253, 433 Spalten-Zugriffsobjekte 224 Definition 224 Speicher, konvertieren 302 Speichereigner Client-Speicher 244 Provider-Speicher 243 Speichern, Verbindungs-String 150 Speicherobjekte BLOB-Daten abrufen 291 BLOB-Daten speichern 289, 296 blockierende 299 Flags 292 Schnittstellen 289 vom Consumer erzeugte 297 vom Provider erzeugte 296 Speicherverwaltung durch Consumer 244 durch Provider 243 für Eigenschaften-Sets 129 Sperrungen, und Isolationsstufen 325 SSP-Dienste 146 SSPI, Schnittstelle für SSP-Dienste 146 Stamm-Enumerator 159 Standard-Positionsmarken 208 Verwendung 208 Stapeloperationen 375 StartTransaction 312f., 331 Statische Zugriffsobjekte 188 Statusbindung, Makros 238 stdafx.h 81 Stored Procedures 87 Stream Siehe Datenstrom (Typ) Strukturierte Dokumente 442 Suchen, Zeilen 218

T Tabelle auswählen, im Assistenten 78 Tabellen 381 Definition 166 in OLE DB 90 öffnen 81, 167 Spalten hinzufügen 392 über Programmcode erstellen 399 zum Zugriff auf Daten 165 Tabellendaten 403

Index Tabellenoperationen 390 Tabellen-Provider 55, 405 Vergleich zu mehrdimensionalen Providern 407 TAccessor 95, 167 Teile/Ganzes-Beziehung 38, 59, 154 äußeres Objektexemplar 38 inneres Objektexemplar 38 Transaction Enlistment Siehe Transaktionseinbindung Transaction Siehe Transaktion (Typ) Transaktion (Typ) 57 Transaktionen 307, 309 abbrechen 310 Alles-oder-Nichts-Prinzip 311 asynchrone 329 Attribute für Erzeugung 342 automatisches Festschreiben 328 Dauerhaftigkeit 312 Einbindung 342 einfache 312 Erneuerung 313 Festgeschriebene Daten lesen (Isolationsstufe) 317 festschreiben 309 Isolation 311 Isolationsphänomene 318 Isolationsstufen 318, 320, 322f., 325 Konsistenz 311 Merkmale 311 Nicht festgeschriebene Daten lesen (Isolationsstufe) 317 Nicht wiederholbares Lesen 317 Notwendigkeit 307 Objektkontext 339 Phantome 317 Phasen 310 Rollback 310 Serialisierbar (Isolationsstufe) 317 Sitzungen ohne Transaktionen 328 Transaktionsattribute 340 und Komponenten 336 unsauberes Lesen 316 Vergleich mit verzögerten Aktualisierungen 275 verschachtelte 331 Verschachtelungsstufe 331 verteilte 333 Wiederholbares Lesen (Isolationsstufe) 317 Transaktionsattribute 340 Objekterzeugung 342 Transaktionseinbindung 59, 153, 342 Architektur 343 Ressourcen-Manager 342 Ressourcen-Verteiler 342

Sandini Bib Index Transaktions-Koordinator 342 Verbindungen 342 Verteiler-Manager 342 Transaktionsergebnisse 330 Transaktions-Koordinator 342 TRowset 167 Tupel Siehe Achsenkoordinaten

U UDA Siehe Universeller Datenzugriff UDA-Komponenten ADO 52 ODBC 52 OLE DB 52 Übergeordnete Zeilenmengen Siehe ElternZeilenmengen Unadvise 277 Undo 273 UNICODE-Format, Initialisierungs-Strings 147 UNICODE-Strings, BSTR 245 Universal Data Access Siehe Universeller Datenzugriff Universeller Daten-Server 50 Architektur 50 Universeller Datenzugriff 26, 49, 51 Untergeordnete Zeilenmengen Siehe KindZeilenmengen Update 266, 272 UpdateAll 272

V VARIANT 114, 131 Variantentypen, und C++-Typen 120 Verbindungen, Transaktionseinbindung 342 Verbindungsobjekte (ADO) 63 Verbindungspunkte 277 Verbindungs-Strings speichern in Datei 161 Möglichkeiten 150 Vererbung, bei Zugriffsobjekt-Klassen 234 Verfahren für Parameterbindung 246 Vergleichsoperatoren für Suche 221 Verschachtelte Transaktionen 331 Verschachtelungsstufe, Transaktionen 331 Verschlüsselung, beim Öffnen von Datenquellen 146 Verteiler-Manager 342 Verteilte Transaktionen 333 Verzögerte Aktualisierungen 266 Änderung rückgängig machen 273 Änderung übertragen 270 Aktualisierungsstatus 267 alle Änderungen rückgängig machen 274 alle Änderungen übertragen 272

471 Unterschied zu sofortigen Aktualisierungen 266 Vergleich mit Transaktionen 275 View Siehe Sicht (Typ) Vorbereiten, Befehle 179

W Wiederverwendung von Komponenten 30 Wiederverwendung von Quellcode, Nachteile 30 WinMain 395 WriteStringToStorage 161 Würfel 405

X XML 26 XML und UDA, Verbindungspunkte 27

Z Zeile (Typ) 57 Zeilen anhand von Kapitelmarken filtern 353 anhand von Kapitelmarken sortieren 353 einfügen 264 in Zeilenmengen 193 kopieren 436 löschen 262, 439 öffnen 432 Referenzzähler 196 verschieben 438 Zeilen erstellen 433 Flags 434 Zeilen suchen 218 mit Client-Cursor-Dienstkomponente 218 Vergleichsoperatoren 221 Zeilenanzahl abrufen 197 in Zeilenmenge 197 Zeilen-Handles 196, 257 Definition 203 Zeilenidentität 203 Zeilenidentität 203 Zeilenindizes 199 Definition 203 Zeilenmenge (Typ) 57, 193 Zeilenmengen 193 Beibehaltung 314 Bindung 92 Cursorbewegung 198 Cursorbewegung rückwärts 198 Cursorbewegung vorwärts 198 Definition 196 durch Zeilen blättern 198 Eigenschaften 209 Eltern-Zeilenmengen 363

Sandini Bib 472 enthaltene Zeilen 193 Felder 193 hierarchische 358 in OLE DB 196 Kind-Zeilenmengen 363 Navigation 91 Positionsmarken 203 Positionsmarkenvergleich 206 Spalten 193 Unterschied zu Datenmengen 405 Vergleich mit Bereichs-Zeilenmengen 422 Zeilen holen 198 Zeilenindizes 199 zum Zugriff auf mehrdimensionale Daten 424 Zeilenmengen-Arrays 214 Zeilenmengen-Eigenschaften, abrufen 209 Zeilenmengen-Klassen 167 in OLE DB Consumer Templates 195 Kombination mit Zugriffsobjekt-Klassen 92 Zeilenmengen-Listener 277f. Zeilenmengen-Objekte 193 Client-Cursor 217 Kapselung 195 Zeilenmengen-Sammelpackungen 210 Zeilenobjekte 432 Implementierung 429 Zeilenordner 442 erstellen 443 Implementierung 429 öffnen 443 Zeilenordner-Objekte 442 Zeilenposition, in Zeilenmenge 197 Zeilen-Referenzzähler, Verwaltung 197 Zeilenreihenfolge, Positionsmarkenvergleich 206 Zeilen-Suchfunktion, in OLE DB-Erweiterungen 219

Index Zeilenverwaltung aktive Zeilen 197 aktuelle Zeile 197 in OLE DB 197 Zeilen-Zugriffsobjekte 434 Zellen 405, 417 Bereichs-Zeilenmengen 421 identifizieren 417 Zellenbereiche abrufen 423 Zellennummern 417 Zugriffsobjekte 419 Zellenbereiche 421 abrufen 423 Zellendaten abrufen 420 Zellennummern 417 abrufen 418 Zugriffsobjekt (Typ) 223 Zugriffsobjekte 223 automatische 241 Bindungen 224f. Definition 223 dynamische 247 Grundregeln 225 Indizes 237, 257 interne Spalten 419 Klassen 231 Typen 224 Zugriffsobjekt-Handles 95, 257 Zugriffsobjekt-Indizes, mehrere Zugriffsobjekte 241 Zugriffsobjekt-Klassen 70, 92, 166f. Kombination mit Zeilenmengen-Klassen 92 Übersicht 233 Vererbung 234 Zusatzachse, Datenmengen 410 Zweidimensionale Daten Tabellen 404

Sandini Bib

Copyright Daten, Texte, Design und Grafiken dieses eBooks, sowie die eventuell angebotenen eBook-Zusatzdaten sind urheberrechtlich geschützt. Dieses eBook stellen wir lediglich als persönliche Einzelplatz-Lizenz zur Verfügung! Jede andere Verwendung dieses eBooks oder zugehöriger Materialien und Informationen, einschliesslich •

der Reproduktion,



der Weitergabe,



des Weitervertriebs,



der Platzierung im Internet, in Intranets, in Extranets,



der Veränderung,



des Weiterverkaufs



und der Veröffentlichung

bedarf der schriftlichen Genehmigung des Verlags. Insbesondere ist die Entfernung oder Änderung des vom Verlag vergebenen Passwortschutzes ausdrücklich untersagt! Bei Fragen zu diesem Thema wenden Sie sich bitte an: [email protected] Zusatzdaten Möglicherweise liegt dem gedruckten Buch eine CD-ROM mit Zusatzdaten bei. Die Zurverfügungstellung dieser Daten auf unseren Websites ist eine freiwillige Leistung des Verlags. Der Rechtsweg ist ausgeschlossen. Hinweis Dieses und viele weitere eBooks können Sie rund um die Uhr und legal auf unserer Website

http://www.informit.de herunterladen