Zohlednění změn více uživatelů

Školení, která pořádám

Webové a databázové aplikace z principu umožňují práci více uživatelům najednou. Obvykle to ničemu nevadí, protože každý uživatel má na starosti buď jen určitou část aplikace nebo s ní pracuje jen v určitém čase, takže nedochází ke kolizím. Pokud se ale oblasti působnosti jednotlivých uživatelů překrývají, je dobré počítat s tím, že si dva uživatelé otevřou pro editaci stejný záznam, provedou v něm různé změny a uloží ho. Možnosti pro řešení těchto kolizí jsou v zásadě tyto:

Pozdější vyhrává
Ten, kdo uloží záznam jako poslední, přepíše změny uživatele, který si záznam otevřel spolu s ním. Jedná se o nejjednodušší a nejobvyklejší způsob.
Zamykání záznamů
Do editované tabulky přidáme sloupec indikující, že je záznam právě editován a na jeho základě uživateli editaci buď znemožníme nebo ho na možnou kolizi alespoň upozorníme. Problém je v tom, že editace záznamu nemusí nutně skončit jeho uložením nebo může trvat dlouho, takže zámky zůstávají otevřené. Řešit to lze přidáním obsluhy události onunload (která však není zcela spolehlivá) a omezením časové platností zámku.
Detekce kolizí záznamů
Do tabulky přidáme sloupec indikující, kdy byl záznam naposledy změněn (ideálně se k tomu hodí typ timestamp). Při editaci záznamu si tuto hodnotu uložíme do skrytého formulářového pole a pokud se při ukládání liší, tak uživatele upozorníme na vznik kolize.
Detekce kolizí buněk
Při editaci záznamu si ke každému sloupci do skrytého formulářového pole poznamenáme, jaká byla jeho původní hodnota. Pokud se sloupec změnil a při uložení je hodnota jiná, oznámíme kolizi. Pokud se nezměnil, tak ho neukládáme. Přestože tento způsob vypadá jako dokonalejší varianta předchozího, může vést k nežádoucím výsledkům. Řekněme, že je ve slovníčku nesprávná dvojice slov ('newspaper', 'časopis'). První uživatel změní 'časopis' na 'noviny', druhý změní 'newspaper' na 'magazine'. Systém vždy přepíše pouze změněný sloupec, takže ke kolizi nedojde, ale výsledkem je opět nesprávná dvojice slov ('magazine', 'noviny').

Realizace detekce kolizí záznamů

<?php
$ulozeno = mysql_fetch_assoc(mysql_query("SELECT * FROM uzivatele WHERE id = " . intval($_GET["select"])));
if ($_POST) {
    if ($ulozeno["zmeneno"] != $_POST["zmeneno"] && ($ulozeno["jmeno"] != $_POST["jmeno"] || $ulozeno["prijmeni"] != $_POST["prijmeni"])) {
        echo "Záznam byl během editace změněn. Po zkontrolování uložených hodnot odešlete formulář znovu.\n";
    } else {
        mysql_query("UPDATE uzivatele SET jmeno = '" . mysql_real_escape_string($_POST["jmeno"]) . "', prijmeni = '" . mysql_real_escape_string($_POST["prijmeni"]) . "' WHERE id = " . intval($_GET["select"]));
        header("Location: .");
        exit;
    }
}

$row = ($_POST ? $_POST : $ulozeno);
echo "<form action='' method='post'>\n";
echo "<input type='hidden' name='zmeneno' value='$ulozeno[zmeneno]' />\n";
echo 'Jméno: <input name="jmeno" value="' . htmlspecialchars($row["jmeno"]) . '" />' . ($row["jmeno"] != $ulozeno["jmeno"] ? " Uloženo: " . htmlspecialchars($ulozeno["jmeno"]) : "") . "<br />\n";
echo 'Příjmení: <input name="prijmeni" value="' . htmlspecialchars($row["prijmeni"]) . '" />' . ($row["prijmeni"] != $ulozeno["prijmeni"] ? " Uloženo: " . htmlspecialchars($ulozeno["prijmeni"]) : "") . "<br />\n";
echo "<input type='submit' value='Uložit' />\n";
echo "</form>\n";
?>

