Allen,

Op de bekende zoekmachine kom ik genoeg voorbeelden tegen van prepared statements i.c.m. transactions.
Maar toch werkt bij mij de rollback niet. Een exception op de 2e tabel, terwijl de 1e wel is toegevoegd.

Kan iemand mij uitleggen wat er fout gaat? Dit is de eerste keer dat ik transactions wil gebruiken.

Ingekort voorbeeld:

<?php
try
{
	$db->beginTransaction();

	$sql = "
	INSERT
		INTO " . TABLE_PREFIX . "profile
		(
			naam
		)
		VALUES
		(
			:naam
		)
	";
	
	$stmt = $db->prepare($sql);
	
	$stmt->bindParam(':naam', $_POST['gegevens']['naam'], PDO::PARAM_STR);

	$stmt->execute();
	
	$profile_id = $db->lastInsertId();
	
	$sql = "
	INSERT
		INTO " . TABLE_PREFIX . "application
		(
			profile_id
		)
		VALUES
		(
			:profile_id
		)
	";
	
	$stmt = $db->prepare($sql);
	
	$stmt->bindParam(':profile_id', $profile_id, PDO::PARAM_INT);

	$stmt->execute();
	
	$db->commit();
}
catch(PDOException $e)
{
	if(isset($db))
	{
		$db->rollBack();
	}
}
?>


Bvd
De rollBack() herstelt alles, dus ook de last insert ID. Als je de $db->lastInsertId() van de prepared statement nodig hebt in een tweede query, wordt je eerste voorbeeld zoiets:


<?php
$sql = '
    INSERT
        INTO ' . TABLE_PREFIX . 'profile
        (
            naam
        )
        VALUES
        (
            :naam
        )
';
$stmt = $db->prepare($sql);
$stmt->bindParam(':naam', $_POST['gegevens']['naam'], PDO::PARAM_STR);

// Vanaf hier willen we alle wijzigingen kunnen terugrollen
$db->beginTransaction();

try {
    $stmt->execute();
    $sql = '
        INSERT
            INTO ' . TABLE_PREFIX . 'application
            (
                profile_id
            )
            VALUES
            (
                ' . $db->lastInsertId() . '
            )
    ';
    $db->query($sql);
    $db->commit();
} catch (PDOException $e) {
    $db->rollBack();
}
?>


Wat je je wel kunt afvragen, is of het mislukken van die tweede INSERT zo erg is. Je wilt kennelijk een profiel toevoegen en met dat nieuwe profiel een applicatie toevoegen. Dat kun je ook splitsen in PHP. Met andere woorden: je datamodel bevat een soort één-op-één-afhankelijkheid die misschien niet per se nodig is. Of misschien zelfs een één-op-één-afhankelijk die eigenlijk gewoon in één tabel thuishoort.
>> De rollBack() herstelt alles, dus ook de last insert ID
Wat bedoel je hiermee? De auto_increment wordt wel opgehoogd, dus het is eerder toevoegen->error->verwijderen, dan voorbereiden->error->herstellen.

Waarom de keuze om try pas vanaf regel 19 te doen en niet bovenaan?

Als de tweede of derde, vierde, etc insert mislukt heeft de eerste ook niet zoveel zin. Het gaat namelijk om één formulier dat wordt opgeslagen. Ik verspreid het over meerdere tabellen omdat ik niet weet hoeveel rijen van alles wordt toegevoegd. En het ook mogelijk is dat de zelfde naam nog een 'application' toevoegt.

Is het gebruik van de last insert id wel goed dan? Want ik ga nou twijfelen door jouw reacties. Hoe doe jij dit? En misschien dat het makkelijker is deze in een koppeltabel te zetten, maar dat is denk ik een heel andere vraag en dan zou je het op de zelfde manier moeten doen.
In de catch voer je nu een rollBack() uit. Maar niet elke PDOException hoeft een fout te zijn die moet worden afgehandeld met het terugrollen van alle INSERTs. Het eerste deel, het voorbereiden van de prepared statements, vraagt dus om een andere foutafhandeling. In dit stadium kun je bijvoorbeeld de databaseverbinding verliezen: voor de data heeft dat dan nog geen gevolgen.

Of het hergebruik van de ID verspreid over meerdere tabellen wel helemaal juist is, is zo moeilijk te beoordelen. Dat is een andere vraag; misschien kun je het inderdaad beter met een koppeltabel oplossen. Kun je iets van het datamodel laten zien?

Ik zou een transactie alléén in zijn geheel laten lukken/mislukken als het echt niet anders kan. Maar als de INSERTs eventueel wel gesplitst mogen worden, dan heb je niet per se een transactie nodig. Een INSERT ... ON DUPLICATE KEY ... is soms bijvoorbeeld een betere oplossing.

>> In de catch voer...
Ja dat klopt wel. Daar had ik zelf nog niet zo aan gedacht. Aan de andere kant, als er geen verbinding kan er ook niks geinsert worden.

