Van de BDE (of SQL Links) naar een SQL DBMS met Delphi 2007 en DBX4 |
BDE Analyse
Laten we beginnen met een kleine GUI om alle mogelijke BDE aliases strings op te halen die op een machine aanwezig zijn, en voor een gekozen alias alle tabellen op te sommen die daarbinnen te vinden zijn. Hier hebben we twee TComboBox componenten voor nodig, genaamd cbDatabases en cbTables. Het vullen van de cbDatabases met de aanwezige alias strings gaat met behulp van een TSession component als volgt, bijvoorbeeld in de FormCreate:
procedure TFormSDE.FormCreate(Sender: TObject); begin Session1.GetDatabaseNames(cbDatabases.Items); end;Hierdoor zal de dbDatabases TCmboBox gevuld worden met de aanwezige BDE alias strings. Als we eenmaal een alias gekozen hebben, kunnen we aan een TDatabase component vragen welke tables er allemaal bekend zijn voor deze alias. Dat gaat met de GetTableNames functie, en doe ik bij voorkeur in de OnChange event van de cbDatabases combobox, zodat je daarmee meteen de cbTables combobox kunt vullen:
procedure TFormSDE.cbDatabasesChange(Sender: TObject); begin Database1.Close; Database1.DatabaseName := cbDatabases.Items[cbDatabases.ItemIndex]; Database1.GetTableNames(cbTables.Items, cbSystemTables.Checked); end;Eenzelfde truc kunnen we uithalen in de OnChange event van de cbTables combobox, waarbij we de dan gekozen tablename kunnen gebruiken om deze tabel te analyseren.
procedure TFormSDE.cbTablesChange(Sender: TObject); begin Table1.CLose; Table1.DatabaseName := Database1.DatabaseName; Table1.TableName := cbTables.Items[cbTables.ItemIndex]; end;Uitgaan de van een BDE TTable waarvan de DatabaseName en TableName zijn opgegeven kunnen we alle benodigde informatie ophalen uit de FieldDefs en IndexDefs collecties. Deze informatie wil ik gaan gebruiken om een SQL CREATE TABLE command te produceren dat de structuur van de table kan reproduceren in een SQL DBMS. Het voordeel is dat we de inhoud van dit SQL CREATE TABLE command kunnen opslaan in een script en (later) op een andere machine kunnen draaien. Een probleem van deze benadering is dat de SQL CREATE TABLE voor ieder DBMS type een iets andere inhoud zal hebben, omdat de veld types niet altijd hetzelfde zullen zijn. Zo kent SQL Server bijvoorbeeld de veld types IMAGE en TEXT terwijl Blackfish SQL die niet kent. En ook een floating point getal wordt door het ene DBMS als DOUBLE aangegeven terwijl de andere een FLOAT wil zien. In dit artikel zal ik voor SQL Server en Blackfish SQL een aantal field mappings aangeven, maar laat het aan de lezer over om voor zijn specifeke DBMS de mapping compleet te maken (zie dan de documentatie van de DBMS voor de benodigde details).
SQL CREATE TABLE
De basis syntax van het SQL CREATE TABLE command is kort gezegd als volgt:
CREATE TABLE <tablename> ( <fieldname> <fieldtype> [size] [[NOT] NULL] )Waarbij je een of meerdere fields kunt aangeven. De size is optioneel (en wordt voornamelijk bij CHAR of VARCHAR velden gebruikt), net als de NULL of NOT NULL (default is NULL). Zoals eerder aangegeven zullen we bij het fieldtype voor een DBMS specifieke waarde moeten kiezen, waar de rest DBMS onafhankelijk is. Als we het fieldtype nog even buiten beschouwing laten, dan kan het DBMS onafhankelijke deel van de SQL CREATE TABLE command met een functie Table2SQL als volgt geproduceerd worden:
function Table2SQL(const Table: TTable): String; var i: Integer; Str: String; begin Table.Open; Table.FieldDefs.Update; Table.IndexDefs.Update; try Str := 'CREATE TABLE ' + ChangeFileExt(Table.TableName, '') + '('; for i:=0 to Table.Fields.Count-1 do begin Str := Str + '"' + Table.Fields[i].DisplayName + '" '; // fieldtype if Table.FieldDefs[i].Required then Str := Str + ' NOT'; Str := Str + ' NULL'; if (i < Table.Fields.Count-1) then Str := Str + ', '; end; Str := Str + ')'; Result := Str; finally Table.Close; end end;Merk op dat de FieldDefs.Update en IndexDefs.Update meestal niet meer nodig zijn als de Table reeds geopend is (en zonder dat de Table geopend is levert FieldDefs.Update toch niet de gewenste informatie op). Voor iedere DBMS die we willen ondersteunen bij de migratie, moeten we nu een eigen field mapping schrijven van het BDE / SQL Links type naar het specifieke DBMS type.
Field Mapping
Voor SQL Server en Blackfish SQL heb ik de volgende field mappings gevonden, waarbij ik aan de BDE / SQL Links kant uitga van de waarde van de Table.FieldDefs[i].FieldClass en aan de DBMS kant het corresponderende type heb gekozen:
BDE / SQL Links | SQL Server | Blackfish SQL |
TStringField | [N]VARCHAR(size) | [N]VARCHAR(size) |
TIntegerField | INTEGER | INTEGER |
TSmallIntField | SMALLINT | SMALLINT |
TFloatField | FLOAT | DOUBLE |
TDateTimeField | DATETIME | TIMESTAMP |
TDateField | DATE | DATE |
TTimeField | TIME | TIME |
TCurrencyField | DECIMAL | DECIMAL |
TBooleanField | BIT | BOOLEAN |
TMemoField | [N]TEXT | [N]VARCHAR |
TGraphicField | IMAGE | VARBINARY |
TBlobField | IMAGE | VARBINARY |
if Table.FieldDefs[i].FieldClass = TStringField then Str := Str + ' VARCHAR(' + IntToStr(Table.FieldDefs[i].Size) + ')' else if Table.FieldDefs[i].FieldClass = TSmallIntField then Str := Str + ' SMALLINT ' else if Table.FieldDefs[i].FieldClass = TIntegerField then Str := Str + ' INTEGER ' else if Table.FieldDefs[i].FieldClass = TDateTimeField then Str := Str + ' DATETIME ' // TIMESTAMP ' else if Table.FieldDefs[i].FieldClass = TMemoField then Str := Str + ' TEXT ' // OBJECT ' else if Table.FieldDefs[i].FieldClass = TGraphicField then Str := Str + ' IMAGE ' // OBJECT ' else if Table.FieldDefs[i].FieldClass = TBLobField then Str := Str + ' IMAGE ' // OBJECT ' else if Table.FieldDefs[i].FieldClass = TFloatField then Str := Str + ' FLOAT ' // DOUBLE ' else raise Exception.Create('Unsupported field type ' + Table.FieldDefs[i].FieldClass.ClassName);Let op de raise Exception.Create aan het eind, die ons zal vertellen wanneer er een ander field type is gevonden, zodat we dan daarvoor ook op zoek moeten naar een bijbehorende SQL mapping. Voor de Country tabel uit de DBDEMOS alias, is hiermee de volgende CREATE TABLE geproduceerd:
CREATE TABLE country("Name" VARCHAR(24) NULL, "Capital" VARCHAR(24) NULL, "Continent" VARCHAR(24) NULL, "Area" FLOAT NULL, "Population" FLOAT NULL)We moeten nu alleen nog de data zelf meenemen, wat de laatste stap is in dit migratieverhaal.
Copy Copy
En dan nu de laatste stap: het overpompen van de data uit de BDE / SQL Links tabel naar de SQL DBMS. Dat kan door gebruik te maken van een aantal DBX4 componenten die de BDE records eerst inlezen in een TClientDataSet, en daarna alles naar het SQL DBMS sturen.
Allereerst is daar de dbExpress SQLConnection component die een verbinding met het SQL DBMS moet maken – hiertoe moet je de juiste connection properties opgeven. Dan is er een SQLDataSet die via zijn SQLConnection property aan de SQLConnection component verbonden is, en een SELECT * FROM uitvoert op de betreffende (nog lege) tabel die we eerder met het CREATE TABLE command hebben aangemaakt. Een TDataSetProvider verbonden met de SQLDataSet en een TClientDataSet verbonden met de DataSetProvider maken het plaatje compleet.
Bij over overkopieren hoeven we nu alleen nog maar door de BDE / SQL Links TTable heen te lopen, en per record een Append te doen in de ClientDataSet (met Append komt ieder volgend record achter het vorige, zodat we dezelfde volgorde aanhouden als in de BDE table). Vervolgens kunnen we van ieder veld de waarde kopiëren door de Fields[i].Value te gebruiken. Wat nog iets beter is (ook voor memo velden) is om Assign te gebruiken om de waarde van de BDE velden toe te kennen aan die van de DBX4 velden. Hierbij ga ik wel uit van het feit dat beide tabellen de velden in exact dezelfde volgorde hebben staan (zodat we niet via FieldByName hoeven te zoeken, maar kunnen volstaan met de mapping van veld i uit de BDE tabel naar veld i van de SQL DBMS tabel).
Omdat de SQLDataSet met een SELECT * FROM werd gestart, zal bij de aanroep van ApplyUpdates van de ClientDataSet automatisch een bijbehorende SQL INSERT worden gegenereerd.
procedure TFormSDE.btnCopyClick(Sender: TObject); var i: Integer; begin ClientDataSet1.Close; Table1.Open; SQlDataSet1.CommandText := 'SELECT * FROM ' + ChangeFileEXt(Table1.TableName,''); ClientDataSet1.Active := True; while not Table1.Eof do begin ClientDataSet1.Append; for i:=0 to Table1.Fields.Count-1 do ClientDataSet1.Fields[i].Assign(Table1.Fields[i]); ClientDataSet1.Post; ClientDataSet1.ApplyUpdates(0); Table1.Next; end; Table1.Close; end;In de code van de laatste listing wordt deze ApplyUpdates voor ieder record aangeroepen (direct na de Post), maar dit kun je ook na iedere X records doen om het proces een klein beetje te versnellen. Op het eind van de code laat ik de ClientDataSet op Active staan. Dat doe ik met opzet, zodat een eventueel DBGrid op het conversie form dan meteen de inhoud kan laten zien.
Conclusie
In dit artikel heb ik laten zien hoe we vanuit een BDE / SQL Links een SQL CREATE TABLE command kunnen produceren, en dit kunnen gebruiken om een nieuwe vergelijkbare tabel in een SQL DBMS te maken, gevolgd door het kopiëren van de data uit de BDE tabel naar de corresponderende tabel in het SQL DBMS. Hiermee kan op eenvoudige wijze BDE data naar een SQL DBMS gemigreerd worden.
Wie nog vragen of opmerkingen heeft kan me altijd per e-mail bereiken of een bezoek brengen aan mijn nieuwe trainingsruimte in Helmond Brandevoort.