iBatis 3.0 preview - část první

Po letech jsme se konečně dočkali třetí verze populární knihovny iBatis. Nová verze přináší velkou řadu novinek a jedná se o kompletní rewrite, který využívá generik, anotací a dalšího API Javy 1.5. iBatis je prozatím ve verzi Beta 1 (doposud ještě není dostupný ani v Maven repository), ale doufejme že nebude dlouho trvat a dočkáme se verze stabilní.

Společně s iBatisem vychází i úplně nový produkt iBATIS Schema Migration System inspirovaný Rails Migrations. Migrations představují podporu pro konzistentní úpravy databázových schémat s důrazem na: konzistenci, opakovatelnost, reverzibilnost, verzování, auditovatelnost a automatizaci. Jedná se o nástroj pro příkazovou řádku, s jehož pomocí je možné systémově vytvářet a spravovat databázové change skripty, které jsou přehledné, dají se kdykoliv revertovat a měly by výrazně ulehčit práci v týmu (pro čtenáře z Forresta: pokud vám to připomíná náš DbAutoupdater, jste doma :-) ).

V této sérii článků se ale soustřeďme na iBatis 3.0 a co nového nás v něm čeká. Sérii zmiňuji pro to, že novinek je příliš mnoho na to, aby se vešly do jediného článku a proto jsem jej rozdělil na dvě části, které vyjdou s týdenním postupem. Těšte se tedy ještě na jeden článek příští pondělí.

Obsah

  1. Rozlišení konfigurace pro typizovaná prostředí
  2. Podpora immutable objektů (podpora předávání dat do konstruktorů POJO)
  3. Práce s tabulkami ve vztahu 1:1

    1. Vnořený select
    2. Vnořený výsledek (join)
  4. Práce s tabulkami ve vztahu 1:N

    1. Vnořený select
    2. Vnořený výsledek (join)
  5. Kompozitní klíče
  6. Oficiální podpora diskriminátoru
  7. OGNL EL v dynamických SQL dotazech
  8. Závěrem

Abychom nezačínali hned tak zhurta, podívejme se na změny v XML konfiguraci, která byla dosud jediným prostředkem jak s iBatisem pracovat. Kdo již máte s iBatisem nějaké zkušenosti nemusíte mít žádné obavy, protože se jedná pouze o výrazné zjednodušení.

Rozlišení konfigurace pro typizovaná prostředí

V iBatisu se poprvé objevuje podpora rozlišení různých běhových prostředí (jedná se o stejný princip, který jsem popisoval v článku Odlišujete v aplikaci vývojové, testovací a produkční prostředí?). V základním konfiguračním souboru SqlMapConfig.xml je možné definovat:


<environments default="production">
    <environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
        </transactionManager>
        <dataSource type="UNPOOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
        </dataSource>
    </environment>
    <environment id="production">
<transactionManager type="MANAGED">
<property name="" value=""/>
        </transactionManager>
        <dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<property name="poolMaximumActiveConnections" value="20"/>
<property name="poolMaximumIdleConnections" value="5"/>
        </dataSource>
    </environment>
</environments>

S tím, že výběr běhové prostředí se definuje při vytváření SqlSessionFactory:


//tímto voláním vytvoříme factory pro defaultní prostředí, což je "production"
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader);
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader,properties);
//výběr development prostředí
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, "development");
//nebo
SqlSessionFactory factory = sqlSessionFactoryBuilder.build(reader, "development",properties);

Podpora immutable objektů (podpora předávání dat do konstruktorů POJO)

První změnou je možnost používat pro vytvářené intance POJO tříd parametrické konstruktory. Tím se nám otevírá možnost definovat POJO jako immutable objekty, které mají daleko lepší použitelnost v paralelním programování.


<resultMap id="selectImmutableAuthor" type="domain.blog.ImmutableAuthor">
    <constructor>
        <idArg column="id" javaType="int"/>
        <arg column="username" javaType="string"/>
        <arg column="password" javaType="string"/>
        <arg column="email" javaType="string"/>
        <arg column="bio" javaType="string"/>
        <arg column="favourite_section" javaType="domain.blog.Section"/>
    </constructor>
</resultMap>

Při definici argumentů ZÁLEŽÍ na pořadí a správném typu, protože Java dosud buhužel v reflexi neumožňuje přístup k názvům argumentů konstruktorů. Rozlišení primárního klíče (idArg element, popřípadě id element pro standardní bean property) není povinné nicméně mělo by výrazně napomáhat výkonnosti iBatisu. Dokumentace tento rozdíl popisuje takto:

