W internecie można znaleźć mnóstwo tutoriali i blogów opisujących łączenie się z bazą SQL. Chciałbym od razu zaznaczyć, że bazami SQL nazywamy mi. MySQL, PostgreSQL, Derby, Firebird, MsSQL czy chociażby SQLite - wszystkie bazy wykorzystujące język SQL :) Niestety Java jest na tyle
rozrośnięta (Java SE, Java EE, Spring Framework, etc.), że ciężko jest znaleźć tutorial, który pasowałby w 100% do naszych potrzeb. A to podejście do tematu jakieś dziwne, a to inny niż stosowany przez nas sposób konfiguracji, a to jeszcze jakieś widzimisię programisty, które psuje klarowność całego przekazu. Można by rzec -
ile tutoriali tyle sposobów na połączenie z bazą. W sieci brakuje jednak tego, czego brakuje większości początkujących programistów - wiedzy na temat podstaw połączenia, mechanizmów i podejścia od podszewki. Nie raz spotkałem się (nawet sam kiedyś popełniłem ten błąd), iż człowiek, który utrzymuje, że zna się na JDBC, MySQL, JPA i Hibernate, zapytany o to jak rozwiązał połączenie z bazą danych odpowiada -
a więc, Spring Framework oferuje automatyczne połączenie za pośrednictwem CRUD repository... Tworzymy takie repozytorium jako obiekt DAO dla naszej Encji i połączenie gotowe.... Następne pytania ze strony osoby zainteresowanej są z serii śmiertelnych -
A gdzie tu miejsce na Hibernate? Co się wydarzyło z JPA? Czy JDBC to tylko Driver w tym przypadku? Jak to działa? Całą rozmowę można zakończyć w tym momencie, gdyż prawdopodobnie nie dostaniemy odpowiedzi od tej osoby.
Wracając jednak do tematu. Powyższej sytuacji na zasadzie -
Działa? Działa! No to o co chodzi? - powinniśmy wystrzegać się jak ognia! W poniższym wpisie postaram się przedstawić
podstawy współpracy Javy i MySQL'a tak, abyśmy mogli w przyszłych wpisach usystematyzować wiedzę na ten temat.
Content:
- JDBC i jego alternatywy
- Prosty przykład połączenia aplikacji konsolowej z bazą MySQL
JDBC i jego alternatywy
Java DataBase Connectivity jest biblioteką która zawiera API's służce do łączenia naszej aplikacji napisanej w języku Java z bazą danych opartą o język SQL. Biblioteka ta pozwala nam:
- tworzyć połączenia z bazą danych
- tworzyć zapytania SQL i MySQL (istnieją różnice)
- wykonywać zapytania na bazie danych
- przeglądać, wyświetlać oraz modyfikować wyniki pobrane z bazy danych
JDBC jest w gruncie rzeczy zbiorem interfejsów, które można wykorzystać przy projektowaniu
całych aplikacji Java, Servletów, Apletów, stron JSP, ziaren EJB.
Architektura JDBC sprowadza się do dwuch elementów:
- JDBC API - odpowiada za łączenie naszej aplikacji z menadżerem JDBC (JDBC Manager)
- JDBC Driver API - odpowiada za połączenie menadżera JDBC ze sterownikiem (JDBC Driver)
Warto w tym miejscu przedstawić z jakich komponentów składa się JDBC API:
- Driver Manager - ta klasa zarządza listą sterowników (driverów) łącząc je z odpowiednimi żądaniami wychodzącymi z naszej aplikacji.
- Driver - ten interfejs jest abstrakcją nakładaną na czynności wykonywane poprzez nasz Driver. Odpowiada on bezpośrednio za kontakt z naszą bazą danych. Driver Manager operuje na obiektach typu Driver.
- Connection - ten interfejs posiada wszystkie niezbędne metody do zarządzania połączeniem z bazą danych. To właśnie przez niego wykonywane są wszystkie czynności związane z łącznością. Connection zwraca kontekst naszego połączenia.
- Statement - interfejs wykorzystywany jako reprezentacja zapytań przekazywanych do bazy danych.
- ResultSet - obiekt zachowujący się jak iterator. Przechowuje w sobie dane pobrane z bazy danych po wykonaniu zapytania przy użyciu obiektu Statement.
- SQLException - klasa obsługująca wyjątki powstałe w trakcie działania aplikacji bazodanowej
Alternatywy
Jest to temat głęboki, a za razem bardzo płytki. Jeśli chodzi o alternatywę dla
JDBC, to w ogólnym rozrachunku mamy
ODBC. Jest to jednak rozwiązanie, które nie powinno za bardzo interesować programistów Java. Dlaczego? Po pierwsze ODBC jest zbiorem interfejsów języka C. Gdyby tego było mało, JDBC implementuje funkcjonalność ODBC, gdyż zostało stworzone na jego podstawie. W tym miejscu zostawmy dalsze rozważania na ten temat - przyjmijmy, że
jedynym słusznym niskopoziomowym API dla Javy jest JDBC.
Ważniejszą kwestią są jego różne poziomy abstrakcji (
API wyższego poziomu), które na przestrzeni czasu powstawały. Ze względu na zamiłowanie do historii mógłbym tutaj wspomnieć o
EJB 1.0 CMP Entity Beans, EJB 2.0 CMP czy chociażby
JDO. Wszystkie one mają dwie cechy wspólne:
- Wszystkie na końcu wykorzystują JDBC do łączności z bazą
- Są do niczego ze względu na skomplikowanie (EJB 1 i 2) lub ze względu na błędne założenia (JDO)
Z czasem jednak pojawiły się rozwiązania, które faktycznie okazały się przydatne, pokrywając swoją
abstrakcją sporą część zbędnego kodu. Znaczy to tyle, że zaoszczędzały czas zapobiegając pisaniu kodu, który jest oczywisty. Mowa oczywiście o Hibernate i JPA(w innym poście porozmawiamy o innych rozwiązaniach tj. MyBatis, JOOQ, Spring JDBC Template, Spring Data czy Sormula).
Prosty przykład połączenia aplikacji konsolowej z bazą MySQL
Poniżej zamieszczam przykład połączenia za pomocą
DriverManager'a (
najbardziej podstawowy sposób). Jest jeszcze możliwość połączenia za pomocą
DataSource (
preferowane rozwiązanie), które daje nam dużo możliwości, mi. wykorzystanie
puli połączeń (
connection pool). Jest to jednak temat na inny post. Przejdźmy za tem do rzeczy:
package connbymanager;
import java.sql.*;
public class ConnByManager {
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver"; // Podajemy gdzie znajduje się nasz sterownik
static final String DB_URL = "jdbc:mysql://localhost:3306/"; // Adres naszego serwera MySQL
static final String USER = "root"; // Nazwa użytkownika bazy
static final String PASS = "root"; // Hasło użytkownika bazy
public static void main(String[] args) {
Connection connection = null; // ustawiamy zmienną referencyjną dla naszego połączenia na NULL
// Próbę nawiązania połączenia zamykamy w bloku try/catch
try {
Class.forName(JDBC_DRIVER); // Za pomocą refleksji wywołujemy klasę naszego sterownika
System.out.println("Otwieram połączenie...");
connection = DriverManager.getConnection(DB_URL, USER, PASS); // W tym miejscu pod nasze
System.out.println("Połączenie utworzone"); // connection ustawiamy obiekt naszego
// DriverManagera
} catch (SQLException se) { // Wyłapujemy błędy związane z MySQL,
se.printStackTrace(); // oraz inne błędy, które mogą powstać podczas
} catch (Exception e) { // realizacji naszego bloku try/catch
e.printStackTrace();
} finally{
connection.close(); // Zamykamy połączenie, aby uniknąć
System.out.println("Połączenie zamknięte"); // wycieku pamięci
}
System.out.println("Gotowe!");
}
}
W powyższym kodzie mamy kilka podstawowych i kilka ważnych elementów. Zacznijmy od początku.
- linie od 7 do 11 - do zmiennych String przypisujemy najważniejsze parametry naszego połączenia. Zmienne dla bezpieczeństwa ustawione jako static i final. (static - dostęp bez instancji klasy; final - nie podlega zmianom referencja do tego obiektu [co to znaczy przeczytasz tutaj].
- linia 14 - deklarujemy zmienną referencyjną, która przyjmie wartość obiektu typu Connection. W tym momencie ustawiamy ją na null, dzięki czemu oczekuje ona na przypisanie wartości (miejsca w pamięci), która będzie reprezentować nasz obiekt. Więcej na temat zmiennych i zmiennych referencyjnych przeczytasz tutaj.
- linie 18 do 33 - blok try/catch - większość młodych programistów zamieszcza go w miejscu, w którym wymaga tego kompilator lub gdy wyświetlają się im błędy podczas kompilacji w danym miejscu... My użyliśmy go bardziej świadomie (bez niego kod również by się skompilował). Połączenie z jakimkolwiek zewnętrznym źródłem danych (pliki, bazy danych, web serwisy) jest zazwyczaj niestabilne i może przebiegać w sposób nieprzewidywalny. Dlatego stosujemy ten blok aby wyłapać wszystkie ewentualne błędy i na nie zareagować. W naszym przypadku do każdego wyjątku stosujemy printStackTrace(), aby było nam łatwiej namierzyć występujący wyjątek.
- linia 19 - przypisujemy JDBC Driver, z którego będziemy korzystać przy tym konkretnym połączeniu. Wykorzystujemy do tego mechanizm refleksji. Można również wykorzystaćmetodę registerDriver() (jednak wymusza to obecność naszego Drivera w czasie kompilacji programu [może prowadzić do NoClassDefFoundError] - Class.forName() wiąże sterownik w momencie w którym jest potrzebny [late binding]).
- linia 22 - w tym miejscu nasz connection zyskuje niezbędne właściwości do nawiązania połączenia, tj. odpowiedni sterownik, adres naszej bazy, login oraz hasło. Wygląda to w ten sposób: DriverManager poprzez metodę getConnection() tworzy połączenie, które jest obiektem Connection. Następnie zostaje ono przekazane jako wartość naszej zmiennej referencyjnej connection.
- linia 30 - blok finally wykonywany jest nawet w momencie, w którym blok try/catch zwróci wyjątek. Instrukcję finally blokuje tylko i wyłącznie system.exit()! Stąd mamy pewność, że połączenie z naszą bazą zostanie bezwarunkowo zakończone.
- linia 31 - metoda close() zamyka połączenie. W tym przykładzie wygląda to mało sensownie, ale za chwilę zobaczymy przykład, który naświetli nam sens tej operacji. Warto jednak wiedzieć, że natychmiastowo zwalnia ona obiekt connection oraz wartości przypisane do JDBC zamiast czekać na ich automatyczne zwolnienie. Pozwala to utrzymać czystość w naszej JVM oraz uniknąć utrzymywania bezsensownego połączenia.
Aby przykład miał większy sens postaramy się w naszym serwerze stworzyć nową bazę danych, oraz wpisać do nie przykładową tabelę. Oto przykład:
package connbymanager;
import java.sql.*;
public class ConnByManager {
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver"; // Podajemy gdzie znajduje się nasz sterownik
static final String DB_URL = "jdbc:mysql://localhost:3306/"; // Adres naszego serwera MySQL
static final String USER = "root"; // Nazwa użytkownika bazy
static final String PASS = "root"; // Hasło użytkownika bazy
public static void main(String[] args) {
Connection connection = null; // deklarujemy zmienną referencyjną dla naszego połączenia na NULL
Statement stmt = null; // deklarujemy zmienną referencyjną dla naszego stmt na NULL
try{
Class.forName(JDBC_DRIVER); // Za pomocą refleksji wywołujemy klasę naszego sterownika
System.out.println("Otwieram połączenie...");
connection = DriverManager.getConnection(DB_URL, USER, PASS); // W tym miejscu pod naszą zmienną
System.out.println("Połączenie utworzone"); // connection ustawiamy obiekt naszego
// DriverManagera
System.out.println("Tworzę pierwsze zapytanie do bazy");
stmt = connection.createStatement(); // Do naszego stmt przypisujemy obiekt
// typu Statement stworzony przez metodę createStatement()
System.out.println("Tworzę nową bazę danych");
String sql_db = "CREATE DATABASE studenci;"; // Do zmiennej sql_db przypisujemy instrukcje dla naszego
// serwera, która stworzy nową bazę - studenci
stmt.executeUpdate(sql_db); // Wykonujemy realizacji wcześniej przygotowanej instrukcji
System.out.println("Nowa baza stworzona");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/studenci", USER, PASS);
System.out.println("Tworzę tabelę REGISTRATION");
String sql = "CREATE TABLE REGISTRATION" +
"(id INTEGER not NULL, " +
" first VARCHAR(255), " + // Przypisujemy do zmiennej sql instrukcje,
" last VARCHAR(255), " + // które wyślemy do naszej bazy studenci
" age INTEGER, " +
" PRIMARY KEY ( id ))";
stmt = connection.createStatement(); // Do stmt przypisujemy dane nowego połączenia
stmt.executeUpdate(sql); // Wykonujemy instrukcje
System.out.println("Tabela stworzona!");
}catch(SQLException se){
se.printStackTrace();
}catch(Exception e){
e.printStackTrace();
}finally{
try{
if(stmt != null) // Sprawdzamy czy zmienna stmt ma przypisaną wartość
connection.close(); // Jeśli tak to zamykamy połączenie
}catch(SQLException e){
e.printStackTrace();
}
}
System.out.println("Gotowe!");
}
}
Zastanówmy się przez chwilę nad nowo dodanymi elementami:
- linia 15 - tworzymy zmienną referencyjną tak samo jak w przypadku connection (linia 14).
- linia 26 - do naszej zmiennej stmt typu Statement przypisujemy obiekt tego samego typu wygenerowany przez metodę createStatement(). Ta metoda służy w tym przypadku do przekazania parametrów z istniejącego obiektu connection do naszego obiektu do którego odwołuje się zmienna stmt. Dzięki takiemu podejściu do tematu, wszystkie elementy odwołują się do tego samego połączenia.
- linia 30 - w tym miejscu do naszej zmiennej sql_db typu String przypisujemy instrukcje, które są następnie przekazywane do naszego serwera/bazy danych. Ta zmienna służy tylko przekazaniu ciągu znaków. Można to zadanie wykonać bez jej użytku, aczkolwiek tworzenie nowej zmiennej ma swoje korzyści, o których napiszę przy innej okazji.
- linia 32 - w tym miejscu wykonuje się główna operacja przekazania instrukcji do bazy danych. Metoda executeUpdate() przyjmuje tylko instrukcje typu INSERT, UPDATE i DELETE lub inne, które nie zwracają żadnych wyników w języku SQL (do zwracania wyników służą inne metody, np. executeQuery())
- linia 35 - zmieniamy wartość zmiennej connection ze względu na zmianę adresu naszej bazy (pierwsze instrukcje wysyłaliśmy do naszego serwera MySQL [jdbc:mysql://localhost:3306/]- tworzenie nowej bazy danych, teraz wysyłamy instrukcje bezpośrednio do naszej nowej bazy studenci [jdbc:mysql://localhost:3306/studenci]). Login i hasło do serwera pozostają takie same...
- linia 43 - ... dodatkowo musimy przypisać naszej zmiennej stmt nowe właściwości połączenia, tak jak robiliśmy to pierwotnie w linii 26.
- linie 51 do 56 - blok try/catch umieszczony wewnątrz instrukcji finally odpowiedzialny za sprawdzenie czy faktycznie do zmiennej stmt została przypisana jakaś wartość. Jeśli tak połączenie ma zostać natychmiast zakończone. Jest to zabezpieczenie przed wyciekami pamięci.
To by było na tyle jeśli chodzi o podstawy podstaw. Gdyby was ktoś kiedyś zapytał, w jaki sposób łączycie się za bazą danych - nie odpowiadajcie
za pomocą Springa. Jeśli wykorzystujecie jakieś rozwiązanie, to zajrzyjcie mu pod skórę, i dowiedzcie się jak to wszystko tak naprawdę działa. W zamian dostaniecie mniej nieprzewidzianych błędów i wiedzę, która prędzej czy później się przyda.
Jak zwykle zachęcam do komentowania i kontaktu.
Brak komentarzy:
Prześlij komentarz