#12 — używamy bazy danych ze Sprigiem

By 13 November 2014 Kurs Javy

W poprzed­niej lekcji poz­nal­iśmy pod­stawy teorii baz danych oraz skład­ni języ­ka SQL — dzisi­aj wyko­rzys­tamy tę wiedzę w praktyce.

Dzisiejsza lekc­ja doty­czyć będzie korzys­ta­nia z bazy danych w Springu bezpośred­nio z uży­ciem JDBC. To ważne, ponieważ Spring zapew­nia także uproszc­zone inter­fe­jsy i klasy które wspier­a­ją pracę z bazą danych z uży­ciem SQL (np. Named­PA­ra­me­ter­Jd­bcTem­plate), ale pod­sta­wowa zasa­da jest ta sama. Tym bardziej, że nie jest to obec­nie częs­to stosowana meto­da, poz­wolimy sobie na uproszcze­nie i jedyne ogólne omówienie.

W Springu do inter­akcji z bazą danych moż­na użyć także mod­ułu Spring Data — o nim dzisi­aj nie będziemy mówić, ponieważ poz­namy go bliżej w kole­jnych lekcjach.

Lekcja

Na początku zajmiemy się samym Data­Source i jego podłącze­niem w Springu. Data­Source to nic innego jak właśnie ‘źródło danych’. Różni­ca pomiędzy Data­Source (nazy­wanego też DS) a bazą danych jest taka, że DS może zaw­ier­ać też ele­men­ty związane z opty­mal­iza­cją zapy­tań, pulę połączeń itp. Pracu­jąc z DS korzys­tamy z State­ments — czyli poje­dynczych zapy­tań, ale o nich powiemy sobie więcej w dal­szej częś­ci lekcji.

Dodajemy DataSource do naszego projektu

Dodanie Data­Source do pro­jek­tu Spring jest try­wial­nie proste, wystar­czy dodać beana, który imple­men­tu­je inter­fe­js javax.sql.DataSource . W tym przykładzie uży­je­my klasy org.springframework.jdbc.datasource.DriverManagerDataSource nato­mi­ast należy mieć świado­mość, że nie jest to rozwiązanie pro­duk­cyjne (wyjas­nie­nie poniżej)! Mając skon­fig­urowaną bazę MySQL dodoa­je­my poniższą deklarac­je 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że­my korzys­tać z naszego DS tak samo jak z każdego innego beana, np. doda­jąc w naszej klasie DAO:

@Autowired
private DataSource dataSource;

DataSource w aplikacjach produkcyjnych

Wspom­ni­ana wcześniej klasa org.springframework.jdbc.datasource.DriverManagerDataSource korzys­ta bezpośred­nio z połączenia inicjowanego w sterown­iku JDBC. Oznacza to, że każde zapy­tanie spowodu­je utworze­nie nowego połączenia do bazy danych — jak łat­wo się domyśleć w przy­pad­ku aplikacji dostęp­nych dla klien­tów, szy­bko spowodu­je to wycz­er­panie lim­i­tu połączeń i jest nieop­ty­malne (naw­iązanie połaczenia częs­to trwa dłużej niż samo zapytanie).

W aplikac­jach pro­duk­cyjnych uży­wamy tzw. puli połączeń — ten rodzaj DS otwiera n połączeń i prze­chowu­je je do użytku w przyszłoś­ci. Dzię­ki temu nie ma potrze­by tworzenia nowego połączenia za każdym razem, co popraw­ia też wyda­jność całego systemu.

Jeśli chodzi o dostęp­ne pule, moim fawory­tem jest c3p0, ale częs­to moż­na się spotkać także z Apache Com­mons DBCP. Nie pode­jmę się wskaza­nia ‘lep­szej’ z tych opcji, bo nigdy nie miałem okazji testować obu rozwiązań jed­nocześnie w prak­tyce. c3p0 było moim wyborem ponieważ w okre­sie kiedy decy­dowałem o wyborze tech­nologii, bib­liote­ka ta była akty­wniej rozwi­jana. Z tego co mi wiado­mo, DBCP od tamtego cza­su sporo się zmieniło, gdy­byś miała jakieś doświad­czenia w tym tema­cie, zachę­cam do podzie­le­nia się nimi w komentarzach :)

Korzystamy z DataSource w kontrolerach (i innych beanach)

