środa, 10 listopada 2010

Jdbc w spring

Dziś omówię dostęp do danych za pomocą JdbcTempate.
Praca z jdbc jest nużąca i nastręcza wiele problemów, stąd zapewne powstały w javie i są bardzo popularne różny ORM-y. Czemu nie piszę zatem o JPA czy hibarnate (przyjdzie i na to czas) - otóż uważam, że w pewnych sytuacjach jdbc jest idealnym wyborem w dostępie do danych. Jak to mawia mój kolega: "nie strzelajmy z armaty do muchy".
Pracując z jdbc musimy obsługiwać transakcje, zarządzać połączeniem, obsługiwać wyjątki (a cóż można zrobić jak poleci SQLException przy zamykaniu połączenia ?). Generalnie trzeba napisać mnóstwo kodu technicznego, obsługującego specyfikę jdbc, aby móc wykonać jakikolwiek zapytanie. I pewnie jest to świetne miejsce aby skorzystać z dobrodziejstw aspektów. Można, ale nie trzeba - inni (deweloperzy ze springsource) zrobili to już za nas dając JdbcTemplate.


Zatem takiego daje nam JdbcTemplate:
  • zarządza obiektem Connection
  • na nim tworzy PreparedStatement
  • uruchamia zapytanie
  • przetwarza wyniki
  • obsługuje wyjątki (tłumaczy je)
Mówiąc inaczej : korzystając z JdbcTemplate jedyne co robimy to piszemy zapytanie i obrabiamy wyniki.
Aby utworzyć obiekt jdbcTemplate potrzebujemy mieć skonfigurowany dataSource (javax.sql.DataSource).
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); 
Spring zapewnia że obekt ten jest bezpieczny w środowisku wielowątkowym i może być z powodzeniem reużywany (nie tworzymy nowego jdbcTemplate per zapytanie).
Spring udostępnia również SimpleJdbcTemplate - jest to powiedzmy JdbcTemplate plus Java 5 (korzysta z generyków, varargs-ów). Jednakże SimpleJdbcTemplate nie posiada wszystkich możliwości JdbcTemplate - wtedy możemy na SimpleJdbcTemplate wywołać metodę getJdbcOperations() i mamy stare JdbcTemplate.
Stwórzmy dwie proste tabele w bazie danych:
drop table T_USER if exists;
drop table T_ACCOUNT if exists;

create table T_USER (ID integer identity primary key, NAME varchar(50) not null);
create table T_ACCOUNT (ID integer identity primary key, USER_ID integer not null, ACCOUNT_NAME varchar(25));
       
alter table T_ACCOUNT add constraint FK_USER_ID foreign key (USER_ID) references T_USER(ID) on delete cascade;
insert into T_USER (ID, NAME) values (1, 'Ala');
Jak widać mamy tabele użytkownika i jego konta, wrzucamy do bazy jednego testowego użytkownika. Nie tworzę sekwencji dla zachowania prostoty przykładu. Powiedzmy, że chcemy dla użytkownika "Ala" dodać konto. Korzystam z bazy hsqldb. Konfiguruję najpierw dataSource:
<jdbc:embedded-database id="dataSource">
    <jdbc:script location="classpath:schema.sql" />
</jdbc:embedded-database>
Korzystam z namespace 'jdbc' i tą prostą konfiguracją załatwiam dataSource dla bazy hsqldb (oczywiście jar z hsqlbd musi być na classpath). Chcę springa konfigurować adnotacyjnie, więc:
<context:component-scan base-package="pl.turo.spring.jdbc" />
<context:annotation-config />
Tworzę dwie klasy domenowe :
public class Account {
    private Integer id;
    private String accountName;
    private Integer userId;
    // get-y, set-y, hashcode i equals
}