The only difference between the two is that id will flag the result as an identifier property to be used when comparing object instances. This helps to improve general performance, but especially performance of caching and nested result mapping (i.e. join mapping).

Další novinkou, která myslím dříve nebyla možná je použití privátních property Java Beany, nicméně to je praktika více než diskutabilní, pokud můžeme využít konstruktorů tříd.

Práce s tabulkami ve vztahu 1:1

V tomto ohledu došlo řekl bych k mírnému pokroku v mezích zákona - jedná se pouze o vylepšení zápisu, které vede k výraznému vylepšení výsledné čitelnosti a použitelnosti. Srovnejme si původní a nové zápisy asociací 1:0..1 mezi objekty:

Vnořený select

Tady ještě rozdíly v zápisu nejsou tak drastické - je potřeba jen jediná definice ResultMapy navíc pro iBatis 2.X:

iBatis 2.x


<resultMap id="blogResult" class="Blog">
    <result property="author" column="blog_author_id"
                 resultMap="Author" select="selectAuthor"/>
</resultMap>
<resultMap id="authorResult" class="Author"/>
<select id="selectBlog" parameterClass="int" resultMap="blogResult">
    SELECT * FROM BLOG WHERE ID = #value#
</select>
<select id="selectAuthor" parameterClass="int" resultMap="authorResult">
    SELECT * FROM AUTHOR WHERE ID = #value#
</select>

iBatis 3.x


<resultMap id="blogResult" type="Blog">
    <association property="author" column="blog_author_id"
                 javaType="Author" select="selectAuthor"/>
</resultMap>
<select id="selectBlog" parameterType="int" resultMap="blogResult">
    SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectAuthor" parameterType="int" resultType="Author">
    SELECT * FROM AUTHOR WHERE ID = #{id}
</select>

Vnořený výsledek (join)

Ovšem tady už začíná být úspora a čitelnost znát:

iBatis 2.x


<resultMap id="blogResult" class="Blog">
    <result property="blog_id" column="id"/>
    <result property="title" column="blog_title"/>
    <result property="author" column="blog_author_id" resultMap="authorResult"/>
</resultMap>
<resultMap id="authorResult" class="Author">
    <result property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
</resultMap>
<select id="selectBlog" parameterClass="int" resultMap="blogResult">
    select
    B.id as blog_id,
    B.title as blog_title,
    B.author_id as blog_author_id,
    A.id as author_id,
    A.username as author_username,
    A.password as author_password,
    A.email as author_email,
    A.bio as author_bio
    from Blog B
    left outer join Author A on B.author_id = A.id
    where B.id = #{id}
</select>

iBatis 3.x


<resultMap id="blogResult" type="Blog">
    <id property="blog_id" column="id"/>
    <result property="title" column="blog_title"/>
    <association property="author" column="blog_author_id" javaType="Author">
        <id property="id" column="author_id"/>
        <result property="username" column="author_username"/>
        <result property="password" column="author_password"/>
        <result property="email" column="author_email"/>
        <result property="bio" column="author_bio"/>
    </association>
</resultMap>
<select id="selectBlog" parameterType="int" resultMap="blogResult">
    select
        B.id as blog_id,
        B.title as blog_title,
        B.author_id as blog_author_id,
        A.id as author_id,
        A.username as author_username,
        A.password as author_password,
        A.email as author_email,
        A.bio as author_bio
    from Blog B
    left outer join Author A on B.author_id = A.id
    where B.id = #{id}
</select>

Vytvoření nového klíčového slova association vylepšuje čitenost v tom, že člověk okamžitě ví bez většího studia konfigurace, že se jedná o multiplicitu 1:0..1. Dále poměrně dost vypomůže inlinování definice mapy do jiné mapy. V iBatis 2.X bylo nutné vždy result mapy definovat samostatně a provazovat je přes id (tato možnost je stále zachována, ale vyplatí se vám pouze v případě reusu mapování).

Práce s tabulkami ve vztahu 1:N

V tomto ohledu je už vyčištění zápisu poměrně drastičtější. Srovnejme si původní a nové zápisy asociací 1:0..* mezi objekty:

Vnořený select

iBatis 2.x


<resultMap id="blogResult" class="Blog">
    <result property="posts" javaType="ArrayList" column="blog_id"
                resultMap="postResult" select="selectPostsForBlog"/>
