#12 – używamy bazy danych ze Sprigiem

By 13 listopada 2014Kurs Javy
Wpis-Header-16

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:

  1. wywołując metodę Connection.createStatement() a następnie metodę Statement.execute(String sql)
  2. 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

Zmody­fikuj pro­gram, który już napisałaś, tak, aby w klasie DAO wszys­tkie metody korzys­tał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ć).

Licencja Creative Commons

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!

  •  
  •  
  •  
  •  
  •  
  • mk

    Hej,
    czy do tej lekcji będzie dołączone rozwiązanie?

    • grzes

      Dołączam się do pytania przedmówcy: jak to ma być skonfigurowane, żeby działało? dodanie beana do projektu koty-webapp oraz @Autowired private DataSource dataSource; powoduje, że aplikacja się wysypuje ;/
      Będę niezmiernie wdzięczny za dodanie rozwiązania do tego zadania.

  • zj

    Hej :) Czy dodacie w najblizszym czasie rozwiązania? Nie moge sobie poradzic z modyfikacja metod w klasie DAO. Musze najpierw stworzyc tabele w ktorej będą koty? Jak zmodyfikowac funkcje zwrcająca wszystkie koty z bazy danych? Wystarczy wpisac polecenie „SELECT*FROM koty”?