Wpisy z kategorii 'webdeveloper'

Obsługa drzew w SQL - część 2 - Model ścieżki zmaterializowanej

W poprzednim wpisie pokazałem typowe podejście do zagadnienia drzew w SQL (notabene: nazywa się on modelem sąsiedztwa), omówiłem też jego podstawowe wady (i zalety) i obiecałem opisać inne podejście - pozbawione wad modelu sąsiedztwa.
Drzewiasta struktura danych
Jeśli przyjrzysz się przykładowemu drzewu, który chcemy zapisać w bazie danych, można zauważyć, że położenie w drzewie można także opisać jako listę wszystkich węzłów nadrzęnych danego węzła. Taka lista nosi nazwę “ścieżki”, i zapisuje się ją licząc od najstarszego rodzica aż po bieżący węzeł. Przykładowo, ścieżka dla węzła “4″ ma postać: “1,2,4″, a dla węzła “11″ - “1,3,10,11″. Dodatkowo, przyda się kolumna informująca, na jakim poziomie znajduje się dany węzeł względem początku drzewa (teoretycznie można tą informację wyciągnąć z długości ścieżki - ale takie zadanie jest, niestety, nierelacyjne, a co za tym idzie - bardzo obciążające dla bazy)

W bazie danych może to wyglądać tak:

table tree_mpath:
id	mpath	level	data
1	1	0	...
2	1,2	1	...
3	1,3	1	...
4	1,2,4	2	...
5	1,2,4,5	3	...
6	1,2,4,6	3	...
7	1,2,7	2	...
itd.

Co daje taki zapis? Przede wszystkim, pozwala z łatwością, pojedynczym zapytaniem, pobieramy fragmenty drzewa:

  1. SELECT id, mpath, level, DATA FROM tree_mpath
  2. WHERE FIND_IN_SET(id, (SELECT mpath FROM tree_mpath WHERE id = 4))
  3. ORDER BY level;

zwróci:

id	mpath	level	data
1	1	0	...
2	1,2	1	...
4	1,2,4	2	...

czyli wszystkie elementy nadrzędne węzła 4 (wraz z tym węzłem, co akurat łatwo wyfiltrować).

Do pobierania elementów podrzędnych węzła możemy użyć zapytania:

  1. SELECT id, mpath, level, DATA FROM tree_mpath
  2. WHERE mpath LIKE (SELECT CONCAT(mpath, '%') FROM tree_mpath WHERE id = 4)
  3. ORDER BY level

które w wyniku da nam:

id	mpath	level	data
4	1,2,4	2	...
5	1,2,4,5	3	...
6	1,2,4,6	3	...

Pobieranie fragmentu drzewa też nie jest trudne: powiedzmy, że chcemy pobrać tylko bezpośrednich potomków węzła o id = 2:

  1. SELECT id, mpath, level, DATA FROM tree_mpath
  2. WHERE mpath LIKE (SELECT CONCAT(mpath, '%') FROM tree_mpath WHERE id = 2)
  3. AND level = (SELECT level+1 FROM tree_mpath WHERE id = 2)

i w efekcie dostaniemy:

id	mpath	level	data
4	1,2,4	2	...
7	1,2,7	2	...

Jak widać - zalety tego modelu są znaczne. Praktycznie dowolny fragment drzewa można czytać pojedynczym zapytaniem, zarówno “w górę” jak i “w dół” od wskazanego węzła. Oczywiście, dla sprawnego działania na taką tabelę należy nałożyć odpowiednie indeksy: na pole id (to chyba oczywiste), mpath oraz level.

Jakie są wady tego modelu?

1. Wysoki koszt aktualizacji. O ile dodanie nowego rekordu jest całkiem znośne, usunięcie również jest całkiem szybkie (choć wymaga dość złożonych warunków, podobnych do tych używanych w zapytaniach SELECT), to przesuwanie węzła w inne miejsce w drzewie jest drogą przez mękę: nie dość, że trzeba zaktualizować przesuwany rekord, to należy też zmodyfikować wszystkie jego węzły potomne. Biorąc pod uwagę, że aktualizacji podlegają indeksy, narzut czasowy jest poważny.

2. Spójność danych. Konstrukcja tabeli nie pozwala na używanie kaskad przy usuwaniu rekordu. W efekcie błąd może doprowadzić do powstania węzłów-zombie, nie powiązanych z pozostałą strukturą drzewa.

