Automatické vytvoření přátelského URL

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

V článku Vytvoření přátelského URL jsem představil funkci, která z nadpisu vytvoří řetězec vhodný pro použití v URL. Pokud chceme tuto funkci používat automaticky, je vhodné nějak vyřešit kolize. Osobně vytvářím unikátní nadpisy, takže ke kolizím docházet nemůže, pokud je ale na serveru zvykem např. pravidelná shrnutí nazývat Co přinesl uplynulý týden, tak ke kolizím docházet bude, stejně jako při automatickém importu článků, kdy text nadpisu nejde ovlivnit. Pominu-li nepohodlné ruční ošetření kolizí, zbývá několik dalších možností:

V URL nechat i ID článku

Konec konců je možné se orientovat jen podle něj a zbytek URL používat jen jako omáčku pro vyhledávače. Je ale vhodné ošetřit situaci, kdy vtipálek zbytek URL vymění, aby nevznikala URL jako 78956-jsme-hlupaci. Pokud bychom chtěli ID článku doplňovat jen v případě kolize, bylo by nutné ho oddělit znakem jinde v URL nepoužitým, protože jinak by i doplnění mohlo kolidovat – pokud existuje URL test a test-3 a já se pokusím vložit URL test s ID 3 odděleným pomlčkou, tak bude kolidovat i to. Typů oddělovačů by v URL mělo být co nejméně, navíc ID článku v době uložení ještě není známé.

<?php
list($id, $url) = explode("-", $url, 2);
mysql_query("SELECT * FROM clanky WHERE id = '$id' AND url = '$url'");
?>

Při kolizi URL přidat rozlišovací příponu

Pro získání nejvyšší obsazené přípony nejde použít SQL příkaz typu SELECT MAX(url) FROM clanky WHERE url LIKE 'test%', protože to jednak strefí i testovani a jednak test-9 je lexikograficky větší než test-10. Řešením by bylo ukládat příponu do samostatného sloupce, to by ale zase zkomplikovalo dotazy pro načtení správného řádku podle URL, které musí být bleskurychlé.

<?php
$max = 0;
mysql_query("LOCK TABLES clanky WRITE");
$result = mysql_query("SELECT url FROM clanky WHERE url LIKE '$url%'"); // RLIKE '^$url(-[0-9]+)?$' je pomalé
while ($row = mysql_fetch_assoc($result)) {
    if (preg_match('~^' . preg_quote($url, '~') . '(-([0-9]+))?$~', $row["url"], $matches)) {
        $max = max($max, ($matches[2] ? $matches[2] : 1));
    }
}
mysql_free_result($result);
mysql_query("INSERT INTO clanky (url) VALUES ('$url" . ($max ? "-" . ($max+1) : "") . "')");
mysql_query("UNLOCK TABLES");
?>

Měnit URL tak dlouho, dokud se záznam nepovede uložit

Pokud je nad sloupcem s URL definován unikátní klíč (měl by být), tak můžeme zkoušet URL modifikovat tak dlouho, dokud se záznam nepovede uložit. Je potřeba dát pozor v případě, kdy měníme i sloupce, které jsou součástí jiných unikátních klíčů – tehdy se nedá spolehnout na číslo chyby 1062, ale musíme parsovat chybovou hlášku (což je ošemetné, protože se v budoucích verzích MySQL může bez varování změnit) a zjistit název klíče, který se pro sloupec používá. Tento způsob je také nevhodný, pokud z URL vznikají dlouhé řady a dat se do databáze přenáší hodně, protože všechna data se přenáší tolikrát, jak je dlouhá řada.

<?php
$MAX_PREFIX = 100; // zabránění nekonečné smyčce v případě komplikací
mysql_query("INSERT INTO clanky (url) VALUES ('$url')");
for ($i=1; $i < $MAX_PREFIX && mysql_errno() == 1062; $i++) {
    mysql_query("INSERT INTO clanky (url) VALUES ('$url-$i')");
}
?>

