iBatis SqlMaps - tak trochu opomíjený ORM

Nedá mi to, abych nenapsal něco o frameworku iBatis. Někteří jej možná znáte, někteří jste možná o něm už slyšeli, ale dle trafficu na java.cz konferenci bych řekl, že jej většina z vás přehlíží. Zůstal nepovšimnut i v našem krají protřelém CZ podcastu číslo 8. Myslím, že je to škoda a proto jsem se rozhodl o malou osvětovou, nebo-li, jak by řekl Roumen, evangelizační práci.

Ještě v úvodu bych rád podotknul, že někomu se může zdát že to není ten pravý "entrprase" framework, není kompatibilní s JPA, nemá anotace a vůbec je celý takový jednoduchý. Že je možná až tak jednoduchý, že jeho použití ani nemůže přinést tu pravou zábavu ve formě hledání příčin mystického chování vaší aplikace. iBatis se navíc nehonosí žádným buzzwordem, který by se dal prodat zákazníkovi. Pokud si to myslíte, máte pravdu - šetřte své oči, už nemusíte číst dál, protože tento článek určitě není pro vás.

...

Po drsnějším úvodu mi doufám zůstali ti praví čtenáři ;). Takže přejděme k práci ...

iBatis je ORM framework, jehož historie sahá někam k roku 2002. Je to stabilní a odladěný kus kódu se skvělou dokumentací a průhlednými zdrojovými kódy, jehož cílem je zjednodušit programátorovi práci s JDBC. Nesnaží se odstínit programátora plně od vlastní databáze, jako to činí JPA compliant frameworky. Zůstává někde na půli cesty mezi plnotučným ORM a prostým JDBC.

Fíčury

Základní myšlenky iBatisu by se daly shrnout možná takto:

  • nenutí programátora učit se o moc víc než to co už umí (JDBC) - learning curve je 3 - 8 hodin ... v podstatě už za 3 hodiny můžete programovat, aniž byste mohli udělat nějakou zásadní chybu, která by vás nutila něco předělávat, když budete framework používat intenzivněji a déle
  • nesnaží se zakrýt práci s DB - tím je jednoduchý a transparentní
  • poskytuje maximální podporu programátorovi při rutinních činnostech
    • mapuje vrácené result sety na Java objekty (dao třídy, jsou stejně tenké, jako když použijete Hibernate)
    • nenutí vás fetchovat celé objekty - klidně si vrácený set může převést na mapu: název sloupce / hodnota nebo jen na primitiv
    • není nutné psát mapování property tříd na sloupce - iBatis podporuje autowire by name
    • jednoduše řeší vytahování záznamů po stránkách
    • poskytuje plugovatelnou cachovací logiku - řízení cache je na vás (což možná nemusí být výkonnostně optimální, zato se nikdy neztratíte)
    • jednoduchá práce s transaction / dávkami příkazů je samozřejmostí
    • stejně jako ostatní ORM vás oddělí od práce s connection a vlastními resultsety - už nikdy nezapomenete na close() ;)
  • odpovědnost za psaní vlastních SQL dotazů nechává na programátorovi - SQL máte plně pod kontrolou a jednoduše a bez hluboké znalosti frameworku vymáčknete z iBatisu i takové chuťovky jako uložené procedury (a stále máte podporu tohoto frameworku - nejste nuceni se snížit k JDBC)
  • odděluje SQL dotazy od kódu - při požadavku na přenositelnost mezi DB se velmi jednoduše pouze upraví nekompatibilní SQL dotazy (což je sice oproti např. Hibernate práce navíc, ale když to vezmete kolem a kolem, té práce není zas až tak moc, pokud máte automatické testy)
  • nenutí vás psát sáhodlouhé XML (s SQL příkazy) - obsahuje řadu vychytávek, které vám ušetří psaní redundantního kódu - dynamické SQL, podmínky v SQL, includy, extenze - a přesto je to všechno strašně jednoduché

Jak iBatis funguje?