</resultMap>
<resultMap id="postResult" class="Post"/>
<select id="selectBlog" parameterClass="int" resultMap="blogResult">
    SELECT * FROM BLOG WHERE ID = #value#
</select>
<select id="selectPostsForBlog" parameterClass="int" resultMap="postResult">
    SELECT * FROM POST WHERE BLOG_ID = #value#
</select>

iBatis 3.x


<resultMap id="blogResult" type="Blog">
<collection property="posts" javaType="ArrayList" column="blog_id"
                ofType="Post" select="selectPostsForBlog"/>
</resultMap>
<select id="selectBlog" parameterType="int" resultMap="blogResult">
    SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectPostsForBlog" parameterType="int" resultType="Author">
    SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>

V zápisu pro iBatis 2.x byl zápis shodný jako pro vztah 1:0..1 - v třetí verzi iBatisu je multiplicita vztahu patrná na první pohled. Navíc opět nepotřebujeme dvojí deklaraci result map - vše je hezky čitelné jediném zápise.

Vnořený výsledek (join)

iBatis 2.x


<resultMap id="blogResult" class="Blog" groupBy="id">
    <result property="id" column="blog_id"/>
    <result property="title" column="blog_title"/>
    <result property="posts" column="post_id" resultMap="postResult"/>
</resultMap>
<resultMap id="postResult" class="Post">
    <result property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <result property="body" column="post_body"/>
</resultMap>
<select id="selectBlog" parameterClass="int" resultMap="blogResult">
    select
        B.id as blog_id,
        B.title as blog_title,
        B.author_id as blog_author_id,
        P.id as post_id,
        P.subject as post_subject,
        P.body as post_body
    from Blog B
    left outer join Post P on B.id = P.blog_id
    where B.id = #value#
</select>

iBatis 3.x


<resultMap id="blogResult" type="Blog">
    <id property="id" column="blog_id" />
    <result property="title" column="blog_title"/>
<collection property="posts" ofType="Post">
        <id property="id" column="post_id"/>
        <result property="subject" column="post_subject"/>
        <result property="body" column="post_body"/>
    </collection>
</resultMap>
<select id="selectBlog" parameterType="int" resultMap="blogResult">
    select
        B.id as blog_id,
        B.title as blog_title,
        B.author_id as blog_author_id,
        P.id as post_id,
        P.subject as post_subject,
        P.body as post_body
    from Blog B
    left outer join Post P on B.id = P.blog_id
    where B.id = #{id}
</select>

Myslím si, že v tomto případě zlepšení čitelnosti zápisu není třeba obhajovat.

Kompozitní klíče

Nově jsou ve vnořených selectech podporovány i kompozitní klíče. Stačí nám k tomu poměrně intuitivní zápis:


<resultMap id="blogResult" type="Blog">
<collection property="posts" javaType="ArrayList" column="{id=blog_id,section=blog_section}"
                ofType="Post" select="selectPostsForBlog"/>
</resultMap>
<select id="selectBlog" parameterType="int" resultMap="blogResult">
    SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectPostsForBlog" parameterType="int" resultType="Author">
    SELECT * FROM POST WHERE BLOG_ID = #{id} AND BLOG_SECTION = #{section}
</select>

Oficiální podpora diskriminátoru

Již v přechozí verzi existovala nezdokumentovaná podpora diskriminátoru - nyní se tato vlastnost stala zdokumentovanou. Diskriminátor vám umožňuje na základu hodnoty sloupce odlišit typ výsledného objektu. Např. podle hodnoty sloupce "vehicle_type" je možné vytvořit buď instanci java třídy cz.example.Car, cz.example.Van nebo cz.example.Truck. To umožňuje velmi pěkně využívat objektové dědičnosti:


<resultMap id="vehicleResult" type="Vehicle">
    <id property="id" column="id" />
    <result property="vin" column="vin"/>
    <result property="year" column="year"/>
    <result property="model" column="model"/>
    <discriminator javaType="int" column="vehicle_type">
        <case value="1" resultType="cz.example.Car">
            <result property="doorCount" column="door_count" />
        </case>
        <case value="2" resultType="cz.example.Truck">
            <result property="maximumLoad" column="load" />
        </case>
        <case value="3" resultType="cz.example.Van">
            <result property="length" column="length" />
            <result property="height" column="height" />
            <result property="width" column="width" />
        </case>
    </discriminator>
</resultMap>

OGNL EL v dynamických SQL dotazech