public class AppUser {
    private Integer id;
    private String name;
    // get-y, set-y, hashcode i equals
}
Jak widać nie mapuję relacji - nie mam takiej potrzeby. Teraz zaczyna się robić ciekawie. Z (Simple)JdbcTemplate można korzystać w różny sposób.
Można podziedziczyć z klasy (Simple)JdbcDaoSupport:
@Repository
public class SimpleJdbcDaoSupportUserDaoImpl extends SimpleJdbcDaoSupport implements UserDao {
    private static final Logger logger = Logger.getLogger(SimpleJdbcDaoSupportUserDaoImpl.class);
 
    @Autowired
    public SimpleJdbcDaoSupportUserDaoImpl(DataSource dataSource) {
        super();
        setDataSource(dataSource);
    }
...
Tutaj do konstruktora wstrzykujemy dataSource i wywołujemy metodę setDataSource z klasy (Simple)JdbcDaoSupport. W tym momencie możemy wywołać
  • getSimpleJdbcTemplate() dla pobrania referencji do obiektu SimpleJdbcTemplate
  • getJdbcTemplate() dla pobrania referencji do obiektu JdbcTemplate

Druga możliwość, to ręczne utworzenie obiektu (Simple)JdbcTemplate:
@Repository
public class SimpleJdbcDaoSupportUserDaoImpl implements UserDao {
    private static final Logger logger = Logger.getLogger(SimpleJdbcDaoSupportUserDaoImpl.class);
    private SimpleJdbcTemplate jdbcTemplate;

    @Autowired
    public SimpleJdbcDaoSupportUserDaoImpl(DataSource dataSource) {
        this.jdbcTemplate = new SimpleJdbcTemplate(dataSource);
    }
...
Wiemy już jak skonfigurować beany aby móc korzystać ze wsparcia springa dla jdbc. Obiekty SimpleJdbcTemplate dają wygodne metody typu queryForXxx:
  • queryForInt użyjemy w przypadku gdy zapytanie zwraca jedną wartość całkowitoliczbową:
    public Integer countAccounts(AppUser user) {
        return getSimpleJdbcTemplate().queryForInt(
                  "select count(id) from T_ACCOUNT where USER_ID = ?",
                   user.getId());
    }
    
    Ponieważ korzystamy z simpleJdbcTemplate, to możemy korzystać z wygody parametrów typu varargs. Parametry zapytania oznaczamy znakami '?', następnie po przecinku podejmy kolejne wartości, dla kolejnych parametrów. W tym przykładzie mam jeden parametr i jest to id użytkownika. Korzystając z simpleJdbcTemplate mamy możliwość podać również nazwane parametry:
    public Integer countAccountsNamedParam(AppUser user) {
        Map<String, Object> params = new HashMap<String, Object>();
        params.put("user_id_param", user.getId());
        return getSimpleJdbcTemplate().queryForInt(
                  "select count(id) from T_ACCOUNT where USER_ID = :user_id_param",
                  params);
    }
    
    Nazwany parametr pojawia się w zapytaniu po znaku ':' i jest to 'user_id_param'. Przed zapytaniem tworzę mapę dla nazwanych parametrów i dodaję jej wartość user.getId() na kluczu 'user_id_param'. Mapę przekazuję jako parametr metody queryForInt. Myślę, że jeden i drugi sposób ma jakieś zastosowanie. W powyższym przykładzie wydaje mi się zbędne tworzenie nazwanych parametrów, jest tylko jeden i chyba nie ma problemów, aby odczytać w piewrszym przykładzie jaka to warość zostanie wstawiona zamiast znaku '?'. Sytuacja zmieni się wraz z wzrostem liczby parametrów zapytania.
  • queryForLong - analogicznie jak queryForInt.
  • queryForObject - metoda przydatna w przypadku jeśli chcemy z wyników zapytania stworzyć obiekt domenowy, przykład:
    public AppUser getById(Integer id) {
        return getSimpleJdbcTemplate().queryForObject("select * from T_USER where id = ?", 
                                                      new AppUserRowMapper(), id);
    }
    