Všechna řešení mají svá pro a proti, žádné mi nepřijde dokonalé.

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

Diskuse

ondra:

dotaz 1: kdyz je nazev clanku velice dlouhy, nemelo by smysl ho do url zkracovat?

dotaz 2: u clanku neni vhodne cool url nepouzivat? preci jen je to pro lidi; a kdo si zapamtuje url clanku, example.com/v-praze-bourali-hasici-zena-prisla-o-nohy/

mozna v url mit id a slova jen pro vyhledavace; ale ktery bfu pochopi ze mu staci pamatovat jen ID

dotaz 3: u stranek mi to pride vhodne; a to model example.com/o-nas/kontakt/, chtel jsem se zeptat, pokud pouzivam model id-parentid ulozeni menu, jak jednoduse, pri generovani id do url (example.com/index.php?p=58) prepisovat?

Pochopil jsem dobre ze pokud pouziju rewrite-map, je to to, ze pomoci php skriptu vygeneruji soubor ktery bude tou mapu; kde z db vypisu zceho na co se to ma prepsat; a mod-rewrite prevezme ten soubor a url tak prepise? Nebo se to resi jinak?

Zapis do .htaccess je asi nesmysl, kdyz menu se meni v cms, tak naka pravidylka asi ne-e

pri pouziti na stavajicim webu by se asi musely stary url presmerovavat na nova; to by zajistoval take mod-rewrite?

diky i za castecne odpovedi

Radim H.:

Já jsem to na jednom svém webu vyřešil tak, že si do databáze ukládám název vygenerované URL (například "vyber-prvni-ikony-na-plose"). Duplicita je vyřešena přidáním pořadového čísla článku.

finc:

Není místo LOCK TABLES lepší použít typ InnoDB a transakce?

Radek:

Zkoušel jste někdy InnoDB používat s větším objemem dat?

finc:

Ano,
mám DB cca s 200 tabulkami, ktere jsou propojeny referenčními integritami. Jedna tabulka obsahuje třeba 200 000 vět.
Jelikož se indexy vytvářejí automaticky po vytvoření foreign key, tak se většinou nemusím po normalizaci tolik starat o optimalizaci uložených dat.
K mému užasu jsem zjistil, že pokud potřebuji použít akční dotaz na tabulku InnoDB, tak se provede mnohem rychleji než na typ MyISAM. Jediné omezení se týká agreghačních dotazů typu COUNT(*), apod. Kde typ InnoDB nemá tyto data přepočítána a není pro tyto typy SQL příkazů optimalizována.
Mluvím z praxe, kde se mi InnoDB osvědčilo ať už díky transakcím (commit, rollback) či pro zachování referenční integrity.
Ono tohle většinou lidé tvořící malé webové aplikace neznají, E-shop zabere max. 10-20 tabulek v DB. Ale u velkých systémů je to o nečem jiném.

Marek V.:

Take si do db ukladam unikatni retezec vytvoreny z nadpisu clanku. Pripadnou duplicitu resim pridanim _ na zacatek retezce.

shion:

Jen neco malo k zamykani:

Po utrpnych zkusenostech se zamykanim tabulek v MySQL, jsme dospeli (nas dev tym) k zaveru, ze jakekoliv zamykani je cesta jen k vetsim problemum s "deadlockem".

Cely DB server pak obvyhle vytuhne a je nutne rucne killovat vlakna mysql procesu, ktera jsou uzamcena v cyklu, kdy cekaji na dokonceni jinych procesu, ktera take cekaji ... :)

Tusim ze nekde jste popsal pouziti funkce "register_shutdown_function", ktera by mela zamknute tabulky opet odemknout, ale nikdy nevite, kdy muze byt php proces kilnut (v php CLI), muze ted dojit k pretizeni apache velice narocnym skriptem ("set_time_limit" na vysoke hodnote) a DB nekdy nemusi frontu zamykani lidove receno  korektne zpracovat a opet je sance ze nastane deadlock.