>> Of het hergebruik
Hier een schets van m'n database
Volgens mij was een koppel inderdaad beter geweest. Dan kan ik ook makkelijker koppelen wanneer er 2 zelfde profielen zijn. Dat kan nu niet. Het is nu alleen om de boel bij elkaar te houden.

>> Ik zou een transactie
True, maar wat heb je aan halve data? Als de eerste 3 tabellen wel worden geinsert, maar de 4e gaat fout, wordt de 5e toch ook niet geinsert? Dus dan is de data incompleet.

Wat doet ON DUPLICATE KEY (UPDATE?) precies en wanneer zou je dit toepassen?
Als je eerst een SELECT COUNT(*) uitvoert om vervolgens bij if (... == 0) een INSERT of een UPDATE uit te voeren, doe je dubbel werk. Bovendien trek je (afhankelijk van de engine) vanwege een eventuele table lock voor de totaaltelling een enorme wissel op de performance: die gooit de hele tabel tijdelijk op slot. Daar kun je beter één INSERT ... ON DUPLICATE KEY UPDATE ... van maken.

In PHP wordt dat "dus" vooral vaak verkeerd aangepakt in mappers, bijvoorbeeld een UserMapper. Dan gaat de ene methode eerst controleren of iets al bestaat voordat één van twee andere methoden respectievelijk een INSERT of een UPDATE kan uitvoeren. Dat is vaak nergens goed voor.
>>> Als je eerst een SELECT COUNT(*) uitvoert om vervolgens bij if (... == 0) een INSERT of een UPDATE uit te voeren, doe je dubbel werk.

Guilty :) Dus INSERT...ON DUPLICATE KEY UPDATE is een controle of bijvoorbeeld een naam al bestaat. Zo ja, updaten, Zo nee dan inserten. Had ik dat maar eerder geweten.

Maar ik snap nog niet helemaal de link met de rollback, want in mijn geval wordt er één formulier ingevuld en opgeslagen. Zou ik dan een controle moeten doen of ingevulde waardes al bestaan in 'profile', dan updaten, anders inserten? Waar zou ik dan op moeten controleren, en wat moet ik updaten, want misschien wil de persoon wel 2 verschillende dingen opslaan... Je maakt het er niet makkelijker op :)

Wat vond je van het datamodel? Koppeltabel gebruiken?
Als je nu al weet dat je de data eigenlijk moet uitsplitsen, zou ik eerst verder gaan normaliseren. Je krijgt anders een oplossing die met "plakband en elastiek" aan elkaar hangt. En dan moet je het later alsnog weer over doen.

Je kunt een INSERT ... ON DUPLICATE KEY UPDATE ... gebruiken om de data die je al hebt alvast op te slaan, zelfs als de gebruiker nog een paar schermen met foutmeldingen en vragen nodig heeft om het geheel af te ronden. Sla de ID op in een sessie en laat die als de DUPLICATE KEY het werk doen.

Je kunt het dus zien als alternatief voor een transactie waarbij echt alles in één keer goed of in één keer fout moet gaan.

De ene oplossing is flexibel, de andere rigide, dus het hangt ervan af wat je precies wilt bereiken.
Wat zou jij anders doen aan dat datamodel dan?

Dat klinkt wel mooi :) Gebruik je die methode zelf ook? Alleen je kunt dan niet eenvoudig een 'rollback' doen lijkt me. Dus als een gebruiker het formulier niet kan afronden door een niet-menselijke fout, blijft er onvolledige data in de database. Of dit een probleem is een tweede.
Gewoon normaliseren zou voldoende moeten zijn. Volgens mij kun jij dat wel, anders helpen we je wel even.

Ja, ik gebruik inderdaad databasetabellen met incomplete gegevens. Een goed voorbeeld zijn online bestellingen: die gaan vaker fout dan goed, maar je hebt de incomplete orders van "shopping cart abandonment" nodig om te zien waar en waarom kopers afhaken.

Wel moet je dus twee dingen elders doen: bij een SELECT moet je er rekening mee houden dat data kunnen ontbreken (in de view en dergelijke) en om de zoveel weken of maanden moet je de database eens opschonen.
>> Gewoon normaliseren zou voldoende moeten zijn
Ik dacht dat ik dat al redelijk had gedaan eigenlijk...

Misschien toch beter om het idee van rollback voor dit maar te laten vallen. Zolang de lege data geen probleem vormt zou het ook niet veel uit moeten maken (inderdaad in de view en zo controleren of de data er werkelijk is, maar dat doe ik eigenlijk uit automatisme al wel). Als je incomplete data voorbij ziet komen weet je wel gelijk dat er wat fout gaat en eventueel waar.

Wat schoon je precies op dan? Je wilt toch wel een historie van je aankopen? Controleer je dan of alles na jouw idee compleet is, zo niet, dan verwijderen? En dan ben je die mislukte-aankopen-historie wel kwijt?

Reageren