La gestione dei dati permanenti (approccio in Delphi 4 o succ.) Introduzione I dati "permanenti" di qualsiasi applicazione sono sempre salvati in contenitori logici chiamati FILE; per esempio i dati relativi ai libri di una biblioteca sono salvati su file e di conseguenza su disco, così come i dati dei dipendenti di una certa azienda. I FILE sono poi memorizzati su alcuni supporti fisici quali i dischi (floppy, hard, CD rom, DVD, Riscrivibili) ed i nastri ( ). Il Sistema Operativo, ed in particolare un suo modulo: il File System, svolge il compito di fornire all'utente una visione logica uniforme delle informazioni memorizzate sui vari tipi di supporti a disposizione tramite una serie di routine di medio livello. Vediamo ora come si colloca il concetto di FILE all'interno di una scala gerarchica dei dati: DATABASE (biblioteca) Insieme di più file, anche diversi tra di loro, e dei legami tra di loro FILE o ARCHIVIO (elenco libri) Sequenza lineare di informazioni omogenee oppure come un insieme di registrazioni RECORD o REGISTRAZIONE (libro) Sequenza, non sempre di uguale lunghezza, di campi CAMPO o FIELD (titolo) Sequenza di byte con una precisa interpretazione CARATTERE o BYTE (U) BIT (0) Sintetizzando possiamo ritenere di classificare l'accesso ai dati su memoria di massa tramite due diversi approcci: a) l'approccio FILE SYSTEM: Accesso ai file tramite le primitive di sistema messe a disposizione dal Sistema Operativo; nel sistema Unix sono le System Call relative al File System, nel sistema Windows sono le API (application programming interface). Per ulteriori approfondimenti si può fare riferimento all'Help di Delphi 4 o succ. al capitolo MS SDK Help Files -> Win32 Developer's References, alla voce File Function (win32 HLP). Accesso ai file le routine messe a disposizione da un linguaggio di programmazione, come Object Pascal (OpenFile, CloseFile, Read, Write, Seek, ecc.); questo approccio verrà ripreso di seguito; b) l'approccio DATA BASE: Accesso ai dati in modo astratto facendo riferimento ad un modello logico standard dei dati: per esempio il modello Relazionale (Tabelle, Query, linguaggio SQL, ecc.); in tal modo l'accesso ai dati è gestito tramite un modulo detto DBMS che 582846450 1/19 Quanto espresso in precedenza è riassunto, in ambiente Delphi 4 o successivi, dal seguente schema: Applicazione Deplhi approccio DATA BASE Approccio FILE SYSTEM (API) (API) Borland Database Engine (BDE) File SQL Links dBase Paradox ODBC File Oracle Informix Approccio FILE SYSTEM (Object Pascal) Driver ODBC per Accesss Access 582846450 Altri driver ODBC SQLServer SyBase 2/19 L'approccio FILE SYSTEM tramite Object Pascal (Delphi 4 o successivi): Una variabile file è ogni variabile dichiarata di tipo file. Ci sono tre classi di file: tipizzati (typed), testo (text) e non tipizzati (untyped) I file tipizzati (typed file) Un file è un insieme ordinato di elementi dello stesso tipo; la sintassi per la dichiarazione del tipo file è: type fileTypeName = file of type dove fileTypeName è ogni identificatore valido e type è un tipo a dimensione fissa. I tipi puntatori (sia impliciti che espliciti) non sono permessi, inoltre un file non può contenere dynamic arrays, long strings, classes, objects, pointers, variants, altri file, o tipi strutturati contenenti uno di essi. Per esempio: type PhoneEntry = record FirstName, LastName: string[20]; PhoneNumber: string[15]; Listed: Boolean; end; PhoneList = file of PhoneEntry; è la dichiarazione di un tipo file di record. Prima di poter usare una variabile file è necessario associarla ad un file esterno tramite una chiamata alla procedure AssignFile. Un file esterno è tipicamente un nome di file su disco, ma esso può anche essere un dispositivo esterno come la tastiera o il display. Il file esterno memorizza le informazioni scritte nella variabile file (file interno) o ritorna le informazioni lette dalla variabile file (file interno). Una volta che è stabilita l'associazione con il file esterno la variabile file deve essere "aperta" per predisporsi alle operazioni di input o output. Un file esistente può essere aperto tramite la chiamata alla procedure Reset, e un nuovo file può essere creato e aperto tramite la chiamata alla procedure Rewrite. Dopo aver aperto una variabile file con Reset (se esistente) o Rewrite (se non esistente) è possibile scriverci o leggerci indifferentemente. Dicevo all'inizio che ogni variabile file è vista come una sequenza lineare di componenti di un certo tipo; i componenti sono numerati partendo da zero. Alle variabili file normalmente si accede sequenzialmente; e cioè quando un componente del file è letto tramite la procedure standard Read o scritto tramite la procedure standard Write, la posizione corrente del file si sposta sulla successiva componente del file (dopo aver letto o scritto la componenete i, la posizione corrente si sposta sulla componente i+1. Oltre ad accedere sequenzialmente è possibile accedere anche in modo casuale tramite la procedure standard Seek, la quale muove la posizione corrente ad una specificata componente. Le funzioni standard FilePos e FileSize possono essere usate per determinare la posizione corrente e la dimensione (in componenti) del file specificato. Quando un programma ha completato l'utilizzo di una variabile file essa deve essere chiusa usando la procedure standard CloseFile. Dopo aver chiuso la variabile file, il file esterno associato viene aggiornato. La variabile file, a questo punto, può essere associata ad un altro file esterno. Per default, tutte le chiamate alle procedure e function standard di I/O sono automaticamente controllate dagli errori, e se accade un errore viene invocata un'eccezione (o il programma termina se l'eccezione non è abilitata). Questo controllo automatico può essere abilitato o non abilitato usando le direttive al compilatore {$I+} 582846450 3/19 e {$I–}. Quando il controllo di I/O è off, cioè quando una chiamata di procedure o function è compilata con {$I–}, se capita un errore di I/O esso non genera un'eccezione; il controllo del risultato dell'operazione di I/O può essere ugualmente effettuato tramite la chiamata alla function standard IOResult. Si deve chiamare la function IOResult per superare l'errore, anche se l'errore non deve essere sostanzialmente gestito. Se non si eleimina l'errore e {$I+} è lo stato corrente, la successiva chiamata ad una function di I/O potrà fallire. Lista delle procedure o function, e loro descrizione, da utilizzare con file tipizzati (Molte di queste operazioni sono presenti nella unti System, la quale è implicitamente compilata con ogni applicazione; altre sono nella unit SysUtils la quale deve essere listata nella clausola uses del programma per il quale deve essere resa disponibile) Operazioni preliminari Procedure AssignFile (var F; Nome:String); Assegna il nome di un file esterno (l'identificatore del file system) ad una variabile file Procedure Reset (var F [:file; DimRec:word]); Apre un file esistente Procedure Rewrite (var F [:file; DimRec:word]); Crea e apre un nuovo file F è una variabile file qualunque L’assegnamento effettuato continua ad esistere fino a quando non viene effettuato un nuovo assegnamento su F. Questa procedura non può essere utilizzata su un file già aperto Se la stringa del nome è la stringa vuota si fa riferimento al file standard di ingresso e al file standard di uscita La dimensione del buffer di trasferimento è opzionale ed usata solo nel file non tipizzati. Se il file non esiste viene generata un’eccezione; è possibile gestire l’eccezione con IOResult Se il file era già esistente viene cancellato, se non esistente viene creato, in ogni caso la posizione corrente è l’inizio del file e l’eof(F) è vera. La dimensione del buffer di trasferimento è opzionale ed usata solo nel file non tipizzati. Operazioni sulla variabile file Procedure CloseFile(var F); Chiude un file aperto Function Eof (var F):boolean; Ritorna lo stato della fine del file F Function FilePos (var F):longint; Ritorna la posizione corrente del file F Function FileSize (var F):longint; Il file associato viene chiuso ed aggiornato (vengono flashati i buffer), si libera il relativo handle DOS per poter essere riutilizzato Se la posizione corrente è successiva all'ultima posizione del file allora ritorna TRUE Se la posizione corrente è precedente all'ultima posizione del file allora ritorna FALSE Se la posizione corrente è all’inizio ritorna 0 Se la posizione corrente è alla fine del file, eof(F) è true, ritorna FileSize(F). Se il file F è vuoto restituisce 0 Ritorna il numero di componenti di F 582846450 4/19 Function IOResult:word; Ritorna un valore intero che è lo stato dell’ultima operazione di I/O: 0 se l’operazione è andata a buon fine, un valore diverso da 0 è il codice dell’errore provocato Procedure Read (F, V1 [,V2, ..,Vn]); Legge dal file uno o più valori e li pone rispettivamente in una o più variabili specificate Procedure Seek( var F; N:longInt); Muove la posizione corrente del file F alla posizione N Procedure Truncate (var F); Tronca le dimensioni del file alla posizione corrente Procedure Write ((F, V1 [,V2, ..,Vn]); Scrive una o più variabili in una o più componenti del file 582846450 Per poter rilevare gli errori non deve essere attivo il controllo degli errori, {$I-} Se avviene un errore di I/O, tutte le successive operazioni di I/O vengono ignorate fino alla prossima chiamata di IOResult. Una chiamata ad IOResult elimina il flag di errore interno. Per ripristinare il controllo automatico degli errori di I/O usare {$I+} Codici di errore: - 100 disk read error (se si legge dopo la fine del file) - 101 disk write error (se il disco è pieno) – 102 file not assigned (il file non è stato assegnato tramite AssignFile) – 103 file not open (il file non è stato aperto) – 104 file not oper for input – 105 file not oper for output – 106 invalid numeric format Ogni variabile specificata deve essere dello stesso tipo del tipo base del file. Per ogni variabile letta, la posizione corrente viene avanzata di una posizione. Se si continua a leggere anche dopo la fine del file si provoca un errore Il numero della prima posizione è 0. È possibile spostarsi sulla fine del file con l’istruzione Seek(F, FileSize(F)) Tutti i record successivi alla posizione corrente vengono cancellati; La posizione corrente diventa la fine del file Ogni variabile specificata deve essere dello stesso tipo del tipo base del file. Per ogni variabile scritta, la posizione corrente viene avanzata di una posizione. Se la posizione su cui si scrive è la fine del file il file stesso viene espanso 5/19 Operazioni sul file esterno Procedure ChDir (S:String); Cambia la directory corrente Procedure Erase (var F); Il file F non deve essere aperto Nella stringa S viene restitutita la directory corrente del drive specificato da D. D=0 specifica il drive corrente, 1 il drive A, 2 il drive B ecc. Crea una subdirectory: il nome S può corrispondere ad un path relativo o assoluto; L’ultimo elemento del percorso non può essere un file Al file associato ad F viene assegnato un nuopvo nome; tutte le successive operazioni effettuate su F opereranno sul file col nuovo nome Il file F non deve essere aperto Cancella il file associato Procedure GetDir (D.Byte; var S:String); Restituisce la directory corrente del drive specificato Procedure MkDir (S:String); Crea una subdirectory Procedure Rename (var F; NuovoNome:String); Rinomina un file Procedure RmDir (S:String); Elimina una subdirectory vuota I file non tipizzati (untyped file) I file non tipizzati sono canali di I/O a basso livello usati primariamente per accedere direttamente ai file del disco senza preoccuparsi del tipo o della struttura. Un file non tipizzato è dichiarato con la parola file e nulla più. Per esempio: var DataFile: file; Per i file non tipizzati, le procedure Reset e Rewrite prevedono un parametro extra per specificare la dimensione del record usato nel trasferimento. Per ragioni storiche, la dimensione di default del record è 128 byte. (Un record di dimensione 1 è il solo valore che riflette la misura esatta per ogni file) Lista delle procedure o function, e loro descrizione, da utilizzare con file non tipizzati Eccetto per Read e Write, tutte le procedure e function standard per i file tipizzati sono disponibili anche per i file tipizzati. In luogo di Read e Write, vi sono due procedure chiamate BlockRead e BlockWrite usate anche per trasferire dati ad alta velocità: procedure BlockRead (var F:file; var Buf; Cont:Word [; var Risultato:Word]); legge uno o più record da un file non tipizzato 582846450 F è una variabile file qualsiasi, Buf è una variabile qualsiasi, Cont e Risultato sono di tipo Word; Legge al più Cont record dal file F; In Risultato viene messo il numero effettivo di record letti (<= Cont) Il blocco trasferito occuperà Cont*DimRec byte (DimRec è specificato nell’apertura, 128 se non specificato nell’apertura) Cont*DimRec deve essere minore di 64K 6/19 Procedure BlockWrite (var F:file; var Buf; Cont:Word [; var Risultato:Word]); legge uno o più record da un file non tipizzato BolckRead provoca l’avanzamento della posizione corrente di Risultato Record Il file deve essere aperto F è una variabile file qualsiasi, Buf è una variabile qualsiasi, Cont e Risultato sono di tipo Word; Scrive dalla memoria al file al più Cont record nel file F; In Risultato viene messo il numero effettivo di record completi scritti (<= Cont) Il blocco trasferito occuperà Cont*DimRec byte (DimRec è specificato nell’apertura, 128 se non specificato nell’apertura) Cont*DimRec deve essere minore di 64K BolckRead provoca l’avanzamento della posizione corrente di Risultato Record Il file deve essere aperto Esempio: Procedure CopyFile (File1, File2: String); (* procedure che copia un file File1 nel file File2 *) var FromF, ToF: file; NumRead, NumWritten: word; Buf: array[1..2048] of char; begin AssignFile(FromF, File1); Reset(FromF,1); AssignFile(ToF, File2); Rewrite(ToF,1); repeat BlockRead(FromF, Buf, SizeOf(Buf), NumRead); BlockWrite(ToF, Buf, NumRead, NumWritten); until (NumRead = 0) or (NumWritten <> NumRead); CloseFile(FromF); CloseFile(ToF); end; I FILE di testo (TEXT FILE) I file di testo (Text) sono file esterni costituiti da una sequenza di caratteri formattata in linee, dove ogni linea è terminata con un marcatore particolare di fine linea (un carriage-return character #13, possibilmente seguito linefeed character #10). Anche e soprattutto per questo fatto il tipo text è distinto dal tipo file of Char. Per i file di tipo text sono definite 2 speciali procedure di lettura e scrittura, che approfondiremo più avanti, vengono sovrascritte le procedure Read e Write e tramite di esse è possibile leggere o scrivere valori anche non di tipo char, in tal caso i valori sono automaticamente trasformati nella corrispondente rappresentazione in caratteri. Per esempio Read(F, I), dove I è una variabile di tipo Integer, legge una sequenza di byte, interpretandola come la rappresentazione di un numero intero relativo e carica il valore corrispondente in I. 582846450 7/19 A differenza dei file tipizzati, per i quali l'accesso ai singoli record è diretto, tramite la routine Seek(F,i), nei file di testo l'accesso ai singoli caratteri è sequenziale, pertanto con tali file la routine seek non è utilizzabile. Passiamo ora in rassegna le operazioni previste sui file tipizzati e per ciascuna di esse diremo se è applicabile anche sui file di testo ed in che modo. Operazioni preliminari Procedure AssignFile (var F; Nome:String); Stesso funzionamento Assegna il nome di un file esterno (l'identificatore del file system) ad una variabile file Procedure Reset (var F [:file; DimRec:word]); Apre un file esistente Procedure Rewrite (var F [:file; DimRec:word]); Stesso funzionamento, tranne per il fatto che dopo tale apertura è possibile solo la lettura e non la scittura. Stesso funzionamento Crea e apre un nuovo file Operazioni sulla variabile file Procedure CloseFile(var F); Stesso funzionamento Chiude un file aperto Function Eof (var F):boolean; Stesso funzionamento Ritorna lo stato della fine del file F Function FilePos (var F):longint; NON DEFINITA Ritorna la posizione corrente del file F Function FileSize (var F):longint; NON DEFINITA Ritorna il numero di componenti di F Function IOResult:word; Ritorna un valore intero che è lo stato dell’ultima operazione di I/O: 0 se l’operazione è andata a buon fine, un valore diverso da 0 è il codice dell’errore provocato Procedure Read (F, V1 [,V2, ..,Vn]); Legge dal file uno o più valori e li pone rispettivamente in una o più variabili specificate Procedure Seek( var F; N:longInt); Stesso funzionamento Avviene una conversione tra la rappresentazione in caratteri del valore letto nella rappresentazione del tipo della variabile specificata. Per ogni variabile letta, la posizione corrente viene avanzata di un numero di caratteri variabile, dipendente sempre dal tipo specificato per ogni variabile Se si continua a leggere anche dopo la fine del file si provoca un errore NON DEFINITA Muove la posizione corrente del file F alla posizione N Procedure Truncate (var F); NON DEFINITA Tronca le dimensioni del file alla posizione corrente Procedure Write ((F, V1 [,V2, ..,Vn]); Scrive una o più variabili in una o più componenti del file 582846450 Avviene una conversione tra la rappresentazione in terna del valore da scrivere nella rappresentazione esterna in caratteri in funzione del tipo della variabile specificata.da scrivere. Per ogni variabile scritta, la posizione corrente viene avanzata di un numero di caratteri variabile, dipendente sempre dal tipo specificato per ogni variabile La posizione su cui si scrive è sempre la fine del file per via della limitazione sequenziale 8/19 Operazioni sul file esterno Procedure ChDir (S:String); Stesso funzionamento Cambia la directory corrente Procedure Erase (var F); Stesso funzionamento Cancella il file associato Procedure GetDir (D.Byte; var S:String); Restituisce la directory corrente del drive specificato Procedure MkDir (S:String); Crea una subdirectory Procedure Rename (var F; NuovoNome:String); Rinomina un file Procedure RmDir (S:String); Elimina una subdirectory vuota Nuove Operazioni definite solo su file di testo (text) Procedure Append (var F:text); Apre un file di testo esistente F e si predispone alla scrittura accodando le informazioni Function Eoln (F:text):boolean; Ritorna lo stato dell'end-of-line, per la posizione corrente, del file F specificato Function SeekEoln (F:text):boolean; Ritorna lo stato dell'end-of-line del file F specificato Function SeekEof (F:text):boolean; Ritorna lo stato dell'end-of-file del file F specificato Stesso funzionamento Stesso funzionamento Stesso funzionamento Stesso funzionamento La posizione corrente è la fine del file Con tale apertura è possibile solo la scrittura Se la posizione corrente si trova sull'end-of-line la funzione ritorna true, diversamente ritorna false La differenza con la funzione precedente è che tale funzione ritorna true anche nel caso in cui tra la posizione corrente e l'end-of-line si trovino caratteri di tabulazione La differenza con la funzione eof è che tale funzione ritorna true anche nel caso in cui tra la posizione corrente e l'endof-file si trovino caratteri di tabulazione. Procedure Flush (F:text); Scarica il buffer del text file F specificato Procedure Readln (F:text; V1 [,V2, ..,Vn]); Legge, come la read, e poi salta all'inizio della linea successiva Procedure Writeln (F:text; V1 [,V2, ..,Vn]); Scrive, come la write, e poi inserisce il marcatore di fine linea 582846450 9/19 Sommario finale delle routines per l’input output in OP La seguente lista rappresenta tutte le routines per la manipolazione dei file di input - output previste da Object Pascal. Procedure o function Descrizione Append Apre un file di testo esistente per appendere informazione (caratteri) in fondo al file stesso. AssignFile Assegna il nome di un file esterno (l'identificatore del file system) ad una variabile file interna ad una Unit. BlockRead Legge uno o più record da un file non tipizzato (untyped file). BlockWrite Scrive uno o più record in un file non tipizzato (untyped file). ChDir Cambia la directory corrente. CloseFile Chiude un file aperto. Eof Ritorna lo stato corrente dell' end-of-file di un file aperto. Eoln Ritorna lo stato dell' end-of-line di un file di testo aperto. Erase Cancella un file esterno. FilePos Ritorna la posizione corrente di un file tipizzato o non tipizzato file aperto. FileSize Ritorna la lunghezza corrente del file aperto (non utilizzabile per file di testo). Flush Svuota, scaricandolo su file, il buffer di un file di testo. GetDir Ritorna la directory corrente del drive specificato in ingresso. IOResult Ritorna un valore intero che è lo stato dell'ultima operazione di I/O. MkDir Crea una subdirectory. Read Legge uno o più valori da un file e li memorizza in una o più variabili specificate. Readln Come la routine precedente, ed in più avanza fino alla linea successiva. (utilizzabile solo per i file di testo). Rename Cambia il nome di un file esterno. Reset Apre in lettura/scrittura un file esistente. Rewrite Crea e apre in scrittura un nuovo file. RmDir Rimuovi una subdirectory vuota. Seek Sposta la posizione corrente di un file tipizzato o non tipizzato. (Non usabile per file di testo). SeekEof Ritorna il valore dell' end-of-file di un file di testo. SeekEoln Ritorna lo stato dell' end-of-line di un file di testo. SetTextBuf Assigna un buffer di I/O ad un file di testo. Truncate Trunca un file tipizzato o non tipizzato alla posizione corrente. Write Scrive uno o più valori nel file specificato. Writeln Come la routine precedente, ed inoltre inserisce nel file di testo un marcatore di fine linea. 582846450 10/19 L'approccio FILE SYSTEM tramite API: E’ possibile accedere ai file tramite le primitive di sistema messe a disposizione dal Sistema Operativo; nel sistema Unix sono le System Call relative al File System, nel sistema Windows sono le API (application programming interface), funzioni C il cui identificatore è riportato qui di seguito (per ulteriori approfondimenti si può fare riferimento all'Help di Delphi 4 al capitolo MS SDK Help Files -> Win32 Developer's References, alla voce File Function (win32 HLP)): - AreFileApisANSI CancelIO CopyFile CopyFileEx CopyProgressRoutine CreateDirectory CreateDirectoryEx CreateFile CreateIoCompletionPort DefineDosDevice DeleteFile FileIOCompletionRoutine FindClose FindCloseChangeNotification FindFirstChangeNotification FindFirstFile FindFirstFileEx FindNextChangeNotification FindNextFile FlushFileBuffers GetBinaryType GetCompressedFileSize GetCurrentDirectory GetDiskFreeSpace GetDiskFreeSpaceEx GetDriveType GetFileAttributes GetFileAttributesEx GetFileInformationByHandle GetFileSize GetFileType GetFullPathName GetLogicalDrives GetLogicalDriveStrings GetQueuedCompletionStatus GetShortPathName GetTempFileName GetTempPath GetVolumeInformation LockFile LockFileEx MoveFile MoveFileEx PostQueuedCompletionStatus QueryDosDevice ReadDirectoryChangesW ReadFile ReadFileEx RemoveDirectory SearchPath SetCurrentDirectory 582846450 - SetEndOfFile SetFileApisToANSI SetFileApisToOEM SetFileAttributes SetFilePointer SetVolumeLabel UnlockFile UnlockFileEx WriteFile WriteFileEx 11/19 Approfondimento su alcune routine API di uso più frequente: Apertura HANDLE CreateFile( LPCTSTR lpFileName, // pointer to name of the file DWORD dwDesiredAccess, // access (read-write) mode DWORD dwShareMode, // share mode LPSECURITY_ATTRIBUTES lpSecurityAttributes, // pointer to security attributes DWORD dwCreationDistribution, // how to create DWORD dwFlagsAndAttributes, // file attributes HANDLE hTemplateFile // handle to file with attributes to copy ); dwDesiredAccess : 0 GENERIC_READ dwShareMode : 0 FILE_SHARE_DELETE lpSecurityAttributes NIL GENERIC_WRITE FILE_SHARE_READ FILE_SHARE_WRITE dwCreationDistribution CREATE_NEW CREATE_ALWAYS OPEN_EXISTING OPEN_ALWAYS Creates a new file. The function fails if the specified file already exists. Creates a new file. The function overwrites the file if it exists. Opens the file. The function fails if the file does not exist. Opens the file, if it exists. If the file does not exist, the function creates the file. TRUNCATE_EXISTING Opens the file. Once opened, the file is truncated so that its size is zero bytes. dwFlagsAndAttributes Attributes FILE_ATTRIBUTE_ARCHIVE, FILE_ATTRIBUTE_COMPRESSED, FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_OFFLINE, FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_SYSTEM, FILE_ATTRIBUTE_TEMPORARY Flag FILE_FLAG_WRITE_THROUGH, FILE_FLAG_OVERLAPPED, FILE_FLAG_NO_BUFFERING, FILE_FLAG_RANDOM_ACCESS, FILE_FLAG_SEQUENTIAL_SCAN, FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_POSIX_SEMANTIC hTemplateFile 0 Return Values: if the function succeeds, the return value is an open handle to the specified file. If the function fails, the return value is INVALID_HANDLE_VALUE. To get extended error information, call GetLastError. Chiusura BOOL CloseHandle( HANDLE hObject ); // handle to file Return Values : if the function succeeds, the return value is nonzero. If the function fails, the return value is zero. 582846450 12/19/ Scrittura BOOL WriteFile( HANDLE hFile, // handle to file to write to LPCVOID lpBuffer, // pointer to data to write to file DWORD nNumberOfBytesToWrite, // number of bytes to write LPDWORD lpNumberOfBytesWritten, // pointer to number of bytes written LPOVERLAPPED lpOverlapped // pointer to structure needed for overlapped I/O ); hFile : Identifies the file to be written to (must have been created with GENERIC_WRITE access) lpBuffer : Points to the buffer containing the data to be written to the file. nNumberOfBytesToWrite : Specifies the number of bytes to write to the file. lpNumberOfBytesWritten : Points to the number of bytes written by this function call. lpOverlapped NIL Return Values: if the function succeeds, the return value is nonzero. If the function fails, the return value is zero. Lettura BOOL ReadFile( HANDLE hFile, // handle of file to read LPVOID lpBuffer, // address of buffer that receives data DWORD nNumberOfBytesToRead, // number of bytes to read LPDWORD lpNumberOfBytesRead, // address of number of bytes read LPOVERLAPPED lpOverlapped // address of structure for data ); hFile file. Identifies the file to be read. The file handle must have been created with GENERIC_READ access to the lpBuffer Points to the buffer that receives the data read from the file. nNumberOfBytesToRead Specifies the number of bytes to be read from the file. lpNumberOfBytesRead Points to the number of bytes read. lpOverlapped NIL Return Values : If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. Posizionamento (seek) DWORD SetFilePointer( HANDLE hFile, // handle of file LONG lDistanceToMove, // number of bytes to move file pointer 582846450 13/19/ PLONG lpDistanceToMoveHigh, // address of high-order word of distance to move DWORD dwMoveMethod ); // how to move hFile Identifies the file whose file pointer is to be moved. The file handle must have been created with GENERIC_READ or GENERIC_WRITE access to the file. lDistanceToMove Specifies the number of bytes to move the file pointer. A positive value moves the pointer forward in the file and a negative value moves it backward. lpDistanceToMoveHigh Points to the high-order word of the 64-bit distance to move. If the value of this parameter is NULL, SetFilePointer can operate only on files whose maximum size is 2^32 - 2. If this parameter is specified, the maximum file size is 2^64 - 2. This parameter also receives the high-order word of the new value of the file pointer. dwMoveMethod Specifies the starting point for the file pointer move. FILE_BEGIN The starting point is zero or the beginning of the file. FILE_CURRENT The current value of the file pointer is the starting point. FILE_END The current end-of-file position is the starting point. Return Values If the SetFilePointer function succeeds, the return value is the low-order doubleword of the new file pointer. If the function fails the return value is 0xFFFFFFFF (-1). 582846450 14/19/ L'approccio DATA BASE tramite BDE (Delphi 4 o succ): I componenti di Delphi per la gestione dei dati su disco Delphi dispone di diversi componenti relativi ai database: la pagina DATA ACCESS della Components Palette raccoglie i componenti utilizzati per interagire con i database: DataSource, Table, Query, StoredProc, DatBase, ecc. (componenti non visuali) la pagina DATA CONTROL della Components Palette che raccoglie i componenti visuali utilizzati per vedere e sottoporre ad editing i dati di un database in un form: DBGrid, DBNavigator, DBText, DBEdit, ecc. la pagina DATA ACCESS la pagina DATA CONTROL (data-aware) Per accedere ai dati in Delphi serve una sorgente di dati descritta nel componente DataSource (nella pagina DATA ACCESS) che, non indica direttamente i dati ma fa riferimento ad una Tabella, o al risultato di una Query o ad una Stored Procedure, per fornire le informazioni ad uno dei componenti visuali della pagina DATA CONTROL come indicato nel seguente schema: Data base Data base BDE ….. Table ….. …. Query DataSource Data Control 582846450 15/19/ COMPONENTI DI DATA ACCESS Centrale è l'oggetto TDataSet, che altro non è che l'astrazione di un insieme ordinato di righe di dati (record), suddivise ognuna secondo un insieme ordinato di colonne, ognuna delle quali identifica un campo all'interno del record. Generalmente non si utilizza direttamente tale classe, ma si ricorre ad uno dei discendenti TTable, TQuery oppure TStoredProc. Questi componenti discendono tutti dalla classe TDBDataSet, la quale discende a sua volta da TDataSet. TTable: la classe TTable incapsula le funzionalità proprie delle tabelle di database. Normalmente si tratta dell'astrazione relativa ad un file che contiene record fisicamente immagazzinati su di un disco. Ogni volta che si ha bisogno di accedere ad una specifica tabella o ad un insieme di tabelle di database si fa riferimento ad una o più istanze di questa classe. Per configurare correttamente le proprietà di un oggetto TTable, dopo averlo posizionato sulla form, occorre impostare il contenuto di alcune proprietà fondamentali, elencate di seguito. Per accedere ad un database, corrispondente ad una directory che contiene un insieme di file nel formato dBase oppure Paradox, oppure corrispondente ad un unico file nel formato MS Access, si deve per prima cosa impostare la proprietà DatabaseName immettendo o la directory in questione, oppure un alias definito nel Borland Database Engine (BDE); non è necessario, una volta configurato BDE, ricordarsi i nomi degli alias disponibili: facendo click alla destra del campo che permette l'immissione della proprietà, Delphi provvede a riempire una combo-box con i nomi di tutti gli alias disponibili. La seconda proprietà da impostare è TableName, ovvero il nome della tabella cui si vuole avere accesso tramite l'oggetto TTable. Come per DatabaseName è sufficiente cliccare alla destra della proprietà per avere una lista delle tabelle disponibili all'interno del database selezionato. La terza proprietà da impostare è Active da impostare a true. Queste sono le uniche proprietà che si devono impostare obbligatoriamente per avere accesso ad una tabella: per le altre sono normalmente sufficienti le impostazioni di default. Altre proprietà utili sono la ReadOnly che, se impostata a valore logico vero (true), fa in modo che qualsiasi tentativo di cambiare i dati contenuti nella tabella fallisca: non si potrà quindi né variare il contenuto di alcun campo, né effettuare cancellazioni o inserimenti di record. La proprietà Exclusive, impostata a true, fa guadagnare accesso esclusivo alla tabella, assicurandoci quindi che nessun altro utente, ma anche nessun altro oggetto TTable, o comunque un Dataset, all'interno della propria applicazione, possa accedere alla tabella. Questa proprietà si applica solo a tabelle locali, quindi non nel caso di accesso a tabelle gestite da server SQL. TQuery: la classe TQuery rappresenta l'insieme di dati restituiti da una query SQL, ovvero una ricerca su una o più tabelle di un database espressa utilizzando un linguaggio sviluppato ad hoc per tali compiti (Structured Query Language). Il risultato di una query SQL, generalmente, non ha controparte diretta su disco, nel senso che i record risultanti possono essere pensati solo come insieme temporaneo, tanto da poterlo immaginare conservato nella sola memoria centrale del computer.(Cfr approfondimento più avanti) TStoredProc: tale classe incapsula una stored procedure all'interno di un server SQL: in pratica, anche se si commette un'imprecisione ad affermare ciò, può essere considerata alla pari di una query, dalla quale differisce per il fatto di essere memorizzata sul server e non sul client come accade con TQuery. All'interno delle tabelle è possibile definire una serie di indici, grazie ai quali è possibile mantenere ordinate, secondo il contenuto di uno o più campi, le righe delle tabelle. Non esiste alcuna classe VCL in grado di rappresentare indici: in generale le funzioni per lo sfruttamento ed il mantenimento di tali strutture di ordinamento vengono fornite direttamente dalla classe TTable. Essendo per natura tabelle temporanee, le query SQL non possono fornire indici per la consultazione. IL BDE Per iniziare a lavorare con i dati è necessario, prima della compilazione, individuarne il modello concettuale (definendo Entità e Relazioni) e successivamente costruire il modello Relazionale corrispondente ed implementare, mediante appositi ambienti (DBMS), le tabelle dei dati; nel nostro caso useremo il sistema di costruzione di Data Base integrato nell'ambiente Delphi 4 o succ. , che è Database Desktop contenuto nel menu TOOLS, oppure il sistema MS ACCESS. Dopo di che è necessario configurare il BDE il motore di Delphi per 582846450 16/19/ l'accesso ai dati su disco. Per raggiungere l'obiettivo dell'indipendenza dei programmi dalla particolare rappresentazione fisica della base di dati, BDE permette di definire una serie di alias in grado di identificare con stringhe di caratteri un particolare database, i cui dettagli implementativi (tipo di server, path, formati di file) vengono inseriti attraverso un'apposita utility di configurazione (BDE Administrator) liberamente distribuibile insieme al Database Engine e richiamabile alla voce BDE Explorer dal menu DATABASE. Nel DataBase Explorer si deve definire: l'alias (nome simbolico del Database) il DataBase driver (quale tipo di data base si utilizza) il path del database su disco (directory se Paradox, file se MS Access) In DataBase Desktop si devono definire: le tabelle (campi, tipi, chiavi, indici, valori predefiniti, vincoli di integrità, ecc.) la directory di lavoro COMPONENTI DI DATA CONTROL Dell'oggetto TTable abbiamo parlato a sufficienza, almeno per quanto riguarda le caratteristiche sfruttate nell'esempio: le novità sono costituite da TDBGrid, elemento di interfaccia con l'utente ed il database e da TDataSource. Cominciando da quest'ultimo, si può dire che esso provvede a collegare la TTable con la griglia di database. In generale si deve ricorrere ad un'istanza di tale oggetto tutte le volte che si ha necessità di collegare un Dataset ad uno o più componenti Data-bound di interfaccia con l'utente. A quest'ultima categoria appartengono una serie di "repliche" degli elementi di interfaccia incontrati nella terza puntata del corso, ovvero i Visual Components. I controlli data-aware, sono il corrispettivo dei visual controls, come list-box, griglie o campi di editing che non solo sono in grado di rappresentare elementi di controllo di Windows, ma sono anche capaci di collegare il loro contenuto informativo ad uno o più campi di una tabella. Il tutto senza passare attraverso il programmatore, ma semplicemente collegandoli ad un oggetto della classe TDataSource, a sua volta collegato ad un Dataset. Approfondimenti su: TQuery - rudimenti di SQL All'interno della gerarchia di oggetti della VCL possiamo trovare un oggetto, chiamato TQuery, che presenta molte analogie con TTable, in quanto opera su dataset e permette di effettuare quasi tutte le operazioni viste durante la scorsa puntata, in particolare quelle di navigazione e di gestione dei campi di dati. La differenza fondamentale tra queste due classi sta nel metodo di definizione del dataset: se con TTable si fa riferimento ad un insieme di record fisicamente presenti su disco, al più filtrati secondo il contenuto di uno o più campi chiave o in relazione master-detail con un insieme di altri record, con TQuery il dataset fa riferimento ad un insieme, generalmente temporaneo, di record che costituiscono il risultato dell'invocazione di una query espressa attraverso un particolare linguaggio, chiamato SQL (Structured Query Language). Generalmente, e questo lo si intuisce anche dalla somiglianza tra TQuery e TTable, il risultato di una query SQL è un insieme di record che, abbiamo visto, in Delphi viene identificato con un dataset. L'istruzione più importante nel linguaggio SQL è la select, che permette di selezionare record da una o più tabelle, rispondenti a determinati criteri. Supponiamo di avere una tabella chiamata Clienti, con i campi Nome, Cognome, Indirizzo e Codice; quest'ultimo campo, di tipo intero, costituisce riferimento per i record della tabella Ordini, costituiti dai campi CodiceCliente, Data, Articolo e Quantità. Se volessimo avere come risposta dalla query una tabella (ovviamente temporanea) contenente Nome e Cognome di tutti i clienti che hanno cognome che inizia con la lettera 'C' oppure con lettere successive, dovremmo eseguire la seguente query: SELECT Nome, Cognome FROM Clienti WHERE Cognome >= 'C' Si capisce immediatamente la sintassi della select che richiede la lista dei campi da includere nella tabella di risposta. Tramite la clausola from si specifica il nome della tabella da cui andare a pescare i dati; con la where si impongono condizioni sui campi: tutti e soli i record che le soddisfano andranno a far parte dell'insieme di record di risposta. 582846450 17/19/ E' importante sottolineare che, sulla tabella Clienti, a parte l'esistenza dei campi menzionati sopra e del loro tipo di dato, non si sono fatte ipotesi sulla presenza di indici particolari, né si è indicata la strada da seguire per ottenere il risultato: con un approccio più di basso livello, quale è ad esempio quello delle TTable, si sarebbero dovuti fare i conti con l'esistenza di indici, per ottimizzare le operazioni di ricerca e selezione. Ad esempio sarebbe stato stupido, in presenza di un indice sul campo Cognome, non utilizzarlo per filtrare i record contenenti un valore minore di 'C'. Ebbene, uno dei punti di forza di SQL è proprio quello di denotare il risultato voluto, lasciando libertà pressoché assoluta al motore di database di scegliere la strategia migliore di accesso ai dati richiesti, ad esempio a seconda della presenza di indici su campi particolari: sempre ritornando all'esempio sopra, se costruisco una procedura di accesso a basso livello che poggia sulla presenza di indici sul campo Cognome, se l'indice viene rimosso, il mio programma necessiterà di profondi cambiamenti. Viceversa, se non sfrutto eventuali indici costruiti a posteriori, magari per altri compiti di ricerca, rischio di realizzare programmi che non ottimizzano le prestazioni della macchina. Con SQL è il gestore di database che pensa a come accedere ai dati: noi richiediamo solo un particolare risultato. Ovviamente si perde qualcosa in efficienza in termini assoluti (basta pensare solo al tempo necessario per decidere il piano di accesso ai dati richiesti, cosa che nel caso di algoritmo codificato "a mano" è istantanea, ma che per query di una certa complessità può richiedere un tempo superiore a qualche minuto secondo) La sintassi di SELECT è la seguente: SELECT [DISTINCT] lista_colonne FROM lista_tabelle [WHERE condizioni_di_ricerca] [ORDER BY lista_campi_ordinamento] [GROUP BY lista_campi_raggruppamento] [HAVING condizioni_sui_gruppi] lista_colonne è l'insieme di colonne delle tabelle specificate in lista_tabelle; le condizioni di ricerca possono essere multiple, sfruttando gli operatori and ed or. E' possibile ricavare, indipendentemente dagli indici presenti sulle tabelle coinvolte dalla query, un insieme di record ordinati a seconda del contenuto del campo. Specificando la parola chiave DISTINCT è possibile ricavare un insieme di righe non duplicate. Facciamo alcuni esempi: SELECT Cognome FROM Clienti seleziona tutti i cognomi dalla tabella clienti. Se ci sono record multipli in Clienti con lo stesso cognome, otteniamo lo stesso numero di cognomi duplicati nella tabella di risposta. Se vogliamo una tabella con tutti i cognomi non duplicati, dobbiamo lanciare: SELECT DISTINCT Cognome FROM Clienti Se poi li vogliamo anche in ordine alfabetico dobbiamo specificare la clausola ORDER BY: SELECT DISTINCT Cognome FROM Clienti ORDER BY Cognome Visto che abbiamo iniziato a parlare di SQL ed abbiamo fatto alcuni esempi di query, è importante sottolineare il fatto che SQL è un linguaggio case-insensitive. L'aver utilizzato lettere maiuscole per le parole chiave SQL ed un misto di maiuscole e minuscole per i campi è assolutamente ininfluente: solitamente, però, si tende ad assumere una convenzione per motivi di leggibilità. Quella mostrata negli esempi è una delle più comuni. Uno dei punti di forza del linguaggio SQL è quello di poter eseguire query che coinvolgono più tabelle, le cosiddette join: supponiamo di dover elencare, sempre a partire dalle tabelle Clienti ed Ordini definite sopra, tutti gli ordini del cliente Filippo Bosi SELECT Ordini.Articolo, Ordini.Quantita, Ordini.Data FROM Clienti,Ordini WHERE Ordini.CodiceCliente = Clienti.Codice AND Clienti.Nome = 'Filippo' AND Clienti.Cognome = 'Bosi' 582846450 18/19/ Si noti come si sia specificato, per ogni campo, il file di appartenenza. In SQL standard questo non è necessario quando i campi delle due tabelle non condividono gli stessi nomi. Operando su tabelle Paradox e dBase, utilizzando quindi database locali, gestiti direttamente dal Borland Database Engine, se non si specificano le tabelle di appartenenza, il risultato, invece di un insieme di record, è un errore a run-time! Nelle query viste finora abbiamo specificato sempre l'insieme di campi da inserire nella tabella di risposta. Molto spesso è desiderabile ottenere in risposta tutte le colonne delle tabelle coinvolte dalla query. Esiste a tale scopo una wildcard che denota "tutti i campi", ovvero "*". Volendo, ad esempio, un insieme di record composto da tutti i campi della tabella Ordini, ordinato per data, dovremmo lanciare la seguente query: SELECT * FROM Ordini ORDER BY Data SQL non viene utilizzato solo per la selezione di record, ma anche per effettuare inserimenti, cancellazioni ed aggiornamenti di record, attraverso le istruzioni INSERT, DELETE ed UPDATE. Se, ad esempio, si vuole cancellare dalla tabella Ordini tutti i record relativi ad un particolare cliente, semplicemente si fa eseguire questa query (ovviamente dopo aver impostato il codice del cliente) DELETE FROM Ordini WHERE CodiceCliente = n Se invece si vogliono incrementare del 20% i prezzi di un ipotetico listino: UPDATE Listino SET Prezzo=Prezzo*1.20 Anche alla UPDATE è possibile aggiungere una clausola WHERE, ad esempio per limitare l'incremento del 20% agli articoli che hanno prezzo inferiore alle 15000 lire: UPDATE Listino SET Prezzo=Prezzo*1.20 WHERE Prezzo<15000 Naturalmente, a differenza di quanto accade con la SELECT, le query di inserimento, cancellazione ed aggiornamento non restituiscono un set di record. Per questo, vedremo, ad esse sarà riservato un trattamento particolare in sede di lancio della query attraverso la classe TQuery. 582846450 19/19/