Transakce je v podstate totez co zamykani, je mi jasne ze je v nekterych pripadech nutne transakce pouzivat (napr. provadeni platby v bankovnictvi), ale obvykle se da vse udelat "rucne" pres dodatecne query (tzv. "rollback-query", ktere procistuji vlozena data, ktera nemela byt vlozena).

Mozna jsem paranoidni, ale vnucenemu zamykani tabulek doporucuji se obloukem vyhnout a nechat zamykani na DB engine (oblibeny engine mam "innodb").

finc:

Souhlas, innoDB je geniální věc, pokud tedy člověk ví jak jej využít. Osobně mám v DB MyISAM jen pro tabulky, které obsahují chybové záznamy stažené ze systémů, které většinou porušují referenční integrity. Zbytek InnoDB. Spíš nechápu, proč lidé stále mají utkvělou představu, že se tento typ nedá použít pro větší objem dat.
Má rada by spíše zněla: Pokud chceš používat MyISAM, rouvno data ukládej do souboru, vyjde tě to skoro nastejno :)))
To co má Oracle už soustu let, má dnes i MySQL, nevím proč se tomu lidé tolik brání. Ale, když chce někdo jezdit v trabantu, tak ať jezdí :))

shion:

Jeste si dovolim upresneni:
Osobne MyISAM nezavrhuji, jelikoz v situacich, kdy potrebujete data jen ulozit do databaze (DB preci jen neni soubor, ze :) moc si neumim predstavit jak pak chcete provadet dotazy na soubor, kdyz nemate index) a neni pro vas dulezita integrita dat (constraints, row level locking).

Prave v rychlosti vidim velkou vyhodu MyISAM oproti Innodb. Inndb je pomalejsi (hlavne u insertu) prave proto, ze kontroluje na ruznych urovnich intergritu dat. Innodb se take lisi od MyISAM zpusobem fyzickeho ulozeni dat disku (jeden velky soubor pro vse tzv. tablespace, od verze 4.1 lze jiz nastavit 1 soubor = 1 tabulka).

Rychlost jiste ocenite pri vkladani non stop ukladani nekolika set radku za minutu.

Muj zaver:
Innodb:
+ konzistence dat, constraints, row level locking
- pomalejsi nez MyISAM
MyISAM:
+ rychlost, rychlost, rychlost
- vse co nema innodb

finc:

Co se týče toho insertu, je to jen o tom, že v defaultním nastavení je auto_commit = 1; což znamená, že transakce se provádí automaticky při jakémkoli akčním dotazu (vyjma truncate, apod. viz manuál). Pokud nechcete tuto automatickou volbu stačí automatické transakce vypnout. Poté, když na tabulku InnoDB provedete akční dotaz, tak zjistíte, že je naopak rychlejší než MyISAM.
Sám to tak používám, ne vždy potřebuji využít transakce. Ale pokud ano, tak pouziji start_transaction a poté commit či rollback.
Ale abych tedy netvrdil jen, jak je InnoDB dokonalé, jsou zde nějaké omezení:
např.
není možné indexovat sloupec typu text;
není možné použít typ sloupce blob;
jelikož se tabulky InnoDB uchovávají strukturou v jednom souboru, je maximální velikost DB odvozena od maximální velikosti souboru v souborovém systému (na druhou stranu, toho stejně téměř nikdo nedosáhne :))

Abych to ukončil. Referenční integrita a transakce je pro mě mnohem důležitější, než si hrát s rychlostní tabulkou, kde může InnoDB prohrát o zanedbatelný čas (ke všemu to není pravda). Mám vlastní srovnání z praxe.