Do proměnné $ulozeno si uložíme aktuální podobu záznamu. Pokud uživatel odeslal formulář, tak zkontrolujeme, jestli se shoduje časová značka. Pokud ne a zároveň se data odeslaná uživatelem neshodují s těmi uloženými v databázi, tak na to uživatele upozorníme. Při výpisu formuláře používáme (přes proměnnou $row) hodnoty odeslané uživatelem (pokud se jedná o opravu kolize) nebo ty, které jsou uložené v databázi.

Všimněte si, že se při aktualizaci záznamu nenastavuje hodnota sloupce zmeneno. Ta je typu timestamp, takže se aktualizuje sama a pokud nedojde ke změně v ostatních sloupcích, tak zůstane nedotčena. To se hodí jako ochrana před uživateli, kteří v datech neprovedli žádné změny a formulář jen zbůhdarma odeslali.

Jakub Vrána, Řešení problému, 16.2.2007, diskuse: 9 (nové: 0)

Diskuse

ikona MiSHAK:

Kůl je 'Location: .' ješte se mám co učit :)

anode:

Z tohohle konkrétně bych se zrovna neučil. Hodnota HTTP hlavičky Location musí obsahovat absolutní, nikoliv relativní adresu. Viz http://rfc.net/rfc2616.html#p135 .
Ale jinak je to pěkné řešení.

ikona Jakub Vrána OpenID:

Je to tak. Všechny prohlížeče se s tím nicméně vyrovnají a omezení mi to přijde nesmyslné, tak ho vědomě porušuji.

error414:

Pripadne to vystourat z $_SERVER a __FILE__.

anode:

Je mi jasné, že tenhle článek byl o něčem zcela jiném, jen se to možná hodilo zmínit. Nakolik je to nesmyslné omezení neumím dobře posoudit, ale většinou se dá napsat funkce, která vrátí absolutní adresu aktuálního skriptu (třeba pokud děláte stránky způsobem popsaným v http://php.vrana.cz/struktura-stranek.php ).

numero:

Pravda, to je zajmavé použití - taky si zapisuju do slovníku.

Jan Zich:

To je sice vsechno pekne, ale vsechno se ty tyka jen jedne tabulky. Me by spise zajimala jina trochu obecnejsi vec, kterou uz v PHP nejakou dobu hledam a nemuzu najit (teda nehledal jsem usilovne).

Chtel bych mit k dispozici skutecne synchronizacni prostredky nebo transakce. Jde o to, ze ve znaznem mnozstvi pripade se pri zpracovani stranky na serveru musi pracovat s vice tabulkama. Neco se precte, smaze, porovna, zmeni atp. Chtel bych mit k dispozici mechanisnum, kde bych mmel zaruceno, ze nikdo nebude delat v tu chvili to same (nebo pracovat se stenymi daty).

Samozrejme, ze ve vetsine pripadu nehrozi niz hrozneho, ale desim se, ze jednou budu muset napsat aplikaci, kde neco takoveho bude skutecne potreba.

Jasne, existuji databazove transakce, ale problem je v tom, ze jednak ne vsechno db je podporuji (nebo ne uplne), logika zamykani tabulek/dat atp. Pripada mi divne, ze tu neni nejaky obecny mechanismum. Prece nemuzu do podrobna studovat, jake presne moznosti zamykani ma ta ktera databaze.

SiseL:

"one ring rule them all" - nope!
Potrebujete vytvárať Vašu budúcu aplikáciu, nakoľko ste aktuálne ešte žiadnu "nenapísal", pre viac DB systémov zároveň?
Ak áno, tak Vám nezostane nič iné, ako naštudovať rozdiely v spracovaní transakcií medzi jednotlivými DB a zohľadniť ich v middleware, ktorý bude k DB pristupovať.

ikona Jakub Vrána OpenID:

Pokud to jde, tak je lepší se zamykání vyhnout, protože jeden (pomalý) proces blokuje všechny ostatní a pokud se třeba zasekne (třeba záměrně), tak stojí všechny ostatní - útok DoS.

Pokud to je opravdu nezbytně nutné a mám jistotu, že zámek se určitě vždy uvolní (minimálně pořádným zkontrolováním kódu a zavoláním register_shutdown_function), tak mě napadají minimálně tři přístupy:

1. Obyčejné zamykání souborů přes flock.
2. Práce se semafory: sem_acquire.
3. Databázové zámky: GET_LOCK v MySQL.

Pokud mi stačí konzistence při práci s databází, tak jsou nejlepší samozřejmě transakce, které umožňují i konkurenční zpracování.

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.