3. Ograniczenia wielkości. Jeśli pole mpath będzie polem typu VARCHAR, trzeba pamiętać, że maksymalna długość tego pola to 255 bajtów. Jeżeli identyfikatory kategorii będą 3-znakowe (100-999), to każdy poziom zagłębienia będzie zabierał 4 bajty (identyfikator + przecinek), co daje maksymalny poziom zagłębienia ok. 60. Przy identyfikatorach 4 znakowych (1000-9999), maksymalny poziom zagłębienia to ok. 50. Przy dłuższych identyfikatorach (np. 36-bajtowy uu_id z MS SQL Server) maksymalna wielkość drzewa drastycznie spada (6 poziomów dla uu_id).

Zmiana typu mpath na TEXT rozwiązuje ten problem, ale może się pojawić problem z indeksowaniem takiego pola - np. MySQL nie pozwala na zakładanie indeksów na pole TEXT w InnoDB.

4. Normalizacja. Konstrukcja pola mpath łamie postać normalną tabeli. Zawartość pola level jest wprost powiązany z zawartością pola mpath (level ma wartość “długość ścieżki - 1″), i teoretycznie mógłaby zostać pominięta.

Istnieje rozwiązanie problemów 2-4 - nazywa się znormalizowanym modelem ścieżki zmaterializowanej. Ale o tym innym razem…

Dropdowny, Ajax i usability

Generalną zasadą budowy dostępnego interfejsu jest takie jego przygotowanie, żeby mogły się nim posługiwać także osoby, które posługują się wyłącznie klawiaturą, lub innymi, niestandardowymi urządzeniami do obsługi komputera. Interfejs powinien być także tak przygotowany, żeby korzystanie z niego było oczywiste, a operacje odwracalne. Nie ma nic gorszego dla aplikacji www, niż użytkownik, który nie jest pewien, co zrobić, aby osiągnąć oczekiwany rezultat. “Don’t make me think”, najważniejsza reguła budowy interfejsu.
Czytaj dalej »

ColdFusion - utracone hasło

Zapisuję to sobie tutaj, bo za każdym razem muszę szukać w sieci ;)

Metoda

1. Lokalizujemy plik neo-security.xml (jeżeli mamy CF zainstalowane na domyślnej ścieżce, to będzie w c:\CFusionMX7\lib\)

2. Szukamy ciągu:

  1. <var name='admin.security.enabled'>
  2.     <boolean value='true'/>
  3. </var>

3. Zamieniamy wartość na ‘false’

4. Zapisujemy i restartujemy serwer CF

Voila!

Nie generuj stron przez POST!

Jakieś 9 miesięcy temu, zirytowany błędami w naszej podstawowej aplikacji, wprowadziłem na rozmowę kwalifikacyjną pytanie o różnice pomiędzy żądaniami GET a POST. Srodze się rozczarowałem: od tego czasu przeprowadziłem z 50 rozmów, i tylko 1 (słownie: jedna!) osoba znała prawidłową odpowiedź.

Typowe odpowiedzi błąkały się wśród szczegółów technicznych: że GET to idzie w nagłówkach żądania, a POST w ciele żądania, że przez POST to można przesłać pliki, a przez GET to nie możliwe itp. Parę osób stwierdziło, że parametrów przesłanych przez POST nie widać w pasku URL przeglądarki. Nie wyciągnęły jednak z tego żadnych dalszych wniosków.

W zasadzie podane odpowiedzi są jak ze starego dowcipu - w 100% poprawne ale nikomu do niczego niepotrzebne. Pomijają najważniejszą sprawę: strony wygenerowanej przez POST nie da się wysłać mailem, przez komunikator czy zapisać w historii przeglądarki. Nie da się jej wykopać czy podlinkować. A nawet odświeżenie nie jest możliwe, bez obejrzenia wariacji na temat “strona wygasła”.

Nie mówię oczywiście, że należy zrezygnować z używania POST. Nie wolno tylko generować kodu HTML po takim żądaniu. Zakładanie konta, oczywiście, powinno być obsłużone przez POST, a po pomyślnym zalogowaniu należy przekierować na stronę z podziękowaniami. Głosowanie w sondach, logowanie, edycja danych i inne tego typu operacje powinny kończyć się wygenerowaniem nagłówka Location i przekierowaniem na nowy adres.

Natomiast każdy formularz, który ma na celu wygenerowanie strony (np. wyszukiwanie), powinien być obsłużony przez GET.