Sam kod wykonu­ją­cy zapy­tanie np. pobra­nia jed­nego 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 zapy­ta­nia. To co się dzieje w powyższym kodzie, to najpierw pobier­amy połącze­nie z DS, tworzymy Pre­pared­State­ment na pod­staw­ie naszego kodu SQL, następ­nie uzu­peł­ni­amy jego para­me­try (te ele­men­ty, które oznaczyliśmy jako ? w zapy­ta­niu SQL), sprawdza­my czy wynik zapy­ta­nia (Result­Set) zwró­cił jeden wier­sz (meto­da next() zwraca wartość true, jeśli następ­ny wier­sz jest dostęp­ny oraz ustaw­ia wskaźnik na ten włas­nie kole­jny wier­sz, jeśli jest to możli­we) i jeśli tak, to tworzymy nowy obiekt typu Kot i wypeł­ni­amy jego pola na pod­staw­ie wier­sza wyniku. Na koniec zamykamy połączenia, zwal­ni­amy zaso­by itd. Jed­nym słowem sporo kodu, który nie jest odpowiedzial­ny za jakąś funkcjon­al­ność a jedynie jest potrzeb­ny w tym przy­pad­ku do wykony­wa­nia czyn­noś­ci ‘dookoła’ (tzw. boil­er­plate code). W kole­jnych lekc­jach zobaczymy jak zro­bić to łatwiej i przyjemniej.

Zas­trzeże­nie — powyższy kod nie do koń­ca przed­staw­ia dobre wzorce w pewnych miejs­cach, np. powin­niśmy kat­e­go­rycznie unikać ‘połyka­nia’ wyjątków, czyli klauzuli catch, która nic nie robi (nie zapisu­je nawet logu). Przykład jed­nak ma obra­zować sposób inter­akcji z bazą danych i dodawanie dodatkowego kodu mogło­by ‘rozmyć’ to, co najważniejsze.

Parametryzacja zapytania

W przy­pad­ku Pre­pared­State­ment może­my para­me­try­zować zapy­ta­nia — tzn. w samym zapy­ta­niu SQL uży­wać tzw. place­hold­erów (znak ‘?’), a następ­nie przyp­isy­wać im wartoś­ci wywołu­jąc metody takie jak PreparedStatement.setInt(…), gdzie pier­wszy argu­ment to pozy­c­ja kole­j­na (patrząc od lewej) para­metru, który chce­my ustaw­ić, a dru­gi argu­ment to wartość, jaką chce­my mu nadać. Co ważne, wyjątkowo w tym przy­pad­ku numeru­je­my od ‘1’ (wszys­tkie pozostałe mod­uły języ­ka Java, które jestem w stanie sobie przy­pom­nieć, zaczy­na­ją numer­ację od ‘0’), więc pier­wszy argu­ment ma indeks 1, dru­gi dwa itp.

Róznica pomiędzy Statement a PreparedStatement

Zapy­ta­nia do bazy danych może­my wykony­wać na dwa sposoby:

  1. wywołu­jąc metodę Connection.createStatement() a następ­nie metodę Statement.execute(String sql)
  2. wywołu­jąc metodę Connection.prepareStatement(String sql) a następ­nie metodę PreparedStatement.execute()

W przy­pad­ku zapy­tań, które zwraca­ją dane (np. zapy­ta­nia typu Select) dru­ga meto­da ma nazwę exe­cute­Query (typ zwracany to Result­Set zami­ast boolean)

Jak sama widzisz, różni­ca jest w tym, kiedy poda­je­my samo zapy­tanie SQL (na końcu vs początku). Jest to spowodowane tym, że Pre­pared­State­ment wstęp­nie kom­pilu­je zapy­tanie SQL skra­ca­jąc czas jego wykony­wa­nia — jest to bard­zo korzystne, kiedy wykonu­je­my podob­ne zapy­tanie wiele razy (np. w pętli) zmieni­a­jąc jedynie pewne para­me­try. Dodatkową korzyś­cią jest to, że Pre­pared­State­ment zabez­piecza nas przed ataka­mi typu SQL injec­tion (dzię­ki temu, że może­my uży­wać para­metrów — w przy­pad­ku zapy­tań typu State­ment sami musimy o to zad­bać i wstaw­ić je do zapy­ta­nia ręcznie, kon­stru­u­jąc String’a). Dobrą prak­tyką jest korzys­tanie właśnie z Pre­pared­State­ment, chy­ba, że nie jest to możli­we (pewne ele­men­ty zapy­tań SQL wyma­ga­ją uży­cia Springowej klasy Named­Pa­ra­me­ter­Jd­bcTem­plate jes­li chce­my uniknąć ręcznego kon­struowa­nia zapy­tań 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ą zapy­tań SQL.

Jak pewnie sama zauważysz, spo­ra część kodu będzie się pow­tarzać. W ramach ćwiczenia z pro­gramowa­nia obiek­towego, możesz spróbować jak najwięcej wspól­nego kodu wyciągnąć do zewnętrznej metody (zop­ty­mal­i­zować).

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przy­dat­ną, mamy małą prośbę: pol­ub nasz fan­page. Dzię­ki temu będziesz zawsze na bieżą­co z nowy­mi treś­ci­a­mi na blogu ( i oczy­wiś­cie, z nowy­mi częś­ci­a­mi kur­su Javy). Dzięki!