Malý tip na závěr: Zkuste si vytvořit DB s InnoDB a referenčními integritami a použijte nástroj MySQL WorkBench a naimportujde tuto DB. Při velkých DB je takové modelování nezbytné. Jen čekám, kdy konečně nebude tento nástroj v beta verzi :(

shion:

Ja se prave ze sve zkusenosti snazim uzivatelskym transakcim vyhnout za kazdou cenu (viz muj prvni post).

Deadlock nas trapil tak mnoho, ze to byl pouziti transakci neunosne. (Je to ten pripad kdy tabulka do ktere vkladate data se zaroven i updatuje jinymi procesy. Pouzite prezistentni spojeni s DB).

Z mysql manualu:
"Deadlocks are a classic problem in transactional databases
InnoDB uses automatic row-level locking. You can get deadlocks even in the case of transactions that just insert or delete a single row."
(http://dev.mysql.com/doc/refman/5.0/en/innodb-deadlocks.html)

finc:

Souhlas, že někdy toto řešení může být problém. InnoDB zamyká  tabulku na úrovni řádku, nikoli celou. Navíc pokud řeším pouze ten jeden "řádek" v tabulce, tak řeším ten, nikoli celou tabulku. Při psaní aplikace a jejím pozdějším využití se s tímto musí počítat.
Já měl např. problém s truncate table, což je navíc příkaz, který rollback není ochoten vrátit.
Při spuštění procesu na naplnění jedné tabulky, před kterou se provede truncate a později insert, jsem se dost divil, když tento proces spustili nezávisle na sobě dva uživatelé v podobný čas. Část dat se samozřejmě zduplikovala.
Ještě na jedno omezení u InnoDB jsem si vzpoměl. Příkaz na nastavení maximálního čísla pro sloupec s auto_increment:
SET auto_increment = xxx;
se neprovede.
Ale i tak, InnoDB nemůže být srovnáno s MyISAM. Opravdová DB používá věci, které MyISAM nemůže nabídnout. Ale pro programátora malých webových aplikací jako např. CMS, E-shop, apod. je to asi jedno. Pro ostatní (mezi ně patřím i já) musí hledat něco, co mu pomůže zpracovávat, zálohovat (např. pomocí triggerů), analyzovat, ...  data přímo v DB. Asi bych se zbláznil, kdybych měl udržovat referenční integrity v PHP či Javě.

Taurendil:

jak takovy velci programatori jako ty resi fulltext vyhledavani ktere v InnoDB neni?:D

finc:

Neřeší :)
Používám MySQL pro velký intranetový systém, kde nepotřebuji řešit fulltextové vyhledávání.
Proto jsem také zastáncem InnoDB, protože jinak je pro takový systém MySQL naprosto nepoužitelná. Pro malé weby, kde MySQL je využitá jen pro uložení pár textů a nějakého menu, tak MyISAM jistě stačí :)
Jinak samozřejmě existuje možnost využít LIKE '%%', což pro obyčejné vyhledávání dost často stačí. Pokud tedy neřešíte nějaké složitější algoritmy vyhledávání.

SUNNY:

Smiem sa spytat na adresu tvojej stranky ktoru tvoris,,.?

DevFist:

Vzhledem k tomu ze programuje intranetovy aplikace tak ti adresu asi neposkytne... Intranet je vnitrni sit...

tonda:

Asi to sem nepatri ale kdyz uz probirate to zamykani tabulek tak si necham poradit.
Nepouzivam zamykani tabulek, docetl jsem se sice v ucebnicich ze to urychluje ale dodnes sem nepochopil jak to funguje a k cemu to je, jak to reaguje pri vice prispevcich no a proste vyhody.
Kdyby jste mi poradili byl bych rad

ikona Jakub Vrána OpenID:

Hlavní důvod, proč se používá zamykání tabulek a proč je použité i tady, je zajištění atomicity operací. Podrobněji o tom píšu na http://php.vrana.cz/atomicita-operaci.php.

Vladimír Vojík:

Ahoj Jakube, můžu se prosím zeptat - jediná možnost, jak dostat z výjimky nebo chybového hlášení název příslušného sloupce je parsování této hlášky, jak popisuješ v 3. příkladu? Děkuji.

ikona Jakub Vrána OpenID:

Mám za to, že ano.

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.