039 —Architecture
Database als source of truth: refactor de data, niet de code
Je erft een 14 jaar oude WordPress-site. Je pullt de repo, grept naar het kapotte gedrag, vindt niets. De logica zit in een rij in wp_options.
Je erft een 14 jaar oude WordPress-site van een ontwikkelaar die twee opdrachten geleden vertrok. Je pullt de repo, zet de lokale stack op, en grept naar het kapotte hero-gedrag. Niets. Twee uur later, na het dumpen van een rij uit wp_options, vind je een geserialiseerde blob van 4 KB met Elementor-instellingen die het template overschrijft waar je de hele middag in zat te werken. Op deze site, zoals bij het meeste verouderde PHP-werk, is de database de source of truth en is de code slechts een viewer.
Dit is het deel van het werk aan een legacy site dat niet in onboardingdocumenten staat. Een aanzienlijk deel van het gedrag in WordPress, Drupal, Joomla, Magento en de meeste custom CMS-projecten zit in MySQL-rijen, niet in versie-gecontroleerde bestanden. Refactor je de repo zonder eerst de database te lezen, dan refactor je het verkeerde artefact.
Waar de logica echt zit
Een korte rondleiding langs de gebruikelijke verdachten:
- WordPress.
wp_optionsbevat de site-configuratie, maar ook de volledige geserialiseerde state van elke plugin die ooitautoload = 'yes'heeft gezet.wp_postmetabevat ACF-velden, Yoast-metadata, en (het pijnlijkst) volledige page-builder layouts als één enorme string per post. - Drupal 7. De
variable-tabel is een key-value dump van de halve site. Views, blocks en field-configuraties worden tijdens runtime gelezen uitcache_*-tabellen. - Drupal 8+. De
config-tabel houdt de actieve configuratie vast als blobs. Code in bestanden is alleen de importbron. - Magento 1 en 2.
core_config_datastuurt betaalmethoden, verzending, btw, valuta en de meeste module-toggles aan. EAV-tabellen sturen de productstructuur aan. De PHP-module is een renderer van deze rijen. - Custom PHP. Een
settings-tabel. Eenfeatures-tabel. Eentemplates-tabel waar iemand in 2014 HTML-rijen heeft gezet en het is vergeten.
Behandel je de repo als source of truth, dan ben je een week kwijt om uit te zoeken waarom een hook twee keer afgaat, terwijl het antwoord een rij is in een settings-tabel die je nog niet hebt geopend.
De read-only sweep
Voordat je ook maar één regel PHP aanraakt, doe je een ronde data-archeologie. Het doel is te begrijpen wat de database de code op dit moment opdraagt. Bij WordPress is de eerste query die ik op een nieuwe site draai de autoload-audit:
SELECT option_name, LENGTH(option_value) AS bytes
FROM wp_options
WHERE autoload = 'yes'
ORDER BY bytes DESC
LIMIT 20;
Elke WordPress-request laadt alle autoloaded options in het geheugen. Een rij van 2 MB van een plugin die drie jaar geleden is gedeactiveerd, wordt nog steeds bij elke pageview ge-unserialised. Ik heb in de praktijk losse rijen van meer dan 8 MB gezien. De wp_load_alloptions reference is het opnieuw doorlezen waard voor je aanneemt dat de cache-laag het knelpunt is.
Bij Magento is het equivalent:
SELECT scope, scope_id, path, value
FROM core_config_data
WHERE path LIKE 'payment/%'
OR path LIKE 'carriers/%';
Meestal vind je drie betaalmethoden die geactiveerd staan, terwijl de front-end ze sinds 2019 niet meer toont, plus een verzendtarief dat hardcoded staat voor een magazijn dat dicht is.
Bij Drupal 8+ dump je de actieve config en diff je die tegen wat er in config/sync in de repo staat:
drush config:export --destination=/tmp/config-snapshot
diff -ruN config/sync /tmp/config-snapshot
De delta is het gat tussen wat de code denkt dat de site is en wat de site werkelijk is. Dat gat is de omvang van je echte refactor.
Refactoren met de database in beeld
Zodra je accepteert dat de database de source of truth is, verandert de volgorde van handelingen bij een destructieve refactor. Een verouderde Magento-betaalmodule verwijderen is dan niet meer 'bestanden weg, deployen, hopen'. Het is:
- Snapshot. Een
mysqldump --single-transaction --quickvan productie, opgeslagen op een plek die je vertrouwt. - Map de rijen. Welke rijen in welke tabellen sturen daadwerkelijk aan wat je wilt wijzigen.
- Muteer de data met dezelfde zorg waarmee je de code zou muteren. Schrijf de UPDATE, review de WHERE-clause, draai hem binnen een transactie, controleer het aantal geraakte rijen voor je commit.
- En dan, pas dan, raak je PHP aan.
Voor het Magento-geval betekent dat een rij-update vóór het verwijderen van bestanden:
START TRANSACTION;
UPDATE core_config_data
SET value = '0'
WHERE path = 'payment/checkmo/active'
AND scope = 'default';
-- inspect: should be exactly 1 row affected
ROLLBACK; -- or COMMIT once you are sure
Sla je de datastap over, dan deploy je een release waar het admin panel nog steeds een betaalmethode aanbiedt waarvan de bijbehorende class niet meer bestaat. De klant gaat naar de checkout. De autoloader fataalt. De bestelling komt niet binnen. Je merkt dit om 23:41 op een vrijdag.
SQL-writes behandelen als code-commits
De moeilijkste omslag is SQL-writes hetzelfde reviewgewicht geven als PHP-commits. Een pull request die functions.php aanpast, gaat door review. Een WP-CLI-commando dat UPDATE wp_options SET option_value = ... op productie draait, meestal niet. Beide veranderen het gedrag van de site. Maar één van de twee laat een spoor achter.
De oplossing is operationeel, niet technisch. Schrijf de SQL in een migratiebestand, ook al rol je het met de hand. Commit het. Tag het. Laat het door dezelfde pipeline lopen als de code-wijziging. Heeft je CMS geen migratiesysteem dat je vertrouwt (Drupal wel, Magento min of meer, WordPress niet), houd dan een map met gedateerde .sql-bestanden bij en een kleine runner die bijhoudt wat waar gedraaid is. Lomp is altijd beter dan ongetraceerd.
Dezelfde logica geldt voor fixtures. Een staging-database die zes maanden achterloopt op productie, is geen staging-database, dat is fictie. Snapshot productie wekelijks, wis de persoonsgegevens, restore in staging. Anders wordt je refactor gevalideerd tegen een site die niet meer bestaat.
Minder tijd in de viewer
De praktische consequentie hiervan is dat een echte refactor van een verouderde site meer uren in MySQL kost dan in PHP. Dat voelt de eerste keer verkeerd, omdat de bestands-editor is waar het spiergeheugen zit. Het voelt niet meer verkeerd zodra de derde fatale fout op vrijdagavond is veroorzaakt door een rij die niemand had gereviewed.
Toen we Pier bouwden liepen we hier op elke klantsite tegenaan, en uiteindelijk hebben we de MySQL editor naast de bestands-editor gezet met dezelfde version history op beide. Een rij-edit en een template-edit verschijnen nu op dezelfde tijdlijn, wat de ergste 'waar komt dit vandaan'-vraag uit het werk haalt.
Het kleinste wat je vandaag kunt doen: open je productiedatabase, draai de autoload-size query van hierboven, en kijk naar de eerste drie rijen. Die vertellen je meer over wat je site aan het doen is dan de laatste drie commits deden.
— Vragen —
Moet ik wp_options direct bewerken of altijd via de WordPress-API gaan?
Voor eenmalige correctieve wijzigingen is directe SQL binnen een transactie prima. Voor alles wat je mogelijk wilt terugdraaien of herhalen, schrijf je het als migratiebestand zodat het een spoor achterlaat.
We gebruiken al Drupal config sync. Moet ik dan nog steeds de database lezen?
Ja. De sync-map is de gewenste state. De actieve config-tabel is de echte state. Ze lopen uit elkaar, vooral over staging-grenzen heen en na handmatige edits in de admin-UI.
Hoe diff ik twee database-states zonder te verzuipen in ruis?
Exporteer de relevante rijen als geordende SQL of als platte key-value dumps, en diff vervolgens die tekstoutputs. Schema-diffs zijn ruisig; rij-niveau diffs zijn wat je wilt voor config-tabellen.