Jak vidno z níže uvedeného schématku vyjmutého z iBatis dokumentace, framework s skládá z následujících části:

  • SqlMapConfig.xml
    XML soubor, který je pouze rozcestníkem k dalším mapovacím souborům a jenž obsahuje "globální" konfiguraci iBatisu - defakto je to obdoba hibernate.properties souboru
  • SqlMap.xml
    Vlastní mapovací soubory (např. User.xml), který obsahuje SQL dostazy spojené se serializací a deserializací objektů do a z databáze - opět obdoba *.hbm z Hibernate
  • Mapped statementy
    Z výše uvedených xml souborů iBatis při inicializaci vytvoří "mapped statementy" - tzn. objektovou reprezentaci vaší konfigurace; za běhu si potom pro tyto mapped statementy iBatis postupně vytvoří a zacachuje prepared statementy
  • Vstupní data
    tedy parametry jednotlivých dotazů - může se jednat o Java beany, primitivní typy, mapy a nebo XML
  • Výstupní data
    tedy výstupní naplněné Java "objekty" - může se jednat o Java beany, primitivní typy, mapy a nebo XML

Schémátko vyňaté z iBatis dokumentace:
Schéma iBatis

Ukázka práce s iBatis

V následujícíh odstavcích bych krátce ukázal, jak se s iBatis pracuje.

Konfigurace

Začneme konfigurací. Ve verzi 2.x musíme dodat vždy dva typy konfiguračních souborů:

SqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
    "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
  <!-- Konfigurace transakčního manažeru, pokud jste v prostředí
        aplikačního serveru, je vhodnější použít jeho transakční
        manažer a managovaný datasource (tedy vytáhnout je z
        JNDI) -->
  <!-- tuto část při použití Springu vynecháváte, jelikož ji máte
         již zkonfigurovanou ve springových konfigurácích -->
<transactionManager type="JDBC" commitRequired="false">
    <dataSource type="SIMPLE">
<property name="JDBC.Driver" value="org.hsqldb.jdbcDriver"/>
<property name="JDBC.ConnectionURL" value="jdbc:hsqldb:."/>
<property name="JDBC.Username" value="sa"/>
<property name="JDBC.Password" value="sa"/>
    </dataSource>
  </transactionManager>
  <!-- Souhrnná konfigurace frameworku - zde je možno
         konfigurovat i základní údaje cache - např. pro vývoj
         cache jednoduše hromadně vypnout, pro runtime
         naopak nahodit -->
  <settings
      cacheModelsEnabled="true"
      enhancementEnabled="true"
      lazyLoadingEnabled="true"
      />
  <!-- Seznam SqlMap konfigurací - nahrávají se z classpath a v
         našem případě se nacházejí v package
         com.mydomain.data... -->
  <sqlMap resource="com/mydomain/data/Account.xml"/>
  <sqlMap resource="com/mydomain/data/Order.xml"/>
  <sqlMap resource="com/mydomain/data/Documents.xml"/>
</sqlMapConfig>
a vlastní SqlMap.xml - v našem případě třebas Account.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap
    PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
    "http://ibatis.apache.org/dtd/sql-map-2.dtd">
<sqlMap namespace="Account">
  <!--
  Použijeme type alias, abychom nemuseli neustále vypisovat celý
  název třídy.
  -->
  <typeAlias alias="Account" type="com.mydomain.domain.Account"/>
  <!--
  Result mapa popisuje mapování mezi sloupci vrácenými v dotazu
  a property java beany. Tato deklarace není nutná v případě, že
  názvy sloupců sedí na názvy property dané beany (kdo zná
  autowire ze Springu, je mu jasno)
  -->
  <resultMap id="AccountResult" class="Account">
    <result property="id" column="ACC_ID"/>
    <result property="firstName" column="ACC_FIRST_NAME"/>
    <result property="lastName" column="ACC_LAST_NAME"/>
    <result property="emailAddress" column="ACC_EMAIL"/>
  </resultMap>
  <!--
  Select bez parametrů využívá deklaraci result mapy pro Account
  třídu.
  -->
