W dzisiejszej lekcji zajmiemy się po raz pierwszy bazami danych — na początku trochę teorii, a następnie użyjemy bazy danych do przechowywania informacji o kotach w naszym systemie :)
Tutaj chcę zwrócić uwagę na pewną rzecz — zmieniając naszą klasę DAO z ArrayListy na bazę danych nie będziemy modyfikować żadnej innej części naszej aplikacji! To tzw. abstrakcja z elementami hermetyzacji (enkapsulacji) — jedna z fundamentalnych zasad programowania obiektowego. Szerzej ten temat omówimy sobie w przyszłości, oczywiście przykład którym się posługujemy jest bardzo uproszczony, ale co do zasady o to właśnie chodzi w projektowaniu oprogramowania — tworzeniu komponentów, które udostępniają pewne funkcjonalności w określony sposób, ale szczegóły w jaki sposób to robią są ukryte, dzięki czemu w prosty sposób możemy podmieniać pewne elementy mając pewność, że nie zmieni to sposobu w jaki działa aplikacja.
Zastrzeżenie! Sposób korzystania z bazy danych, który tutaj odpisujemy, jest już praktycznie nieużywany w nowych projektach z uwagi na dostępność lepszych, wygodniejszych narzędzi. Niemniej często w pracy zawodowej zdarza się, że pracujemy z kodem tworzonym wcześniej i z tego powodu nie korzystającego z najnowszych technologii. W określonych przypadkach opisane podejście także jest preferowane (np. w sytuacjach gdzie kluczowa jest wydajność lub wykorzystujemy bardzo specyficzne elementy naszej bazy danych). To jeden z powodów, dla których lekcja ta znalazła się w tym kursie. Drugi to pokazanie, w jakim kierunku dąży rozwój Javy i powiązanych standardów (kolejne dwie lekcje będą dotyczyły tego samego zagadnienia, będziemy zmieniali naszą klasę DAO korzystając z różnych narzędzi. Zaczynamy?
Lekcja
Relacyjne bazy danych — teoria
Na wstępie należy zaznaczyć, że nie będziemy zajmować się teorią baz danych od strony matematycznej (zainteresowanych zachęcam do zapoznania się z artykułem na wikipedii lub dokumentem “A Relational Model of Data for Large Shared Data Banks” autorstwa Edgara Codda), ale spojrzymy na to, co to oznacza dla nas od strony praktycznej. Nie będziemy się też skupiać na projektowaniu baz danych, postaciach normalnych itp — to omówimy szerzej w uzupełniającym wpisie w przyszłości. To, czemu ma służyć ta lekcja to zapoznanie się z podstawami baz danych, aby rozumieć co robimy w kodzie i być w stanie samodzielnie szukać informacji i poszerzać wiedzę według potrzeb.
Tabele
Tabele to podstawowy sposób organizacji danych w relacyjnych bazach danych (można też spotkac się z określeniem RDBMS — relational database management system). Tabele wyglądają podobnie jak w rzeczywistości — mają kolumny oraz wiersze, gdzie kolumny są określone przez strukturę tabeli, a poszczególne wiersze zawierają dane (działa to trochę jak np. w Excelu, z tą różnicą, że kolumny mają też nazwy). Kolumny określają też, jakie dane można w nich przechowywać (np. liczba, dane tekstowe o określonej długości itp).
Wiersze tabeli są też określane krotkami lub encjami (nie jest to do końca prawda, ponieważ określenia te są bardziej pojemne i w bazach danych obejmują także powiązane rekordy i informacje; nie można więc powiedzieć, że wiersz w tabeli to krotka czy encja, ale nie są to pojęcia oddalone — intuicyjnie, słysząc ‘encja’ czy ‘krotka’ można więc mieć na myśli właśnie wiersz tabeli).
Tabele mają określoną strukturę, tzn kolumny są z góry określone (choć oczywiście można zmienić to w trakcie działania aplikacji, ale jest to zmiana struktury tabeli i wynika ona jedynie z operacji na tabeli, poprzez manipulację danymi nie da się zmienić struktury tabeli / deklaracji kolumn) i definiują one, jaki typ danych możemy przechowywać w danej kolumnie oraz jaki jest rozmiar tej kolumny (tzn. jaka jest precyzja przechowywanych liczb, ich maksymalne/minimalne wartości lub maksymalna długość ciągu znaków).
Tabele są właśnie tą relacją, od której nazwę wzięły relacyjne bazy danych. Jest to nieco nieintuicyjne w jezyku polskim, ponieważ o relacjach mówimy także w kontekście zależności między tabelami (omówimy je za chwilę). Sięgając jednak do języka angielskiego jest jednak różnica (Matematyczne relacje — ‘relation’ — są tym, o co chodzi w nazwie ‘relational database’; zależności pomiędzy tabelami określane sa terminem relationship; dyskusję na temat róznicy w znaczeniu tych pojęć można znaleźć np. pod adresem http://english.stackexchange.com/a/15211) — warto te różnice zapamiętać i mieć je na uwadze czytając dokumentacje związaną z bazami danych lub komunikując się z innymi. Relacją (matematyczną) nazywamy więc tabelę, rozumianą jako jej schemat (nazwę, kolumny) oraz dane razem.
Relacje pomiędzy tabelami (klucze obce)
Ponownie, w tym przypadku powiemy sobie tylko czym są relacje pomiędzy tabelami ale na ten moment nie będziemy zgłębiać ich szczegółów. Złączenia tabel w języku SQL to obszerny temat sam w sobie, który można samodzielnie zgłębiać mając podstawową znajomość SQL, a w codziennej pracy współczesnego programisty nie jest on wykonywany zbyt często.
Bardzo często wystepuje sytuacja, że jeden obiekt jest powiązany z innym za pomocą jakichś zależności (np. w aplikacji która służyłaby do zarządzania projektami w firmie, istnieje zależność typu jeden-do-wielu (o typach zależnosci powiemy sobie za chwilę): jedna firma ma wiele projektów, ale jeden projekt jest przypisany tylko do jednej firmy (oczywiście to pewne uproszczenie rzeczywistości, ale załóżmy, że tak jest). W języku polskim takie powiązanie między tabelami także określamy mianem relacji.
W tym miejscu należy jeszcze krótko wspomnieć o procesie normalizacji i postaciach normalnych (normal form, NF). Normalizacja to formalny proces, który ma zapewnić, że w bazie danych nie ma niepotrzebnych powtórzeń, odpowiednie tabele są prawidłowo połaczone i model jest spójny i integralny. O samym procesie normalizacji i jak go wykonać można poczytać w artykule na Wikipedii, w uproszczeniu polega on na doprowadzeniu modelu do założonej postaci normalnej (najczęściej czwartej) poprzez iteracyjne doprowadzenie najpierw do pierwszej postaci normalnej, drugiej itp. Każda postać normalna ma pewne założenia, które model musi spełniać, żeby można było o nim powiedzieć, że jest w danej postaci normalnej. Przykładowe założenia to np, że każda kolumna zawiera tylko jedną informację (więc imię i nazwisko trzymamy w osobnych kolumnach) czy też, że informacja nie jest duplikowana (w przypadku np. firmy i pracowników, dane firmy trzymamy w osobnej tabeli i odnosimy się do nich jedynie za pomocą klucza). Z procesem normalizacji wiąże się także denormalizacja — czyli świadome odejście od zasad normalizacji. Czasem robi się tak, ponieważ albo można pewne rzeczy założyć (np. że samochód ma jednego właściciela, albo że właściciele samochodów mieszkają zawsze pod różnymi adresami) albo jest to ważne z punktu widzenia wydajności zapytań. Natomiast kluczowa jest świadomość, co się robi oraz jak to wpłynie na system, decyzja o denormalizacji powinna być podejmowana tylko przez doświadczonych programistów/projektantów i tylko w wyjątkowych sytuacjach.
Istnieją trzy logiczne typy relacji:
— jeden-do-wielu
— jeden-do-jednego — ten typ relacji jest najmniej intuicyjny, jego praktyczne zastosowanie sprowadza się najczęściej do optymalizacji (czasu zapytań lub rozmiaru danych). Oczywiście ma on swoje uzasadnienie podczas procedury normalizacji. Przykładem mogą być szczegóły użytkownika, np. jego preferencje czy komplet danych (np. w profilu portalu społecznościowego). Mamy więc użytkownika oraz szczegóły użytkownika, jeden rekord jest zawsze związany z jednym rekordem drugiego typu, ale podział ma sens (np. z użyciem tabeli użytkowników dokonujemy uwierzytelnienia, pobieranie za każdym razem wszystkich danych profilu mija się z celem i byłoby niewydajne). Druga sytuacja kiedy często jest to stosowane to wybiórcze dane (np. przy dziedziczeniu i hierarchiach) — np. w szkole, mamy uczniów i nauczycieli, obie te role to po prostu osoby. Osoba ma imię i nazwisko, nauczyciel dodatkowo przedmiot, doświadczenie, stopień i klasę, której jest wychowawcą, uczeń z kolei ma rok nauki i klasę w której jest. Można trzymać wszystkie te dane w jednej tabeli, można całkiem osobno w 2, ale najwygodniej jest trzymać część wspólną (imię, nazwisko) w jednej, szczegóły uczniów w drugiej i szczegóły nauczycieli w trzeciej. Rekord osoby jest więc powiązany z zero lub jednym rekordem szczegółów ucznia (i analogicznie nauczyciela), a szczegóły ucznia są powiązane zawsze z jednym rekordem osoby. To też dobry przykład do zobrazowania różnicy pomiędzy encją (krotką) a wierszem w tabeli (rekordem) — rekord obejmuje jedną tabelę. Encja to wszystkie te informacje, czyli np. wszystkie informacje o uczniu ‘pobrane’ z 2 tabel
— wiele-do-wielu — w praktyce wymaga to utworzenia dodatkowej tabeli, która jest w relacji jeden-do-wielu z obiema tabelami, które są logicznie połączone relacją wiele-do-wielu. Przykładem zastosowania może być system do zarządzania planami lekcji, w których mamy zajęcia (lekcje) oraz uczniów. Każdy uczeń może uczęszczać na wiele zajęć, ale też każde zajęcia są uczęszczane przez wielu uczniów.
Dygresja — NoSQL, czym jest i zastosowania
Ostatnio dość popularne są tzw. bazy NoSQL, na pewno pracując w IT w niedługim czasie spotkasz się z tym określeniem lub nawet będziesz mogła używać takiego oprogramowania w praktyce.
NoSQL to ogólna nazwa dla wszystkich baz danych, które odchodzą od modelu relacyjnego — rodzajów takich baz jest wiele (key-value store, column family itp), dlatego termin ten nie określa jakiego rodzaju jest dana baza danych, a raczej jakiego rodzaju nie jest. W tym kursie nie będziemy zajmować się bazami NoSQL, warto jednak wiedzieć, że często oferują lepszą wydajność od baz relacyjnych oraz bardziej elastyczny model. Potencjalnymi wadami tych rozwiązań są niestandardowe sposoby odpytywania ich, elastyczny model (tak, to zarówno wada jak i zaleta) oraz mniejsze możliwości w zakresie powiązań pomiędzy obiektami. Branża cały czas ‘uczy się’ z nich korzystać, ale światowi giganci nie mogliby osiągnąć obecnego rozmiary bez ich użycia — np. Google używa własnego rozwiązania o nazwie BigData, podczas gdy Amazon także korzysta (i udostępnia w ramach usług AWS) z własnego systemu o nazwie DynamoDB. Koncepcyjnie systemy te bardzo się różnią, zainteresowanych odsyłam do prac naukowych oraz blueprintów na ten temat (pierwsze linki po wpisaniu tych fraz w google).
Podstawy SQL
Przede wszystkim — ten dział traktuj bardziej jako sugestię, co dalej czytać niż jako pełnoprawny kurs. Poniższe informacje to tylko wierzchołek góry lodowej jeśli chodzi o tematykę baz danych i SQL.
W tej części nie będziemy poruszać kwestii wiązania danych w tabelach. Opiera się ono o te same zapytania z dodatkowymi słowami kluczowymi, ale sama teoria i konsekwencje są dość obszernym tematem, który być może poruszymy w przyszłości w ramach dodatkowych kursów.
Pobieranie danych — SELECT
Podstawowym rodzajem zapytań są zapytania typu SELECT, które pozwalają nam pobierać dane z tabeli (lub wielu tabel jednocześnie). Składania wygląda następująco:
SELECT [pola] FROM [tabela] WHERE [warunki]
Przy czym WHERE [warunki] nie jest obowiązkowym elementem. Po kolei:
- [pola] — to lista kolumn i wyrażeń, które chcemy pobrać; kolumny opisujemy po prostu wpisując ich nazwę, możemy też użyć symbolu *, który znaczy ‘pobierz wszystkie dostępne kolumny’; możemy też użyć innych wyrażeń, np. funkcji lub wyrażeń matematycznych (np. cena_netto+podatek, zakładając, że cena_netto i podatek to prawidłowe nazwy kolumn, lub NOW() , która zwraca aktualny czas )
- [tabela] to po prostu nazwa tabeli, z której chcemy pobrać dane
- [warunki] to ciąg logiczny, w którym określamy warunki, jakie musi spełniać każdy wiersz który chcemy pobrać, np. cena_netto>10 lub cena_netto>10 AND cena_netto<30 — do łączenia wielu warunków używamy słów kluczowych AND oraz OR
Wstawianie danych — INSERT
Zapytanie INSERT służy do wstawiania do tabel nowych rekordów. Składnia wygląda następująco:
INSERT INTO [tabela] ([kolumny]) VALUES ([wartosci kolumn])
gdzie:
- [tabela] to oczywiście określenie tabeli, do której chcemy wstawić nowy rekord; podobnie jak w zapytaniu select, wpisujemy po prostu nazwę tabeli
- [kolumny] — to lista kolumn, do których chcemy wpisać dane wymieniona po przecinku; jeśli nie wymienimy tutaj jakiejś kolumny, otrzyma ona wartość NULL (jeśli baza danych na to pozwala, jeśli nie, zostanie zwrócony błąd)
- [wartości kolumn] — to wartości do umieszczenia w poszczególnych kolumnach; ilosć i kolejnosć musi się zgadzać z tym, co wpisaliśmy w [kolumny], w przeciwnym razie otrzymamy błąd; także format danych musi być zgodny z formatem kolumny (szczegółowe informacje można znaleźć w dokumentacji używanej przez nas bazy danych — np. daty najczęściej wpisujemy jako ciąg znaków — w apostrofach — w formacie RRRR-MM-DD)
Aktualizacja danych — UPDATE
Ten rodzaj zapytań jest używany do modyfikacji istniejących już wierszy. Jego składnia przedstawia się następująco:
UPDATE [tabela] SET [nowe wartości] WHERE [warunki]
Tak jak w przypadku zapytania SELECT, WHERE [warunki] jest opcjonalne, ale praktycznie zawsze stosowane (ponieważ chcemy zmienić wybrany wiersz/wiersze, a nie wszystkie).
Elementy [tabela] oraz [warunki] mają taką samą postać jak w przypadku zapytania SELECT. Z kolei [nowe wartości] to lista par kolumna-wyrażenie (np. kolumna1=wartosc1) oddzielona od siebie przecinkami (jeśli zmieniamy wiele kolumn jednocześnie). Co ważne, wyrażenie może odwoływać się do innych kolumn.
Przykład: chcąc zaktualizować stawkę podatku dla produktów które do tej pory miały podatek 22%, możemy wywołać następujące zapytanie:
UPDATE produkty SET vat=0.23, cena_brutto=1.23*cena_netto WHERE vat=0.22
Usuwanie danych — DELETE
To zapytanie ma najprostszą składnie, wygląda ona następująco:
DELETE FROM [tabela] WHERE [warunki]
Podobnie jak w powyższych, część WHERE [warunki] jest opcjonalna. Warunki i tabelę określa się identycznie jak w pozostałych zapytaniach. To zapytanie usunie z tabeli wszystkie wiersze, które spełniają warunki (lub wszystkie wiersze, jeśli nie określimy wspomnianych warunków).
CRUD
Powyższe zapytania stanowią składowe CRUD — akronimu od słów Create, Read, Update, Delete . Te cztery operacje to podstawowe operacje na danych i wszystkie odwołania z poziomu aplikacji do bazy danych sprowadzają się właśnie do tych czterech elementów.
Wartość NULL w baziach danych
Mówiąc o bazie danych należy też wspomnieć o wartości NULL. W programowaniu null oznacza ‘nic’ — np. w Javie porównianie tego rodzaju:
Object o1 = null;
Object o2 = null;
if (o1 == o2) {...}
będzie prawdziwe (tłumacząc na ‘nasz’ — nic, ma dokładnie tą samą wartość co nic).
W bazach danych jest inaczej, ponieważ null oznacza nie ‘nic’, a ‘nie wiem’. Najczęściej nie będzie to problemem ale warto o tym wiedzieć bo możemy się kiedyś spotkać z problemem tego rodzaju np. pisząc zapytanie do bazy danych.
Konsekwencją takiego rozumienia jest sposób działania, w języku SQL zapytanie:
NULL == NULL zwróci NULL. Tak samo zapytanie NULL != NULL także zwróci NULL. Każde porównanie lub uogólniając operacja (technicznie, porównanie to też operacja, podobnie jak dodawanie, mnożenie itp), w którym jeden z operantow to NULL zwróci NULL. Dlatego nieintuicyjny będzie wynik zapytania:
... WHERE tabela.pole = NULL;
które nie zwróci żadnych wartości (nawet, jeśli są rekordy w tabeli tabela, dla których kolumna o nazwie pole ma wartość NULL). Dzieje się tak, ponieważ wynikiem porównania ‘czy nie wiem co to to samo co nie wiem co’ jest zawsze ‘nie wiem’, a więc warunek nie jest spełniony. Prawidłowo zapytanie to powinno wyglądać:
... WHERE ISNULL(tabela.pole);
które można przetłumaczyć jako ‘czy nie wiesz, co jest w kolumnie pole’ .
Kwestia przyzwyczajenia, ale ponownie, jest to coś, co warto mieć na uwadze pracując jako programista.
Zadanie
W ramach zadania na tą lekcję, przygotuj zapytania SQL do tabeli koty, które pozwolą nam wykonywać operacje które mamy w klasie DAO. Zamiast parametrów, użyj póki co stałych wartości.
Pobierz rozwiązanie tego zadaniaJeśli uważasz powyższą lekcję za przydatną, mamy małą prośbę: polub nasz fanpage. Dzięki temu będziesz zawsze na bieżąco z nowymi treściami na blogu ( i oczywiście, z nowymi częściami kursu Javy). Dzięki!