    public class AppUserRowMapper implements RowMapper<AppUser> {
        @Override
        public AppUser mapRow(ResultSet rs, int i) throws SQLException {
            return new AppUser(rs.getInt("id"), rs.getString("name"));
       }
    }
    
    Widzimy 3 paramtry:
    1. string z zapytaniem
    2. obiekt typu AppUserRowMapper. Klasa ta implementuje springowy interfejs RowMapper<AppUser> definujący jedną metodę. Jest to przykład wykorzystania wzorca Inversion of control (IoC). Po wykonaniu zapytania spring przetwarza wynikowy ResultSet i dla wynikowego wiersza wywoła naszą metodę mapRow. Dzięki temu możemy wyciągnąć co chcemy z wyników zapytania i stworzyć wynikowy obiekt. Zauważmy, że oczekujemy tutaj dokładnie jednego wynikowego wiersza z zapytania
    3. parametr zapytania. (tak jak w przykładzie wyżej moglibyśmy skorzystać z mapy parametrów nazwanych).
  • queryForMap. Jeśli zapytanie zawraca pojedyńczy wiersz możemy chcieć dostać wyniki w postaci mapy (nazwa_kolumny -> wartość):
    public Map<String, Object> getAccountValues (Account account) {
        Map<String, Object> resultMap = jdbcTemplate.queryForMap(
                                              "select user_id, account_name from T_ACCOUNT where id = ?", 
                                              account.getId());
        return resultMap;
    }
    
    Wynikowa mapa posiada wartości pod kluczami 'user_id', 'account_name' - dokładnie tymi, które podaliśmy w zapytaniu. Osobiście bardzo mi się podoba ten feature.
  • queryForList. A co w przypadku, gdy nie mamy pojedyńczego wyniku - możemy dostać listę takich map. Każdy obiekt listy to mapa taka sama jak w queryForMap:
    public Set<Account> getAccounts(AppUser user) {
        Set<Account> accounts =  new HashSet<Account>();
        List<Map<String, Object>> queryForList = getSimpleJdbcTemplate().queryForList(
                                                             "select * from T_ACCOUNT where user_id = ?",
                                                             user.getId());
        for (Map<String, Object> rs : queryForList) {
            accounts.add(new Account(
                             (Integer) rs.get("id"), 
                             (String)  rs.get("account_name"), 
                             (Integer) rs.get("user_id")));
        }
        return accounts;
    }
    
