Gebruik van verschillende Datasets

(DEV)
Programma’s waarbij je een database gebruikt, worden dikwijls ontwikkeld op een andere databank dan diegene die uiteindelijk zal gebruikt worden.
Stel dat de ontwikkeling gebeurt op server (DEV), met databank employee. Die server kan evengoed een virtuele machine, je eigen desktop of laptop zijn.

Structuur (DEV) –> (PRODUCTION)
Op een bepaald moment wordt het programma in gebruik genomen, en maak je dus een definitieve databank aan op de server (PRODUCTION), die werkelijk gebruikt wordt. Waarschijnlijk houd je je DEV voor verdere ontwikkeling, met een kleine test-database met relevante gegevens voor wat je aan het ontwikkelen bent, terwijl die van de (PRODUCTION) server met rasse schreden in volume toeneemt.

Data (PRODUCTION) –> (TEST)
Die verdere ontwikkeling testen met echte productie data wil je misschien laten doen door iemand anders, terwijl jij voortontwikkelt. Dan is het handig om een (TEST) systeem te hebben. De data kan je uit de productieserver halen, zodat je op grote aantallen data kan testen, wat realistischer is.

(DEV) ? (TEST) ? (PRODUCTION)
Als je nu vanop je ontwikkelomgeving programma’s wil gebruiken met data van TEST (vers geupdate van PRODUCTION), moet je de gegevens van je verbinding met de databank in het programma aanpassen. Waarschijnlijk in een settings- of configuratiebestand, wat het iets gemakkelijker maakt. Je kan dat settingsbestand dan in versies DEV en TEST maken, en met een shell script het te gebruiken configuratiebestand kopiëren over het standaard configuratiebestand. Of kan het vanuit je software?

Dataset
Concept: je kan in je programma voorzien om verschillende datasets te gebruiken. Gevolg is dat je in een menu kan omschakelen naar “dev”, “test”, of “production”, en dat de juiste connectiegegevens geladen worden.

Ik gebruik een aparte module voor de data (1)
MData

Ik nummer mijn datasets, zodat ik ze gemakkelijk kan aflopen maar ook in de Settings kan opslaan: 0, 1, 2…
Ik houd in een variabele in de data module bij welke dataset momenteel in gebruik is:
PRIVATE iCurrentDataSet AS Integer

Ik geef de dataset een standaard naam in de module die de data gebruikt:
PRIVATE CONST sDataSetName = "dbdata"

Voor de eigen data heb ik dus db configuraties:
dbdata0, dbdata1, dbdata2 (0-dev, 1-test, 2-production).

In de Settings file komt bv:
[dbdata0]
servertype="mysql"
datasetname="dbdatadev"
database="maindb"
host="localhost"
login="myapp"
password="ldfjq5sehrqz"
...

[dbdata1]
servertype="mysql"
datasetname="dbdatatest"
database="maindb"
host="test.copyleft.loc"
login="myapp"
password="ldfjq5sehrqz"
...

Om de instelling van de juiste dataset “sDataset” op te halen, te bewaren moet je hem natuurlijk kunnen benoemen:

Settings["dbdata1/host"]

Of via universelere code
sDataset & "/host"
waarbij
sDataset = getDataSetString(iCurrentDataSet)

Die is samengesteld van de naam plus het nummer, een procedure uit de betreffende module levert die:

PUBLIC SUB getDataSetString(i as Integer) AS String
   RETURN sDataSetName & Str$(iCurrentDataSet)
END

Hiermee kan je de voorkeuzelijst vullen.


NB:

Als je meer data modules gebruikt:

Een aparte module voor de data, import, export, enz, met telkens hetzelfde systeem.
MData
MImport
MExport

krijgt in elke module de dataset een *andere* naam:

dbdata = voor de eigen data van het programma
dbimport = voor data die geïmporteerd wordt uit een andere databank
..

Voor de import data heb ik dan datasets:
dbimport0, dbimport1, dbimport2

In de settingsfile zijn die dan terug te vinden:

[dbimport0]
servertype="mysql"
datasetname="dbimportdev"
database="maindb"
host="localhost"
login="readimport"
password="ldfjq5sehrqz"
...

[dbimport1]
servertype="mysql"
datasetname="dbimporttest"
database="maindb"
host="test.copyleft.loc"
login="readimport"
password="ldfjq5sehrqz"
...

Een aanzet tot ietwat universele code:

PUBLIC CONST sDataSet AS String = "dbimport" ' <---- Change this
' to change for each module - base for name in settings file

PRIVATE iCurrentDataSet AS Integer = 0 ' no of dataset, 0 is default, alternatives 1, 2, .. defined in settings

PRIVATE sCurDataSetName AS String ' 

PUBLIC $hconData AS NEW Connection



PUBLIC SUB getLastError() AS String
  
  RETURN sLastError
  
END

PUBLIC SUB resetError()
  
  sLastError = ""
  
END


PUBLIC SUB curDataSetString() AS String
  
  RETURN getDataSetString(iCurrentDataSet)
  
END

PRIVATE SUB getDataSetString(i AS Integer) AS String
  
  RETURN sDataSet & Str$(iCurrentDataSet)
  
END

PUBLIC SUB getDataSetName() AS String
  
  RETURN sCurDataSetName
  
END


PUBLIC SUB loadDataSet(iDataSet AS Integer) AS Boolean
  
  DIM sData AS String
  
  iCurrentDataSet = iDataSet
  sData = getDataSetString(iCurrentDataSet)
  
  DEBUG Settings.Path &/ Application.Name & ".conf"
  IF (Exist(Settings.Path &/ Application.Name & ".conf"))
    sCurDataSetName = Settings[sData & "/datasetname"]
    WITH $hconData
      .Host = Settings[sData & "/host"]
      .Name = Settings[sData & "/database"]
      .Login = Settings[sData & "/login"]
      .Password = Settings[sData & "/password"]
      .Type = Settings[sData & "/servertype"]
    END WITH 
    RETURN TRUE
  ELSE 
    iCurrentDataSet = -1
    sLastError = "Could not find data connection settings " & Error.Text
    RETURN FALSE
  ENDIF   
  
END


PUBLIC SUB goConnect() AS Boolean
  
  TRY $hconData.Close
  TRY $hconData.Open
  IF ERROR 
    sLastError = Error.Text
    RETURN FALSE
  ELSE 
    RETURN TRUE
  ENDIF 
  
END

Foutafhandeling (TRY – IF ERROR)

ps: Lijst van foutmeldingen gambas2 zijn hier nog terug te vinden:
http://files.allbasic.info/Gambas/help/help/error.html

Gambas heeft de TRY – IF ERROR combinatie.
Als de kans groot is dat zich een fout voordoet kan je de lijn beginnen met TRY. Als zich dan een fout voordoet crasht de applicatie niet, maar wordt akte genomen van de fout en loopt de applicatie door. Om waarschijnlijk verder toch in de problemen te komen?
Daarom is er de tweede stap: IF ERROR …
Deze instructie zet je na de lijn die begint met TRY.
Als er een fout is opgetreden, wordt de IF ERROR lijn uitgevoerd.

Dat is handig want je kan hier twee zaken mee doen:

  1. De foutmelding gaan ophalen, en er iets zinnigs mee doen, als loggen/opslaan, tonen, …
  2. De loop van het programma aanpassen aan het zich voordoen van de fout; kans geven om iets opnieuw te proberen, toestand van buttons / menu’s wijzigen zodat geen verdere akties genomen kunnen worden die de applicatie zouden doen crashen, een procedure starten om de fout effectief op te lossen, …

1. De foutmelding

De foutmelding zit in dat geval in Error.Text; je kan ze opvragen:

IF ERROR
  DEBUG Error.Text
ELSE
   ...
ENDIF

Terwijl je bezig bent met programmeren is het nuttig en nodig om de foutmeldingen te zien te krijgen. Als het programma crasht zie je ze. Soms wil je echter voortwerken aan een ander stuk van het programma, waarvoor deze fout niet belangrijk is. Je kan die dan “wegmoffelen” door voor de programmalijn die de fout levert, TRY te zetten. Het programma loopt na de fout door, maar: je krijgt de foutmelding niet meer te zien! Je ziet dus bv niet of ze verandert, zich niet meer zou voordoen, enz. Je vergeet ze misschien en dat levert laten een “bug” op.

Met deze DEBUG zie je ze tenminsten nog onderaan op je terminal-achtige venster in Gambas. Maar misschien is het beter ineens een procedure te maken om die fout ook in het lopende programma bij de gebruiker te gebruiken, want na compilatie zijn de DEBUG’s weg.

Eigenlijk is het minimum dat je ze opslaat in een tekstvariabele:

IF ERROR
  sLastError = Error.Text
...