<select id="selectAllAccounts" resultMap="AccountResult">
    select * from ACCOUNT
  </select>
  <!--
  Jednoduší příklad selectu, kde se obejdeme bez result mapy.
  Všimněte si že názvy sloupců odpovídají názvům property v
  beaně. Další novinkou je použití vstupního parametru typu
  java.lang.Integer (většina základích typů má již předvytvořené
  type aliasy, takže místo java.lang.Integer nám stačí "int")
  - v dotazu nám stačí použít #id#.
  -->
<select id="selectAccountById" parameterClass="int"
    resultClass="Account">
    select
      ACC_ID as id,
      ACC_FIRST_NAME as firstName,
      ACC_LAST_NAME as lastName,
      ACC_EMAIL as emailAddress
    from ACCOUNT
    where ACC_ID = #id#
  </select>
  <!--
  A teď si ukážeme něco z dynamických selectů.
  Chceme vyselektovat account podle:
    1) jména nebo příjmení
    2) emailové adresy
    3) konkrétního id.
  -->
<select id="dynamicGetAccountList" resultMap="Account">
    select * from ACCOUNT
    <dynamic prepend="WHERE">
      <isNotNull prepend="AND" property="firstName"
          open="(" close=")">
          ACC_FIRST_NAME = #firstName#
        <isNotNull prepend="OR" property="lastName">
          ACC_LAST_NAME = #lastName#
        </isNotNull>
      </isNotNull>
      <isNotNull prepend="AND" property="emailAddress">
        ACC_EMAIL like #emailAddress#
      </isNotNull>
      <isGreaterThan prepend="AND" property="id" compareValue="0">
        ACC_ID = #id#
      </isGreaterThan>
    </dynamic>
    order by ACC_LAST_NAME
  </select>
  <!--
  Ukázka INSERT statementu. Vstupním parametrem je pro nás
  třída Account. Pro vložení hodno nám slouží výrazy
  #nazevProperty#.
  V našem příkladě vkládáme i ID, pokud bychom ale chtěli
  vrátit automaticky generované id databází, vložili bychom
  deklaraci selectKey:
<selectKey resultClass="int" >
     kde například pro Oracle by se uvedlo
     SELECT "nazevSekvence".NEXTVAL AS ID FROM DUAL
     pro MSSQL
     SELECT @@IDENTITY AS ID
     atd.
  </selectKey>
  -->
  <insert id="insertAccount" parameterClass="Account">
    insert into ACCOUNT (
      ACC_ID,
      ACC_FIRST_NAME,
      ACC_LAST_NAME,
      ACC_EMAIL
    values (
      #id#, #firstName#, #lastName#, #emailAddress#
    )
  </insert>
  <!--
  Příklad update - nijak se neodlišuje od příkladu s INSERTEM.
  -->
  <update id="updateAccount" parameterClass="Account">
    update ACCOUNT set
      ACC_FIRST_NAME = #firstName#,
      ACC_LAST_NAME = #lastName#,
      ACC_EMAIL = #emailAddress#
    where
      ACC_ID = #id#
  </update>
  <!--
  A příklad delete - vstupním parametrem je pouze int s id
  záznamu.
  -->
  <delete id="deleteAccountById" parameterClass="int">
    delete from ACCOUNT where ACC_ID = #id#
  </delete>
</sqlMap>

A to je vše. Tím máme zkonfigurováno. Již z tohoto příkladu si můžete udělat obrázek, že iBatis neprovádí žádnou náročnou magii jako například Hibernate nebo TopLink. Vše je až neuvěřitelně průhledné, jednoduché a tím pádem minimálně náchylné k chybám.

DAO - Třída

Nuže a nyní si ukážeme příklad DAO třídy. Bude to také krátké a průrazné. Navíc od verze 3.0 by nám měla stačit už pouze deklarace interface ... o vlastní implementaci by se iBatis postaral sám.

AccountDao.java

package com.mydomain.data;
import com.ibatis.sqlmap.client.SqlMapClient;
import com.ibatis.sqlmap.client.SqlMapClientBuilder;
import com.ibatis.common.resources.Resources;
import com.mydomain.domain.Account;
import java.io.Reader;
import java.io.IOException;
import java.util.List;
import java.sql.SQLException;
/**
 * Vzato z jednoduchécho příkladu přikládaného k iBatis.
 * Pozor toto neberte jako příklad BEST PRACTISES a radši jukněte
 * na příklad JPetStore 5.0 na http://www.ibatis.com
 *
 * V případě integrace se Spring bude vše ještě jednodušší.
 * Stačí pouze podědit z SqlMapClientDaoSupport a o inicializaci
 * máme postaráno.
 */
public class SimpleExample {
  /**
   * Instance SqlMapClient instances jsou thread safe,
   * takže nám stačí pouze jedna. V našem příkladě použijeme
   * singleton.
   */
  private static SqlMapClient sqlMapper;
  /**
   * Opět toto není ideální způsob inicializace, ale pro účely
   * jednoduchého příkladu nám to stačí.
   *
   * Opět v případě použití Springu, tady nemusíme nic dělat.
   * V kódu by nám pak stačilo vždy jen zavolat metodu
   * getSqlMapClientTemplate().
   */
  static {
    try {
      //načtem konfiguraci
      Reader reader = Resources
           .getResourceAsReader("com/mydomain/data/SqlMapConfig.xml");
      //vytvoříme klienta na kterém budeme volat jednotlivé příkazy
      sqlMapper = SqlMapClientBuilder.buildSqlMapClient(reader);
      reader.close();
    } catch (IOException e) {
      // Fail fast.
      throw new RuntimeException(
           "Something bad happened while building the SqlMapClient" +
           " instance." + e, e);
    }
  }
  public static List selectAllAccounts () throws SQLException {
    return sqlMapper.queryForList("selectAllAccounts");
  }
  public static Account selectAccountById  (int id) throws SQLException {
    return (Account) sqlMapper.queryForObject("selectAccountById", id);
  }
  public static void insertAccount (Account account) throws SQLException {
    sqlMapper.insert("insertAccount", account);
  }
  public static void updateAccount (Account account) throws SQLException {
    sqlMapper.update("updateAccount", account);
  }
  public static void deleteAccount (int id) throws SQLException {
    sqlMapper.delete("deleteAccount", id);
  }
}

Tím je hotové i DAO a skončili jsme. Další nespornou výhodou je, že o logování se vám postará iBatis, takže když je potřeba, v lozích detailně vidíte, co se vám vlastně na datové vrstvě děje.

Skvělá dokumentace

Pro rychlou learning curve je dobrá dokumentace nezbytným základem. V tohle ohledu je iBatis řekl bych opravdu na špičce. Doporučuji si udělat rychlý přehled o použití frameworku na:

  1. SimpleExample aplikaci (která je součásti bundle iBatis a částečně jsem ji využil i v uvedených příkladech) - na čtyřech třídách si rychle uděláte přehled
  2. pokračovat krátkým tutorialem, který vás rychle nakopne ... a v podstatě už můžete začít programovat
  3. prostudovat komplexnější JPetStore aplikaci, kde jsou ukázané trochu pokročilejší fíčury spolu s Developer dokumentací - tam je přehledně vše důležité
  4. když nakouknete do zdrojových kódů, lehce zjistíte, že nejsou nijak rozsáhlé, jsou také dobře zdokumentované a přehledné

iBatis vs. JDBC4

V diskusích se párkrát objevil názor, zda má iBatis po zavedení JDBC stále smysl. Existují různé názory, ale pro iBatis mluví stále pár argumentů:

  • JDBC 4.0 je Java 1.6+ / iBatis je 1.4+ (do verze 2.20 1.3+)
  • iBatis je tu a umí pracovat i s drivery verze 3.0 a 2.0 (dvojkové JDBC bude ale od určité verze deprecated ... možná už od 2.20, podobně jako JDK 1.3) - většina velkých vendorů ještě JDBC 4.0 drivery ještě ani nemají, nebo mohou podporovat jen nové verze databází (např. Oracle od 11g, která je teprve beta ...)
  • iBatis je odladěný a vychytaný, s novými JDBC 4.0 drivery nám každý vendor přinese svou snůšku bugů, které jim postupně společně teprve vyladíme
  • iBatis má možnosti dynamických SQL dotazů, extenzí a includů, které JDBC 4.0 nepodporuje
  • JDBC 4.0 je zafixované a nebude se rozhodně dynamicky rozvíjet tak jako může iBatis, který s sebou netáhne takovou "mrtvou váhu"
  • JDBC 4.0 je možné konfigurovat s pomocí anotací (což je poměrně sympatická vlastnost) - iBatis prozatím nikoliv, ale podpora anotací je již v roadmap pro verzi 3.0 spolu s dalšími velmi sympatickými fíčurkami, jako např:
    • Interface binding - pro napsání DAO nám bude stačit deklarace interface, o implementaci se postará iBatis sám
    • Anotace
    • Konfigurace konvencí - tak tohle bude asi největší masáž a částečné odchýlení od principů, které jsem uvedl na začátku; v podstatě se jedná o to, že by iBatis sám dokázal vygenerovat jednoduché SQL dotazy pro základní sadu jednoduchých SQL příkazů (jako jsou např. insert / update / delete + jednoduchý select) pouze na základě deklarace metody v interface; okolo tohoto bodu se ale ještě vedou další debaty
    • Více úrovňová konfigurace - vzestupně podle priority konvence, anotace (může předefinovat konvenci), XML (může předefinovat konvenci a anotace) a java kód (může předefinovat cokoliv)

Odkazy na primární zdroje (kdybych náhodou něco interpretoval špatně ;)):