    Mamy w wyniku listę map, iterujemy się przez listę i wyciągamy co nas interesuje.
Omówione wyżej zapytania dotyczyły SimpleJdbcTemplate, korzystały z generyków i varargs-ów. Poznaliśmy interfejs RowMapper do mapowania pojedyńczego wyniku na obiekt domenowy.
Teraz pokażę, co można zrobić na JdbcTemplate. Załóżmy, że chcemy wykonać zapytanie, ale nie chcemy wyników mapować na obiekty, może chcemy je zapisać do jakiegoś strumienia, albo wysłać e-mail, albo wygenerować raport, albo cokolwiek innego. Mamy do dyspozycji interfejs RowCallbackHandler:
public void listAccountsRowCallbackHandler(AppUser user) {
    getJdbcTemplate().query("select * from T_ACCOUNT where user_id = ?", new Object[] {user.getId()}, 
    new RowCallbackHandler() {
        @Override
        public void processRow(ResultSet rs) throws SQLException {
            logger.info(new Account(
                            (Integer) rs.getInt("id"),
                            (String)  rs.getString("account_name"),
                            (Integer) rs.getInt("user_id")
                            ));
        }
    });
}
Podobnie jak w przypadku RowMapper, korzystając z wzorca IoC, nasza metoda zostanie wywołana i dostanie w parametrze pojedyńczy wiersz z wyniku zapytania. Nie zwraca go, może z nim zrobić cokolwiek innego, ja dla prostoty przykładu jedynie loguję co jest przetwarzane.
Zauważmy pewną rzecz: ponieważ SimpleJdbcTemplate pozwala na podawanie wartości do parametrów zapytania w postaci argumentów varargs - to oczywistym jest że parametr tego typu musi być ostatnim parametrem metody. W przypadku JdbcTemplate wartości parametrów podajemy w tablicy typu Object[] i jest to nie ostatni, a drugi (zaraz po stringu z zapytaniem) paramert metody.

Do tej pory przetwarzaliśmy pojedyńcze wiersze wyniku zapytania, jeśli chcielibyśmy dostać cały ResultSet skorzystajmy z JdbcTemplate oraz interfejsu ResultSetExtractor:
public Set<Account> getAccountResultSetExtractor(AppUser user) {
    return getJdbcTemplate().
               query("select * from T_ACCOUNT where user_id = ?", 
                     new Object[] {user.getId()}, 
                     new UserAccountsResultSetExtractor());
}

public class UserAccountsResultSetExtractor implements ResultSetExtractor<Set<Account>> {
    @Override
    public Set<Account> extractData(ResultSet rs) throws SQLException, DataAccessException {
        Set<Account> accounts = new HashSet<Account>();
        while (rs.next()) {
            Integer id = rs.getInt("id");
            String accountName = rs.getString("account_name");
            Integer user_id = rs.getInt("user_id");
            accounts.add(new Account(id, accountName, user_id));
        }
        return accounts;
    }
}
Kiedy go użyć - jeśli chcemy cały wynik zapytania móc zmapować do jednego obiektu (tutaj był to akurat zbiór).
Do zapamiętania:
  • SimpleJdbcTemplate - varargs -> parametry zapytania na końcu; RowMapper
  • JdbcTemplate -> parametry zapytania w tablicy typu Object[]; ResultSetExtractor; RowCallbackHandler;

Idźmy dalej z odkrywaniem uroków jdbc :-). Jak uruchomić zapytanie typu insert, update bądź DDL ?
public void persist(AppUser user) {
    getSimpleJdbcTemplate().update("insert into T_USER (ID, NAME) values (?, ?)", user.getId(), user.getName());
}
Proste. Pozostaje do omówienia temat procedur składowanych, dla prostych przypadków wystarczy:
getSimpleJdbcTemplate().update("call paczka.procedura(?)", 1l);
Odpalamy procedurę z parametrem. Dla bardziej zaawansowanych potrzeb polecam lekturę.
Wyjątki. JdbcTemplate tłumaczy wyjątki SQLException na jeden ze swoich wyjątków (dziedziczących z DataAccessException) na postawie kodu wyjątku. Można samemu napisać taki translator, ale wydaje mi się to niepotrzebne.
Spring daje o wiele więcej niż to, co opisałem, niejmniej jednak myślę, że jest to dobry początek aby wygodnie i efektywnie pracować z jdbc. Jak zwykle można pobrać projekt, na którym testowałem opisywane funkcjonalności.

3 komentarze:

  1. Należy się jeszcze słowo komentarza. Nie wspomniałem, że zapytania typu queryFor (Object/Map/List) oczekują że zapytanie zwróci wynik. Jeśli nie zrwóci żadnego rekordu to otrzymamy wyjątek. Jeśli zatem nasze zapytanie może nie zwrócić żadnej wartości korzystajmy z metody query + RowMapper.

    OdpowiedzUsuń
  2. Hej, w międzyczasie (Spring 3.1) SimpleJdbcTemplate została oznaczona jako przestarzała: The JdbcTemplate and NamedParameterJdbcTemplate now provide all the functionality of the SimpleJdbcTemplate.

    BTW polecam w programach wstrzykiwać interfejsy zamiast konkretnych implementacji (nazwy *Operations, np. JdbcOperations i JmsOperations). W zamierzchłych czasach pomagało to w mockowaniu, ale i dzisiaj chyba wiązać się z interfejsami a nie konkretnymi klasami, nawet jeśli istnieje tylko jedna implementacja.

    OdpowiedzUsuń
  3. Dzięki za info i za słuszną uwagę :-)

    OdpowiedzUsuń