Je kan dat lokaal doen, bv in elke module, Class, enz, lokaal een variabele maken om de fout op te slaan, en een procedure om ze op te vragen:

PRIVATE sLastError AS String
 
PUBLIC SUB getLastError() AS String
  RETURN sLastError
END


IF ERROR
  sLastError = Error.Text
...

De aanroepende code moet dan nog wel te weten komen dat er een fout was. Dat wordt meestal gedaan door
– een extreem andere waarde terug te geven als een waarde verwacht wordt (return value), bv 0 of -1.
– als geen return value nodig is, geef dan toch een TRUE als alles ok is, en FALSE terug als zich een fout voordeed.

In de aanroepende code:

IF module1.DoSomething(..)
  ...
ELSE
  Message.Error(module1.getLastError())
ENDIF

of, bv als je -1 als teken van fout gebruikt:

IF module1.CalcSomething(..) < 0
  Message.Error(module1.getLastError())
ELSE
  ...
ENDIF

Enkele ideeën:

  • Maak een procedure om de fouten op te vangen in je hoofdprogramma; bv doError(s AS String), die je de foutmelding laat weergeven in een speciaal foutmeldingsvenster in de app, en/of laat opslaan in een logbestand
  • Maak een class om de foutafhandeling te doen. Dan moet wel extra informatie door gegeven worden over waar de fout vandaan kwam.

Database velden met prefix van tabelnaam

Naar aanleiding van een vroeger artikel over hergebruik van code, kwam ik op de kwestie van de standaardisering van de veldnamen in een databank. Algemeen zie ik twee systemen gebruikt worden:

1. Met prefix:

Meestal een korte prefix die de naam van de tabel aangeeft, zodat je altijd kan zien waar de veldnaam op slaat:

# Name Type Null Default Extra
1 emp_id bigint(20) No None AUTO_INCREMENT
2 emp_name int(11) No 0
3 emp_data2 int(11) No 0
8 emp_creat timestamp Yes CURRENT_TIMESTAMP
9 emp_creby char(24) Yes NULL
10 emp_updat datetime Yes NULL
11 emp_updby char(24) Yes NULL

Inderdaad de employee tabel, “emp” of beter “employee”.

2. Zonder prefix

Zonder tabelnaam (afkorting) in veldnamen:

# Name Type Null Default Extra
1 id bigint(20) No None AUTO_INCREMENT
2 name int(11) No 0
3 data2 int(11) No 0
8 creat timestamp Yes CURRENT_TIMESTAMP
9 creby char(24) Yes NULL
10 updat datetime Yes NULL
11 updby char(24) Yes NULL

In dit geval moet je de (afkorting voor) de veldnaam niet meegeven. Als de database vermeld wordt is het toch duidelijk:

emp.id
employee.name
employee.fieldname1
employee.creat
employee.creby
employee.updat
employee.updby

eventueel met korte alias

emp.id
emp.name
emp.fieldname1
emp.creat
emp.creby
emp.updat
emp.updby

Veel code blijft korter, maar een luie blik op de resultaten van een query kan iets meer aandacht gaan vragen (om de oorspronkelijke tabelnaam te zoeken), en misschien worden je aliassen langer e.veld wordt emp.veld voor table employee.

Vgl:

e.emp_id, e.emp_name
emp.id, emp.name

De prefix blijkt vooral een overblijfsel te zijn van vroeger, toen alle veldnamen uniek moesten zijn (ook buiten de tabel).

De algemene raadgeving is: geen prefix gebruiken:

  • korter
  • eenduidiger
  • goede tabelnamen en aliassen te gebruiken
  • betere standaardisatie

Pro prefix:

  • oude systemen waar het moet
  • geen probleem met gereserveerde woorden (commanodo’s in de query taal)*
  • gemakkelijk bij export want tabelnaam staat in veld.
  • soms beter leesbaar omdat velden altijd dezelfde vorm hebben.

* stel dat je een veld hebt waarin je aangeeft dat er een update gebeurd is of moet gebeuren:
employee.update
Helaas, UPDATE is een gereserveerd woord in de SQL taal (SELECT update FROM …), en zo zijn er nog woorden die door de syntax highlighting kunnen aangewezen worden als “gereserveerd”. Door prefixen te gebruiken moet je daar nooit op letten. Maar je kan ook ‘quotes’ gebruiken rond het gereserveerd woord als veldnaam.

Zie ook discussie op StockOverflow: (1) en (2)