W poprzedniej lekcji poznaliśmy podstawy teorii baz danych oraz składni języka SQL — dzisiaj wykorzystamy tę wiedzę w praktyce.
Dzisiejsza lekcja dotyczyć będzie korzystania z bazy danych w Springu bezpośrednio z użyciem JDBC. To ważne, ponieważ Spring zapewnia także uproszczone interfejsy i klasy które wspierają pracę z bazą danych z użyciem SQL (np. NamedPArameterJdbcTemplate), ale podstawowa zasada jest ta sama. Tym bardziej, że nie jest to obecnie często stosowana metoda, pozwolimy sobie na uproszczenie i jedyne ogólne omówienie.
W Springu do interakcji z bazą danych można użyć także modułu Spring Data — o nim dzisiaj nie będziemy mówić, ponieważ poznamy go bliżej w kolejnych lekcjach.
Lekcja
Na początku zajmiemy się samym DataSource i jego podłączeniem w Springu. DataSource to nic innego jak właśnie ‘źródło danych’. Różnica pomiędzy DataSource (nazywanego też DS) a bazą danych jest taka, że DS może zawierać też elementy związane z optymalizacją zapytań, pulę połączeń itp. Pracując z DS korzystamy z Statements — czyli pojedynczych zapytań, ale o nich powiemy sobie więcej w dalszej części lekcji.
Dodajemy DataSource do naszego projektu
Dodanie DataSource do projektu Spring jest trywialnie proste, wystarczy dodać beana, który implementuje interfejs javax.sql.DataSource . W tym przykładzie użyjemy klasy org.springframework.jdbc.datasource.DriverManagerDataSource natomiast należy mieć świadomość, że nie jest to rozwiązanie produkcyjne (wyjasnienie poniżej)! Mając skonfigurowaną bazę MySQL dodoajemy poniższą deklaracje do naszego pliku XML:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/koty" />
<property name="username" value="login" />
<property name="password" value="haslo" />
</bean>
Od tej pory możemy korzystać z naszego DS tak samo jak z każdego innego beana, np. dodając w naszej klasie DAO:
@Autowired
private DataSource dataSource;
DataSource w aplikacjach produkcyjnych
Wspomniana wcześniej klasa org.springframework.jdbc.datasource.DriverManagerDataSource korzysta bezpośrednio z połączenia inicjowanego w sterowniku JDBC. Oznacza to, że każde zapytanie spowoduje utworzenie nowego połączenia do bazy danych — jak łatwo się domyśleć w przypadku aplikacji dostępnych dla klientów, szybko spowoduje to wyczerpanie limitu połączeń i jest nieoptymalne (nawiązanie połaczenia często trwa dłużej niż samo zapytanie).
W aplikacjach produkcyjnych używamy tzw. puli połączeń — ten rodzaj DS otwiera n połączeń i przechowuje je do użytku w przyszłości. Dzięki temu nie ma potrzeby tworzenia nowego połączenia za każdym razem, co poprawia też wydajność całego systemu.
Jeśli chodzi o dostępne pule, moim faworytem jest c3p0, ale często można się spotkać także z Apache Commons DBCP. Nie podejmę się wskazania ‘lepszej’ z tych opcji, bo nigdy nie miałem okazji testować obu rozwiązań jednocześnie w praktyce. c3p0 było moim wyborem ponieważ w okresie kiedy decydowałem o wyborze technologii, biblioteka ta była aktywniej rozwijana. Z tego co mi wiadomo, DBCP od tamtego czasu sporo się zmieniło, gdybyś miała jakieś doświadczenia w tym temacie, zachęcam do podzielenia się nimi w komentarzach :)
Korzystamy z DataSource w kontrolerach (i innych beanach)
Sam kod wykonujący zapytanie np. pobrania jednego kota wygląda następująco:
String sql = "SELECT * FROM koty WHERE kot_id = ?";
Connection conn = null;
try {
conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, kotId);
Kot kot = null;
ResultSet rs = ps.executeQuery();
if (rs.next()) {
kot = new Kot();
kot.setId(rs.getInt("kot_id"));
//... itp.
}
rs.close();
ps.close();
return kot;
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {}
}
}
Jak widać, sporo tutaj kodu dookoła samego zapytania. To co się dzieje w powyższym kodzie, to najpierw pobieramy połączenie z DS, tworzymy PreparedStatement na podstawie naszego kodu SQL, następnie uzupełniamy jego parametry (te elementy, które oznaczyliśmy jako ? w zapytaniu SQL), sprawdzamy czy wynik zapytania (ResultSet) zwrócił jeden wiersz (metoda next() zwraca wartość true, jeśli następny wiersz jest dostępny oraz ustawia wskaźnik na ten własnie kolejny wiersz, jeśli jest to możliwe) i jeśli tak, to tworzymy nowy obiekt typu Kot i wypełniamy jego pola na podstawie wiersza wyniku. Na koniec zamykamy połączenia, zwalniamy zasoby itd. Jednym słowem sporo kodu, który nie jest odpowiedzialny za jakąś funkcjonalność a jedynie jest potrzebny w tym przypadku do wykonywania czynności ‘dookoła’ (tzw. boilerplate code). W kolejnych lekcjach zobaczymy jak zrobić to łatwiej i przyjemniej.
Zastrzeżenie — powyższy kod nie do końca przedstawia dobre wzorce w pewnych miejscach, np. powinniśmy kategorycznie unikać ‘połykania’ wyjątków, czyli klauzuli catch, która nic nie robi (nie zapisuje nawet logu). Przykład jednak ma obrazować sposób interakcji z bazą danych i dodawanie dodatkowego kodu mogłoby ‘rozmyć’ to, co najważniejsze.
Parametryzacja zapytania
W przypadku PreparedStatement możemy parametryzować zapytania — tzn. w samym zapytaniu SQL używać tzw. placeholderów (znak ‘?’), a następnie przypisywać im wartości wywołując metody takie jak PreparedStatement.setInt(…), gdzie pierwszy argument to pozycja kolejna (patrząc od lewej) parametru, który chcemy ustawić, a drugi argument to wartość, jaką chcemy mu nadać. Co ważne, wyjątkowo w tym przypadku numerujemy od ‘1’ (wszystkie pozostałe moduły języka Java, które jestem w stanie sobie przypomnieć, zaczynają numerację od ‘0’), więc pierwszy argument ma indeks 1, drugi dwa itp.
Róznica pomiędzy Statement a PreparedStatement
Zapytania do bazy danych możemy wykonywać na dwa sposoby:
- wywołując metodę Connection.createStatement() a następnie metodę Statement.execute(String sql)
- wywołując metodę Connection.prepareStatement(String sql) a następnie metodę PreparedStatement.execute()
W przypadku zapytań, które zwracają dane (np. zapytania typu Select) druga metoda ma nazwę executeQuery (typ zwracany to ResultSet zamiast boolean)
Jak sama widzisz, różnica jest w tym, kiedy podajemy samo zapytanie SQL (na końcu vs początku). Jest to spowodowane tym, że PreparedStatement wstępnie kompiluje zapytanie SQL skracając czas jego wykonywania — jest to bardzo korzystne, kiedy wykonujemy podobne zapytanie wiele razy (np. w pętli) zmieniając jedynie pewne parametry. Dodatkową korzyścią jest to, że PreparedStatement zabezpiecza nas przed atakami typu SQL injection (dzięki temu, że możemy używać parametrów — w przypadku zapytań typu Statement sami musimy o to zadbać i wstawić je do zapytania ręcznie, konstruując String’a). Dobrą praktyką jest korzystanie właśnie z PreparedStatement, chyba, że nie jest to możliwe (pewne elementy zapytań SQL wymagają użycia Springowej klasy NamedParameterJdbcTemplate jesli chcemy uniknąć ręcznego konstruowania zapytań SQL).
Zadanie
Zmodyfikuj program, który już napisałaś, tak, aby w klasie DAO wszystkie metody korzystały z bazy danych za pomocą zapytań SQL.
Jak pewnie sama zauważysz, spora część kodu będzie się powtarzać. W ramach ćwiczenia z programowania obiektowego, możesz spróbować jak najwięcej wspólnego kodu wyciągnąć do zewnętrznej metody (zoptymalizować).
Jeś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!