Momenteel ben ik bezig met een aantal PHP-projecten, zowel hobbymatig als op mijn werk, waarbij gebruik wordt gemaakt van een database met enkele tientallen tabellen. Er staan verschillende gegevens in de database. Gegevens worden gelezen (vaak) en geschreven (minder vaak). Ik ben op zoek naar de meest geschikte manier om dat te doen vanuit de applicaties. Het gaat niet om de manier waarop de database moet worden benaderd (MySQLi vs. PDO bijvoorbeeld), maar de manier waarop de applicatie om gaat met het opvragen en opslaan van de gegevens. Dit kan rechtstreeks met de database geregeld worden, maar ook via een aantal classes die het werk doen.
Ik vind het zeer wenselijk om de communicatie met de database en de SQL-commando's slechts in een beperkte groep classes te hebben. Deze bieden eenvoudige functies aan de applicaties om de gegevens te verkrijgen. Daarnaast geeft het meer structuur en kan ik caching gemakkelijker regelen. Als ik het echter ga realiseren, loop ik tegen allerlei problemen aan en lukt het niet om classes te bedenken die SQL in de applicatie zelf overbodig maken.
Ik ben op zoek naar de beste manier om dit aan te pakken.
Waar ik tegenaanloop wil ik beschrijven aan de hand van een vereenvoudigd voorbeeld.
In de database staan gegevens over plaatsen. Andere gegevens kunnen daar naar toe verwijzen. Iedere plaats heeft een uniek ID. De informatie van een plaats is in drie tabellen te vinden:
Je kunt een class schrijven die met de database communiceert en gegevens van een plaats leest of wegschrijft:
Voor elke plaats waarvan je gegevens gebruikt, maak je een object. Dit lijkt in eerste instantie erg aantrekkelijk, zeker in een beheer-gedeelte. Maar voor het overgrote deel van applicatie is dit overkill. Meestal wil je een naam hebben van één bepaalde plaats in de door de applicatie gebruikte taal.
Ik kan er een functie aan toevoegen, plaatsnaam($plaats_id), die de naam teruggeeft. Dit kan echter niet in bovenstaande class, want een object correspondeert met één bepaalde plaats. Eventueel kun je er een static functie van maken. Ik ben daar echter geen fan van.
Dus nog maar een class:
Dit is een aantrekkelijke oplossing. Je maakt alleen objecten als je de functies nodig hebt. Voor elke soort data (useraccounts, producten, transacties, etc...) een class.
Helaas, opnieuw problemen:
1. De class cPlaats wordt eigenlijk alleen maar gebruikt door het admin-gedeelte. Alleen daar wordt met alle mogelijke gegevens van een plaats gewerkt. De verleiding is groot om vanuit het admin-gedeelte gewoon rechtstreeks de database aan te roepen, maar dan is de SQL-code weer niet op één plek.
2. Beide classes bevatten overlappende functionaliteit. Voorbeeld: Als een plaatsnaam niet beschikbaar is in de taal van de applicatie, dan wordt de plaatsnaam genomen in de plaatselijke taal. Is die niet beschikbaar, dan Engels. Is die niet beschikbaar, dan wordt de eerste naam gepakt die in de database staat voor de plaats. De functie plaatsnaam in cPlaatsfuncties doet dat. Maar ook de functie naam in cPlaats. Ik kan natuurlijk vanuit plaats de functie plaatsnaam aanroepen in cPlaatsfuncties, maar dan worden opnieuw gegevens gelezen uit de database en dat is zonde.
3. Ik kan beide classes fuseren. lees_plaats en schrijf_plaats werken dan met arrays waar alle gegevens van één plaats in staan. Nadeel: lees_plaats en schrijf_plaats worden alleen door het admin-gedeelte gebruikt, maar maken de veel gebruikte class wel groter.
4. Er zijn enorm veel mogelijke acties op plaatsgegevens. Dit geeft vele functies. Bijvoorbeeld het zoeken van een plaats. Soms is een rijtje met namen+id's voldoende. Soms wil je in het rijtje namen alle beschikbare namen (verschillende talen), soms alleen de naam één bepaalde taal. Soms wil je alleen maar een rij id's hebben. Dit levert veel functies op, of functies met veel argumenten.
Splitsen lijkt een oplossing: een zoek-functie geeft alleen id's terug, een andere functie kan de namen er in verschillende varianten bijleveren.
Nadeel: Er worden vele afzonderlijke queries uitgevoerd voor taken, die ook in één query kunnen.
"SELECT plaats, naam FROM plaatsnaam WHERE naam LIKE ? AND taal=1" is sneller, dan "SELECT plaats FROM plaatsnaam WHERE naam LIKE ? AND taal=1", en vervolgens x maal "SELECT naam FROM plaatsnaam WHERE plaats=? AND taal=1"
Een heel verhaal, misschien wat van de hak op de tak, maar ik hoop dat de vraag waar het om gaat duidelijk genoeg is.
Ik vind het zeer wenselijk om de communicatie met de database en de SQL-commando's slechts in een beperkte groep classes te hebben. Deze bieden eenvoudige functies aan de applicaties om de gegevens te verkrijgen. Daarnaast geeft het meer structuur en kan ik caching gemakkelijker regelen. Als ik het echter ga realiseren, loop ik tegen allerlei problemen aan en lukt het niet om classes te bedenken die SQL in de applicatie zelf overbodig maken.
Ik ben op zoek naar de beste manier om dit aan te pakken.
Waar ik tegenaanloop wil ik beschrijven aan de hand van een vereenvoudigd voorbeeld.
In de database staan gegevens over plaatsen. Andere gegevens kunnen daar naar toe verwijzen. Iedere plaats heeft een uniek ID. De informatie van een plaats is in drie tabellen te vinden:
SQL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| CREATE TABLE plaats ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, aantal_inwoners INTEGER NOT NULL, PRIMARY KEY (tekstnr, taal_id), UNIQUE KEY (tekstnr, taal_id) ); CREATE TABLE plaatsnaam ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, plaats INTEGER UNSIGNED NOT NULL REFERENCES plaats(id), taal INTEGER UNSIGNED NOT NULL REFERENCES taal(id), naam VARCHAR(80) NOT NULL, PRIMARY KEY (id), UNIQUE KEY (id), UNIQUE KEY (plaats, taal) ); CREATE TABLE beschrijving ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, plaats INTEGER UNSIGNED NOT NULL REFERENCES plaats(id), taal INTEGER UNSIGNED NOT NULL REFERENCES taal(id), tekst VARCHAR(255) NOT NULL, PRIMARY KEY (id), UNIQUE KEY (id), UNIQUE KEY (plaats, taal) ); |
Je kunt een class schrijven die met de database communiceert en gegevens van een plaats leest of wegschrijft:
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| class cPlaats { public $id; public $aantal_inwoners; public $namen; public $beschrijvingen function __construct($id) { $lees_plaats($id); } public function lees_plaats($id) { $this->id=$id; // uit database lezen // $namen[$taal]=$naam_in_bepaalde_taal // $namen[$beschrijving]=$beschrijving_in_bepaalde_taal } public function schrijf_plaats($id) { } public function naam() { // return naam van plaats in door applicatie gebruikte taal } } |
Voor elke plaats waarvan je gegevens gebruikt, maak je een object. Dit lijkt in eerste instantie erg aantrekkelijk, zeker in een beheer-gedeelte. Maar voor het overgrote deel van applicatie is dit overkill. Meestal wil je een naam hebben van één bepaalde plaats in de door de applicatie gebruikte taal.
Ik kan er een functie aan toevoegen, plaatsnaam($plaats_id), die de naam teruggeeft. Dit kan echter niet in bovenstaande class, want een object correspondeert met één bepaalde plaats. Eventueel kun je er een static functie van maken. Ik ben daar echter geen fan van.
Dus nog maar een class:
PHP:
1
2
3
4
5
6
7
8
9
10
11
| class cPlaatsfuncties { public function plaatsnaam($plaats_id) { // return naam van plaats in door applicatie gebruikte taal } public function infotekst($plaats_id, $taal) { // return infotekst in taal $taal } } |
Dit is een aantrekkelijke oplossing. Je maakt alleen objecten als je de functies nodig hebt. Voor elke soort data (useraccounts, producten, transacties, etc...) een class.
Helaas, opnieuw problemen:
1. De class cPlaats wordt eigenlijk alleen maar gebruikt door het admin-gedeelte. Alleen daar wordt met alle mogelijke gegevens van een plaats gewerkt. De verleiding is groot om vanuit het admin-gedeelte gewoon rechtstreeks de database aan te roepen, maar dan is de SQL-code weer niet op één plek.
2. Beide classes bevatten overlappende functionaliteit. Voorbeeld: Als een plaatsnaam niet beschikbaar is in de taal van de applicatie, dan wordt de plaatsnaam genomen in de plaatselijke taal. Is die niet beschikbaar, dan Engels. Is die niet beschikbaar, dan wordt de eerste naam gepakt die in de database staat voor de plaats. De functie plaatsnaam in cPlaatsfuncties doet dat. Maar ook de functie naam in cPlaats. Ik kan natuurlijk vanuit plaats de functie plaatsnaam aanroepen in cPlaatsfuncties, maar dan worden opnieuw gegevens gelezen uit de database en dat is zonde.
3. Ik kan beide classes fuseren. lees_plaats en schrijf_plaats werken dan met arrays waar alle gegevens van één plaats in staan. Nadeel: lees_plaats en schrijf_plaats worden alleen door het admin-gedeelte gebruikt, maar maken de veel gebruikte class wel groter.
4. Er zijn enorm veel mogelijke acties op plaatsgegevens. Dit geeft vele functies. Bijvoorbeeld het zoeken van een plaats. Soms is een rijtje met namen+id's voldoende. Soms wil je in het rijtje namen alle beschikbare namen (verschillende talen), soms alleen de naam één bepaalde taal. Soms wil je alleen maar een rij id's hebben. Dit levert veel functies op, of functies met veel argumenten.
Splitsen lijkt een oplossing: een zoek-functie geeft alleen id's terug, een andere functie kan de namen er in verschillende varianten bijleveren.
Nadeel: Er worden vele afzonderlijke queries uitgevoerd voor taken, die ook in één query kunnen.
"SELECT plaats, naam FROM plaatsnaam WHERE naam LIKE ? AND taal=1" is sneller, dan "SELECT plaats FROM plaatsnaam WHERE naam LIKE ? AND taal=1", en vervolgens x maal "SELECT naam FROM plaatsnaam WHERE plaats=? AND taal=1"
Een heel verhaal, misschien wat van de hak op de tak, maar ik hoop dat de vraag waar het om gaat duidelijk genoeg is.