iBatis DAO

To co jsem doposud popisoval byly iBatis SqlMaps. Z rodiny iBatis pochází ještě jedna "utilitka" nazvaná iBatis DAO, jejímž cílem je stadnardní cestou programátorovi zpřístupnit práci s transakcemi. iBatis SqlMaps a DAO jsou dvě nezávislé knihovny, které lze použít i samostatně. Já například používám jen SqlMaps knihovnu, jelikož o transakce a práci s connection se mi stará Spring.

Pro ty, kteří Spring nepoužívají se možná iBatis DAO bude hodit. Umožňuje vám zkonfigurovat přípojení k databázi (simple JDBC, DBCP, JNDI), transakčního manažera a na úrovni kódu vám poskytuje zástupné třídy pro práci s transakcemi. Transakční manažeři jsou pluggovatelní a v současné době si můžete vybrat mezi JDBC, JTA, SqlMap, Hibernate a external transakčním manažerem.

Pro vlastní dao třídy existují Template třídy frameworku, které vám zpřístupňují takové ty metody jako getConnection() (JDBC, JNDI), getSession() (Hibernate) nebo getSqlMapExecutor() (SqlMaps) - podobně jako v případě Springu.

Myslím, si že z tohoto krátkého odstavečku jste asi nemohli pochopit, co přesně iBatis DAO dělá, a proto bych vás rád odkázal na kraťoučkou dokumentaci, která veškerou práci s knihovnou přehledně popisuje.

Závěrem

Vřele doporučuji na iBatis kouknout i v případě, že používáte Hibernate, TopLink nebo jiné ORM. Opakuji to tu alespoň po páté, ale budete překvapeni jednoduchostí, průhledností a bezproblémovostí iBatisu. Programátoři s velkou zkušeností s Hibernate pravděpodobně nepřejdou - Hibernate je opravdu mocnější nástroj, ale má řadu úskalí, o které si člověk může ošklivě nabít. Pro programátory začátečníky doporučuji místo JPA / Hibernate / Toplink, začít nejdříve na iBatisu. Ušetří vám opravdu velké množství práce, nebudete muset do něj pronikat nijak dlouho, nenachytá vás do žádné pasti a odmění se vám bezproblémovou službou.