Guarda! Senza mani! - Settima parte
a cura di Sabrina Cosolo e Diego Cattaruzza (requisiti: conoscenza intermedia di Sql Server e di .Net)La creazione di un Database: parte Sql Server
Prima di lanciarci nella stesura di codice C# o VB, dedichiamo un po' di attenzione al codice SQL.Questa serie di articoli è rivolta a lettori che abbiano una conoscenza intermedia di Sql Server e di .Net, ma riteniamo ugualmente utile esporre qualche nozione di Sql, che per qualcuno sarà forse noiosa, per qualcun altro si rivelerà un richiamo di concetti noti, ma per più di qualcuno costituirà una novità assoluta.
Non si vuole, qui, riscrivere i Books Online di SQL Server 2005, ma solo esporre i punti interessanti per la nostra serie di articoli.Cominciamo con la specifica degli statement T-SQL per la generazione di un database, che troviamo sui Books Online di SQL Server 2005, esaminandola pezzo per pezzo.
-- Creazione database CREATE DATABASE database_name [ ON [ PRIMARY ] [ <filespec> [ ,...n ] [ , <filegroup> [ ,...n ] ] ] [ LOG ON { <filespec> [ ,...n ] } ] [ COLLATE collation_name ] [ WITH <external_access_option> ] [;]Se osserviamo il codice base, notiamo che, tranne il parametro database_name, tutto il resto è compreso fra parentesi quadre, che, nelle convenzioni dei books on line, indicano che il dato tra esse racchiuso è un dato facoltativo. Pertanto, la forma più semplice di script per creare un database è:
CREATE DATABASE DatabaseDiProvaQuesto semplice script ci genererebbe un database di nome DatabaseDiProva, facendo una copia del database MODEL contenuto nell'installazione standard di SQL Server 2005.
Sulla macchina di Sabrina, viene generato un database formato da due file, DatabaseDiProva.mdf e DatabaseDiProva_Log.ldf, con un solo filegroup standard che si chiama obbligatoriamente PRIMARY. Le dimensioni dei file sono impostate in modo standard: per il file di dati, dimensione iniziale 3 MegaByte con crescita illimitata di 1 MegaByte alla volta quando il sistema ne ha bisogno, mentre per il file di log, dimensione iniziale di 1 MegaByte e crescita limitata a 2 GigaByte con crescita del 10 per cento della dimensione del file stesso quando il sistema ne ha bisogno.Nel paragrafo precedente abbiamo introdotto alcuni concetti fondamentali sull'architettura di SQL Server.
Pur non essendo questo articolo dedicato a spiegare il suo funzionamento, introdurremo e definiremo l'abc di questa architettura per coloro che non avessero mai lavorato con SQL Server.SQL Server 2005 è un Relational Database Management System, il suo nocciolo è il servizio MSSQLSERVER, motore principale del server dati.
Vi sono poi altri servizi complementari che non vedremo, anche se in seguito accenneremo a qualcosa relativamente al SQL Server Agent, quando parleremo di Backup, però, visto che tutto quello che c'è in questi articoli è applicabile a tutte le versioni di SQL Server, fra cui SqlServer 2005 Express Edition che non possiede l'Agent, si tratterà di cenni.La struttura di SQL Server può essere osservata utilizzando SQL Server Management Studio, la cui versione Express è liberamente scaricabile dal sito Microsoft.
La versione che vedete è una developer edition, che va in coppia con Visual Studio Professional, pertanto potrebbe essere diversa da altre edizioni del sistema. La parte più importante della struttura di SQL Server sono i suoi 4 database di sistema. Se questi database vengono danneggiati o eliminati, il sistema non funziona più. Il contenuto di questi database di sistema non dovrebbe MAI essere modificato manualmente.
Master contiene tutta la struttura dell'istanza di SQL Server, tutti i dati di configurazione, tutti i dati relativi alla sicurezza, tutti i dati relativi ai database Utente ospitati in SQL Server 2005. Model è il modello database usato per creare i nuovi database; può essere configurato in modo diverso da quello standard, se necessario, e viene usato come matrice nella creazione dei nuovi database, anche se è possibile indicare dei parametri diversi nel comando CREATE DATABASE, che vedremo in seguito. MSDB è il database che contiene tutti i dati accessori, soprattutto quelli relativi al SQL Server Agent, tutti i JOB automatici, tutti gli storici dei backup e le operazioni con schedulazione assegnate all'Agent. TempDb è il database dove SQL Server crea tabelle temporanee per la gestione del proprio lavoro, sia durante le interrogazioni che nel normale funzionamento. Questo database viene automaticamente rigenerato ad ogni restart di SQL Server. Ogni database, sia esso un database di sistema oppure un database utente, è formato da almeno due file: un file Dati, con estensione .mdf, che contiene tutti i dati inseriti nel database, le sue tabelle di sistema e di configurazione, dati di sicurezza, tabelle, viste, stored procedure, user defined functions, trigger eccetera; un file di Log, con estensione .ldf, il Log delle Transazioni, in cui vengono memorizzate tutte le transazioni effettuate sui dati e prima che queste vengano registrate nel file dati in modo definitivo (Commit). Il funzionamento esatto di questi file non è argomento di questa serie di articoli: vogliamo semplicemente dar modo a chi non ne sa nulla di orientarsi in questa struttura.
Ogni database contiene al suo interno almeno un fileGroup, PRIMARY. Il fileGroup è un'unità logica formata da uno o più file in cui si popssono suddividere le tabelle dati del database. L'esempio pratico che viene insegnato all'esame di certificazione è quello della suddivisione in 3 gruppi, tabelle di sistema nel filegroup standard PRIMARY, tabelle anagrafiche modificate raramente in un filegroup separato (es.: FG_DATIBASE), e tabelle di produzione, con inserimento intensivo e modifica intensiva in un terzo filegroup (es.: FG_PRODTABLES). Come già accennato, ogni fileGroup corrisponde ad almeno un file. Il primo file del filegroup Primary ha estensione .mdf, tutti gli altri file, sia di Primary che degli eventuali altri filegroup, hanno estensione .ndf.
Ogni file del database viene inizialmente dimensionato in base alle necessità di ciò che in esso deve essere memorizzato, viene anche definita la sua massima espansione ed un criterio di crescita per il file. Queste impostazioni permettono a SQL Server di gestire autonomamente i propri database senza alcun intervento da operatore, quando lo spazio che è stato definito per ciascuno di essi si esaurisce. I parametri di dimensione e crescita dei file di database sono tre: SIZE, che stabilisce la dimensione iniziale, MAXSIZE, che stabilisce la massima dimensione permessa (è possibile impostarla UNLIMITED, ma non è auspicabile farlo, perché è sempre meglio tener conto delle risorse effettivamente disponibili sulla macchina che ospita i Database e SQL Server), GROWTH, che permette di stabilire la misura di crescita dei file del database, in unità fisiche (Megabyte, Gigabyte) oppure in percentuale (è sempre opportuno tarare la crescita di un database in modo tale che non debba essere richiamata troppo spesso, in quanto è un'operazione onerosa per il servizio e la macchina e quindi rallenterebbe le prestazioni di SQL Server; se il vostro Database cresce di 10 Megabyte al mese, mettete 10MB di crescita, se cresce di 1GB al mese, mettete 1GB, e dategli un limite di crescita in base alle dimensioni dei dischi del server - se il disco è da 100GB e 20 sono per il sistema e i programmi, limitate i dati a 60GB e il log a 15GB, così che, qualora il vostro database crescesse troppo, il sistema avrebbe comunque un minimo di risorse per permettervi la sopravvivenza fino a che non viene installato un disco più capiente o cambiata la macchina.
Prima di tornare alla funzione di Creazione database, una nota sull'installazione di SQL Server: sia SQL Server che SQLExpress, quando vengono installati, ci danno modo di configurare la cartella ove il server metterà i propri dati. Per fare questo, quando vi appare questa schermata:
Selezionate Data Files e usate il tasto Browse per crearvi una cartella specifica. Questo serve ad evitare che l'installazione automatica ponga i propri files dati su C:\Programmi\Microsoft SQL Server\..., cosa che è altamente sconsigliabile, primaditutto perché nella cartella Programmi non si dovrebbero scrivere dati, secondariamente perché le cartelle con nomi troppo lunghi possono provocare stranezze, specie quando si tratta di fare backup. Ergo, ricordatevi, all'installazione, di crearvi una cartella apposita, C:\sql.dir, ad esempio, con due sottocartelle C:\sql.dir\data e C:\sql.dir\backup, che poi userete per tutti i vostri database.
SQL Server, quando gli specificherete la cartella di installazione, creerà questa struttura di cartelle:
Nella installazione sul PC di Sabrina è stato indicato il percorso D:\sql.dir\data e SQL Server ha creato sotto a questa cartella due sottocartelle suddivise in ulteriori sottocartelle. MSSQL.1 contiene le cartelle per la gestione database standard e i database di sistema, MSSQL.2 quelle per la gestione dei database OLAP per la creazione di Datawarehouse e Cubi statistici.
Le altre cartelle sono quelle in cui andremo a generare i nostri database personali. Per chi installasse SQL Server seguendo i dettami di un buon sistemista e definisse quindi un utente specifico per il servizio invece di utilizzare LOCAL SYSTEM, questo utente di servizio dovrà avere i diritti di pieno controllo sulla cartella sql.dir e sottostanti, in modo da essere in grado di svolgere tutte le sue mansioni.
Ed ora torniamo a Create Database e vediamo come risulta lo script di creazione quando viene usato con alcune delle possibili opzioni.
CREATE DATABASE DatabaseDiProva2 ON ( NAME = DbProva2Dati, FILENAME = 'D:\SQL.DIR\DbProva2Dati.mdf', SIZE = 10MB, MAXSIZE = 50MB, FILEGROWTH = 5MB ) LOG ON ( NAME = DbProva2Log, FILENAME = 'D:\SQL.DIR\DbProva2Log.ldf', SIZE = 5MB, MAXSIZE = 25MB, FILEGROWTH = 5MB )Abbiamo utilizzato le opzioni ON FileSpecification e LOG ON FileSpecification per decidere il nome, la dimensione, i parametri di crescita, la cartella destinazione dei file del database. Vediamo come è fatta una FileSpecification:
-- Forma di una file specification (filespec) <filespec> ::= { ( NAME = logical_file_name , FILENAME = 'os_file_name' [ , SIZE = size [ KB | MB | GB | TB ] ] [ , MAXSIZE = { max_size [ KB | MB | GB | TB ] | UNLIMITED } ] [ , FILEGROWTH = growth_increment [ KB | MB | GB | TB | % ] ] ) [ ,...n ] }Una FileSpecification è una serie di parametri che ci permettono di configurare, alla creazione, la forma, le dimensioni, le modalità di crescita, i limiti di crescita dei file che compongono un database. Come possiamo notare, ci sono due parametri obbligatori e tre parametri facoltativi, inoltre è indicato che FileSpec è un'array che può comparire N volte nella definizione di un database. Cosa significa N volte? Che, se generiamo un database di produzione che riteniamo si riempirà velocemente e che quindi deve essere distribuito su più hard disk, possiamo generare più file dati su più hard disk da subito e poi lasciare che SQL Server si occupi di riempirli in modo ordinato.
Vediamo un altro modo di configurare la Create Database, utilizzando un array di FileSpecification:
CREATE DATABASE DatabaseDiProva3 ON PRIMARY ( NAME = DbProva3Dati01, FILENAME = 'D:\SQL.DIR\DbProva3Dati01.mdf', SIZE = 100MB, MAXSIZE = 200MB, FILEGROWTH = 20MB ), ( NAME = DbProva3Dati02, FILENAME = 'D:\SQL.DIR\DbProva3Dati02.mdf', SIZE = 100MB, MAXSIZE = 200MB, FILEGROWTH = 20MB ), ( NAME = DbProva3Dati03, FILENAME = 'D:\SQL.DIR\DbProva3Dati03.mdf', SIZE = 100MB, MAXSIZE = 200MB, FILEGROWTH = 20MB ) LOG ON ( NAME = DbProva3Log01, FILENAME = 'D:\SQL.DIR\DbProva3Log01.ldf', SIZE = 100MB, MAXSIZE = 200MB, FILEGROWTH = 20MB ), ( NAME = DbProva3Log02, FILENAME = 'D:\SQL.DIR\DbProva3Log02.ldf', SIZE = 100MB, MAXSIZE = 200MB, FILEGROWTH = 20MB )Questo codice, oltre a specificare forma e dimensioni dei file dati e di log, specifica anche il filegroup PRIMARY come gruppo cui appartengono tutti i file dati. Come già scritto, i filegroup sono una unità logica per la suddivisione dei file dati che compongono un database complesso, vediamone ora la definizione:
-- Forma di un file group (filegroup) <filegroup> ::= { FILEGROUP filegroup_name [ DEFAULT ] <filespec> [ ,...n ] }Come possiamo vedere, un filegroup ha solo un attributo obbligatorio, il suo nome e la necessità di avere almeno un File al suo interno; inoltre, ad uno dei filegroup che compongono un database è possibile assegnare l'attributo DEFAULT, che indica come tutte le tabelle e gli oggetti creati senza specificare loro un filegroup di destinazione, devono essere posti in quello specifico filegroup, ma questo, magari, lo vedremo in seguito sul campo.
CREATE DATABASE DatabaseDiProva4 ON PRIMARY ( NAME = DbProva4DatiPr01, FILENAME = 'D:\SQL.DIR\DbProva4DatiPr01.mdf', SIZE = 10MB, MAXSIZE = 50MB, FILEGROWTH = 15% ), ( NAME = DbProva4DatiPr02, FILENAME = 'D:\SQL.DIR\DbProva4DatiPr02.ndf', SIZE = 10MB, MAXSIZE = 50MB, FILEGROWTH = 15% ), FILEGROUP FileGroupProva1 ( NAME = DbProva4DatiFg101, FILENAME = 'D:\SQL.DIR\DbProva4DatiFg101.ndf', SIZE = 10MB, MAXSIZE = 50MB, FILEGROWTH = 5MB ), ( NAME = DbProva4DatiFg102, FILENAME = 'D:\SQL.DIR\DbProva4DatiFg102.ndf', SIZE = 10MB, MAXSIZE = 50MB, FILEGROWTH = 5MB ), FILEGROUP FileGroupProva2 ( NAME = DbProva4DatiFg201, FILENAME = 'D:\SQL.DIR\DbProva4DatiFg201.ndf', SIZE = 10MB, MAXSIZE = 50MB, FILEGROWTH = 5MB ), ( NAME = DbProva4DatiFg202, FILENAME = 'D:\SQL.DIR\DbProva4DatiFg202.ndf', SIZE = 10MB, MAXSIZE = 50MB, FILEGROWTH = 5MB ) LOG ON ( NAME = DbProva4DatiLog, FILENAME = 'D:\SQL.DIR\DbProva4Log.ldf', SIZE = 5MB, MAXSIZE = 25MB, FILEGROWTH = 5MB )In questa versione, abbiamo generato due filegroup, FileGroupProva1 e FileGroupProva2, ciascuno con due file all'interno, e per ciascun file la sua propria specifica, ed un solo file di log, ma anche i log, come abbiamo già visto nell'esempio precedente, possono essere più d'uno. Non abbiamo utilizzato la clausola DEFAULT su alcuno dei due filegroup, pertanto PRIMARY rimarrà il filegroup di Default, dove saranno generate le tabelle salvo diversa indicazione.
Non abbiamo ancora usato un'altra opzione, l'opzione COLLATE:
CREATE DATABASE DatabaseDiProva5 ON ( NAME = DbProva5Dati, FILENAME = 'D:\SQL.DIR\DbProva5Dati.mdf', SIZE = 10MB, MAXSIZE = 50MB, FILEGROWTH = 5MB ) LOG ON ( NAME = DbProva5Log, FILENAME = 'D:\SQL.DIR\DbProva5Log.ldf', SIZE = 5MB, MAXSIZE = 25MB, FILEGROWTH = 5MB ) COLLATE Latin1_General_CI_ASQuesta opzione ci permette di cambiare il modo in cui i dati vengono memorizzati in SQL Server, decidendo qual è la codepage, decidendo qual è il metodo di ordinamento e se il database deve essere CASE SENSITIVE oppure no. In altri termini con COLLATE si imposta il tipo di collazione cui deve attenersi SQL Server. I tipi di collation più comuni sono listati in questa tabella, ma per altre info su questo argomento (e su altri su cui non ci diffonderemo) vi rimandiamo ai books on line.
Il consiglio è, comunque, anche quando fosse necessario usare la ricerca CASE SENSITIVE, di predisporre il Database in modalità CASE INSENSITIVE e indicare invece la collation direttamente sui campi delle tabelle che devono permettere la ricerca CASE SENSITIVE. Infatti, 90 volte su cento sono davvero pochi i campi delle tabelle per cui è necessario questo tipo di utilizzo. La collation standard per l'italiano è Latin1_General_CI_AS, mentre i database provenienti da SQL Server 7.0 hanno usualmente la collation SQL_Latin1_General_CP1_CI_AS. Fateci attenzione, perché cambiare la collation di un Database dopo che è stato generato, è drammatico, perché la collation è replicata su ciascun campo stringa del database, pertanto cambiare la collation al database modificherà solo la collation delle tabelle e dei campi generati dopo il cambio, per cambiarle a ciò che esiste prima deve essere modificata la collation di ogni campo stringa di ogni tabella.-- Opzione di attivazione accesso esterno <external_access_option> ::= { DB_CHAINING { ON | OFF } | TRUSTWORTHY { ON | OFF } }Questa opzione, raramente usata, serve per la Cross Ownership dei database da parte di più server, qualcosa di complesso che non riguarda ciò che faremo in questa serie di articoli.
-- Opzione di attivazione del Service Broker <service_broker_option> ::= { ENABLE_BROKER | NEW_BROKER | ERROR_BROKER_CONVERSATIONS }Anche questa opzione, che riguarda la gestione del Service Broker, non verrà presa in considerazione in questa serie di articoli.
-- Creazione di un database snapshot CREATE DATABASE database_snapshot_name ON ( NAME = logical_file_name, FILENAME = 'os_file_name' ) [ ,...n ] AS SNAPSHOT OF source_database_name [;]La generazione di un database snapshot è una interessantissima funzionalità di SQL Server 2005 e dà la possibilità di congelare (fotografare un'istantanea) un database ad un certo momento del tempo. Questo risultato si ottine registrando sullo snapshot l'inverso di tutte le operazioni di modifica effettuate dopo la generazione dello snapshot, in modo da poter risalire a quel punto. Per capirci, se viene aggiunto un record nel database, nello snapshot viene registrato il comando per cancellarlo, se viene modificato un campo, nello snapshot viene registrato il comando per ripristinare il suo valore, e così via. Una specie di RollBack, insomma. Un database snapshot non è una copia del database originale, ma si basa su di esso per registrare le istruzioni da eseguire per tornare al punto iniziale. Un'idea piuttosto interessante e innovativa.
-- Attach di un database CREATE DATABASE database_name ON <filespec> [ ,...n ] FOR { ATTACH [ WITH <service_broker_option> ] | ATTACH_REBUILD_LOG } [;]La possibilità di effettuare l'attach di un database esistente è stata inserita probabilmente per l'implementazione specifica di SQL Express, che effettua l'attach all'avvio della prima connessione e il detach alla chiusura dell'ultima connessione. Anche questa opzione non ci interessa ai fini della serie di articoli, pertanto la trascureremo.
La creazione di un Database: parte .Net
Bene, ora che abbiamo esaminato gli script SQL che dobbiamo essere in grado di eseguire, ragioniamo su cosa ci serve per creare un oggetto che ci permetta di generare un database.
Abbiamo bisogno di una classe per ospitare i dati necessari alla generazione di un database, quindi vediamo di fare una lista di classe, metodi e proprietà:
DatabaseGenerator classe che genera il database NomeDatabase
Stringa DataFileGroups
Collezione di filegroups x i Dati DataFileGroup
Entità FileGroup Name
Nome del filegroup FileSpecifications
Collezione di Specifiche di file FileSpecification
Entità di tipo Specifica di file Name
Stringa FileName
Stringa Size
Entità FileSize MaxSize
Entità FileMaxSize FileGrowth
Entità FileGrowth (Log)FileSpecifications
Collezione specifiche di file di log (Log)FileSpecification
Entità di tipo specifica di un file di log Collation
Stringa CreaDatabase
Metodo per creare il database Lista accessoria per le unità di misura:
Unità di misura per la dimensione iniziale KB, MB, GB, TB Unità di misura per la dimensione massima ammessa KB, MB, GB, TB, UNLIMITED Unità di misura per la dimensione di crescita KB, MB, GB, TB, % Abbozzato così il progetto, la prima cosa che faremo sarà aggiungere alla nostra collezione dei setting tre valori, contenenti una lista CSV di unità di misura per le dimensioni iniziali, massime e per la crescita, per permetterci di creare delle combobox per questi valori, prevedendo anche l'eventualità per quanto remota che possano essercene di nuovi.
Aggiungiamo tre setting a UsingSqlServerConfig
Per avere a disposizione le tre collezioni di unità di misura che ci servono, utilizzeremo i nostri setting, visto che li abbiamo preparati, in modo tale che, se domattina una nuova release di SQLServer introducesse un nuovo modo di dimensionare i database, potremo aggiungerlo senza dover neppure ricompilare. Facciamo questo esercizio anche per dimostrare come utilizzare i nostri setting e come ottenere qualcosa di diverso da una semplice stringa con poche righe di codice.Iniziamo subito le modifiche e andiamo ad aggiungere alla classe UsingSqlServerConfig una direttiva di importazione namespace:
using System.Collections.Specialized;Imports System.Collections.SpecializedImportiamo il namespace Collections.Specialized perchè vi si trova la classe StringCollection, che vogliamo far utilizzare come datasource alla combobox in cui mostreremo le unità di misura.
Aggiungiamo un'altra serie di costanti per la gestione dei setting:
private const string DEFAULT_GrowthUmi = "KB;MB;GB;TB;%"; private const string DEFAULT_MaxSizeUmi = "KB;MB;GB;TB;UNLIMITED"; private const string DEFAULT_SizeUmi = "KB;MB;GB;TB"; private const string STT_APPDbGrowthUmi = "DbGrowthUmi"; private const string STT_APPDbMaxSizeUmi = "DbMaxSizeUmi"; private const string STT_APPDbSizeUmi = "DbSizeUmi";Private Const DEFAULT_SizeUmi As String = "KB;MB;GB;TB" Private Const DEFAULT_GrowthUmi As String = "KB;MB;GB;TB;%" Private Const DEFAULT_MaxSizeUmi As String = "KB;MB;GB;TB;UNLIMITED" Private Const STT_APPDbSizeUmi As String = "DbSizeUmi" Private Const STT_APPDbGrowthUmi As String = "DbGrowthUmi" Private Const STT_APPDbMaxSizeUmi As String = "DbMaxSizeUmi"Le costanti contengono i valori di default assunti dal setting che ospita la nostra collezione di unità di misura e i nomi dei tre nuovi setting.
Quindi aggiorniamo subito il metodo LoadSettings, per non dimenticarcene:public static void LoadSettings() { // dopo if (!AppSettings.ExistSetting(STT_APPSqlDatabase)) if (!AppSettings.ExistSetting(STT_APPDbSizeUmi)) { AppSettings.Add(STT_APPDbSizeUmi, DEFAULT_SizeUmi); hasToBeSaved = true; } if (!AppSettings.ExistSetting(STT_APPDbMaxSizeUmi)) { AppSettings.Add(STT_APPDbMaxSizeUmi, DEFAULT_MaxSizeUmi); hasToBeSaved = true; } if (!AppSettings.ExistSetting(STT_APPDbGrowthUmi)) { AppSettings.Add(STT_APPDbGrowthUmi, DEFAULT_GrowthUmi); hasToBeSaved = true; } // prima di TAndTSettings.LoadSettings(mUserFileName, ref mUserSettings); }Public Shared Sub LoadSettings() ' dopo il If Not AppSettings.ExistSetting(STT_APPSqlDatabase) Then If Not AppSettings.ExistSetting(STT_APPDbSizeUmi) Then AppSettings.Add(STT_APPDbSizeUmi, DEFAULT_SizeUmi) hasToBeSaved = True End If If Not AppSettings.ExistSetting(STT_APPDbMaxSizeUmi) Then AppSettings.Add(STT_APPDbMaxSizeUmi, DEFAULT_MaxSizeUmi) hasToBeSaved = True End If If Not AppSettings.ExistSetting(STT_APPDbGrowthUmi) Then AppSettings.Add(STT_APPDbGrowthUmi, DEFAULT_GrowthUmi) hasToBeSaved = True End If ' prima di TAndTSettings.LoadSettings(mUserFileName, mUserSettings) End SubLe due righe commentate indicano il punto in cui aggiungere il nuovo codice, evitando di riscriverlo tutto. La modifica a questo metodo permette che, alla prima esecuzione della classe, vengano generati i tre setting relativi alle unità di misura con i valori di default, rendendoli disponibili alla form di modifica, ma anche all'applicazione, se l'utente non dovesse aprirla.
Adesso possiamo dedicarci con calma all'implementazione delle proprietà che rendano utilizzabili i nuovi setting:public static StringCollection SizeUmiList { get { StringCollection umiList = new StringCollection(); umiList.AddRange(AppSettings[STT_APPDbSizeUmi].Value.Split(new char[] { ';', ' ' }, StringSplitOptions.RemoveEmptyEntries)); return umiList; } set { string[] sArr = new string[value.Count]; value.CopyTo(sArr, 0); AppSettings[STT_APPDbSizeUmi].Value = String.Join(";", sArr); } } public static StringCollection MaxSizeUmiList { get { StringCollection umiList = new StringCollection(); umiList.AddRange(AppSettings[STT_APPDbMaxSizeUmi].Value.Split(new char[] { ';', ' ' }, StringSplitOptions.RemoveEmptyEntries)); return umiList; } set { string[] sArr = new string[value.Count]; value.CopyTo(sArr, 0); AppSettings[STT_APPDbMaxSizeUmi].Value = String.Join(";", sArr); } } public static StringCollection GrowthUmiList { get { StringCollection umiList = new StringCollection(); umiList.AddRange(AppSettings[STT_APPDbGrowthUmi].Value.Split(new char[] { ';', ' ' }, StringSplitOptions.RemoveEmptyEntries)); return umiList; } set { string[] sArr = new string[value.Count]; value.CopyTo(sArr, 0); AppSettings[STT_APPDbGrowthUmi].Value = String.Join(";", sArr); } }Public Shared Property SizeUmiList() As StringCollection Get Dim umiList As New StringCollection umiList.AddRange(AppSettings(STT_APPDbSizeUmi).Value.Split( _ New Char() {";"c, " "c}, StringSplitOptions.RemoveEmptyEntries)) Return umiList End Get Set(ByVal value As StringCollection) Dim sArr(value.Count - 1) As String value.CopyTo(sArr, 0) AppSettings(STT_APPDbSizeUmi).Value = String.Join(";", sArr) End Set End Property Public Shared Property MaxSizeUmiList() As StringCollection Get Dim umiList As New StringCollection umiList.AddRange(AppSettings(STT_APPDbMaxSizeUmi).Value.Split( _ New Char() {";"c, " "c}, StringSplitOptions.RemoveEmptyEntries)) Return umiList End Get Set(ByVal value As StringCollection) Dim sArr(value.Count - 1) As String value.CopyTo(sArr, 0) AppSettings(STT_APPDbMaxSizeUmi).Value = String.Join(";", sArr) End Set End Property Public Shared Property GrowthUmiList() As StringCollection Get Dim umiList As New StringCollection umiList.AddRange(AppSettings(STT_APPDbGrowthUmi).Value.Split( _ New Char() {";"c, " "c}, StringSplitOptions.RemoveEmptyEntries)) Return umiList End Get Set(ByVal value As StringCollection) Dim sArr(value.Count - 1) As String value.CopyTo(sArr, 0) AppSettings(STT_APPDbGrowthUmi).Value = String.Join(";", sArr) End Set End PropertyCome potete vedere, abbiamo utilizzato una semplice funzione di trasformazione per rendere la stringa CSV del setting direttamente utilizzabile in una combobox, cioè una StringCollection. Ecco un esempio pratico di uno dei vantaggi offerti dall'uso delle classi: nasconderne la complessità a chi la usa, incapsulando la logica noiosa. Chi usa la SizeUmiList, dall'esterno vede solo una collezione di stringhe, non una stringa CSV da convertire, o una serie di valori.
Le classi per la gestione delle dimensioni
Per la rappresentazione delle tre tipologie di dimensioni necessarie per definire un file all'interno di un comando CREATE DATABASE, abbiamo bisogno di una classe che ospiti un valore intero e una stringa con l'unità di misura. In teoria una sola, perché le tre tipologie sono simili, ma in pratica invece no, perché la dimensione massima ha la possibilità di utilizzare l'unità di misura UNLIMITED, mentre le altre due no, e la dimensione di crescita può assumere l'unità di misura percentuale. Pertanto non è possibile usare una classe unica per tutte e tre, il che ci offre il magnifico destro per realizzare una classe astratta e quindi obbligatoriamente ereditabile, con la maggior parte del codice già pronto da usare come 'base', da cui svilupperemo tre classi derivate per le singole rispettive particolarità. In più, come ciliegina sulla torta, aggiungeremo una enumerazione. Quindi si tratta di qualcosa di più adatto di un'interfaccia, che non contiene codice.
E' ovvio che, anche in questo caso, la nostra è solo una delle possibili soluzioni per implementare questo tipo di oggetto, ve ne sono di certo cento altre più belle, professionali e performanti, ma bisogna pur iniziare da qualche parte...La classe astratta DbFileSizeBase
Questa classe è una Entity, esattamente come TAndTSetting che abbiamo già sviluppato, con alcune cose in più.
Ma il posto giusto e sotto TAndT.Data. Quindi aggiungiamo a questo progetto una cartella Entities, all'interno della quale creeremo la classe.using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Xml.Serialization; using TAndT.Base.Entities; using TAndT.Base; using TAndT.Base.Xml; namespace TAndT.Data.Entities { [Serializable, XmlRoot(Namespace = "http://www.visual-basic.it")] public abstract class DbFileSizeBase : IEntity { private readonly static string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name; } }Imports System Imports System.Collections.Generic Imports System.ComponentModel Imports System.Text Imports System.Xml.Serialization Imports TAndT.Base Imports TAndT.Base.Entities Imports TAndT.Base.Xml <Serializable(), XmlRoot(Namespace:="http://www.visual-basic.it")> _ Public MustInherit Class DbFileSizeBase Implements IEntity Private Shared ReadOnly mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod.ReflectedType.Name End ClassLa struttura di base è la solita, facciamo solo notare che abbiamo aggiunto nelle References la TAndT.Base, per poter aggiungere le ultime direttive, il namespace di appartenenza, e l'aggiunta della parola chiave abstract (MustInherit) e che implementa l'interfaccia IEntity. La classe inoltre è stata dotata degli attributi Serializable e XmlRoot per la serializzazione XML.
Non riporterò, per non tediare, alcuni metodi già trattati che rispettano il contratto con l'interfaccia (gli operatori di uguaglianza e diversità, i metodi Equals e GetHashCode, i metodi di serializzazione, l'evento PropertyChanged) se non quando presentano qualche differenza non formale. Nel codice allegato, troverete che qualcuno di essi è qualificato virtual (Overridable) o Protected, in rispetto dell'astrazione della classe.protected const string FMP_ToSqlString = "{0}{1}"; protected const string FMP_ToString = "Dimensione: {0} {1}"; public const string VAL_SpecialUmiPercent = "%"; public const string VAL_SpecialUmiUnlimited = "UNLIMITED"; public const string VAL_SizeUmiKB = "KB"; public const string VAL_SizeUmiMB = "MB"; public const string VAL_SizeUmiGB = "GB"; public const string VAL_SizeUmiTB = "TB";Protected Const FMP_ToSqlString As String = "{0}{1}" Protected Const FMP_ToString As String = "Dimensione: {0} {1}" Public Const VAL_SpecialUmiPercent As String = "%" Public Const VAL_SpecialUmiUnlimited As String = "UNLIMITED" Public Const VAL_SizeUmiKB As String = "KB" Public Const VAL_SizeUmiMB As String = "MB" Public Const VAL_SizeUmiGB As String = "GB" Public Const VAL_SizeUmiTB As String = "TB"Le costanti. Abbiamo predisposto due formati per la presentazione dell'oggetto in un metodo ToString e la generazione della versione per gli script SQL. Inoltre abbiamo predisposto i valori per le unità di misura speciali e normali usate dai vari tipi di dimensione.
private string mSizeUmi; private int mSizeValue;Private mSizeUmi As String Private mSizeValue As IntegerI due campi contenenti i dati, cioè unità di misura e valore della dimensione.
public string SizeUmi { get { return mSizeUmi; } set { mSizeUmi = value.Trim().ToUpper(); OnPropertyChanged(new PropertyChangedEventArgs("SizeUmi")); } } public int SizeValue { get { return mSizeValue; } set { mSizeValue = value; OnPropertyChanged(new PropertyChangedEventArgs("SizeValue")); } }Public Property SizeUmi() As String Get Return mSizeUmi End Get Set(ByVal value As String) mSizeUmi = value.Trim.ToUpper OnPropertyChanged(New PropertyChangedEventArgs("SizeUmi")) End Set End Property Public Property SizeValue() As Integer Get Return mSizeValue End Get Set(ByVal value As Integer) mSizeValue = value OnPropertyChanged(New PropertyChangedEventArgs("SizeValue")) End Set End PropertyLe proprietà che espongono il valore dei due campi della classe e scatenano l'evento OnPropertyChanged quando vengono assegnate.
protected DbFileSizeBase() { mSizeValue = 0; mSizeUmi = VAL_SizeUmiKB; } public DbFileSizeBase(int pSize, string pUmi) { mSizeUmi = pUmi; mSizeValue = pSize; }Protected Sub New() mSizeValue = 0 mSizeUmi = VAL_SizeUmiKB End Sub Public Sub New(ByVal size As Integer, ByVal umi As String) mSizeUmi = umi mSizeValue = size End SubAbbiamo predisposto due costruttori, un costruttore standard senza parametri, che serve perché possa essere utilizzato dalla funzione di serializzazione di default, la quale pretende un costruttore senza parametri. E' qualificato protected perché deve essere accessibile non dall'esterno, ma solo da parte delle classi derivate. Il secondo costruttore, quello usabile al momento in cui la classe viene istanziata, richiede i due dati che la classe custodisce.
public virtual int CompareTo(object obj) { int ret = -1; try { if (obj is DbFileSizeBase) { DbFileSizeBase val = (DbFileSizeBase)obj; int comparator = 0; comparator = this.SizeValue.CompareTo(val.SizeValue); if (comparator == 0) { comparator = CompareHelper.StringComparer(this.SizeUmi, val.SizeUmi); } ret = comparator; } } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } return (ret); }Public Overridable Function CompareTo(ByVal obj As Object) As Integer _ Implements System.IComparable.CompareTo Dim ret As Integer = -1 Try If TypeOf obj Is DbFileSizeBase Then Dim val As DbFileSizeBase = DirectCast(obj, DbFileSizeBase) Dim comparator As Integer = Me.SizeValue.CompareTo(val.SizeValue) If comparator = 0 Then comparator = CompareHelper.StringComparer(Me.SizeUmi, val.SizeUmi) End If ret = comparator End If Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try Return ret End FunctionLa funzione di comparazione, ridefinibile, per cui due DbFileSizeBase sono uguali quando hanno stesso valore e stessa unità di misura.
Poiché dobbiamo predisporre diversi tipi di dimensione, è opportuno definirli in un tipo enumerato:
public enum DbSizeType : int { Size = 0, MaxSize, Growth }Public Enum DbSizeType As Integer Size = 0 MaxSize Growth End EnumQuesto è il nostro primo tipo enumerato. I tipi enumerati sono dei valori numerici che si possono chiamare per nome (come costanti) per definire un tipo di valore. Probabilmente ne vedremo altri in seguito. Questo, che viene definito al di fuori della classe, in modo da potersi riferire ad esso usando il solo namespace, rappresenta i tre tipi di dimensione per Database che siamo in grado di gestire. Se ne arrivasse un quarto, basterebbe aggiungerlo a questa enumerazione e buona parte del lavoro della sua gestione sarebbe fatto.
public abstract DbSizeType GetDbFileSizeType(); public abstract bool IsValid();Public MustOverride Function IsValid() As Boolean Implements Base.Entities.IEntity.IsValid Public MustOverride Function GetDbFileSizeType() As DbSizeTypeUn'altra novità di questa classe è la dichiarazione di due metodi abstract (MustOverride), che ogni classe derivata da questa deve per forza implementare. Questa imposizione è dovuta al fatto che esistono tipi diversi di dimensione (quelli enumerati in DbSiseType, appunto) e che i criteri per la validità sono differenti e, ovviamente, non ignorabili.
public virtual string ToSqlString() { try { return (string.Format(FMP_ToSqlString, SizeValue, SizeUmi)); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name + ": " + ex.Message, ex); } }Public Overridable Function ToSqlString() As String Try Return String.Format(FMP_ToSqlString, SizeValue, SizeUmi) Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionUn metodo specifico, ridefinibile, per generare la forma SQL della nostra classe. Per questa piccola classe, potrebbe coincidere con il ToString, ma iniziamo con le buone abitudini: cerchiamo di far capire a chi userà la classe (anche noi stessi quando la useremo fra un anno o due) esattamente a cosa serve ciascun metodo.
public override string ToString() { try { return (string.Format(FMP_ToString, SizeValue, SizeUmi)); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name + ": " + ex.Message, ex); } }Public Overrides Function ToString() As String Try Return String.Format(FMP_ToString, SizeValue, SizeUmi) Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionIl metodo ToString generico, che potremo usare nei messaggi.
Con questo abbiamo concluso la nostra prima classe abstract (Must Inherit). Da essa deriveremo tre diverse nuove classi, che useremo per la gestione delle diverse dimensioni usate per generare un database.
La classe DbFileSize
La prima classe derivata che generiamo (nella stessa cartella come le altre) è la classe che ci permette di gestire la dimensione iniziale del database.using System; using System.Collections.Generic; using System.Text; using System.Xml.Serialization; namespace TAndT.Data.Entities { [Serializable, XmlRoot(Namespace = "http://www.visual-basic.it")] public class DbFileSize : DbFileSizeBase { private static readonly string mClassName = System.Reflection.MethodBase.GetCurrentMethod().ReflectedType.Name; } }Imports System Imports System.Collections.Generic Imports System.Text Imports System.Xml.Serialization <Serializable(), XmlRoot(Namespace:="http://www.visual-basic.it")> _ Public Class DbFileSize Inherits DbFileSizeBase Private Shared ReadOnly mClassName As String = _ System.Reflection.MethodBase.GetCurrentMethod.ReflectedType.Name End ClassTutto standard nella impostazione, salvo che eredita dalla classe astratta appena creata, il che ci crea automaticamente le firme dei metodi seguenti.
public override DbSizeType GetDbFileSizeType() { return (DbSizeType.Size); } public override bool IsValid() { try { return (this.SizeValue > 0 && this.SizeUmi != null && this.SizeUmi.Length > 0 && this.SizeUmi != VAL_SpecialUmiPercent && this.SizeUmi != VAL_SpecialUmiUnlimited); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } }Public Overrides Function GetDbFileSizeType() As DbSizeType Return DbSizeType.Size End Function Public Overrides Function IsValid() As Boolean Try Return Me.SizeValue > 0 AndAlso Not Me.SizeUmi Is Nothing AndAlso Me.SizeUmi.Length > 0 _ AndAlso Not Me.SizeUmi = VAL_SpecialUmiPercent _ AndAlso Not Me.SizeUmi = VAL_SpecialUmiUnlimited Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionI due metodi che obbligatoriamente dobbiamo implementare perché la nostra classe sia valida.
Il primo, molto semplice, ritorna il valore del tipo enumerato DbSizeType, che identifica questo tipo di oggetto.
Il secondo metodo, anch'esso molto semplice, verifica che la dimensione sia diversa da zero, che l'unità di misura non sia vuota e che non assuma uno dei due valori speciali percentuale o Unlimited, che in questo caso non sono supportati.public DbFileMaxSize(int pSize, string pUmi) : base(pSize, pUmi) { }Public Sub New(ByVal size As Integer, ByVal umi As String) MyBase.New(size, umi) End SubIl costruttore richiama il costruttore parametrico della classe base.
La classe DbFileMaxSize
La seconda classe derivata che realizziamo è identica alla precedente, ma se ne differenzia per il fatto che accetta Unlimited come valore dell'unità di misura. Quando questo si verifica, la stringa SQL ignora la dimensione, qualsiasi essa sia. Mostriamo qui solamente i metodi diversi da quelli della classe precedente, per evitare ripetizioni noiose:public override bool IsValid() { try { return (this.SizeValue > 0 && this.SizeUmi != null && this.SizeUmi.Length > 0 && this.SizeUmi != VAL_SpecialUmiPercent); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } } public override DbSizeType GetDbFileSizeType() { return (DbSizeType.MaxSize); }Public Overrides Function GetDbFileSizeType() As DbSizeType Return DbSizeType.MaxSize End Function Public Overrides Function IsValid() As Boolean Try Return Me.SizeValue > 0 AndAlso Not Me.SizeUmi Is Nothing AndAlso Me.SizeUmi.Length > 0 _ AndAlso Not Me.SizeUmi = VAL_SpecialUmiPercent Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionIn questo caso, il DbSizeType è MaxSize, e il metodo di validazione considera il valore valido a meno che l'unità di misura non assuma il valore percentuale.
public override string ToSqlString() { if (this.SizeUmi == VAL_SpecialUmiUnlimited) { return (VAL_SpecialUmiUnlimited); } else { return base.ToSqlString(); } }Public Overrides Function ToSqlString() As String If Me.SizeUmi = VAL_SpecialUmiUnlimited Then Return VAL_SpecialUmiUnlimited Else Return MyBase.ToSqlString() End If End FunctionRidefiniamo anche il metodo ToSqlString della classe base, perché, nel caso la massima crescita assuma Unlimited come valore di unità di misura, non è necessario (anzi sarebbe errato) aggiungere un valore numerico alla stringa sql.
La classe DbFileGrowth
La terza ed ultima classe di tipo dimensione, è identica alla classe DbFileSize, ma se ne differenzia per il fatto che ammette un valore percentuale.public override bool IsValid() { try { bool validationBase = this.SizeValue > 0 && this.SizeUmi != null && this.SizeUmi.Length > 0 && this.SizeUmi != VAL_SpecialUmiUnlimited; bool validationExtended = true; if (this.SizeUmi == VAL_SpecialUmiPercent) { validationExtended = this.SizeValue <= 100; } return (validationBase && validationExtended); } catch (Exception ex) { throw new ApplicationException(" " + mClassName + "." + System.Reflection.MethodBase.GetCurrentMethod().Name, ex); } } public override DbSizeType GetDbFileSizeType() { return (DbSizeType.Growth); }Public Overrides Function GetDbFileSizeType() As DbSizeType Return DbSizeType.Growth End Function Public Overrides Function IsValid() As Boolean Try Dim validationBase As Boolean = Me.SizeValue > 0 AndAlso Not Me.SizeUmi Is Nothing AndAlso _ Me.SizeUmi.Length > 0 AndAlso Not Me.SizeUmi = VAL_SpecialUmiUnlimited Dim validationExtended = True If Me.SizeUmi = VAL_SpecialUmiPercent Then validationExtended = Me.SizeValue <= 100 End If Return validationBase And validationExtended Catch ex As Exception Throw New ApplicationException(" " + mClassName + "." _ + System.Reflection.MethodBase.GetCurrentMethod().Name, ex) End Try End FunctionOltre al diverso valore della funzione GetDbFileSizeType, abbiamo modificato la funzione IsValid in modo tale da controllare che il valore non superi 100 quando è una percentuale. Anche se la crescita percentuale potrebbe essere superiore al 100%, riteniamo che non sarebbe molto utile una crescita esponenziale dei file del database. Per questo imponiamo come regola per questa nostra applicazione che la crescita percentuale si fermi al 100%.
Arrivederci alla prossima puntata
Terminiamo qui la prima parte della generazione database. Nella prossima puntata, proseguiremo con le classi che ci permetteranno di creare una applicazione per generare database.
Come al solito, il codice fin qui prodotto è scaricabile dall'Area Download.
(nota: il codice è 'sempre' aggiornato, con eventuali correzioni al codice precedente, non citate nell'articolo).
Potete scrivere Feedback (commenti, critiche, suggerimenti, correzioni) sul blog di Sabrina.