iBatis 3.0 preview - část druhá

V předchozím článku jsme si ukázali vylepšení iBatisu v souvislosti s XML deklaracemi. Tento navazující článek rozebírá novinky v oblasti Java API. Základem pro toto rozšíření se staly vlastnosti dostupné od verze Javy 1.5 - tedy generiky a anotace. Jednou z velkých kritik původního iBatisu bylo množství XML, které bylo nutné psát. Našlo se mnoho lidí, kterým tento přístup vadil a kteří by spíše uvítali mít vše na jednom místě v kódu. Autoři tyto kritiky vyslyšeli a vytvořili plnohodnotné API, před které je možné využít libovolnou funkcionalitu iBatisu.

Před tím, než se pustím do rozebírání detailů bych se s Vámi rád podělil o jeden velmi zajímavý úryvek z dokumentace, který má s výše uvedeným souvislost:

Java Annotations are unfortunately limited in their expressiveness and flexibility. Despite a lot of time spent in investigation, design and trials, the most powerful iBATIS mappings simply cannot be built with Annotations – without getting ridiculous that is. C# Attributes (for example) do not suffer from these limitations, and thus iBATIS.NET will enjoy a much richer alternative to XML. That said, the Java Annotation based configuration is not without its benefits.

Zdá se mi to jen nebo se povzdechy nad rigiditou Javy stávají normálním trendem?

Obsah

  1. Využití generik a typově bezpečného kódu
  2. Využití anotací pro psaní SQL dotazů
  3. Využití SQL statementů vytvořených přímo Java kódem
  4. Rozšíření extension pointů
  5. Integrace se Springem
  6. Použité zdroje a závěr

Využití generik a typově bezpečného kódu

Pro mě asi nejzásadnější pokrok znamenají automaticky generované DAO implementace na základě mnou definovaného interface. Klíčový princip v novém iBatisu je jednoznačná vazba mezi interfacem DAO třídy a XML konfiguračním souborem na základě stejnojmenného namespace. Každý konfigurační XML konfigurační soubor by měl ve své hlavičce deklarovat unikátní namespace odpovídající packagové cestě DAO interface, ke kterému se vztahuje:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
        "http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="domain.blog.mappers.AuthorMapper">
... další definice ...

Propojení metody interface se statementem definovaným v XML se děje přes jméno metody. iBatis sám automaticky vygeneruje implementaci interfacu a propojí ho se statementy v XML. Provede i statickou kontrolu vstupních a výstupních parametrů statementů. Dotazy přes tzv. mappery probíhají následujícím (vcelku intuitivním) způsobem:


SqlSession session = sqlMapper.openSession();
try {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  List posts = mapper.selectAllPosts(null, 0, 2);
  assertEquals(2, posts.size());
  assertEquals(1, posts.get(0).get("ID"));
  assertEquals(2, posts.get(1).get("ID"));
} finally {
  session.close();
}

Tedy nikdy více už repetitivní implementace tupých DAO objektů. Stačí deklarovat interface a už fungujeme. V kombinaci s transakčními atributy Sprignu (@Transactional), máme v podstatě všechno, co bychom mohli potřebovat.

Prozatím jsem nikde nenašel zmínku o automaticky generovaných SQL statementech pouze na bázi konvence a signatury metody, přestože tato věc byla ve white paperu pro iBatis 3.0 také probírána. Na druhou stranu, ztráta této vlastnosti mi ani moc nevadí - naopak, tím že v iBatisu není, dávají autoři najevo, že iBatis vždy chtěl být a nadále zůstává naprosto transparentní. Není v něm žádná magie automatického generování SQL jaké je vidět v "plnotučných" ORM typu Hibernate.

Využití anotací pro psaní SQL dotazů

Anotace se těší velké oblibě a proto se pro jejich zavedení rozhodli i autoři iBatisu. S pomocí anotací by mělo být možné zapsat vše (respektive téměř vše), čeho dosáhneme v XML. V krajním případě se tedy můžeme úplně obejít bez jediné čárky v XML. Ukázkové DAO by nyní mohlo vypadat třeba takhle:


public interface BoundBlogMapper {
  @Select({"SELECT * FROM blog"})
  List selectBlogs();  
  @Select({"SELECT * FROM blog"})
  List selectBlogsAsMaps();
  @Select("SELECT * FROM post ORDER BY id")
  @TypeDiscriminator(
      column = "draft",
      javaType = String.class,
      cases = {@Case(value = "1", type = DraftPost.class)}
  )
  List selectPosts();  
  @Select("SELECT * FROM  blog WHERE id = #{id}")
  Blog selectBlog(int id);  
}

Na druhou stranu je velmi lehké z ošklivého XML přejít do ještě ošklivějších anotací. Například v tomto případě, bych já osobně volil spíše přehlednější XML:


@Select("SELECT * FROM post ORDER BY id")
  @Results({
    @Result(id = true, property = "id", column = "id")
      })
  @TypeDiscriminator(
      column = "draft",
      javaType = int.class,
      cases = {@Case(value = "1", type = DraftPost.class,
          results = {@Result(id = true, property = "id", column = "id")})}
  )
List selectPostsWithResultMap();
@ConstructorArgs({
    @Arg(column = "AUTHOR_ID", javaType = Integer.class)
      })
  @Results({
    @Result(property = "username", column = "AUTHOR_USERNAME"),
    @Result(property = "password", column = "AUTHOR_PASSWORD"),
    @Result(property = "email", column = "AUTHOR_EMAIL"),
    @Result(property = "bio", column = "AUTHOR_BIO")
      })
  @Select({
      "SELECT ",
      "  ID as AUTHOR_ID,",
      "  USERNAME as AUTHOR_USERNAME,",
      "  PASSWORD as AUTHOR_PASSWORD,",
      "  EMAIL as AUTHOR_EMAIL,",
      "  BIO as AUTHOR_BIO",
      "FROM AUTHOR WHERE ID = #{id}"})
Author selectAuthor(int id);

Anotace je možné využívat nejen pro Select statementy ale i pro Insert, Update, Delete, definici Cachí apod. K anotacím se však vztahují dvě velké nevýhody:

  • jsou napevno zkompilované v kódu a není je možné za běhu změnit
  • pokud potřebujeme podporovat více databázových strojů nezbyde nám než duplikovat interfacy nebo jít cestou XML

Z pochopitelného důvodu nechce iBatis v anotacích pokrýt všechny kombinace a všechny možné způsoby použití. Řekl bych, že anotace se perfektně hodí k jednoduchým jednoúčelovým aplikacím, kde se nemusíme obávat portace na jiný DB server a jde nám především o rychlost implementace. V jiných případech bych asi pořád volil staré dobré XML.

iBatis umožňuje poměrně bezpečný fallback k XML - pokud totiž máme na metodě interfacu nadefinovanou anotaci a iBatis najde stejně pojemenovaný statement v XML mapování, dá přednost XML. Takže i v případě zakompilované anotace máme šanci s tím stále něco udělat.

Využití SQL statementů vytvořených přímo Java kódem

Dlouho se diskutovalo také nad tím, že velmi komplikované SQL dotazy jsou, přes veškeré možnosti dynamických dotazů, v XML stále obtížně čitelné. Z toho důvodu zavedli autoři možnost tyto výjimečné typy dotazů vytvářet přímo v Javě. Princip je následující - v rozhranní Mapperu je třeba definovat následující anotaci:


public interface BoundBlogMapper {
  @SelectProvider(type = BoundBlogSql.class, method = "selectBlogsSql")
  List selectBlogsUsingProvider();
}

Následně ve třídě BoundBlogSql nadefinovat metodu selectBlogsSql, která vytvoří požadovaný SQL dotaz:


public class BoundBlogSql {
  public String selectBlogsSql() {
    return "select * from BLOGS";
  }
}

Do daného dotazu je možné také předat jeden zvolený parametr libovolného typu. Pro tvorbu SQL dotazů v Javě iBatis nabízí jednoduchý nástroj, který může ušetřit kus práce a více zčitelnit kód sestavující SQL. Tím je nástrojem je třída SelectBuilder obsahující sadu statických metod pro vytváření logických celků SQL dotazu. Jeho použití si pojďme ukázat na následujícím příkladě:


private static String example1() {
	BEGIN();
	SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
	SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
	FROM("PERSON P");
	FROM("ACCOUNT A");
	INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
	INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
	WHERE("P.ID = A.ID");
	WHERE("P.FIRST_NAME like ?");
	OR();
	WHERE("P.LAST_NAME like ?");
	GROUP_BY("P.ID");
	HAVING("P.LAST_NAME like ?");
	OR();
	HAVING("P.FIRST_NAME like ?");
	ORDER_BY("P.ID");
	ORDER_BY("P.FULL_NAME");
	return SQL();
}
private static String example2(String id, String firstName, String lastName) {
	BEGIN();
	SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
	FROM("PERSON P");
	if(id != null) {
		WHERE("P.ID like #id#");
	}
	if(firstName != null) {
		WHERE("P.FIRST_NAME like #firstName#");
	}
	if(lastName != null) {
		WHERE("P.LAST_NAME like #lastName#");
	}
	ORDER_BY("P.LAST_NAME");
	return SQL();
}

Osobně nepatřím mezi kritiky XML zápisu, proto bude tohle asi jedna z vlastností, kterou spíš nevyužiji než naopak. Věřím ale, že se najde plno vývojářů, kteří tuto možnost v portfoliu iBatisu uvítají.

Rozšíření extension pointů

V předchozí verzi iBatisu byly k dispozici jediné dva extension pointy:

  • ResultObjectFactory - rozhranní, které iBatis používá pro vytvoření všech instancí při zpracovávání dotazů (efektivně bylo možno využít např. k obalování tříd dynamickými proxy objekty atp.)
  • TypeHandlerCallback / TypeHandler - rozhranní, které iBatis používá k převodu SQL objektů na Java objekty a zpět (bylo nutné použít například, pro převod hodnoty sloupce na Enum, popřípadě na další custom typy)

Ve verzi 3.0 tyto rozhraní samozřejmě zůstávají. Rozdíl je pouze v ResultObjectFactory, která byla přejmenována na ObjectFactory a má podporu pro parametrizovatelné konstruktory. Nově je přidána tzv. Pluginů, které vám umožní vstoupit (intercept) do volání iBatisu na dalších místech:

  • Executor - pro přerušení následujících typů volání: update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
  • ParameterHandler - pro přerušení nastavení parametrů do SQL Statementu (volání metody setParameter)
  • ResultSetHandler - pro přerušení načítání objektů z SQL ResultSetu
  • StatementHandler - pro přerušení volání nad SQL Statementem: prepare, parameterize, batch, update, query

Tyto extension pointy jsou záměrně velmi nízkoúrovňové - což s sebou nese mj. i riziko, že pokud do jejich chodu nějak výrazně nekompatibilně zasáhnete, koledujete si o kompletní selhání iBatisu jako takového. Takže s rozmyslem! :-)

Integrace se Springem

V iBatisu jako takovém není podpora pro Spring. Očekává se, že naopak Spring začne v dalších verzích podporovat novou verzi iBatisu. Pro překlenovací období se už na mailing listu objevily první implementace integračních tříd pro Spring 3.0 lehce modifikovatelné pro práci se Spring 2.5. V zásadě by měla stačit pouze velmi tenká integrační vrstva.

Použité zdroje a závěr

Pokud byla minulá verze iBatisu oblíbená, věřím, že ta nejnovější si získá ještě větší množství příznivců. Výsledné API, které vzniklo mi připadá poměrně čisté a ucelené. Úvodní dokumentace je více než dostačující a bezchybnost vlastního kódu ukáže budoucnost (nicméně 2000 automatických testů je poměrně vypovídající ukazatel).

Pokud se tedy přistihnete, že na Hibernate pořád nadáváte - zkuste dát iBatisu šanci. Myslím, že mezi rozhodně nebudete mezi uživateli iBatisu prvními odpadlíky od JPA like frameworků.