FAQ VC++ et MFCConsultez toutes les FAQ
Nombre d'auteurs : 20, nombre de questions : 545, dernière mise à jour : 5 avril 2013 Ajouter une question
Cette faq a été réalisée pour répondre aux questions les plus fréquement posées sur le forum Développement Visual C++
Je tiens à souligner que cette faq ne garantit en aucun cas que les informations qu'elle contient sont correctes ; Les auteurs font le maximum, mais l'erreur est humaine. Si vous trouvez une erreur, ou si vous souhaitez devenir redacteur, lisez ceci.
Sur ce, je vous souhaite une bonne lecture. Farscape
- Comment MFC prend en charge SQL ?
- Comment sont transmises les requêtes SQL dans l'environnement VC++ ?
- Comment la bibliothèque MFC prend-elle en charge les bases de données ?
- Quelle est l'interface ODBC ?
- Quelles sont les classes MFC qui prennent en charge ODBC ?
- Comment enregistrer sa base de données ODBC auprès de l'administrateur de source de données ODBC ?
- Comment configurer la création d'un projet ODBC avec AppWizard ?
- Quelles différences il y a entre les options snapshot et dynaset ?
- Qu'est-ce qu'un jeu de lignes ?
- Qu'est-ce qu'un jeu de lignes snapshot ?
- Qu'est-ce qu'un jeu de ligne dynaset ?
- Quelle est la différence entre un jeu de ligne snapshot et dynaset ?
- A quelle règle obéit l'association des tables à un jeu de lignes selon qu'il est un snapshot ou dynaset ?
- Quelle est l'intérêt de CRecordset::GetDefaultConnet ?
- Quelle est l'intérêt de CRecordset::GetDefaultSQL ?
- Quelle est le rôle de CRecordset::DoFieldExchange ?
- Qu'est-ce qu'une vue de lignes ?
- Comment associer les champs de la table aux contrôles de l'objet vue de lignes ?
- Quelle est la relation entre base de données, jeu de lignes et vue de lignes ?
- Comment associer l'objet vue de ligne à son objet jeu de ligne ?
- Comment afficher les données de ma base dans mon projet ODBC ?
- Comment trier sur un champ d'une table ?
- Comment exécuter une transaction dans un enregistrement ODBC ?
- Qu'est-ce une transaction ODBC ?
- Comment relancer à nouveau la requête ?
- Comment gérer l'accès simultanée à un même enregistrement ?
- Comment supprimer des enregistrements ?
- Comment modifier des enregistrements ?
- Comment ajouter des enregistrements ?
- Comment déterminer le caractère modifiable d'un jeu d'enregistrement ODBC ?
- Comment utiliser les paramètres d'un jeu d'enregistrement ?
- Comment faire un tri d'enregistrement ?
- Comment réaliser un filtrage d'enregistrements ODBC ?
- Comment ne pas perdre sa position originale après plusieurs défilement dans un jeu d'enregistrement ?
- Comment parcourir les enregistrement ?
- Comment créer et fermer des jeux d'enregistrement ?
- Quelle est l'architecture d'un jeu d'enregistrement ODBC ?
- Comment se connecter-déconnecter à une source de données spécifique ?
- Comment généraliser de la chaîne de connexion ?
- Comment configurer la source de données ?
- Comment gérer les connexions à ma source de données ?
- Qu'est-ce une source de données (ODBC) ?
- Comment réutiliser une connexion existante à une base de données déjà ouverte ?
- Comment définir un paramètre de filtre dans la clause WHERE d'une requête SQL ?
- Comment placer la clause WHERE dans une requête SQL ?
- Comment faire intervenir une autre table de la base de données ODBC dans une application MFC ?
- Comment affecter le titre de l'application du nom de la base de données ?
- Quand est-ce que la base de données est ouverte ?
La prise en charge des bases de données par les MFC fait appel à SQL pour formuler les requêtes et opérations exécutées sur les tables de base de données, par l'intermédiaire d'un ensemble de classes spécialisées.
Lorsque nous faisons appel à SQL dans un programme MFC, nous n'avons généralement pas besoin d'écrire d'instructions SQL complètes, car le squelette ou canevas de base de l'application se charge de construire une instruction et de la fournir au moteur de base de données utilisé.
Les requêtes SQL sont transmises sous forme de chaînes. Si le nom d'une table comporte des espaces comme Product ID, la syntaxe SQL exige qu'il soit placé entre guillements comme "Product ID".
Ex1: SELECT "Product ID" FROM Products
Or en C++ les guillements délimitent les chaînes de caractères ce qui prête confusion si nous encadrons le nom des objets de base de données (lignes ou colonnes) par des guillements.
C'est pourquoi, lorsque nous référençons dans l'environnement Visual C++ une table de base de données ou un nom de colonne comprenant des espaces, nous devons remplacer les guillemets par les crochets. En l'ocurrence, écrivons [Product ID].
Ex2: SELECT [Product ID] FROM Products
Les MFC abordent les base de données selon deux approches chacune d'elle utilisant son propre jeu de classes MFC. Il s'agit de DAO et ODBC. Mais il y en a de plus récentes comme OLE DB et ADO.NET.
La première approche DAO (Data Acess Objects) propose une interface avec le moteur de base de données Jet, élément logiciel généralisé qui permet de stocker des données dans divers systèmes de gestion de base de données et de les extraires.
Jet est le moteur utilisé par le SGBD Microsoft Access. Chaque fois que vous manipulez une base de données sous Access, c'est en réalité Jet qui fournit l'essentiel du travail.
Jet est optimisé pour l'accès direct aux fichiers de base de données Access (.mdb), mais il vous permet également de vous connecter à toutes base de données prennant en charge ODBC.
Outre Microsoft Acess, Jet vous permet d'accéder à des bases de données comme Oracle, dBase 5, Btrieve 6.0 et FoxPro 2.6 et d'autres encore.
La seconde approche est spécifique à ODBC (Open DataBase Connectivity).
Vous pouvez manier des bases de données de tout format pour lesquelles vous possédez le pilote ODBC approprié et même pour les fichiers .mdb d'Access.
ODBC est une interface, indépendante du système, avec un environnement de base de données qui exige un pilote ODBC pour chaque système de base de données à partir duquel vous voulez gérer les données.
ODBC définit un ensemble d'appels de fonctions associées à des opérations de base de données, indépendants du système.
Vous pouvez utiliser une base de données avec ODBC seulement si vous disposez de la DLL qui contient le pilote compatible avec le format de fichier de cette base de données.
L'objectif du pilote est d'assurer l'interface entre le jeu d'appels indépendants du système, pour des opérations de base de données, utilisés dans votre programme, et les spécificités de la mise en oeuvre d'une base de données particulière.
ODBC fait appel aux cinq classes suivantes:
CDatabase : Un objet de cette classe représente une connexion à votre base de données, que vous devez établir
avant d'effectuer des opérations sur la base de données. Aucune classe d'espace de travail n'est
utilisée avec une base de données ODBC.
CRecordset : Un objet d'une classe dérivée de cette classe représente le résultat d'une opération SQL SELECT,
Il s'agit du concept mentionné lors de la description de la classe CDaoRecordset
CRecordView : Un objet d'une classe dérivée de cette classe permet d'afficher les informations en cours d'un jeu de lignes associé.
Il s'agit du concept mentionné lors de la description de la classe CDaoRecordView
CFieldExchange : Cette classe permet l'échange de données entre la base de données et un jeu de lignes, comme
pour les bases de données DAO.
CDBException : Les objets de cette classe constituent des exceptions survenant dans les opérations de base de
données ODBC.
Les classes ODBC ressemblent étrangement à un sous-ensemble des classes DAO, ce qui s'explique dans la mesure où l'interface qu'elles proposent est identique à celle des classes DAO équivalentes.
Elles diffèrent par leur processus sous-jacent d'accès à la base de données.
Avant de pouvoir utiliser une base de données via ODBC, vous devez l'inscrire dans la base de registres.
Pour ce faire cliquer sur le panneaux de configuration, sélectionner Outils d'administration et enfin Source de données ODBC.
Une fois que la boîte s'affiche à l'écran sélectionner l'onglet DSN utilisateur ( Source de données utilisateur).
Cliquez sur Ajouter... pour ajouter une source de données.
Une autre boîte s'affiche, et vous sélectionner le pilote soit ODBC Microsoft Acess Driver (*.mdb) (c'est un exemple).
Cliquez sur Terminer.
Une autre boite s'affiche à nouveau, et vous tapez le nom de votre source de données. Ce nom sera utilisé pour identifier
la base de données lors de la création de votre application par l'intermédiaire de AppWizard (VC++ 6.0).
Cliquez sur le bouton Sélectionner... pour accéder à la dernière boîte de dialogue où vous pouvez sélectionner le fichier qui représente votre base de donnée.( par exemple un fichier *.mdb)
Enfin vous cliquez sur tous les trois boutons OK et l'enregistrement est terminer.
Vérifier à l'aide de l'explorateur Window que votre fichier(base de données) n'est pas en lecture seule.
On exécute l'assistant AppWizard avec l'interface SDI ou MDI qui présente à sa seconde étape des options comme la prise en charge de fichiers avec les options d'affichage de base de données.
La prise en charge des fichiers fait référence à la sérialisation du document, qui n'est généralement pas nécessaire puisque les jeux de lignes de votre application se charge de toutes entrées-sorties nécessaire sur une base de données.
Quant au document, il est en quelque sorte accessoire pour les opérations sur une base de données, car ce sont les objets jeu de lignes et vue de lignes qui se chargent de l'essentiel.
Le rôle principal du document consiste à stocker les jeux de lignes c'est à dire les CRecordset.
On choisit par exemple l'option "Database view without file support" on clique sur Data Source...
et on indique la base de données que l'application va utiliser. L'option ODBC est déjà sélectionnée.
AppWizard fournit automatiquement une classe jeu de lignes et vue de lignes.
L'option "Table" s'applique seulement si vous sélectionnez DAO.
ODBC offre le choix entre deux options "Snapshot" et "Dynaset".
En effet, dans la configuration de notre projet ODBC, deux types de jeux de lignes interviennent.
Il s'agit du jeu de lignes instantané (snapshot) et de la feuille de réponse dynamique (dynaset).
D'une manière très générale on peut répondre comme ceci.
Si votre table est en lecture seule alors choisissez l'option snapshot.
Si votre table permet les mises à jours alors choisissez l'option dynaset.
N'oubliez pas que ce sont des conclusions générales mais si vous voulez comprendre le pourquoi de ces conclusions, les questions suivantes développées dans les Q/R qui suivent en explique la raison.
Il s'agit des questions suivantes :
Ces mots "jeu de lignes" comme vous avez pu le constater reviennent très souvent.
C'est un objet de la classe dérivée de CRecordset
Voici un exemple de cette classe qui s'appelle "CDBSet" appartenant à un projet SDI nommé "DB".
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | class CDBSet : public CRecordset { public: CDBSet(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(CDBSet) // Field/Param Data //{{AFX_FIELD(CDBSet, CRecordset) longm_ProductID; CStringm_ProductName; longm_SupplierID; longm_CategoryID; CStringm_QuantityPerUnit; CStringm_UnitPrice; intm_UnitsInStock; CStringm_UnitsOnOrder; intm_ReorderLevel; BOOLm_Discontinued; //}}AFX_FIELD // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDBSet) public: virtual CString GetDefaultConnect();// Default connection string virtual CString GetDefaultSQL(); // default SQL for Recordset virtual void DoFieldExchange(CFieldExchange* pFX);// RFX support //}}AFX_VIRTUAL // Implementation #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif }; |
Nous savons qu'une table de base de données est constituée des colonnes qui représentent les champs de la table et des lignes qui représentent les enregistrements.
Très souvent et la plupart des cas les colonnes sont beaucoup moins nombreuses que les lignes.
Par exemple une table peut avoir une dizaine de colonnes mais des milliers voir même des centaines de milliers de lignes et d'ailleurs jusqu'au million de lignes et même plus.
Dans un programme MFC avec les bases de données, le programmeur ne voit pas dans son code toutes ces lignes simultanément mais il les voit une par une car son champ de vision de la table est réduit à une ligne de cette table.
Et bien, c'est le cas du jeu de ligne qui représente un et un seul enregistrement de la table.
Cela lui vaut bien son nom de "recordset" celui qui définit un enregistrement.
Voici les champs d'une table attaché au recordset CBDSet :
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Field/Param Data //{{AFX_FIELD(CDBSet, CRecordset) longm_ProductID; CStringm_ProductName; longm_SupplierID; longm_CategoryID; CStringm_QuantityPerUnit; CStringm_UnitPrice; intm_UnitsInStock; CStringm_UnitsOnOrder; intm_ReorderLevel; BOOLm_Discontinued; //}}AFX_FIELD |
NB : La table je l'ai crée sur Access et s'appelle Product.
Par conséquent si le programmeur veut voir les autres lignes, il doit se déplacer de ligne en ligne dans la table en utilisant les fonctions membres héritées de CRecordset.
Le jeu de ligne représente un enregistrement de la table, et c'est par lui que se fait l'opération SQL SELECT
Toutes les opérations SQL que l'on fait sur SELECT s'appliquent sur le jeu de ligne.
Si je veux sélectionner quelques champs de ma table, ou je veux trier, ou alors placer des conditions de sélection dans la clause WHERE, c'est le jeu de ligne que j'utilise.
C'est la classe MFC ODBC préparée à cela.
Voir les classes ODBC dans les Q/R plus haut.
NB: Pour mieux comprendre les différences entre snapshot et dynaset je vous prie de vous reporter à la question Qu'est-ce qu'un jeu de lignes ?
Votre objet jeu de ligne snapshot vous fournit le résultat d'une opération SELECT sur la base de données.
Dans le cas d'un jeu de lignes snapshot, la requête est exécutée une seule fois et le résultat est stocké dans la mémoire.
Votre objet jeu de ligne peut ensuite rendre disponible n'importe quelle ligne de la table issue de la requête.
Par conséquent un snapshot est principalement statique par nature.
Aucune modification apportée à la base de données à la suite d'une mise à jour par d'autres utilisateurs n'est répercutée dans les données que vous obtenez à l'aide de votre jeu de lignes snapshot.
Si vous voulez voir ces modifications, vous devez réexécuter l'instruction SELECT.
Le choix entre DAO et ODBC détermine une autre fonctionnalité des jeux de ligne snaphot.
Votre programme ne peut pas modifier un jeu de ligne snapshot DAO: il est en lecture seule.
Un snapshot ODBC peut être en lecture seule ou modifiable .
Un snapshot modifiable écrit toutes les modifications apportées à la table dans la base de données sous-jacente, et votre programme peut détecter les changements.
D'autres programmes comportant un snapshot de la base de données ne détectent pas cependant les modifications tant qu'ils n'interrogent pas de nouveau la base.
NB: Pour mieux comprendre les différences entre snapshot et dynaset je vous prie de vous reporter à la question Qu'est-ce qu'un jeu de lignes ?
Votre objet jeu de lignes dynaset rafraîchit automatiquement la ligne en cours de la base de données lorsque que vous passez d'une ligne à une autre de la table créée par la requête du jeu de lignes.
En conséquence, la ligne disponible dans le jeu de lignes reflète l'état de la base de données lors de l'accès à la ligne, non lors de la première ouverture du jeu de lignes.
Le rafraîchissement survient uniquement lorsque votre objet jeu de lignes accède à la ligne.
Si un autre utilisateur modifie les données de la ligne en cours, ce changement est pris en compte dans votre objet jeu de lignes uniquement lorsque vous passez à une autre ligne puis revenez vers la ligne initiale.
Un jeu de ligne dynaset génère le contenu de chaque ligne de façon dynamique, au moyen d'un index désignant les tables de base de données concernées.
Après avoir lu les définitions sur un jeu de ligne snapshot et dynaset. Nous pouvons affirmer ceci:
Pour un jeu de ligne snapshot, l'utilisateur de la base de données se rendra compte qu'une table a été modifiée lorsqu'il reprend son instruction SELECT sur la table à l'aide de son snapshot c'est à dire son recordset.
Ainsi entre la première ouverture et la reprise de l'instruction SELECT, d'autres utilisateurs peuvent modifier la table à l'insu du premier utilisateur et donc tant qu'il n'a pas repris l'instruction SELECT il croit toujours que les données de sa table sont saines alors qu'elles sont déjà altérées.
Pour un jeu de ligne dynaset, l'utilisateur se rendra compte de toutes modifications sur une table de la base de données lorsqu'il se déplace simplement d'une ligne à une autre de la table sans qu'il n'ait besoin de réexécuter l'instruction SELECT car le dynaset met automatiquement à jour dès qu'il accède à une ligne de la table.
C'est pourquoi il est dit dynamique et l'utilisateur peut se rendre compte des changements qui se produisent dans son programme un peu plus tôt que celui qui utilise un snapshot.
Par conséquent un snapshot convient pour les tables en lecture seule où aucun utilisateur ne modifie la table ou convient alors dans les situations où l'utilisateur est seul à manipuler la table c'est à dire dans un environnement non client/serveur.
Et un dynaset convient pour les tables qui acceptent les mise à jours et dans le cas où plusieurs utilisateurs peuvent interroger la base sur une même table cas d'un environnement client/serveur.
Pour les jeux de lignes snapshot on peut y associer plusieurs tables. Cela équivaut à une opération de jointure des tables.
Tandis que pour les jeux de lignes dynaset on y associe qu'une seule table car les classes de base de données des MFC ne prennent pas en charge la mise à jour de jeux de lignes impliquant la liaison de plusieurs tables.
Si nous revenons à la configuration d'un projet ODBC, en cliquant sur OK, l'assistant affiche toutes les tables de la base de données.
On peut faire une sélection unique ou multiple dans le cas d'un snapshot mais une sélection unique dans le cas d'un dynaset.
Pour le CDBSet j'ai réalisé une sélection unique et j'ai choisit un jeu de ligne snapshot.
On peut le voir dans le constructeur
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | CDBSet::CDBSet(CDatabase* pdb) : CRecordset(pdb) { //{{AFX_FIELD_INIT(CDBSet) m_ProductID = 0; m_ProductName = _T(""); m_SupplierID = 0; m_CategoryID = 0; m_QuantityPerUnit = _T(""); m_UnitPrice = _T(""); m_UnitsInStock = 0; m_UnitsOnOrder = _T(""); m_ReorderLevel = 0; m_Discontinued = FALSE; m_nFields = 10; //}}AFX_FIELD_INIT m_nDefaultType = snapshot; // here } |
Voici la déclaration de cette fonction dans CDBSet
Code c++ : | Sélectionner tout |
virtual CString GetDefaultConnect();// Default connection string
Code c++ : | Sélectionner tout |
1 2 3 4 | CString CDBSet::GetDefaultConnect() { return _T("ODBC;DSN=Gestion de stock"); } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #include "DBSet.h" class CDBDoc : public CDocument { protected: // create from serialization only CDBDoc(); DECLARE_DYNCREATE(CDBDoc) // Attributes public: CDBSet m_dBSet; //... }; |
Voici la déclaration de son construteur
Code c++ : | Sélectionner tout |
CDBSet(CDatabase* pDatabase = NULL);
Ce qui est encore plus intéressant c'est qu'un CRecordset est toujours associé à un CDatabase car il représente une connexion à la base de données.
NB: Vous n'avez pas à appeler GetDefaultConnect, c'est le canevas de l'application qui le fait pour vous.
Vous n'avez qu'à définir la chaîne.
soit "ODBC;DSN=Gestion de stock" où ODBC représente l'interface, et Gestion de stock le DSN que j'ai fournit à l'administrateur ODBC dans Panneaux de configuration.
C'est au moyen de cette chaîne que l'on se connecte à notre base de données car la charpente s'en sert comme identification de notre base de données.
Si on veux utiliser un ID utilisateur et un password on écrit
Code c++ : | Sélectionner tout |
1 2 3 4 | CString CDBSet::GetDefaultConnect() { return _T("ODBC;DSN=Gestion de stock;UID=Gabrielly;PWD=Angel"); } |
Code c++ : | Sélectionner tout |
return _T("ODBC;");
Cette fonction retourne un CString qui représente la requête SELECT par défaut appliqué au jeu de ligne.
Déclaration dans CDBSet
Code c++ : | Sélectionner tout |
virtual CString GetDefaultSQL(); // default SQL for Recordset
Implémentation :
Code c++ : | Sélectionner tout |
1 2 3 4 | CString CDBSet::GetDefaultSQL() { return _T("[Product]"); // équivalent à SELECT * FROM Product } |
N'oubliez pas les MFC transmettent les requêtes sous forme de chaines et fabrique la syntaxe SQL correcte qu'il fournit au moteur de base de données.
Ce qui montre que vous n'avez pas besoin d'écrire une instruction SQL complète car le canevas de l'application s'en charge.
Si le jeu de ligne est associer à deux tables on a :
Code c++ : | Sélectionner tout |
1 2 3 4 | CString CMyRecordset::GetDefaultSQL() { return _T("[Table1], [Table2]"); // équivalent à SELECT * FROM Table1, Table2 } |
Comme résultat, c'est une jointure des deux tables
NB : Ici aussi vous n'avez pas à appeler GetDefaultSQL, c'est le travail de la charpente d'application.
Le rôle de cette fonction est d'assurer l'échange de données entre la base de données et le jeu de lignes.
Voici un exemple de code :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void CDBSet::DoFieldExchange(CFieldExchange* pFX) { //{{AFX_FIELD_MAP(CDBSet) pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Long(pFX, _T("[ProductID]"), m_ProductID); RFX_Text(pFX, _T("[ProductName]"), m_ProductName); RFX_Long(pFX, _T("[SupplierID]"), m_SupplierID); RFX_Long(pFX, _T("[CategoryID]"), m_CategoryID); RFX_Text(pFX, _T("[QuantityPerUnit]"), m_QuantityPerUnit); RFX_Text(pFX, _T("[UnitPrice]"), m_UnitPrice); RFX_Int(pFX, _T("[UnitsInStock]"), m_UnitsInStock); RFX_Text(pFX, _T("[UnitsOnOrder]"), m_UnitsOnOrder); RFX_Int(pFX, _T("[ReorderLevel]"), m_ReorderLevel); RFX_Bool(pFX, _T("[Discontinued]"), m_Discontinued); //}}AFX_FIELD_MAP } |
Elle fonctionne exactement de la même manière que DoDataExchange avec les boites de dialogues et les vues.
NB: Là aussi c'est la charpente qui fait automatiquement le stockage et l'extraction de données de la base de données.
Le mode CFieldExchange::outputColumn indique que les données doivent être échangées entre la colonne de la base de données et l'argument correspondant précisé dans chacun des appels suivants de fonctions RFX_()
C'est un objet de la classe dérivée de CRecordView. En d'autres termes il s'agit d'un objet représentant une sorte de formulaire et d'ailleurs CRecordView dérive de CFormView.
Voici un exemple de cette classe qui s'appelle "CDBView" appartenant à un projet SDI nommé "DB".
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | class CDBSet; class CDBView : public CRecordView { protected: // create from serialization only CDBView(); DECLARE_DYNCREATE(CDBView) public: //{{AFX_DATA(CDBView) enum{ IDD = IDD_DB_FORM }; CDBSet* m_pSet; // NOTE: the ClassWizard will add data members here //}}AFX_DATA // Attributes public: CDBDoc* GetDocument(); // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CDBView) public: virtual CRecordset* OnGetRecordset(); virtual BOOL PreCreateWindow(CREATESTRUCT& cs); protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support virtual void OnInitialUpdate(); // called first time after construct virtual BOOL OnPreparePrinting(CPrintInfo* pInfo); virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo); virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo); //}}AFX_VIRTUAL // Implementation public: virtual ~CDBView(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif protected: // Generated message map functions protected: //{{AFX_MSG(CDBView) // NOTE - the ClassWizard will add and remove member functions here. // DO NOT EDIT what you see in these blocks of generated code ! //}}AFX_MSG DECLARE_MESSAGE_MAP() }; |
L'objectif d'un objet vue de ligne consiste à afficher les informations d'un objet jeu de ligne dans la fenêtre de l'application.
Autrement dit il affiche les informations de la table enregistrement par enregistrement car il les récupère du recordset et donc ligne par ligne.
Ce qui est à remarquer est qu'à un objet vue de lignes, on associe toujours un objet jeu de ligne et bien sur le formulaire d'ID IDD_DB_FORM.
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class CDBSet; class CDBView : public CRecordView { protected: // create from serialization only CDBView(); DECLARE_DYNCREATE(CDBView) public: //{{AFX_DATA(CDBView) enum{ IDD = IDD_DB_FORM }; CDBSet* m_pSet; // NOTE: the ClassWizard will add data members here //}}AFX_DATA //... }; |
m_pSet est le membre de CDBView qui stocke l'adresse de l'objet jeu de ligne CDBSet.
La charpente fournit toujours ce pointeur pour associer une vue de lignes à un jeu de lignes.
Autrement dit sans jeu de lignes, la vue de lignes est sans intérêt.
D'abord il faut placer les contrôles sur le formulaire. Soient des contrôles static et des edits.
Et à l'aide de classwizard on fait l'associer directement avec les membres de l'objet jeu de lignes m_pSet de CDBView en écrivant m_pSet->.
Voici DodataExchange après associations
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | void CDBView::DoDataExchange(CDataExchange* pDX) { CRecordView::DoDataExchange(pDX); //{{AFX_DATA_MAP(CDBView) DDX_FieldText(pDX, IDC_CATEGORYID, m_pSet->m_CategoryID, m_pSet); DDX_FieldText(pDX, IDC_PRODUCTID, m_pSet->m_ProductID, m_pSet); DDX_FieldText(pDX, IDC_PRODUCTNAME, m_pSet->m_ProductName, m_pSet); DDX_FieldText(pDX, IDC_UNITPRICE, m_pSet->m_UnitPrice, m_pSet); DDX_FieldText(pDX, IDC_UNITSINSTOCK, m_pSet->m_UnitsInStock, m_pSet); DDX_FieldText(pDX, IDC_UNITSONORDER, m_pSet->m_UnitsOnOrder, m_pSet); //}}AFX_DATA_MAP } |
Notre vue de lignes est prêt et est directement connecté aux champs de la table Product par le CDBSet.
1. A la source nous avons la base de données, avec une table Product.
2. Ensuite le jeu de lignes CDBSet qui est positionné sur une ligne de la table Product et est déclaré dans CDBDoc
3. A l'aide de CDBSet::DoFieldExchange les échanges entre le jeu de lignes et la table sont assurés.
4. A l'autre extrémité j'ai une vue de lignes CDBView qui contient un pointeur m_pSet sur CDBSet.
5. A l'aide de CDBView::DoDataExchange les échanges entre les champs de la table Product par CDBSet et les contrôles de CDBView sont assurés.
6. Et avec CDBView::OnInitialUpdate les données sont affichées.
Pour associer l'objet vue de lignes CDBOrderView au jeu de lignes CDBOrderSet, il faut d'abord créer une ressource boîte de dialogue comme IDD_ORDERS_FORM avec les options de style Child et celui de la bordure None (pour notre application SDI DB).
À l'aide de ClassWizard on crée la classe qui dérive de CRecordView. En cliquant sur OK, l'assistant vous demande de choisir l'objet jeu de lignes à associer à la vue.
Cette boîte doit vous proposer CDBOrderSet; à défaut sélectionnez cette option dans la liste et cliquez sur le bouton OK.
Vérifiez le destructeur de la classe CDBOrderView, que ClassWizard a mis en œuvre au moyen du code suivant :
Code C++ : | Sélectionner tout |
1 2 3 4 5 | CDBOrderView::~CDBOrderView() { //if (m_pSet) // delete m_pSet; } |
Le membre m_pSet désigne un pointeur sur CDBOrderSet qui est déclaré dans le document et donc nous pouvons supprimer sa destruction dans la vue comme nous l'avons fait.
Voici un code très court de CDBView::OnInitialUpdate pour notre projet SDI nommé DB connecté à la table Product
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | void CDBView::OnInitialUpdate() { // On récupère le jeu de lignes déjà construit // et connecté à la base de données m_pSet = &GetDocument()->m_dBSet; CRecordView::OnInitialUpdate(); GetParentFrame()->RecalcLayout(); ResizeParentToFit(); } |
2. On l'associe au jeu de lignes par son membre m_pSet
3. On appelle CRecordView::OnInitialUpdate() qui ouvre le jeu de lignes pour transférer les données de la base vers le jeu
4. On redimensionne le formulaire
A ce niveau on peut tester l'application. Vérifier que votre table a des données une fois créée.
Dans le contexte MFC/ODBC; Pour trier sur un champ d'une table, il suffit de définir le membre CRecordset::m_strSort comme une chaîne contenant le nom de colonne sur lequel nous voulons effectuer le tri.
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 | void CDBView::OnInitialUpdate() { m_pSet = &GetDocument()->m_dBSet; m_pSet->m_strSort = "[ProductID]";// trie suivant la colonne ProductID de la table Product CRecordView::OnInitialUpdate(); GetParentFrame()->RecalcLayout(); ResizeParentToFit(); } |
Dans le jeu de lignes, nous avons attribué à m_strSort le nom de la colonne ProductID.
Les crochets sont utiles même si un nom est dépourvu d'espace, car ils différencient les chaînes contenant ces noms d'autres chaînes; pour vous permettre de sélectionner immédiatement le nom de colonnes.
Pour effectuer le tri sur plusieurs colonnes, indiquez le nom de chacune d'elles dans la chaîne, en les séparant par une virgule.
Code C++ : | Sélectionner tout |
m_strSort = "[ProductID], [ProductName]"
Un seul niveau des transactions est pris en charge ; vous ne pouvez pas imbriquer les transactions.
Pour exécuter une transaction dans un jeu d'enregistrements
Appelez la fonction membre BeginTrans de l'objet CDatabase.
Appelez aussi souvent que nécessaire les fonctions membres AddNew/Update, Edit/Update et Delete d'un ou de plusieurs objets de jeu d'enregistrements de la même base de données.
Enfin, appelez la fonction membre CommitTrans de l'objet CDatabase.
Si une erreur se produit pendant l'une des mises à jour ou que vous décidiez d'annuler les modifications, appelez la fonction membre Rollback.
Voici un exemple qui ilustre deux jeux d'enregistrements pour supprimer l'inscription d'un étudiant d'une base de données gérant les inscriptions d'une école.
L'étudiant est ainsi retiré de tous les cours pour lesquels il était inscrit.
Les appels Delete doivent réussir dans les deux jeux d'enregistrements, ce qui implique l'utilisation d'une transaction.
Cet exemple suppose l'existence de m_dbStudentReg, variable membre de type CDatabase déjà connectée à la source de données, et des classes de jeux d'enregistrements CEnrollmentSet et CStudentSet. La variable strStudentID contient une valeur fournie par l'utilisateur.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | BOOL CEnrollDoc::RemoveStudent( CString strStudentID ) { // la fonction recoit l'ID de l'étudiant à supprimer if ( !m_dbStudentReg.BeginTrans( ) ) // On démarre la transaction return FALSE; // Je constuis le recordset associé à une table qui reprend tous les cours des étudiants CEnrollmentSet rsEnrollmentSet(&m_dbStudentReg); // je prépare le filtre rsEnrollmentSet.m_strFilter = "StudentID = " + strStudentID; // j'exécute la requête suivant le filtre. Le recordset est modifiable // Cette requête me donne tous les cours associés à cet étudiant if ( !rsEnrollmentSet.Open(CRecordset::dynaset) ) return FALSE; // Je construis le recordset associé à une table qui reprend tous les étudiants enrollés CStudentSet rsStudentSet(&m_dbStudentReg); // je prépare le filtre rsStudentSet.m_strFilter = "StudentID = " + strStudentID; // j'exécute la requête suivant le filtre. Le recordset est modifiable // Cette requête me donne en principe un seul enregistrement qui reprend // les informations de l'étudiant à supprimer if ( !rsStudentSet.Open(CRecordset::dynaset) ) return FALSE; TRY // Biensûr un bloc try-catch est nécessaire pour les exceptions éventuelles { while ( !rsEnrollmentSet.IsEOF( ) ) // je défile en avant { rsEnrollmentSet.Delete( ); rsEnrollmentSet.MoveNext( ); } // je viens de supprimer tous les cours relatifs à l'étudiant // Je supprime également l'étudiant de la table des étudiants enrollés rsStudentSet.Delete( ); // enfin je valide toutes les opérations effectués pour affecter la source de données m_dbStudentReg.CommitTrans(); // après le commit, // j'aurais pu appeler CDatabase::GetCursorCommitBehavior() // pour tester l'effet de la transaction sur mes recordsets } CATCH_ALL(e) { // tiens!!! j'ai échoué. // Je supprime donc la transaction m_dbStudentReg.Rollback(); // après le rollback, // j'aurais pu appeler CDatabase::GetCursorRollbackBehavior() // pour tester l'effet de la transaction sur mes recordsets return FALSE; } END_CATCH_ALL rsEnrollmentSet.Close(); // bon je ferme mes recordset rsStudentSet.Close(); // le destructeur pouvait fair ça à ma place return TRUE; } |
Car tentative d'imbrication des transaction.
Une transaction constitue une façon de regrouper une série de mises à jour dans une source de données de manière à ce qu'elles soient toutes validées en même temps, ou qu'aucune ne soit validée si vous annulez la transaction.
Si vous n'utilisez pas une transaction, les modifications apportées à la source de données sont validées automatiquement au lieu d'être validées sur demande.
Tous les pilotes de base de données ODBC prennent en charge les transactions.
Appelez la fonction membre CanTransact de votre objet CDatabase ou CRecordset pour savoir si votre pilote prend en charge les transactions pour une base de données particulière.
Sachez cependant que CanTransact ne vous indique pas si la source de données assure une prise en charge complète des transactions.
Vous devez également appeler CDatabase::GetCursorCommitBehavior et CDatabase::GetCursorRollbackBehavior après CommitTrans et Rollback pour vérifier l'effet de la transaction sur l'objet CRecordset ouvert.
Les appels adressés aux fonctions membres AddNew et Edit d'un objet CRecordset affectent immédiatement la source de données lorsque vous appelez Update.
Les appels Delete entrent également en vigueur immédiatement.
En revanche, vous pouvez utiliser une transaction composée de plusieurs appels à AddNew, Edit, Update et Delete, qui sont exécutés mais pas validés tant que vous n'avez pas explicitement appelé CommitTrans.
En établissant une transaction, vous pouvez exécuter une série d'appels de ce type tout en conservant la possibilité de les annuler.
Si une ressource indispensable n'est pas disponible ou que tout autre facteur empêche l'exécution de l'intégralité de la transaction, vous pouvez annuler cette transaction au lieu de la valider. Dans ce cas, aucune des modifications appartenant à la transaction n'affecte la source de données.
Les transactions sont particulièrement utiles lorsque vous devez mettre à jour plusieurs enregistrements à la fois.
Dans ce cas, vous devez éviter qu'une transaction soit seulement à moitié terminée, ce qui peut se produire si une exception est générée avant l'exécution de la dernière mise à jour.
Le groupement de ces mises à jour dans une transaction permet une récupération (annulation) après modifications et fait repasser les enregistrements dans l'état qu'ils avaient avant la transaction.
Un seul niveau des transactions est pris en charge.
Il est impossible d'imbriquer des transactions et une transaction ne peut pas englober plusieurs objets de base de données.
Pour lancer une nouvelle requête sur un objet jeu d'enregistrements Appelez la fonction membre Requery de l'objet.
Autre solution, fermez le jeu d'enregistrements original et réouvrez-le.
Dans les deux cas, le nouveau jeu d'enregistrements représente l'état courant de la source de données.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | //... if( !rsStudent.Open( ) ) // Est-il ouvert? return FALSE; // Ok, je prépare à nouveau mes trie et mes filtres. // rsStudent.Open(); // non non non, le recordset est déjà ouvert!!! rsStudent.Requery(); // c'est comme ça qu'on relance une nouvelle fois //... |
C'est en procédant par des verrouillages des enregistrements.
Lorsque vous utilisez un jeu d'enregistrements pour modifier un enregistrement de la source de données,
votre application peut verrouiller l'enregistrement afin qu'aucun autre utilisateur ne puisse le modifier en même temps.
Comme l'état d'un enregistrement modifié simultanément par deux utilisateurs est imprévisible,
il importe que le système puisse garantir que deux utilisateurs ne peuvent pas modifier simultanément un même enregistrement.
Il existe deux types de verrouillage supportés par la classe CRecordset.
-le verrouillage optimiste (mode par défaut)
-le verrouillage pessimiste.
Le verrouillage optimiste verrouille l'enregistrement de la source de données uniquement pendant l'appel de la fonction Update.
Si vous utilisez le verrouillage optimiste dans un environnement multi-utilisateur, l'application doit gérer une condition d'échec de la fonction Update.
Le verrouillage pessimiste verrouille l'enregistrement dès que vous appelez Edit et ne le libère pas tant que vous n'avez pas appelé Update
(les échecs sont signalés via CDBException, et non par la valeur FALSE retournée par Update).
Le verrouillage pessimiste pénalise potentiellement les performances des autres utilisateurs,
car les accès simultanés au même enregistrement doivent attendre la fin complète du processus Update de votre application.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //... rsStudent.SetLockingMode(CRecordset::optimistic); // je choisit ce mode de vérouillage car je suis optimiste rsStudent.Edit(); // le vérouillage n'a pas encore commencé // j'édite mes quelques champs rsStudent.m_strStreet = strNewStreet; rsStudent.m_strCity = strNewCity; rsStudent.m_strState = strNewState; rsStudent.m_strPostalCode = strNewPostalCode; if( !rsStudent.Update() ) // bon, je vérouille pour garantir que je suis seul à modifier cet enregistrement { AfxMessageBox( "Impossible de mettre à jours." ); return FALSE; } // le vérouillage est libéré après l'appel à Update. // d'autres utilisateurs peuvent maintenant modifier cet enregistrement. |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //... rsStudent.SetLockingMode(CRecordset::pessimistic); // je suis vraiment pessimiste!!! rsStudent.Edit(); // je préfère commencer le vérouillage ici // j'édite mes quelques champs rsStudent.m_strStreet = strNewStreet; rsStudent.m_strCity = strNewCity; rsStudent.m_strState = strNewState; rsStudent.m_strPostalCode = strNewPostalCode; //Hé oui, jusqu'à Update je suis vraiment le seul utilisateur à modifier cet enregistrement if( !rsStudent.Update() ) { AfxMessageBox( "Impossible de mettre à jours." ); return FALSE; } // le vérouillage est libéré après l'appel à Update. // d'autres utilisateurs peuvent maintenant modifier cet enregistrement. // Excusez-moi les autres users si les appels entre Edit et Update me prennent 5 secondes de temps processeur. // Car je suis vraiment pessimiste et je veux que personne n'y accède pendant que je modifie. |
Remarque: Très peu de pilotes ODBC prennent actuellement en charge le verrouillage pessimiste.
Vous pouvez supprimer des enregistrements si la fonction membre CanUpdate retourne une valeur différente de zéro.
Assurez-vous que le jeu d'enregistrements est modifiable.
Accédez à l'enregistrement à mettre à jour.
Appelez la fonction membre Delete de l'objet jeu d'enregistrements.
Delete marque immédiatement l'enregistrement comme supprimé, à la fois dans le jeu d'enregistrements et dans la source de données.
Contrairement à AddNew et Edit, Delete n'appelle pas la fonction Update.
Accédez par défilement à un autre enregistrement.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | if( !rsStudent.Open( ) ) // Est-il ouvert? return FALSE; if( !rsStudent.CanUpdate() ) //Puisse-je modifier? return FALSE; rsStudent.SetAbsolutePosition(3); // j'accède à l'enregistrement à supprimer rsStudent.Delete(); // je le détruit if(rsStudent.IsDelete()) // Est-il vraiment détruit? rsStudent.MoveNext(); // bon, je me positionne sur l'enregistrement suivant |
Vous pouvez modifier des enregistrements existants si la fonction membre CanUpdate retourne une valeur différente de zéro.
Assurez-vous que le jeu d'enregistrements est modifiable.
Accédez à l'enregistrement à mettre à jour.
Appelez la fonction membre Edit de l'objet jeu d'enregistrements.
Edit prépare le jeu d'enregistrements de telle sorte qu'il fasse office de tampon d'édition.
Tous les membres de données de type champ sont marqués de sorte que le jeu d'enregistrements peut savoir, par la suite, s'ils ont été modifiés.
Les nouvelles valeurs des membres de données de type champ modifiés sont écrites dans la source de données quand vous appelez Update.
Définissez les valeurs des membres de données de type champ du nouvel enregistrement.
Affectez des valeurs aux membres de données de type champ. Les membres auxquels ne sont pas attribuées de valeurs demeurent inchangés.
Appelez la fonction membre Update de l'objet jeu d'enregistrements.
Update termine la modification en écrivant l'enregistrement modifié dans la source de données.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | if( !rsStudent.Open( ) ) // Est-il ouvert? return FALSE; if( !rsStudent.CanUpdate() ) //Puisse-je modifier? return FALSE; rsStudent.SetAbsolutePosition(3); // j'accède à l'enregistrement à mettre à jours rsStudent.Edit( ); // bon, je prépare le tampon d'édition // j'édite mes quelques champs rsStudent.m_strStreet = strNewStreet; rsStudent.m_strCity = strNewCity; rsStudent.m_strState = strNewState; rsStudent.m_strPostalCode = strNewPostalCode; if( !rsStudent.Update() ) // je n'oublie de mettre à jours. C'est la validation { AfxMessageBox( "Impossible de mettre à jours." ); return FALSE; } |
Pour annuler un appel de AddNew ou Edit, effectuez simplement un nouvel appel de AddNew ou Edit ou appelez Move avec le paramètre AFX_MOVE_REFRESH.
Les membres de données sont réinitialisés à leurs valeurs précédentes et vous demeurez en mode Edit ou Add.
Attention:
Lorsque vous vous préparez à modifier un jeu d'enregistrements en appelant Update,
prenez soin que le jeu d'enregistrements comporte toutes les colonnes composant la clé primaire de la table (ou toutes les colonnes d'un index unique de la table).
Dans certains cas, l'infrastructure ne peut utiliser que les colonnes sélectionnées dans le jeu d'enregistrements pour identifier l'enregistrement de la table à modifier.
Sans toutes les colonnes nécessaires, plusieurs enregistrements risquent d'être modifiés dans la table et l'intégrité référentielle de celle-ci peut s'en trouver altérée.
Dans ce cas, l'infrastructure lève une exception quand vous appelez Update.
De préférence placez le code dans les blocs try-catch pour récupérer d'éventuelles exceptions.
Vous pouvez ajouter de nouveaux enregistrements à un jeu d'enregistrements si sa fonction membre CanAppend retourne une valeur différente de zéro.
Assurez-vous que des enregistrements peuvent être ajoutés au jeu d'enregistrements.
Appelez la fonction membre AddNew de l'objet jeu d'enregistrements.
AddNew prépare le jeu d'enregistrements de telle sorte qu'il fasse office de tampon d'édition.
Tous les membres de données de type champ sont définis avec la valeur particulière Null et marqués comme non modifiés,
de sorte que seules les valeurs modifiées (« dirty ») sont écrites dans la source de données lorsque vous appelez Update.
Définissez les valeurs des membres de données de type champ du nouvel enregistrement.
Affectez des valeurs aux membres de données de type champ.
Ceux qui ne reçoivent pas de valeurs ne sont pas écrits dans la source de données.
Appelez la fonction membre Update de l'objet jeu d'enregistrements.
Update termine l'ajout en écrivant le nouvel enregistrement dans la source de données.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | if( !rsStudent.Open( ) ) // Est-il ouvert? return FALSE; if( !rsStudent.CanAppend() ) //Puisse-je ajouter? return FALSE; rsStudent.AddNew(); // bon, je prépare le tampon d'édition // j'édite mes quelques champs rsStudent.m_strName = strName; rsStudent.m_strCity = strCity; rsStudent.m_strStreet = strStreet; if( !rsStudent.Update( ) ) // je n'oublie de mettre à jours. C'est la validation { AfxMessageBox( "Impossible de mettre à jours" ); return FALSE; } |
Les membres de données sont réinitialisés à leurs valeurs précédentes et vous demeurez en mode Edit ou Add.
Attention:
Lorsque vous vous préparez à modifier un jeu d'enregistrements en appelant Update,
prenez soin que le jeu d'enregistrements comporte toutes les colonnes composant la clé primaire de la table (ou toutes les colonnes d'un index unique de la table).
Dans certains cas, l'infrastructure ne peut utiliser que les colonnes sélectionnées dans le jeu d'enregistrements pour identifier l'enregistrement de la table à modifier.
Sans toutes les colonnes nécessaires, plusieurs enregistrements risquent d'être modifiés dans la table et l'intégrité référentielle de celle-ci peut s'en trouver altérée.
Dans ce cas, l'infrastructure lève une exception quand vous appelez Update.
De préférence placez le code dans les blocs try-catch pour récupérer d'éventuelles exceptions.
En principe, lorsque vous ajoutez, modifiez ou supprimez des enregistrements, le jeu d'enregistrements modifie aussitôt la source de données.
Le tableau suivant résume les options disponibles pour les jeux d'enregistrements, accompagnées des différentes caractéristiques de mise à jour.
Type | Lecture | Modification d'un enregistrement |
suppression d'un enregistrement |
Ajout d'un enregistrement |
En lecture Seule | oui | non | non | non |
Ajout Seul | oui | non | non | oui |
Pleinement Modifiable | oui | oui | oui | oui |
Un objet jeu d'enregistrements est modifiable si la source de données l'est et que le jeu d'enregistrements a été ouvert comme étant modifiable.
C'est le cas lorsque le recordset est ouvert avec comme type CRecordset::dynaset.
Son caractère modifiable dépend également de l'instruction SQL que vous utilisez, des capacités du pilote ODBC et de l'éventuel chargement en mémoire de la bibliothèque de curseurs ODBC.
Il est impossible de modifier une source de données ou un jeu d'enregistrements en lecture seule.
C'est le cas lorsqu'il est ouvert comme type CRecordset::snapshot avec pour option CRecordset::readOnly.
Si le jeu d'enregistrement doit permettre uniquement l'ajout d'enregistrement le recordset peut être ouvert avec l'option CRecordset::appendOnly.
Pour déterminer si le jeu d'enregistrements est modifiable.
Appelez la fonction membre CanUpdate de l'objet jeu d'enregistrements.
CanUpdate retourne une valeur différente de zéro si le jeu d'enregistrements est modifiable.
Par défaut, les jeux d'enregistrement sont entièrement modifiables (vous pouvez effectuer les opérations AddNew, Edit et Delete).
Mais vous pouvez également utiliser l'option appendOnly pour ouvrir des jeux d'enregistrements modifiables.
Un jeu d'enregistrements ouvert de cette façon ne permet que l'ajout de nouveaux enregistrements à l'aide de AddNew.
Il n'est pas possible de modifier ou de supprimer des enregistrements existants.
Vous pouvez tester si un jeu d'enregistrements est ouvert uniquement pour permettre l'ajout en appelant la fonction membre CanAppend.
CanAppend retourne une valeur différente de zéro si le jeu d'enregistrements est entièrement modifiable ou ouvert uniquement pour permettre l'ajout.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | if( !rsStudentSet.Open() ) // rsStudentSet est de type CStudentSet dérivée de CRecordset return FALSE; if( !rsStudentSet.CanUpdate() ) // vérifie s'il est modifiable { AfxMessageBox( "Impossible de mettre à jours le recordset." ); return; } if( !rsStudentSet.CanAppend() ) // vérifie s'il permet l'ajout d'enregistrement { AfxMessageBox("Impossible d'ajouter des enregistrements"); return; } |
Dans la classe du jeu d'enregistrement ou jeu de lignes: les champs et les paramètres
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | class CStudentSet : public CRecordset { // Field Data CString m_strFirstName; CString m_strLastName; long m_StudentID; CString m_strGradYear; // Param Data CString m_strGradYrParam; long m_StudebtIDParam; }; |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | CStudentSet::CStudentSet(CDatabase* pdb) : CRecordset(pdb) { // Field Data m_strFirstName = _T(""); m_strLastName = _T(""); m_StudentID = 0; m_strGradYear = _T(""); m_nFields = 4; // Param Data m_strGradYrParam = _T(""); m_StudentIDParam = 0; m_nParams = 2; } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void CStudentSet::DoFieldExchange(CFieldExchange* pFX) { // Field Data pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Text(pFX, _T("[FirstName]"), m_strFirstName); RFX_Text(pFX, _T("[LastName]"), m_strLastName); RFX_Long(pFX, _T("[StudentID]"), m_StudentID); RFX_Text(pFX, _T("[GradYear]"), m_strGradYear); // Param Data // l'ordre des appels RFX_() sur les paramètres est important!!! pFX->SetFieldType(CFieldExchange::param); RFX_Text(pFX, _T("[GradYear]"), m_strGradYrParam); // 1 ier paramètre RFX_Long(pFX, _T("[StudentID]"), m_StudentIDParam); // 2 ième paramètre } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 | CString CStudentSet::GetDefaultSQL() { return _T("[Student]"); // la requête SELECT par défaut de la table Student } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | CDatabase mydb; if( !mydb.OpenEx( _T( "ODBC;DSN=MYDATASOURCE;UID=JOES" )) { AfxMessageBox("Impossible d'ouvrir la base de donnée"); return; } CStudentSet rsStudents(mydb); rsStudents.m_strFilter = "[GradYear] <= ? AND [StudentID] >= ? AND [StudentID] <= '1000'"; // je prépare le filtre // Hééée!!! l'ordre des points d'interrogation est important // je dois avoir 2 points d'interrogations car m_nParams = 2 // le premier est pour m_strGradYrParam et le deuxième pour m_StudentIDParam dans cet ordre définit dans DoFieldExchange!!! // rsStudents.m_strFilter = "[StudentID] >= ? AND [StudentID] <= '1000' AND [GradYear] <= ?"; // very bad rsStudent.m_strSort = "LastName DESC, FirstName DESC"; // je prépare le trie // Cette fonction d'aide me donne l'année académique courante CString strGradYear = GetCurrentAcademicYear(); // Cette fonction d'aide me donne un ID d'étudiant inférieure à 1000 long StudentID = GetCurrentStudentID(); // j'initialise mes paramètres rsStudents.m_strGradYrParam = strGradYear; rsStudents.m_StudentIDParam = StudentID; rsStudents.Open(); // j'ouvre le jeu if(rsStudents.IsOpen()) { // bon il est ouvert je peux naviguer à travers les enregistrements } rsStudents.Close(); // bon, je préfère fermer explicitement mydb.Close(); // je ferme aussi la connexion |
Vous pouvez définir une ou plusieurs colonnes à partir desquelles effectuer le tri, et choisir un ordre de tri ascendant ou descendant (ASC ou DESC, ASC étant la valeur par défaut) pour chaque colonne.
Par exemple, si vous indiquez deux colonnes, les enregistrements sont d'abord triés sur la première colonne, puis sur la seconde.
La clause SQL ORDER BY définit une instruction de tri.
Quand l'infrastructure ajoute la clause ORDER BY à la requête SQL du jeu d'enregistrements, la clause contrôle le tri de la sélection.
Vous devez établir un ordre de tri du jeu d'enregistrements après avoir construit l'objet mais avant d'appeler sa fonction membre Open (ou la fonction membre Requery d'un objet jeu d'enregistrements existant dont la fonction membre Open a été appelée préalablement).
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 | CStudentSet rsStudent( NULL ); // définit le trie rsStudent.m_strSort = "LastName DESC, FirstName DESC"; // good // rsStudent.m_strSort = "ORDER BY LastName DESC, FirstName DESC"; // bad // exécute la requête rsStudent.Open(); |
Filtrer un jeu d'enregistrements c'est sélectionner un sous-ensemble particulier au sein des enregistrements disponibles.
Par exemple, considérons le recordset suivant:
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | class CCourse : public CRecordset { public: CCourse(CDatabase* pDatabase = NULL); ... CString m_strCourseID; CString m_strCourseTitle; CString m_strIDParam; }; |
Un filtre est une condition de recherche définie par le contenu d'une clause SQL WHERE.
Quand l'infrastructure l'ajoute à l'instruction SQL du jeu d'enregistrements, la clause WHERE limite la sélection.
Vous devez établir un filtre d'objet jeu d'enregistrements après avoir construit l'objet mais avant d'appeler
sa fonction membre Open (ou la fonction membre Requery d'un objet jeu d'enregistrements existant dont la fonction membre Open a été appelée préalablement).
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | CCourse rsCourseSet( NULL ); rsCourseSet.m_strFilter = "CourseID = 'MATH101'"; // very good // rsCourseSet.m_strFilter = "WHERE CourseID = 'MATH101'"; // very bad, pas de WHERE // rsCourseSet.m_strFilter = "CourseID = MATH101"; // very bad, mais où sont les guillemets simples? rsCourseSet.Open( CRecordset::snapshot, NULL, CRecordset::readOnly ); if( !rsCourseSet.IsOpen() ) return; // Affiche tous les titres pour lequel CourseID = 'MATH101' while ( !rsCourseSet.IsEOF( ) ) { CString strTitle = rsCourseSet.m_strCourseTitle; // j'obtient un titre TRACE("%s\n", (LPCTSTR) strTitle); rsCourseSet.MoveNext( ); } rsCourseSet.m_strFilter = "CourseID = ?"; rsCourseSet.m_strIDParam = "PROGRAMMATION"; // very good // rsCourseSet.m_strIDParam = "'PROGRAMMATION'"; // very bad, pourquoi des guillemets simples? // aller on refait la requête si m_strIDParam a été definit correctement (m_nParams et DoFieldExchange) rsCourseSet.Requery(); // Réaffiche à nouveau tous les titres pour lequel CourseID = 'PROGRAMMATION' while ( !rsCourseSet.IsEOF( ) ) { CString strTitle = rsCourseSet.m_strCourseTitle; // j'obtient un titre TRACE("%s\n", (LPCTSTR) strTitle); rsCourseSet.MoveNext( ); } |
Lorsque vous naviguez au sein d'un jeu d'enregistrements, il arrive fréquemment que vous ayez besoin de revenir sur un enregistrement donné.
Deux méthodes, le signet et la position absolue, permettent de répondre à ce besoin.
Signets dans ODBC MFC
Un signet identifie un enregistrement de façon unique.
Lorsque vous naviguez au sein d'un jeu d'enregistrements,
vous ne pouvez pas toujours vous fier à la position absolue d'un enregistrement dans la mesure où les enregistrements peuvent être supprimés du jeu d'enregistrements.
La solution la plus fiable pour conserver la trace de la position d'un enregistrement consiste à utiliser un signet.
La classe CRecordset propose des fonctions membres pour :
obtenir le signet de l'enregistrement courant, de telle sorte que vous puissiez le sauvegarder dans une variable (GetBookmark) ;
atteindre rapidement un enregistrement donné en indiquant son signet, préalablement sauvegardé dans une variable (SetBookmark).
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | CCustSet rsCustSet( NULL ); // ouvre le recordset; rsCustSet.Open( CRecordset::snapshot, NULL, CRecordset::useBookmarks); // utilise des signets //... CString strSaveRecord = rsCustSet.m_strData; CDBVariant varRecordToReturnTo; if(rsCustSet.CanBookmark()) rsCustSet.GetBookmark( varRecordToReturnTo );// pour l'enregistrement courant on obtient un signet dans un CDBVariant // lignes de code de défilement du recordset //... if(rsCustSet.CanBookmark()) rsCustSet.SetBookmark( varRecordToReturnTo ); // à l'aide du signet j'atteint immédiatement mon enregistrement if(strSaveRecord == rsCustSet.m_strData) { AfxMessageBox("j'ai retrouvé ma position"); } |
Par exemple, si vous effectuez une opération Requery (lancement d'une nouvelle requête) sur un jeu d'enregistrements, il se peut que les signets ne soient plus valides.
Appelez CDatabase::GetBookmarkPersistence pour vérifier si vous pouvez faire appel à SetBookmark en toute sécurité.
Positions absolues dans ODBC MFC
Outre les signets, la classe CRecordset permet de définir l'enregistrement courant en indiquant sa position ordinale.
Cette méthode est appelée positionnement absolu.
Le positionnement absolu n'est pas disponible pour les jeux d'enregistrement en avant seulement (CRecordset::forwardOnly).
Pour déplacer le pointeur de l'enregistrement courant à l'aide du positionnement absolu,
appelez CRecordset::SetAbsolutePosition. Lorsque vous transmettez une valeur à SetAbsolutePosition,
l'enregistrement correspondant à cette position ordinale devient l'enregistrement courant.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 | long nRows = 3; // je n'oublie pas de revenir au 3 ième enregistrement comme enregistrement courant // lignes de code de défilement du recordset //... rsCustSet.SetAbsolutePosition(nRows); // bon, je veux revenir au 3 ième enregistrement |
La classe CRecordset propose les fonctions membres Move pour faire défiler un jeu d'enregistrements.
Ces fonctions déplacent l'enregistrement courant par jeu de lignes.
Si vous avez implémenté l'extraction globale de lignes,
l'opération Move repositionne le jeu d'enregistrements en fonction de la taille du jeu de lignes.
Dans le cas contraire, l'appel de la fonction repositionne le jeu d'enregistrements d'un enregistrement à la fois.
Pour défiler :
vers l'avant d'un enregistrement ou d'un jeu de lignes à la fois, appelez la fonction membre MoveNext ;
vers l'arrière d'un enregistrement ou d'un jeu de lignes à la fois, appelez la fonction membre MovePrev ;
jusqu'au premier enregistrement du jeu d'enregistrements, appelez la fonction membre MoveFirst ;
jusqu'au dernier enregistrement du jeu d'enregistrements ou jusqu'au dernier jeu de lignes, appelez la fonction membre MoveLast ;
de N enregistrements à partir de la position courante, appelez la fonction membre Move.
Pour tester le début ou la fin d'un jeu d'enregistrements
-Vous êtes-vous déplacé au-delà du dernier enregistrement ? Appelez la fonction membre IsEOF.
-Si vous avez atteint le premier enregistrement (déplacement vers l'arrière), Appelez la fonction membre IsBOF.
IsEOF (End Of File) retourne une valeur différente de zéro si le jeu d'enregistrements est positionné au-delà du dernier enregistrement.
IsBOF (Begin Of File) retourne une valeur différente de zéro si le jeu d'enregistrements est positionné avant le premier enregistrement.
Dans les deux cas, il n'y a pas d'enregistrement actuel sur lequel opérer.
Si vous appelez MovePrev alors que IsBOF a déjà la valeur TRUE,
ou si vous appelez MoveNext alors que IsEOF a déjà la valeur TRUE,
l'infrastructure lève une exception CDBException.
Vous pouvez aussi utiliser IsBOF et IsEOF pour vérifier si un jeu d'enregistrements est vide.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | CCustSet rsCustSet( NULL ); // ouvre le recordset; l'enregistrement courant est en première position rsCustSet.Open( ); // Suis-je avant le premier enregistrement? if( rsCustSet.IsBOF( ) ) return; // le recordset est vide // je lis le champ Data du premier enregistrement Cstring strFirstData = rsCustSet.m_strData; if( !rsCustSet.CanScroll() ) // Puisse-je défiler? return; // dommage!!! // Scroll à la fin du recordset, jusqu'à se positionner au-delà du dernier enregistrement while ( !rsCustSet.IsEOF( ) ) { // je lis le champ Data de l'enregistrement courant CString strCurrentData = rsCustSet.m_strData; rsCustSet.MoveNext( ); } // je move au dernier record rsCustSet.MoveLast( ); // je lis le champ Data du dernier enregistrement CString strLastData = rsCustSet.m_strData; // Scroll au début du recordset, jusqu'à se positionner avant le premier enregistrement while( !rsCustSet.IsBOF( ) ) { // je lis le champ Data de l'enregistrement courant CString strCurrentData = rsCustSet.m_strData; rsCustSet.MovePrev( ); } // je move vers le premier record rsCustSet.MoveFirst( ); // je lis le champ Data du premier enregistrement strFirstData = rsCustSet.m_strData; |
De la façon la plus naturelle, il s'agit d'un appel au couple Open()/Close() du recordset.
D'abord on suppose que notre jeu d'enregistrement est construit à partir d'une classe dérivée de CRecordset.
Dans le constructeur, transmettez un pointeur à un objet de CDatabase,
ou la valeur NULL pour utiliser un objet temporaire de base de données que l'infrastructure construira et
ouvrira en fonction de la chaîne de connexion retournée par la fonction membre GetDefaultConnect.
L'objet de CDatabase peut ou non être déjà connecté à une source de données.
Et enfin appelez la fonction membre Open de l'objet.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | // Construit un snapshot CStudentSet rsStudent( NULL ); // construction implicite d'un CDatabase non connecté à une base de données // définit les options d'ouverture // utilise la chaîne de connexion par défaut de CRecordset::GetDefaultConnect if(!rsStudent.Open(CRecordset::snapshot, NULL, CRecordset::readOnly)) return FALSE; // Utilise le snapshot pour agir sur ses enregistrements... // ... rsStudent.Close(); |
L'architecture d'un jeu d'enregistrements (jeu de lignes) est définit par ses données membres.
En effet considérons une classe dérivée de CRecordset
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | class CCourse : public CRecordset { public: CCourse(CDatabase* pDatabase = NULL); ... CString m_strCourseID; CString m_strCourseTitle; CString m_strIDParam; }; |
les membres de données qui composent l'architecture d'un objet jeu d'enregistrements sont :
- les membres de données de type champ
- les membres de données de type paramètre.
Membres de données de type champ :
Ce sont les membres des données les plus important.
Il s'agit de :
- m_strCourseID pour la colonne "CourseID" de la table Course
- m_strCourseTitle pour la colonne "CourseTitle" de la table Course
Pour que les données de la source de données soient fournies à vos membres de données de type champ la fonction CCourse::DoFieldExchange doit être implémentée et le nombre de champs CRecordset::m_nFields pris en charge par votre jeu d'enregistrement doit être fixé de préférence dans le constructeur de CCourse
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void CCourse::DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Text(pFX, "CourseID", m_strCourseID); RFX_Text(pFX, "CourseTitle", m_strCourseTitle); } CCourse::CCourse(CDatabase* pdb) : CRecordset(pdb) { m_strCourseID = ""; m_strCourseTitle = ""; m_nFields = 2; // absolument recommandé } |
Membres de données de type paramètre:
Si la classe est « paramétrée », elle possède un ou plusieurs membres de données de type paramètre.
Il s'agit de m_strIDParam.
Une classe paramétrée vous permet de baser une requête de jeu d'enregistrements sur des informations obtenues ou calculées au moment de l'exécution.
En général, le paramètre vous aide à affiner la sélection, l'objet jeu d'enregistrements pourrait exécuter l'instruction SQL suivante :
SELECT [CourseID], [CourseTitle] FROM [Course] WHERE [CourseID] = ? Le « ? » constitue un emplacement réservé pour la valeur de paramètre fournie à l'exécution.
Lorsque vous construisez le jeu d'enregistrements et définissez son membre de données m_strIDParam sur « MATH101 » , l'instruction SQL effective du jeu d'enregistrements devient :
SELECT [CourseID], [CourseTitle] FROM [Course] WHERE [CourseID] = MATH101 Pour que ODBC le considère comme paramètre il faut fixer le nombre de paramètres CRecordset::m_nParams et ensuite ajouter un peu de code dans DoFieldExchange()
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void CCourse::DoFieldExchange(CFieldExchange* pFX) { pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Text(pFX, "CourseID", m_strCourseID); RFX_Text(pFX, "CourseTitle", m_strCourseTitle); pFX->SetFieldType(CFieldExchange::param); RFX_Text(pFX, "CourseID", m_strIDParam); // le second argument est ignoré } CCourse::CCourse(CDatabase* pdb) : CRecordset(pdb) { m_strCourseID = ""; m_strCourseTitle = ""; m_nFields = 2; m_nParams = 1; // absolument recommandé } |
Pour vous connecter à une source de données spécifique, votre source de données doit déjà être configurée à l'aide de l'Administrateur ODBC
Construisez un objet CDatabase.
Appelez sa fonction membre OpenEx ou Open.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | CDatabase mydb; if( !mydb.OpenEx( _T( "ODBC;DSN=MYDATASOURCE;UID=JOES" ), CDatabase::openReadOnly) { AfxMessageBox("Impossible d'ouvrir la base de donnée en lecture seule"); return; } else { AfxMessageBox("Test d'ouverture réussi"); mydb.ExecuteSQL("CREATE TABLE OFFICES (OfficeID TEXT(4)" ", OfficeName TEXT(10))"); } CRecorset r(&mydb); //... utilisation r.Close(); // Je ferme d'abord le recordset mydb.Close(); // Je me déconnecte // je me reconnecte à une autre source de données if( !mydb.OpenEx( _T( "ODBC;DSN=SQLServer_Source;Trusted_Connection=Yes;" ), CDatabase::forceOdbcDialog) { AfxMessageBox("Impossible d'ouvrir la base de donnée à l'aide de l'authentification Windows NT"); return; } else { AfxMessageBox("Test d'ouverture réussi"); } // ... |
Vous devez fermer tous les jeux de lignes (CRecordset) ouverts avant d'appeler la fonction membre Close de CDatabase.
Dans les jeux de lignes associés à l'objet CDatabase que vous voulez fermer, toutes les instructions AddNew ou Edit et toutes les transactions en attente sont annulées.
Pour vous déconnecter d'une source de données
Appelez la fonction membre Close de l'objet CDatabase.
Détruisez l'objet, sauf si vous voulez le réutiliser.
Vous pouvez réutiliser un objet CDatabase après une déconnexion, que ce soit pour vous reconnecter à la même source de données ou pour vous connecter à une source de données différente.
Pour réutiliser un objet CDatabase
Fermez la connexion initiale de l'objet.
Plutôt que de détruire l'objet, appelez à nouveau sa fonction membre OpenEx ou Open.
Avant tout établissement de communications avec un système de gestion de base de données (SGBD) pour accéder aux données, une chaîne de connexion est demandée.
C'est chaîne comprend des informations qui renseignent le pilote ODBC sur la façon d'ouvrir une connexion à la base de donnée.
Pour des projets générés à l'aide des assistants code, ces derniers fournissent une chaîne de connexion par défaut qui est retournée par la fonction membre virtuelle CRecordset::GetDefaultConnect.
Cette fonction est appelée par la charpente MFC lors de votre première communication à la base de données.
Vous pouvez redéfinir cette fonction pour imposer aux utilisateurs de votre application un mode d'ouverture à votre base de données.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 | CString CMyAppSet::GetDefaultConnect() { // les utilisateurs seront forcés de sélectionner une source de données, // et de rentrer un username et un password à travers une boîte de dialogue de connexion return "ODBC;"; } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 | CString CMyAppSet::GetDefaultConnect() { // les utilisateurs seront forcés de rentrer un username et un password // à travers une boîte de dialogue de connexion // mydb est le nom de ma source de données return "ODBC;DSN=mydb;"; } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 | CString CMyAppSet::GetDefaultConnect() { // les utilisateurs seront forcés de rentrer un password qui // correspond au username myuserid attendu à travers une boîte // de dialogue de connexion return "ODBC;DSN=mydb;UID=myuserid;"; } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 | CString CMyAppSet::GetDefaultConnect() { // la chaîne est codée en dure et la connexion est directe // Il convient d'éviter de coder de manière irréversible un mot de passe, // ou de spécifier un mot de passe vide puisque cela compromet sérieusement la sécurité. return "ODBC;DSN=mydb;UID=sa;PWD=777;"; } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 | CString CApp1Set::GetDefaultConnect() { // Cette chaîne de connexion spécifie une connexion approuvée // (qui utilise la sécurité intégrée de Windows NT). return "ODBC;DSN=afx;Trusted_Connection=Yes;"; } |
L'Administrateur ODBC est utilisé pour configurer vos sources de données.
Vous pouvez également utiliser l'Administrateur ODBC après l'installation pour ajouter ou supprimer des sources de données.
Lorsque vous créez des applications,
vous pouvez diriger vos utilisateurs vers l'Administrateur ODBC pour leur permettre d'ajouter des sources de données,
ou créer cette fonctionnalité dans votre application en effectuant des appels d'installation ODBC directs.
Vous pouvez utiliser un fichier Excel comme source de données que vous devez configurer afin qu'il soit enregistré et
qu'il apparaisse dans la boîte de dialogue Source de données.
Pour utiliser un fichier Excel comme source de données
Cliquez sur le panneau de configuration, sélectionnez Outils d'administration, et enfin Source de données ODBC.
Sous l'onglet DSN fichier, cliquez sur Ajouter.
Dans la boîte de dialogue Créer une nouvelle source de données, sélectionnez un pilote Excel, puis cliquez sur Suivant.
Cliquez sur Parcourir et sélectionnez le nom du fichier à utiliser comme source de données.
Comme remarque vous devrez peut-être sélectionner Tous les fichiers dans le menu déroulant pour afficher tous les fichiers .xls.
Cliquez sur Suivant, puis sur Terminer.
Dans la boîte de dialogue Installation ODBC pour Microsoft Excel, sélectionnez la version et le classeur de la base de données.
Pour utiliser votre base de donnée access comme source de données la procédure est semblable.
Cliquez sur le panneau de configuration, sélectionnez Outils d'administration, et enfin Source de données ODBC.
Une fois que la boîte s'affiche à l'écran sélectionner l'onglet DSN utilisateur ( Source de données utilisateur).
Cliquez sur Ajouter... pour ajouter une source de données.
Une autre boîte s'affiche, et vous sélectionnez le pilote ODBC Microsoft Access Driver (*.mdb)
Cliquez sur Terminer.
Une autre boite s'affiche à nouveau, et vous tapez le nom de votre source de données.
Ce nom sera utilisé pour identifier la base de données dans vos projets VC++.
Cliquez sur le bouton Sélectionner... pour accéder à la dernière boîte de dialogue où vous pouvez sélectionner le fichier qui représente votre base de donnée.(fichier *.mdb)
Enfin vous cliquez sur tous les trois boutons OK et la configuration est terminée.
La connexion à une source de données signifie l'établissement de communications avec un système de
gestion de base de données (SGBD) pour accéder aux données. Lorsque vous vous connectez à une source
de données à partir d'une application par le biais d'un pilote ODBC, le pilote établit la connexion pour vous,
localement ou via un réseau.
Vous pouvez vous connecter à toute source de données pour laquelle vous possédez un pilote ODBC.
Les utilisateurs de votre application doivent également disposer du même pilote ODBC pour leur source de données.
La gestion des connexions passe
- Par la configuration de la source de données,
- Par son accès dans un environnement multi-utilisateur
- Par la généralisation d'une chaîne de connexion à la source de données
- Par la façon de se connecter et de se déconnecter à la source de données
- Par la réutilisation de la connexion c'est à dire celle de l'objet CDatabase.
Dans la terminologie de base de données, une source de données représente un ensemble spécifique de données,
les informations nécessaires pour accéder à ces données et l'emplacement de la source de données,
pouvant être décrite par un nom de source de données.
Pour utiliser la classe CDatabase, vous devez configurer la source de données à l'aide de l'Administrateur ODBC.
Une base de données distante s'exécutant sur Microsoft SQL Server au sein d'un réseau ou un fichier
Microsoft Access dans un répertoire local constitue des exemples de sources de données.
Vous pouvez accéder à toute source de données pour laquelle vous possédez un pilote ODBC à partir de votre application.
Une ou plusieurs sources de données peuvent être actives simultanément dans votre application,
Chacune étant représentée par un objet CDatabase.
Plusieurs connexions simultanées à une source de données peuvent également être actives.
Vous pouvez vous connecter à des sources de données distantes ou locales,
selon les pilotes installés et les fonctionnalités de vos pilotes ODBC.
C'est en transmettant le membre CRecordset::m_pDatabase d'un jeu de lignes à un autre.
m_pDatabase est un pointeur sur un objet CDatabase qui représente une connexion à une base de données.
Puisque les jeux de lignes CDBset et CDBOrderSet sont attachés aux tables [Product], [Orders] et [Order Details] d'une même base de données. Il serait une perte de temps de créer à chaque fois un objet CDatabase pour un CRecordset.
Code c++ : | Sélectionner tout |
1 2 3 | CDBSet(CDatabase* pDatabase = NULL); // pour notre CDBSet CDBOrderSet(CDatabase* pDatabase = NULL); // pour notre CDBOrderSet |
Pour construire le CDatabase nous aurions pu écrire
Code c++ : | Sélectionner tout |
1 2 | m_pSet->m_pDataBase = new CDatabase(); // m_pSet est un pointeur sur un CRecordset |
Les deux jeux de lignes sont déclarés dans le document CDBDoc. Dans le constructeur CDBDoc::CDBDoc les constructeurs des jeux sont appelés avec un pointeur NULL (argument par défaut) et les membres des deux jeux CRecordset::m_pDatabase sont nuls.
Le CDatabase est construit implicitement dans les coulisses des appels des membres CRecordset::Open ou CRecordView::OnInitialUpdate qui en fin de compte appelle CRecordset::Open.
Si CRecordset::Open se rend compte que CDatabase n'est pas crée, il le construit puis appelle CDatabase::Open Pour le cas de notre exemple le CDatabase est construit au premier affichage de CDBView par son membre CDBView::OnInitialUpdate() qui appelle soit CRecordView::OnInitialUpdate ou directement CRecordset::Open.
Mais quant à CDBOrderSet, son membre m_pDatabase est toujours nul d'où la réutilisation.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | void CDBOrderView::OnInitialUpdate() { BeginWaitCursor(); CDBDoc* pDoc = (CDBDoc*) GetDocument(); // Obtient le document m_pSet = &pDoc->m_OrderSet;// Obtient le jeu de ligne associé // Utilise la BD ouverte pour le jeu de lignes CDBSet m_pSet->m_pDatabase = pDoc->m_dBSet.m_pDatabase; // Réutilisation de la connexion // Définit le code produit en cours comme paramètre // Initialise le paramètre du filtre m_pSet->m_ProductIDParam = pDoc->m_dBSet.m_ProductID; // Définit le filtre comme colonne code produit m_pSet->m_strFilter = "[ProductID] = ? AND [Orders].[OrderID] = [Order Details].[OrderID]"; m_pSet->Open(); // Appel du membre CDBOrderSet::Open if (m_pSet->IsOpen()) { CString strTitle = "Nom de la table :"; // la chaîne est vide Pourquoi? Parce que nous avons 2 tables CString strTable = m_pSet->GetTableName(); // et donc 2 noms de tables if (!strTable.IsEmpty()) strTitle += _T(":") + strTable; GetDocument()->SetTitle(strTitle); } CRecordView::OnInitialUpdate(); EndWaitCursor(); } |
Lire Comment placer la clause WHERE dans une requête SQL? pour comprendre l'enchaînement.
Soit m_ProductIDParam une variable de type long utilisée comme paramètre de filtre
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class CDBOrderSet : public CRecordset { public: CDBOrderSet(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(CDBOrderSet) // Field/Param Data //{{AFX_FIELD(CDBOrderSet, CRecordset) longm_OrderDetailID; longm_OrderID; // champ de la table [Orders] attaché à la colonne [Orders].[OrderID] longm_ProductID; //... d'autres déclarations des colonnes longm_OrderID2;// champ de la table [Order Detail] attaché à la colonne [Order Details].[OrderID] //... d'autres déclarations des colonnes //}}AFX_FIELD long m_ProductIDParam;// définition du paramètre qui remplace le symbole ? dans le filtre //... |
Dans le constructeur :
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | CDBOrderSet::CDBOrderSet(CDatabase* pdb) : CRecordset(pdb) { //{{AFX_FIELD_INIT(CDBOrderSet) m_OrderDetailID = 0; m_OrderID = 0; m_ProductID = 0; //... autres initialisations m_OrderID2 = 0; //... autres initialisations m_nFields = 29; //nombre de champs //}}AFX_FIELD_INIT m_ProductIDParam = 0L;//initialisation du paramètre m_nParams = 1;//nombre de paramètres m_nDefaultType = snapshot; //type de jeu de lignes } |
On initialise le paramètre m_ProductIDParam et on fixe le nombre de paramètres à l'aide du membre CRecordset::m_nParams. Comme vous le constatez on peut utiliser plusieurs paramètres et fixer ainsi leur nombre à l'aide du membre m_nParams.
Il doit y avoir concordance sinon le fonctionnement est compromis.
Pour identifier la variable m_ProductIDParam de la classe comme un paramètre de substitution dans le filtre de CDBOrderSet nous devons également ajouter du code au membre de la classe DoFieldExchange()
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void CDBOrderSet::DoFieldExchange(CFieldExchange* pFX) { //{{AFX_FIELD_MAP(CDBOrderSet) pFX->SetFieldType(CFieldExchange::outputColumn); RFX_Long(pFX, _T("[OrderDetailID]"), m_OrderDetailID); RFX_Long(pFX, _T("[Order Details].[OrderID]"), m_OrderID); RFX_Long(pFX, _T("[ProductID]"), m_ProductID); //... autres définitions de mappage RFX_Long(pFX, _T("[Orders].[OrderID]"), m_OrderID2); //... autres définitions de mappage //}}AFX_FIELD_MAP //Définit le type de colonne comme paramètre pFX->SetFieldType(CFieldExchange::param);// définit le mode des appels RFX_()suivants sur param RFX_Long(pFX, _T("ParamProductID"), m_ProductIDParam); } |
SetFieldType(CFieldExchange::param) définit le mode des appels RFX_() suivants sur param.
Le troisième argument de tous les appels RFX_() suivants est alors interprété comme un paramètre qui doit remplacer un point d'interrogation dans le filtre du jeu de lignes.
Si vous avez plusieurs paramètres, ils remplacent les points d'interrogation dans la chaîne m_strFilter, de gauche à droite.
Il importe de veiller à ce que les appels RFX_() soient dans le bon ordre.
Lorsque le mode est défini sur param, le second argument de l'appel RFX_() est ignoré, vous pouvez donc le mettre à NULL ici, ou une autre chaîne si vous voulez.
En quelques mots :
Déclarer les paramètres dans le jeu de lignes.
Initialiser ces paramètres et fixer le nombre de paramètres à l'aide de m_nParams. Utitliser DoFieldExchange pour les identifier comme paramètres de substitution.
Dans le context ODBC avec les MFC;
c'est en ajoutant un filtre à la requête par l'attribution d'une chaîne au membre CRecordset::m_strFilter de l'objet jeu de lignes.
Soit l'instruction suivante :
Code C++ : | Sélectionner tout |
m_pSet->m_strFilter = "[ProductID] = ? AND [Orders].[OrderID] = [Order Details].[OrderID]";
Ici, dans le cas de notre exemple, m_pSet est le pointeur sur CDBOrderSet de la classe CDBOrderView.
[Orders] et [Order Details] sont deux tables associés au jeu de lignes CDBOrderSet.
Chacune des tables définit son champs [OrderID]. Ici le champ est précédé du nom de sa table.
[ProductID] est un champ de la table [Order Details]
Relativement à cette clause WHERE, il s'agit de produire une table à l'aide de l'opération SQL SELECT sur l'objet CDBOrderSet pour lequel les champs [OrderID] des deux tables sont égaux et le champ ProductID de CDBOrderSet correspond à la valeur actuelle du champs ProductID de CDBSet attaché à la table [Product].
Cette valeur courante de ProductID de CDBSet est représentée par le point d'interrogation Ce symbole ? sera remplacé par un paramètre du filtre qu'il faut définir...
Voir Comment définir un paramètre de filtre dans la clause WHERE d'une requête SQL?
C'est en ajoutant des jeux de lignes supplémentaires à notre application.
Si notre premier jeu de lignes CDBSet est connecté à la table Product qui fournit les informations sur les produits nous pouvons insérer une seconde table qui elle gèrera les informations de commandes sur les produits de la base de données.
En effet, soit CDBOrderSet un autre recordset attaché à deux tables [Orders] et [Order Details] qui contiennent toutes les commandes correspondant à un produit et soit CDBOrderView l'objet vue de lignes associé qui affiche les données de CDBOrderSet.
À l'aide de classwizard (VC++ 6.0), on clique sur Add Class... puis sur New.
On saisit le nom de la classe CDBOrderSet et on sélectionne la classe de base CRecordset.
Cliquez ensuite sur OK, Classwizard vous amène à la boite de dialogue permettant de sélectionner la base de données associées et sous recordset type on conserve le paramétrage snapshot.
Cliquez ensuite sur OK qui nous amène à la sélection des tables de la base de données.
Soient deux tables Orders et Orders Détails préalablement créées qu'on associe à CDBOrderSet, puis cliquez sur OK pour terminer la procédure.
voici le code généré:
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | class CDBOrderSet : public CRecordset { public: CDBOrderSet(CDatabase* pDatabase = NULL); DECLARE_DYNAMIC(CDBOrderSet) // Field/Param Data //{{AFX_FIELD(CDBOrderSet, CRecordset) longm_OrderDetailID; longm_OrderID; longm_ProductID; CTimem_DateSold; doublem_Quantity; CStringm_UnitPrice; doublem_Discount; CStringm_SalePrice; CStringm_SalesTax; CStringm_LineTotal; longm_OrderID2; longm_CustomerID; longm_EmployeeID; CTimem_OrderDate; CStringm_PurchaseOrderNumber; CTimem_RequiredByDate; CTimem_PromisedByDate; CStringm_ShipName; CStringm_ShipAddress; CStringm_ShipCity; CStringm_ShipState; CStringm_ShipStateOrProvince; CStringm_ShipPostalCode; CStringm_ShipCountry; CStringm_ShipPhoneNumber; CTimem_ShipDate; longm_ShippingMethodID; CStringm_FreightCharge; doublem_SalesTaxRate; //}}AFX_FIELD //... |
Deux tables [Orders] et [Order Details] sont associées à notre snapshot.
On peut ne pas avoir besoin de toutes les colonnes. On peut supprimer quelques unes à l'aide de ClassWizard.
Ajoutons un membre donnée m_OrderSet dans la définition de la classe document CDBDoc et en insérant le fichier "DBOrderSet.h" dans "DBDoc.h"
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include "DBSet.h" #include "DBOrderSet.h" class CDBDoc : public CDocument { protected: // create from serialization only CDBDoc(); DECLARE_DYNCREATE(CDBDoc) // Attributes public: CDBSet m_dBSet; CDBOrderSet m_OrderSet; //... |
C'est en appelant le membre
Code C++ : | Sélectionner tout |
CString CDatabase::GetDatabaseName() const
de CDatabase pour obtenir le nom de la base de données.
Pour obtenir le nom de la table attachée au recordset c'est le membre
Code C++ : | Sélectionner tout |
const CString& CRecordset::GetTableName() const
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | void CDBView::OnInitialUpdate() { m_pSet = &GetDocument()->m_dBSet; // trie suivant la colonne ProductID de la table Product m_pSet->m_strSort = "[ProductID]"; // ouverture du jeu de ligne CRecordView::OnInitialUpdate(); if(m_pSet->IsOpen()) { CString strTitle = m_pSet->m_pDatabase->GetDatabaseName(); CString strTable = m_pSet->GetTableName(); if(!strTable.IsEmpty()) strTitle += _T(":") + strTable; GetDocument()->SetTitle(strTitle); } GetParentFrame()->RecalcLayout(); ResizeParentToFit(); } |
En général, vous devez vous assurer que vous obtenez une chaîne retournée par la fonction GetTableName().
Plusieurs conditions peuvent empêcher la définition d'un nom de table, par exemple le jeu de lignes peut faire intervenir plusieurs tables.
Le transfert de données entre la base de données et le jeu de lignes a lieu par un appel direct à CRecordset::Open() de l'objet jeu de lignes.
Pour vérifier que le jeu est déjà ouvert on appelle CRecordset::IsOpen().
Dans l'exemple de notre programme le membre OnInitialUpdate de la classe de base de notre vue CRecordView appelle la fonction membre Open() de l'objet jeu de lignes.
C'est pourquoi la définition du membre m_strSort pour le tri est définit avant l'appel à OnInitialUpdate de la classe de base.
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 | void CDBView::OnInitialUpdate() { //... m_pSet->m_strSort = "[ProductID]";// trie suivant la colonne ProductID de la table Product CRecordView::OnInitialUpdate();// appel implicite de CRecordset::Open //... } |
Voici le code de CRecordView::OnInitialUpdate() définit dans <DBVIEW.cpp>
Code C++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void CRecordView::OnInitialUpdate() { CRecordset* pRecordset = OnGetRecordset(); // recordset must be allocated already ASSERT(pRecordset != NULL); if (!pRecordset->IsOpen()) { CWaitCursor wait; // ouverture du jeu de ligne par l'appel à OnInitialUpdate() pRecordset->Open(); } CFormView::OnInitialUpdate(); } |
On peut ouvrir directement le jeu sans passer par OnInitialUpdate par un appel directe à Open() du jeu.
Proposer une nouvelle réponse sur la FAQ
Ce n'est pas l'endroit pour poser des questions, allez plutôt sur le forum de la rubrique pour çaLes sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2024 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.