Dynamické SQL doznalo velmi výrazných změn. Byla opuštěna cesta vlastních podmínkových elementů jako je známe z verze 2 (tj. <isNotPropertyAvailable/>, <isNotNull/> atd.) ve prospěch obecně použitelnějšího expression language. Volba padla na OGNL, které používá i řada dalších projektů jako např. Struts 2, Tapestry nebo Spring WebFlow. Z toho také vyplývá, že řada z vás již bude s tímto jazykem dobře obeznámena.

To umožnilo snížit počet potřebných podmínkových elementu na čtyři základní:

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

Níže uvádím pár příkladů využití, které si myslím jsou v podstatě samopopisné:


<select id="findActiveBlogLike"
        parameterType="Blog" resultType="Blog">
    SELECT * FROM BLOG WHERE state = ‘ACTIVE’
    <if test="title != null">
        AND title like ${title}
    </if>
    <if test="author != null && author.name != null">
        AND title like ${author.name}
    </if>
</select>
<select id="findActiveBlogLike"
        parameterType="Blog" resultType="Blog">
    SELECT * FROM BLOG WHERE state = ‘ACTIVE’
    <choose>
        <when test="title != null">
            AND title like ${title}
        </when>
        <when test="author != null && author.name != null">
            AND title like ${author.name}
        </when>
        <otherwise>
            AND featured = 1
        </otherwise>
    </choose>
</select>
<select id="selectPostIn" resultType="domain.blog.Post">
    SELECT *
    FROM POST P
    WHERE ID in
    <foreach item="item" index="index" collection="list"
             open="(" separator="," close=")">
        #{item}
    </foreach>
</select>

Situace je trošičku složitější s Trim elementem. Ten nám umožňuje řešit situace, kdy by mohlo zapodmínkováním částí dotazu dojít k vytvoření nevalidního SQL statementu. Typický příklad:


<select id="findActiveBlogLike"
        parameterType="Blog" resultType="Blog">
    SELECT * FROM BLOG WHERE
    <if test="title != null">
        AND title like ${title}
    </if>
    <if test="author != null && author.name != null">
        AND title like ${author.name}
    </if>
</select>

Může na základě různých hodnot dojít k následujícím dvěma nevalidním variantám:


SELECT * FROM BLOG WHERE;
SELECT * FROM BLOG WHERE AND title like ?;

Přesně tuto situaci nám umožňuje řešit TRIM element a jeho dvě varianty WHERE a SET - které mají stejný význam, ale jsou optimalizované pro jednoduché použití. Výše uvedný příklad by se jednoduše řešil tímto zápisem:


<select id="findActiveBlogLike"
        parameterType="Blog" resultType="Blog">
    SELECT * FROM BLOG
    <where>
        <if test="title != null">
            AND title like ${title}
        </if>
        <if test="author != null && author.name != null">
            AND title like ${author.name}
        </if>
    </where>
</select>

Ten pro iBatis znamená, že použije klíčové SQL slovo WHERE pouze za přepokladu, že obsah tohoto elementu není prázdný (tj. alespoň jedna podmínka se vyhodnotila jako pravdivá). Navíc pokud tento obsah začíná klíčovými slovy AND nebo OR, dokáže iBatis toto slůvko odříznout. To elegantně a jednoduše řeší celý problém. WHERE je pouze optimalizovaná varianta TRIM - výše uvedenému zápisu odpovídá toto:


<select id="findActiveBlogLike"
        parameterType="Blog" resultType="Blog">
    SELECT * FROM BLOG
<trim prefix="WHERE" prefixOverrides="AND |OR ">
        <if test="title != null">
            AND title like ${title}
        </if>
        <if test="author != null && author.name != null">
            AND title like ${author.name}
        </if>
    </trim>
</select>

V atributech elementu TRIM říkáme iBatisu:

  • pokud je vrácený obsah elementu neprázdný, prefixuj ho slovem WHERE
  • pokud vrácený obsah začíná na AND nebo OR odřízni tento začátek (pozn. mezera za těmito slovy je důležitá - iBatis obsah porovnává naprosto přesně)

Vidíme tedy, že TRIM je víceúčelové slovo, které si můžeme přizpůsobit na míru svým potřebám, nicméně 90% případů by nám měly vyřešit předdefinované kombinace WHERE a SET.

Závěrem

Tímto uzavíráme první díl preview iBatisu verze 3.0. V tom následujícím, který vyjde přesně za týden, si popíšeme novinky související s Java přístupem k iBatis dotazům, využitím anotací a generik.