Diskuse s reakcemi
Školení, která pořádám
Málokterý webový publikační systém se obejde bez komponenty pro diskuse. Na weblozích jsou poměrně často k vidění diskuse ploché, bez znázornění vláken, všechny příspěvky pěkně pod sebou. Reakce na příspěvky jsou obvykle znázorněny odkazem na původní příspěvek. Já tento druh diskusí rád nemám, zdá se mi nepřehledný. Má samozřejmě i několik výhod – v jednom příspěvku se lze odkazovat na více původních příspěvků (což by ale mohlo jít i u ostatních způsobů a při špatném použití je to naopak nevýhoda) a díky chronologickému řazení jsou na první pohled patrné nové příspěvky. Základní implementace je triviální, při zdokonalování funkcí souvisejících s odkazováním se samozřejmě trochu zesložiťuje.
U webových magazínů jsou v podstatě pravidlem diskuse se znázorněním vláken. Kvůli nechronologickému řazení může být při opakované návštěvě problém rozpoznat nové příspěvky, proto některé servery nabízí zobrazení pouze nových příspěvků. Tato funkce ale nebývá dvakrát přátelská – nové příspěvky se zobrazí bez kontextu, takže příspěvky jako „To je nesmysl, já si myslím pravý opak.“ jsou nečitelné. Lepší mi přijde zobrazovat vždy všechny příspěvky a nové jenom zvýrazňovat.
Jak diskuse se znázorněním vláken naprogramovat? Tradiční přístup je přes tabulku diskuse(id int, rodic int, …)
. Pokud je rodic
NULL, jedná se o příspěvek první úrovně, jinak o příspěvek reagující. Ukládání záznamů je přímočaré, výpis rekurzivní funkci práce taky moc nedá. Výkon je ale žalostný, obzvlášť pokud běží databázový server na jiném stroji – v tom případě je lepší získat všechny příspěvky jedním dotazem a do stromu si je poskládat v PHP, ale to zase výrazně vzroste spotřeba paměti.
Při realizace diskuse je potřeba zohlednit, že její výpis je mnohem častější operace než vložení příspěvku. Tomuto faktu je vhodné přizpůsobit strukturu databáze a za cenu rychlejšího výpisu oželet i mírně pomalejší vložení příspěvku. Tyto požadavky splňuje tabulka diskuse(id int, poradi int, hloubka int, …)
, kde poradi
a hloubka
je pořadí a hloubka příspěvku tak, jak se bude zobrazovat. Výpis je triviální, stačí použít ORDER BY poradi
, vložení příspěvku dá ale trochu práce:
<?php
mysql_query("LOCK TABLES diskuse WRITE");
// zjištění pořadí a hloubky příspěvku, na který se reaguje
if ($_POST["re"] && ($row = mysql_fetch_assoc(mysql_query("SELECT poradi, hloubka FROM diskuse WHERE id = " . intval($_POST["re"]))))) {
// zjištění pořadí příspěvku, na jehož místo se bude vkládat - první následující s menší nebo stejnou hloubkou jako rodič
$row = mysql_fetch_row(mysql_query("SELECT MIN(poradi) - 1, $row[hloubka] + 1 FROM diskuse WHERE poradi > $row[poradi] AND hloubka <= $row[hloubka]"));
if ($row[0]) { // bude se vkládat doprostřed tabulky, posunout následující záznamy
mysql_query("UPDATE diskuse SET poradi = poradi + 1 WHERE poradi > $row[0]");
} else { // bude se vkládat na konec tabulky
$row = mysql_fetch_row(mysql_query("SELECT MAX(poradi), $row[1] FROM diskuse"));
}
} else { // pokud se nejedná o reakci, vloží se na konec
$row = mysql_fetch_row(mysql_query("SELECT MAX(poradi), 0 FROM diskuse"));
}
mysql_query("INSERT INTO diskuse (poradi, hloubka) VALUES (" . ($row[0] + 1) . ", $row[1])");
mysql_query("UNLOCK TABLES");
?>
Existují i další metody, šikovné mi přijde např. Traverzování kolem stromu, popsané v článku na Intervalu. Článek je podle mého názoru z větší části přebraný z jiného článku a na rozdíl od něj u této metody neobsahuje popis vložení záznamu, takže je asi lepší podívat se na originál, byť je v angličtině.
Pro nasazení diskusí na svůj web samozřejmě můžete použít i existující systémy, např. miniBB, čas od času ale bývají cílem hromadných útoků.
Přijďte si o tomto tématu popovídat na školení Návrh a používání MySQL databáze.
Diskuse
Napadlo tě někdy, Jakube, udělat diskusi, u které by si uživatel zvolil, jestli ji chce zobrazit jako strom, nebo jako plochou s čísly v hranatých závorkách? Já už nad tím asi dva týdny přemýšlím a nepřipadá mi to zas tak těžké.
Cožpak o to, různě zobrazit diskuzi nemusí být těžké.
Těžší bude motivovat uživatele s plochým zobrazením, aby u reakce na jiný příspěvek nezakládali nové vlákno. Nebo je přimět, aby nereagovali na více příspěvků najednou, jak jsou zvyklí z plochých diskuzí.
Myslím, že si přidáváte jeden rozměr. Diskuze označovaná jako plochá (plocha = 2 rozměry) by měla být spíš nazývaná "lineární". Naopak vláknová diskuze by se dala označit jako plochá.
Když jsem si zakládal blog, hodně jsem přemýšlel, jaký typ diskuze zvolit. Mám pocit, že při počtu komentářů < 20 je to naprosto jedno. Při větším počtu komentářů se začínají projevovat neduhy obou systémů (nepřehlednost u lineárního a "výhoda první reakce" u stromového). Těžko říct, co je horší... Ideální systém nejspíš teprve čeká na objevení...
Já jsem zatím volil také jednoduchou nevláknovou diskusi v komentářích.
- čekám tam primárně reakce na příspěvek a ne diskuse mezi sebou :)
- těch reakcí se zatím neobjevuje moc
- přispěvatel má možnost souhlasit (odkázat) v textu i předchozí reakce
- a musím přiznat, že jedním z pádných důvodů byla i jednoduchost implementace :)
Pokud si libujete v možnostech volit zobrazení diskusí, tak se mrkněte jak jsou řešeny na Slashdotu.
Nebylo by lepší $_POST["re"] testovat pomocí isset? Na nultý příspěvek (<?=var_export((bool) '0')?>) sice moc reakcí asi nebude (ani nemusí existovat), ale i tak by bylo aspoň zřejmější co se testuje.
Předpokládá se, že id je AUTO_INCREMENT, takže nulu nikdy nedostane. Také je možné, že skript pro odeslání příspěvku bude 're' posílat vždy a když to nebude reakce, tak pošle prázdný řetězec. Za určitých okolností by se samozřejmě isset() použít mohlo.
MaReK:
To neni mozny tak den predtimto clankem jsem jedno z reseni daval na
http://penguin_007.bloguje.cz :-)... Asi jsem opisoval, kradl a zverejnil drive
Tvoje řešení je zásadně odlišné.
Co jsem na to koukal, tak ty to řešíš tak, že příspěvky seřadíš až při zobrazování. Zatímco v tomhle článku je postup, jak je seřadit při ukládání - při zobrazování se to už rovnou načte patřičně seřazené.
MaReK:
Jojo, presne tak, mam (konecne) moznost si sahnout do navrzene databaze, tak si tu uroven zanoreni tam pridam, byla tam jeste jedna chybka, kterou nevim ani proc jsem udelal (reset() pred foreach());... No proste musim to dat dohromady...
Jakube, diky za reakci u me... Ja uz nejak dokonverguji k rozumnemu reseni...
Sněho:
Prosím všechny co mají nějaké funkční php fórum at mi ho pošlou na : info@autoprodej.wz.cz díky
BB breta.b@atlas.cz:
Koukám že můj testovací komentář je už smazán:-) nicméně děkuji za inspiraci, tento způsob jsem právě pokusně nasadil do diskuse na www.budlez.cz a podle prvních poznatků splňujě na 100%.
Za předchozí pokus se ještě jednou omlouvám:-)
Zdraví BB
Pavel Z.:
Ahoj, lze také nějak elegantně (jednou podmínkou v dotaze) smazat celé vlákno, když známe pořadí a hloubku příspěvku, kterému jsou všechny další podřazeny?
Teď to dělám tak, že si příspěvky vyberu stejně jako pro výpis a while se breakne, když dojdeme na příspěvek se stejnou hloubkou jako má rodič (v tomto je právě ten problém, díval jsem se do manuálu a MySQL nic jako break s podmínkou neumožňuje :-/ )
<?php
/*vybrání jako pro výpis*/
while($r=mysql_fetch_assoc($result)){
if($r["poradi"] > $del_poradi AND $r["hloubka"] == $del_hloubka) break;
if(! (($r["poradi"] > $del_poradi AND $r["hloubka"] > $del_hloubka) OR $r["poradi"] == $del_poradi)) continue;
/* kód smazání */
}
?>
V článku je uveden dotaz pro získání následujícího příspěvku. S využitím tohoto dotazu se dá provést i smazání, na jeden dotaz to ale není:
<?php
mysql_query("LOCK TABLES diskuse WRITE");
// zjištění pořadí a hloubky příspěvku, který se maže
$row = mysql_fetch_assoc(mysql_query("SELECT poradi, hloubka FROM diskuse WHERE id = '$_POST[del]'"));
if ($row) {
// zjištění pořadí příspěvku, do kterého se bude mazat - první následující s menší nebo stejnou hloubkou jako rodič
$do = mysql_result(mysql_query("SELECT MIN(poradi) FROM diskuse WHERE poradi > $row[poradi] AND hloubka <= $row[hloubka]"), 0);
mysql_query("DELETE FROM diskuse WHERE poradi >= $row[poradi]" . ($do ? " AND poradi < $do" : ""));
if ($do) { // maže se zprostředku tabulky, posunout následující záznamy
mysql_query("UPDATE diskuse SET poradi = poradi - " . ($do - $row["poradi"]) . " WHERE poradi >= $do");
}
}
mysql_query("UNLOCK TABLES");
?>
Při traverzování kolem stromu by to bylo jednodušší. Myšlenka:
<?php
$row = mysql_fetch_assoc(mysql_query("SELECT lft, rgt FROM diskuse WHERE id = '$_POST[del]'"));
$posun = $row["rgt"] - $row["lft"] + 1;
mysql_query("DELETE FROM diskuse WHERE lft >= $row[lft] AND rgt <= $row[rgt]");
mysql_query("UPDATE diskuse SET lft = lft - $posun, rgt = rgt - $posun WHERE lft > $row[lft]");
?>
Pavel Z.:
No ano, tak přece jenom to lze provést elegantně! Díky moc za odpověď, takhle to je lepší, když jsem napsal dotaz DELETE do cyklu tak to mám potenciálně nebezpečnější. Hned to jdu zkusit . :-)
Frenky:
Hezký článek, velmi poučný. Ale našel jsem menší chybičku v příkazu, který by měl aktualizovat hodnoty traverzace kolem stromu, při mazání příspěvku se všemi jeho podvětvemi:
mysql_query("UPDATE diskuse SET lft = lft - $posun, rgt = rgt - $posun WHERE lft > $row[lft]");
tento dotaz by nezaktualizoval pravou stranu všech nadřazených uzlů k mazanému uzlu, takže správně by to mělo být nějak takto:
mysql_query("UPDATE diskuse
SET lft = IF(lft<$row[lft], lft, lft - $posun),
rgt = rgt - $posun
WHERE rgt > $row[rgt]");
krasota...:
usetril si mi den rozmyslania. dakujem.
sziroco:
Ahoj,
když jsem si přečet tvůj článek, rozhod jsem se, že tvoje řešení použiji s malou úpravou. Totiž, že se vlákna nebudou zobrazovat od nejstaršího po nejmladší ale obráceně.
Nestačilo jen změnit ORDER BY rank na ORDER BY rank DESC ale bylo potřeba udělat i změny v kódu, který zajistí vložení příspěvku.
Dlouho jsem se s tim babral a nakonec jsem do kódu (ač nerad) přidal jeden výběrový dotaz navíc. Není to řešení ideální ale funguje.
<?php
if ($_POST["re"] && ($row = mysql_fetch_assoc(mysql_query("SELECT rank, deep FROM diskuse WHERE id = '$_POST[re]'")))) {
// zjisteni poctu predeslych reakci
$numOfReactions = mysql_fetch_row(mysql_query("SELECT count(*) FROM diskuse WHERE deep = $row[deep]+1"));
if($numOfReactions["0"] == 0) {
//neni predesla reakce, bude se vkladat hned za
$row = mysql_fetch_row(mysql_query("SELECT MIN(rank) - 1, $row[deep] + 1 FROM diskuse WHERE rank=$row[rank]"));
}
else {
//je vice predeslych reakci
$row = mysql_fetch_row(mysql_query("SELECT MIN(rank) - 1, $row[deep] + 1 FROM diskuse WHERE rank<$row[rank] AND deep = $row[deep]+1"));
}
//posunoutí následujících záznamů
mysql_query("UPDATE diskuse SET rank = rank - 1 WHERE rank <= ($row[0])");
$row[0] = $row[0] - 1;
}
else { //neni reakce, ulozeni na konec
$row = mysql_fetch_row(mysql_query("SELECT MAX(rank), 0 FROM diskuse"));
}
?>
Jakube píšeš perfektní blog. Jen tak dál.
echo1:
Tento script nefunguje. Zkouším jej přepracovat, ale zatím nic.
Jack:
Prosím PHP se učím takže jsem trochu mimo mám pár dotazů:
SELECT poradi, hloubka - Jak má vypadat sql soubor aby mi to přidávalo komentáře a fungovalo s tímto??
Jinak: Kde a jak naberu hodnot poradi a hloubka?
Kubík:
Dobrý den,
potřeboval bych poradit s následující situací:
ve svém diskuzním foru mám vnořování příspěvku řešeno rekurzivní funkcí, ale nevím jak na stránkování, protože rekurzivní funkce vypisuje db po řádcích a proto jí nelze nastavit limit. předem děkuji
kozotoč:
Kubík: Jakub v tomto článku ukázal, že to jde jednoduše a že rekurze a problémy s ní spojené nejsou potřeba. (To byl smysl toho článku.)
Váš problém by se dal řešit např. buď globální proměnnou (nebo proměnnou globální v rámci třídy), kterou byste nastavil na určitou hodnotu (délka stranky) a po vypsání každého příspěvku (v jakékoli úrovni) snížil o jedničku. Jakmile by klesla na nulu, zajistíte, aby běh programu postupně ze všech řídicích struktur vyskočil.
kozotoč:
(J@J, teď jsem si teprve všimnul, že ten člověk se ptal před 3/4 rokem. ;-))
Jira:
Tazatel sice problém asi dávno vyřešil, ale mně vaše reakce pomohla... děkuji
Zádrhel ve Vašem případě bude pravděpodobně v SQL dotazu pro výpis příspěvků, a to v sekci ORDER BY. Zkusil bych něco jako ORDER BY datum>$nove,poradi
kde $nove obsahuje datum ve formátu "Y-m-d".
ORDER BY datum>$nove DESC,poradi
nebo
ORDER BY datum<=$nove,poradi
mxiq:
Pojal mne podobný nápad, nové komentářé dávat na první místo, ale reakce na stávající komentáře přidávat pod ně. Oblíbený výkřik "PRVNÍ!" se tak nedrží na začátku, ale postupně padá ke dnu :-), čitelnost vláken diskuse je zachována.
Jediná změna proti zde uvedenému skriptu byla, že nový komentář s nulovou hloubkou dávám na začátek (pořadí 1) místo na konec a samozřejmě musím posunout pro všechny stávající komentáře pořadí o 1. Přibyl jeden aktualizační dotaz, jinak vše podle návrhu p.Koska. Zároveň děkuji za ušetřený čas při vymýšlení jak zařídit řazení diskuse podle vláken.
Mě u tohoto přístupu vadí, že v rámci vlákna jsou příspěvky seřazeny vzestupně a na hlavní úrovni sestupně. Není to konzistentní.
A můžu se zeptat, co je to za skript pana Koska?
mxiq:
Řazení mi nepřide jako vyloženě špatné - nový příspěvek nahoru, odpověďi tam, kam patří (POD rodiče).
Asi je to věc názoru.
"skript pana Koska?" je výsledek celodenního čučení do kódu, příliš dlouho v kuse, takže už nevím ani jak se jmenuju, natož, jak se jmenuje autor :-)
Podělím se o menší zkušenost. Udělala jsem pro klienta (v rámci širší služby) diskuzi s reakce ve stylu, jaký zde ukázal Jakub. Klient postupně vše schválil a těsně před koncem řekl: "Apropó - taková maličkost: chci, aby nejnovější příspěvky byly nahoře." :-*
Nesnažila jsem se ho o ničem přesvědčovat - udělala jsem, jak poručil a při té příležitosti jsem "sešrotovala" u té diskuze jsem schopnost větvení, protože.. kdo kdy viděl diskuzi s reakcemi větvenou nahoru? ;)
Tomáš:
Dobrý den. Taková malá otázečka: Jak vytvořit "čáry" od komentářů ke komentáři, co reagoval. Je to třeba v diskuzi na spolužácích. děkuju
pojízdná kočka:
Otázečka je to možná malá, ale odpověď na ní - alespoň mně - dělá problémy. Ale je to zajímavý mozkolam ;-)
Nedokázala bych to udělat na jeden "průjezd", ale (snad) na dva, navíc s tím, že celou diskuzi (resp. její úrovně) bych musela mít v poli, a celé by se to inklinovalo do složitosti O(n^2).
V prvním průjezdu bych si načetla úrovně do pole polí - index by bylo pořadí (číslováno od nultého <=> prvního v pořadí <=> nejstaršího až po ten poslední <=> nejnovější) - a hodnota by bylo pole od první do aktuální úrovně. Základní hodnota na dané pozici by vždy byla nula a dále +1 pro "čáru doprava" a +2 pro "čáru dolů". Při prvním průjezdu bych optimisticky předpokládal, že na všech úrovních nižších (tím myslím blíže ke 'kořenu') než aktuální bude "čára dolů". Vždy, když bych narazil na případ, že úroveň na novém řádku je vyšší (blíž ke kořenu) než ta v tom naposledy čteném, cyklem bych se vracela zpět a umazávala "čáry dolů" (vpodstatě jen xorovala dvojkou) v úrovních, které tvoří rozdíl mezi těmito dvěmi (např. pokud bych načetla po sobě úroveň 10 a následně 6, pak mažu v úrovních 6-10). Ve zpětném cyklu bych šla až k první řádce nebo do okamžiku, než se dostanu k příspěvku v té úrovni co je ta na aktuálním řádku (6). Toto by pak bylo potřeba udělat i na posledním příspěvku (de facto simulovat, že za ním je příspěvek v úrovni -1).
Alespoň myslím ;-)
Asi si to vyzkouším ;-)
bukowski:
pokud chcete nejnovejsi nahore
<?php
mysql_query("LOCK TABLES diskuse WRITE");
// zjištění pořadí a hloubky příspěvku, na který se reaguje
if ($_POST["re"] && ($row = mysql_fetch_assoc(mysql_query("SELECT poradi, hloubka FROM diskuse WHERE idDiskuse = '$_POST[re]'")))) {
// zjištění pořadí příspěvku, na jehož místo se bude vkládat - první následující s menší nebo stejnou hloubkou jako rodič
$row = mysql_fetch_row(mysql_query("SELECT MIN(poradi) - 1, $row[hloubka] + 1 FROM diskuse WHERE poradi > $row[poradi] AND hloubka <= $row[hloubka]"));
if ($row[0]) { // bude se vkládat doprostřed tabulky, posunout následující záznamy
mysql_query("UPDATE diskuse SET poradi = poradi + 1 WHERE poradi > $row[0]");
} else { // bude se vkládat na konec tabulky
$row = mysql_fetch_row(mysql_query("SELECT MAX(poradi), $row[1] FROM diskuse"));
}
mysql_query("INSERT INTO diskuse (poradi, hloubka,uid,zprava) VALUES (" . ($row[0] + 1) . ", $row[1],$myuid,'$zprava')");
} else { // pokud se nejedná o reakci, vloží se na konec
$row = mysql_fetch_row(mysql_query("SELECT MIN(poradi), 0 FROM diskuse"));
mysql_query("UPDATE diskuse SET poradi = poradi + 1");
mysql_query("INSERT INTO diskuse (poradi, hloubka,uid,zprava) VALUES (" . ($row[0]) . ", $row[1],$myuid,'$zprava')");
}
mysql_query("UNLOCK TABLES");
?>
Martin:
vypada to dobre, ale narazil jsem na problem pri funkci (resp. SQL dotazu), ktery by vratil id komentaru, ktere patri do vetve nad urcitym uzlem. Potrebuji to pro posilani reakci na prispevek. Order musi byt prirozene mensi nez order prispevku, ale jak zajistit, ale aby to nebylo z jine vetve?
PeTaX:
Nechci působit jako rejpal, ale zdá se mi, že $row[0] bude za všech okolností NULL.
PeTaX:
Tak jsem se to, Jakube, pokusil nasimulovat a opravdu je tomu tak. Dotaz:
<?php
$query = "
SELECT MIN(`poradi`) - 1,
$row[hloubka] + 1
FROM `diskuse`
WHERE `poradi` > $row[poradi]
AND `hloubka` <= $row[hloubka]
";
?>
vrací $row[0] = NULL ať se reaguje na kterýkoliv řádek v tabulce.
A trochu mi i uniká, proč si vracíš v dotazu:
<?php
$query = "
SELECT MAX(`poradi`),
0
FROM
`diskuse`
";
?>
tu nulu.
PeTaX:
Sry - ta nula je jasná, přehlédl jsem se.
PeTaX:
V tom MySQL dotazu musí být hloubka vyšší o jedničku, jinak bude dotaz vždy prázdný.
Aaron :):
Ahojte, prosím Vás...
Jak je možné mazat více příspěvků s různými identifikačními čísly (např.: 1,4,10 atd.) přes checkboxy? Zkoušel jsem to mnoha způsoby, ale ani jeden my nefunguje... Poradí někdo ?? :)
Jakub Vrána :
Musíš uvést, kde to chceš mazat (v jakém systému). Jinak ti nikdo nemá jak poradit. Nejlepší ale bude obrátit se na podporu onoho systému.
Aaron :):
Mám web a potřebuju hromadně mazat staré příspěvky v databázi MYSQL pomocí příkazu DELETE za použití checkboxů
Aaron :):
Promiň zapomněl jsem uvést že to chci spravovat přímo z webové stránky, protože možnost mazat příspěvky má i více lidí a pochybuji, že některý z nich má tu schopnost se orientovat v "My adminovi" (tedy v Admineru).... ale děkuji za dočasné řešení...
Pokud by tě napadlo jakékoliv lepší řešení napiš.
Předem děkuji Aaron :)
martin:
zdravim, je momentalne nekdo pristupny potreboval bych poradit?
Hogo:
Mě to nefunguje… :(
Diskuse je zrušena z důvodu spamu.