InterBase van Delphi naar Kylix |
Cross-Platform
De class library van Kylix heet CLX, wat staat voor Component Library for X-platform.
Deze CLX zal ook in Delphi 6 zitten (die daarnaast ook de VCL zal blijven ondersteunen).
Echter, Delphi 6 is (op het moment van schrijven) nog niet uit.
En al is Delphi 6 uitgekomen, dan nog heb ik een grote vezamling bestaande Delphi toepassingen die gebruik maken van de VCL.
En de grote vraag is hoe makkelijk (of moeilijk) die om te zetten zijn naar Kylix.
Voor de meeste zaken geldt dat hercompileren van de source code in Kylix voldoende is.
Er zijn projecten die een paar minuten werk kosten.
Er zijn er ook die wat meer tijd vergen, met name de database toepassingen.
Geen van de data access mogelijkheden van Delphi 5 zijn namelijk beschikbaar onder Kylix.
Geen BDE, geen SQL Links, geen ADO Express en geen InterBase Express.
Maar wel dbExpress, de nieuwe cross-platform data access laag.
Om voort te bouwen op het dbExpress artikel van Cary Jensen van de vorige keer, zal ik in dit artikel met name aandacht besteden aan een probleem dat veel mensen zullen hebben die van Delphi 5 naar Kylix (of straks Delphi 6) gaan, namelijk de omschakeling naar dbExpress.
Van BDE naar dbExpress
De dbExpress tab van het component palette van Kylix bestaat uit een aantal nieuwe componenten zoals TSQLConnection, TSQLDataSet, TSQLClientDataSet en TSQLMonitor.
Daarnaast zijn er nog drie componenten die wel nieuw, maar toch niet echt noodzakelijk zijn: TSQLTable, TSQLQuery en TSQLStoredProc.
Deze laatste drie componenten zijn in mijn ogen slechts toegevoegd om de migratie van BDE (en BDE-achtige) toepassingen naar dbExpress te vergemakkelijken.
De TSQLDataSet kan in zijn eentje namelijk alles wat de TSQLTable, TSQLQuery en TSQLStoredProc samen kunnen.
Maar laat ik niet vooruit lopen op de zaken.
Ik zal nu eerst in Delphi 5 een kleine InterBase toepassing bouwen, gebruikmakend van de standaard BDE data access componenten die we allemaal gewend zijn.
Daarna zal ik laten zien hoe je dit project naar Linux kunt transporteren, en vervolgens welke veranderingen je in Kylix allemaal moet aanbrengen om het weer te laten werken.
Delphi 5
In Delphi gaan we gebruik maken van een TDatabase component, een TTable en een TQuery component.
Alledrie normale BDE componenten, die dus onder Kylix niet beschikbaar zijn.
Zet de TDatabase op een Form (of datamodule als je netjes wilt werken), en kies IBLocal als AliasName.
Deze zorgt ervoor dat we met de (local) InterBase Employee database praten die in c:\program files\common files\borland shared\data\employee.gdb staat.
Geef nu ook de DatabaseName property een waarde, zoals IBL (zodat de TTable en TQuery via deze gemeenschappelijke TDatabase component bij de InterBase database kunnen).
Om de verbinding te testen kun je de Connected property op True zetten.
In de Database Login dialog moet je voor user SYSDBA als password "masterkey" invullen.
Zet nu zowel een TTable als een TQuery naast de TDatabase component.
Geef van zowel de TTable als de TQuery als de DatabaseName property de waarde IBL.
Selecteer vervolgens voor de TTable als waarde voor de TableName property de EMPLOYEE.
Gebruik een TDataSource en TDBGrid component on de inhoud van deze EMPLOYEE tabel te vertonen.
Nu gaan we verder met de Query, en ik wil van alle medewerkers zien aan welke projecten ze meewerken.
Daarvoor heb ik zowel de PROJECT tabel als de EMPLOYEE_PROJECT koppeltabel nodig.
De SQL property krijgt de volgende waarde:
SELECT * FROM PROJECT, EMPLOYEE_PROJECT WHERE (EMPLOYEE_PROJECT.EMP_NO = :EMP_NO) AND (EMPLOYEE_PROJECT.PROJ_ID = PROJECT.PROJ_ID)De waarde van de :EMP_NO parameter (van type Smallint) kunnen we ophalen uit de tabel, door de DataSource property van de Query naar de DataSource van de Tabel te laten wijzen. Hierdoor wordt het huidige record van de Table als het ware als input gebruikt voor de parameter van de Query. Het resultaat is een mooie master-detail relatie tussen EMPLOYEE en PROJECT via de EMPLOYEE_PROJECT tabel. Al tijdens design-time kunnen we de data zien:
Tot zover lijkt het een vrij triviale oefening. Echter, onder Linux zal het toch iets anders zijn.
En Kylix?
Het daadwerkelijk migreren van een bestaand Delphi project naar Kylix heeft meer voeten in de aarde dan ik deze keer wil laten zien.
Ik wil nu echter de nadruk leggen op de database en met name de data access componenten.
Om met het makkelijke te beginnen: stel dat we een lokale InterBase database onder Windows hebben, zoals employee.gdb, die naar Linux moet, hoe krijgen we dan al die records van de Windows InterBase database naar de Linux InterBase database?
Officieel moet je eerst een backup maken in tranportable format op je source server (Windows), dan de backup file kopieeren (van Windows naar Linux), en vervolgens een restore te doen op je destination server (Linux).
In praktijk blijkt echter dat voor Intel Linux (waar Kylix ook draait), je een IB6 Windows database gewoon kunt kopieeren van Windows naar Linux!
De employee.gdb van InterBase voor Windows (Delphi 5) in c:\program files\common files\borland shared\data\employee.gdb is ook aanwezig bij InterBase voor Linux (Kylix) maar dan als /usr/interbase/examples/employee.gdb.
In beide gevallen is het password voor SYSDBA gelijk aan "masterkey".
Overigens staat InterBase 5.6 op de CD-ROM van Kylix zelf, maar kunnen we InterBase versie 6 vinden op de Companion Tools CD-ROM, zodat je zelf kunt kiezen welke versie van InterBase je wilt installeren (een InterBase 6 database kun je niet met InterBase 5.6 lezen).
De database benadering met dbExpress gaat echter wel iets anders.
We moeten deze keer beginnen met een TSQLConnection component (te vergelijken met de TDatabase component).
De TSQLConnection zal de verbinding maken met de InterBase database door de ConnectionName property op IBLocal te zetten.
Zoals Cary Jensen vorige keer al opmerkte: als je dit voor de eerste keer doet moet je nog even met de rechter muisknop op de TSQLConnection component klikken om in de Connection Properties dialoog de Database naar de juiste plek te laten wijzen (dus /usr/interbase/examples/employee.gdb in ons geval).
We kunnen de verbinding weer testen door de Connected property van de SQLConnection component op True te zetten (hierbij moeten we weer het gebruikelijke password opgeven voor SYSDBA).
Zet nu een TSQLTable component op het Form, en laat zijn SQLConnection property wijzen naar de SQLConnection property van zojuist.
Als TableName kiezen we weer voor EMPLOYEE.
Deze keer kunnen we echter niet zomaar een TDataSource component en TDBGrid component neerzetten om de data uit de TSQLTable te laten zien.
De reden is dat de TSQLTable een zogenaamde uni-directional cursor teruggeeft.
Eenrichtingsverkeer dus (alleen maar van begin tot eind), en nog read-only ook.
En de TSQLTable component hoeft niet eens open te zijn om al een foutmelding te (laten) genereren zodra we de TDBGrid verbinden met de TDataSource component:
Daar hebben we dus niks aan, of toch wel? Er zijn nu twee mogelijkheden, en ik zal ze allebei laten zien. De eerste mogelijkheid is het gebruiken van ee TDataSetProvider en een TClientDataSet component. De TDataSetProvider verbinden we met de TSQLTable, en de TClientDataSet met de TDataSetProvider. Op die manier worden alle records uit de TSQLTable gehaald (éénmaal, van voor naar achteren) en via de TDataSetProvider in de TClientDataSet gezet, die ze vervolgens in het geheugen houdt en via een TDataSource aan de TDBGrid kan voeren. Een bijkomend effect is dat eventuele wijzigingen in de tabel via de ApplyUpdates methode van de TClientDataSet naar de tabel geschreven kunnen worden (iets dat ook niet mogelijk is met de TSQLTable component).
Het nadeel van deze eerste oplossing is dat we - vergeleken met vroeger - twee extra componenten nodig hebben (voor elke TSQLTable, TSQLQuery, TSQLStoredProc of TSQLDataSet component). Speciaal hiervoor is de TSQLClientDataSet aan dbExpress toegevoegd, die deze functionaliteit al in zich heeft. De tweede oplossing voor de uni-directionele TSQLTable is dan ook het gebruik van de TSQLClientDataSet component. Ook deze is de koppelen aan de TSQLConnection component, al gaat het nu via de DBConnection property en niet via de SQLConnection property (waarom deze property nu anders heet is mij niet geheel duidelijk). Vervolgens kunnen we de CommandType property ot ctTable zetten, en in de CommandText voor EMPLOYEE kiezen. En nu kunnen we via de TDataSource en TDBGrid wederom de inhoud van deze tabel zien tijdens design-time.
Master-Detail
Maar het verhaal gaat verder: we moeten nog query maken.
Als we even verder gaan met de TSQLClientDataSet component, kunnen we een tweede TSQLClientDataSet component proberen te gebruiken voor de query.
Laat ook deze via de DBConnection property wijzen naar de SQLConnection component.
En vul bij CommandText de query weer in die we ook bij Delphi hadden gebruikt:
SELECT * FROM PROJECT, EMPLOYEE_PROJECT WHERE (EMPLOYEE_PROJECT.EMP_NO = :EMP_NO) AND (EMPLOYEE_PROJECT.PROJ_ID = PROJECT.PROJ_ID)Bij de Delphi variant konden we vervolgens de DataSource property naar die van de tabel laten wijzen om de EMP_NO parameter automatisch de juiste waarde uit de tabel te "voeren". Echter, de TSQLClientData component heeft helemaal geen published DataSource property. Er is wel een public DataSource property (die we door wat code te schrijven zouden kunnen gebruiken), maar die is read-only. En als we de MasterSource property een waarde geven krijgen we een vreemde foutmelding bij het gebruik van de MasterKey property (het ziet eruit als een bug):
Met andere woorden: de TSQLClientDataSet is heel fijn om in één klap een TSQLDataSet, TDataSetProvider en TClientDataSet te hebben, maar het verbergt wel een aantal belangrijke interne zaken, zoals de DataSource property van de TSQLDataSet waardoor we dus geen query met parameters kunnen schrijven (in ieder geval niet als we de parameter automatisch willen laten invullen via de DataSource property).
Master-Detail (2)
Hoe moet het dan wel?
De oplettende lezer weet het natuurlijk al: we moeten terug naar onze eerste benadering (zonder TSQLClientDataSet) en hebben weer een TSQLQuery (of TSQLDataSet) nodig, expliciet samen met de TDataSetProvider en TClientDataSet.
De SQLConnection property van TSQLQuery krijgt de waarde SQLConnection1, terwijl de SQL property de query krijgt die we hierboven al tweemaal zagen.
De DataSource property van de TSQLQuery moet wijzen naar DataSource1 (van de eerste tabel).
Het jammere is dat deze benadering niet werkt.
De DataSource property - die ervoor zorgt dat de parameter van de query automatisch wordt ingevuld - is namelijk afkomstig van de ClientDataSet, en het resultaat van de TSQLQuery component wordt eenmaal doorlopen en vervolgens gevoerd aan de tweede ClientDataSet.
Het gevolg is dat we niet het resultaat krijgen wat we willen hebben (de projecten die horen bij de huidige employee uit de eerste ClientDataSet).
Dat is het nadeel van unidirectionele cursors.
Er zit niks anders op dan het SQL statement van de query zodanig aan te passen dat we geen gebruik meer maken van de EMP_NO parameter.
De nieuwe query ziet er dan als volgt uit:
SELECT * FROM EMPLOYEE_PROJECT, PROJECT WHERE (EMPLOYEE_PROJECT.PROJ_ID = PROJECT.PROJ_ID) ORDER BY EMP_NODe "Order By" heb ik er met opzet bijgezet, zodat we in het tussenresultaat mooi kunnen zien dat er verschillende employees zijn die aan meerdere projecten werken. Het resultaat van deze query in TSQLQuery stoppen we via een TDataSetProvider weer in een TClientDataSet. Hierin staan dus alle employees en al hun projecten. Het enige dat we nog moeten doen is om een master-detail relatie te maken tussen de eerste ClientDataSet (met de employees) en de tweede ClientDataSet (met hun projecten). Deze keer kunnen we wel de MasterSource property gebruiken van de tweede ClientDataSet (de foutmelding blijft achterwege omdat we nu geen parameters gebruiken in de query), en als MasterKey moeten we de EMP_NO's van beide ClientDataSets aanelkaar koppelen. Het resultaat is eindelijk wat we willen zien:
Merk op dat we wel ruimschoots meer componenten nodig hebben als vroeger met Delphi 5. Laten we daarom nog een laatste keer kijken of we hetzelfde resultaat toch ook met TSQLClientDataSets voor elkaar kunnen krijgen.
Master-Detail (3)
Laten we teruggaan naar het gebruik van de TSQLClientDataSet.
We zagen dat een tweede TSQLClientDataSet geen SQL query met daarin een parameter kan hebben.
Maar we kunnen wel een query maken zonder parameter, zoals we net voor de TSQLQuery deden.
Deze keer draai ik de volgorde van Project en Employee_Project om, maar het resultaat is hetzelfde:
SELECT * FROM PROJECT, EMPLOYEE_PROJECT WHERE (PROJECT.PROJ_ID = EMPLOYEE_PROJECT.PROJ_ID) ORDER BY EMP_NOOk nu zal de TSQLClientDataSet de volledige lijst van employees met hun projecten bevatten, gesorteerd op EMP_NO. Nu moeten we nog zorgen dat we alleen de projecten van de huidige employee zien, en dat gaat weer door als MasterSource de DataSource van de eerste TSQLClientDataSet te nemen, en als MasterKey de EMP_NO velden te koppelen. Het resultaat is gelijk aan wat we zojuist onder Linux zagen, alleen hebben we er nu weer maar vijf data access componenten voor nodig (in plaats van negen zonet):
Toch is deze dbExpress oplossing niet helemaal gelijk aan de BDE oplossing onder Windows. Door gebruik te maken van een parameter onder Windows, werd de query namelijk iedere keer dynamisch aangepast en uitgevoerd, en zagen we iedere keer alleen maar de projecten van de zojusit geselecteerde employee. Weinig caching dus, en veel rekenen. De benadering met de ClientDataSets on Linux heeft als voordeel dat (zeker voor de tweede ClientDataSet) de volledige verzameling van alle employees met hun projecten al bij voorbaat "uitgerekend" wordt. Vanaf deze ene keer zit de informatie in de ClientDataSet, en bouwen we een master-detail relatie die alleen maar zorgt dat we een subset zien (het geheel blijft echter in het geheugen staan). En daarmee heb ik in feite ook meteen het nadeel van de ClientDataSet benadering aangegeven: als de verzameling van alle employees en hun projecten erg groot is (oftewel: de join is vele malen groter dan de selectie), dan wordt er wellicht teveel gebruik gemaakt van het geheugen van de machine, want de ClientDataSets bewaren hun records in het geheugen. En bij veel meer dan enkele tienduizenden records kun je je voorstellen dat dat op den duur niet meer echt fijn werkt. Een afweging dus tussen (reken)tijd of geheugen. Nadeel is dat je (met Kylix) onder Linux de keuze niet lijkt te hebben, vanwege het probleem met de parameters in SQL. Onder Windows hadden we de tweede benadering ook kunnen kiezen (maar dan had ik niet zo leuk kunnen laten zien dat het porten/migreren niet altijd één-op-één kan). Ik verwacht overigens dat het parameter probleem van dbExpress in Delphi 6 niet meer aanwezig zal zijn, en dat er voor Kylix nog wel een patch of update zal komen om dit op te lossen. Tot die tijd is het in ieder geval fijn om te weten dat er altijd meer wegen naar Rome zijn...
Conclusie
Het porten van data access zaken van Delphi 5 naar dbExpress is niet geheel triviaal.
Toch is het fijn dat Kylix ons nu al de mogelijkheid biedt om ervaring op te doen met dbExpress, omdat het straks ook onder Delphi 6 de aanbevolen manier zal zijn.
Ook al zit de BDE nog in Delphi 6, de voordelen van dbExpress (snelheid, eenvoud in configuratie) wegen ruimshoots om tegen de potentiële nadelen (het moeten migreren van bestaande Delphi projecten), zeker als ik nu vast lekker kan oefenen op Kylix.
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via .