Ibatorで件数制限つきのselectByExampleを自動生成する

データベースを検索して複数件の結果を取得するような場合、iBATISではSqlMapClient#queryForList メソッドを使用する。queryForList には skip と max という引数があり、これを指定するとMySQLで言うところの SELECT ... LIMIT (skip), (max) のような結果が得られる。

ただ、どうもこれは SELECT ... LIMIT (skip), (max) といったSQLを発行してくれているわけではなく、全件取得してクライアント側で絞っているようなので、普通に件数の絞り込みを行う場合のようにサーバサイドで絞るためには、LIMIT句を書いた <select> 要素をSQL Mapファイルに書く必要がある。

で、Ibatorでたくさんのテーブルに対して SQL Mapファイルや Example クラスを生成したときに、いちいち全てのテーブル用にそういった絞り込みのためのSQL Map定義やらを行うのは面倒なので、勝手に件数制限機能を追加してくれるようなプラグインを書いてみる。とりあえず目標は OracleMySQL

まず、こんな感じの親クラスを作成。

package example.ibatis.plugins;

import java.util.List;

import org.apache.ibatis.ibator.api.IbatorPluginAdapter;
import org.apache.ibatis.ibator.api.IntrospectedTable;
import org.apache.ibatis.ibator.api.dom.java.Field;
import org.apache.ibatis.ibator.api.dom.java.FullyQualifiedJavaType;
import org.apache.ibatis.ibator.api.dom.java.JavaVisibility;
import org.apache.ibatis.ibator.api.dom.java.Method;
import org.apache.ibatis.ibator.api.dom.java.Parameter;
import org.apache.ibatis.ibator.api.dom.java.TopLevelClass;
import org.apache.ibatis.ibator.api.dom.xml.Attribute;
import org.apache.ibatis.ibator.api.dom.xml.Element;
import org.apache.ibatis.ibator.api.dom.xml.XmlElement;
import org.apache.ibatis.ibator.internal.util.JavaBeansUtil;

public class SelectLimitPluginBase extends IbatorPluginAdapter {

    @Override
    public boolean validate(List<String> warnings) {
        return true;
    }

    @Override
    public boolean modelExampleClassGenerated(TopLevelClass topLevelClass,
            IntrospectedTable introspectedTable) {

        addProperty(topLevelClass,
                new FullyQualifiedJavaType("Integer"), getOffsetPropertyName());
        addProperty(topLevelClass,
                new FullyQualifiedJavaType("Integer"), getLimitPropertyName());

        return true;
    }

    protected String getOffsetPropertyName() {
        return properties.getProperty("offsetPropertyName", "offset");
    }

    protected String getLimitPropertyName() {
        return properties.getProperty("limitPropertyName", "limit");
    }

    // privateフィールド、getter、setter を追加
    private void addProperty(TopLevelClass topLevelClass,
            FullyQualifiedJavaType propertyType, String property) {
        String validProperty = JavaBeansUtil.getValidPropertyName(property);

        Field field = new Field();
        field.setVisibility(JavaVisibility.PROTECTED);
        field.setType(propertyType);
        field.setName(validProperty);
        topLevelClass.addField(field);

        Method getter = new Method();
        getter.setVisibility(JavaVisibility.PUBLIC);
        getter.setReturnType(propertyType);
        getter.setName(JavaBeansUtil.getGetterMethodName(validProperty, propertyType));
        getter.addBodyLine(String.format("return %s;", validProperty));
        topLevelClass.addMethod(getter);

        Method setter = new Method();
        setter.setVisibility(JavaVisibility.PUBLIC);
        setter.setName(JavaBeansUtil.getSetterMethodName(validProperty));
        setter.addParameter(new Parameter(propertyType, validProperty));
        setter.addBodyLine(String.format("this.%s = %s;", validProperty, validProperty));
        topLevelClass.addMethod(setter);
    }

    // XML定義を「流れるようなインタフェース」で書けるようにする
    public static class FluentXmlElement extends XmlElement {
        public FluentXmlElement(String name) {
            super(name);
        }
        public FluentXmlElement elem(Element element) {
            super.addElement(element);
            return this;
        }
        public FluentXmlElement attr(String name, String value) {
            super.addAttribute(new Attribute(name, value));
            return this;
        }
    }
}

これを継承して、Oracle用とMySQL用のSQL Map XMLの定義を追加するクラスを作成する。

まずOracle用。かなり面倒くさい(なんでOracleMySQLのような簡単な記法をいつまでも提供してくれないんだろうか……)。

package example.ibatis.plugins;

import org.apache.ibatis.ibator.api.IntrospectedTable;
import org.apache.ibatis.ibator.api.dom.xml.TextElement;
import org.apache.ibatis.ibator.api.dom.xml.XmlElement;

public class OracleSelectLimitPlugin extends SelectLimitPluginBase {

    @Override
    public boolean sqlMapSelectByExampleWithoutBLOBsElementGenerated(
            XmlElement element, IntrospectedTable introspectedTable) {

        String limit = getLimitPropertyName();
        String offset = getOffsetPropertyName();

        // 以下のようなXML断片を、selectByExampleの最初に追加
        // <isParameterPresent>
        //   <isNotNull property="limit">
        //     <isNull property="offset">
        //       select * from (
        //     </isNull>
        //     <isNotNull property="offset">
        //       select * from ( select inner_.*, rownum rownum_ from (
        //     </isNotNull>
        //   </isNotNull>
        // </isParameterPresent>
        element.addElement(0, new FluentXmlElement("isParameterPresent")
            .elem(new FluentXmlElement("isNotNull")
                .attr("property", limit)
                .elem(new FluentXmlElement("isNull")
                    .attr("property", offset)
                    .elem(new TextElement("select * from ( "))
                    )
                    .elem(new FluentXmlElement("isNotNull")
                    .attr("property", offset)
                    .elem(new TextElement("select * from ( select inner_.*, rownum rownum_ from ( "))
                    )
                )
            );

        // 以下のようなXML断片を、selectByExampleの最後に追加
        // <isParameterPresent>
        //   <isNotNull property="limit">
        //     <isNull property="offset">
        //       ) where rownum &lt;= #limit:DECIMAL#
        //     </isNull>
        //     <isNotNull property="offset">
        //       ) inner_ ) where rownum_ &lt;= #limit:DECIMAL#+#offset:DECIMAL# and rownum_ &gt; #offset:DECIMAL#
        //     </isNotNull>
        //   </isNotNull>
        // </isParameterPresent>
        element.addElement(new FluentXmlElement("isParameterPresent")
            .elem(new FluentXmlElement("isNotNull")
                .attr("property", limit)
                .elem(new FluentXmlElement("isNull")
                    .attr("property", offset)
                    .elem(new TextElement(" ) where rownum &lt;= #" + limit + ":DECIMAL#"))
                    )
                .elem(new FluentXmlElement("isNotNull")
                    .attr("property", offset)
                    .elem(new TextElement(" ) inner_ )"
                            + " where rownum_ &lt;= #" + offset + ":DECIMAL#+#" + limit + ":DECIMAL#"
                            + " and rownum_ &gt; #" + offset + ":DECIMAL#"))
                    )
                )
            );

        return true;
    }
}

次にMySQL用。こちらは簡単。

package example.ibatis.plugins;

import org.apache.ibatis.ibator.api.IntrospectedTable;
import org.apache.ibatis.ibator.api.dom.xml.TextElement;
import org.apache.ibatis.ibator.api.dom.xml.XmlElement;

public class MySQLSelectLimitPlugin extends SelectLimitPluginBase {

    @Override
    public boolean sqlMapSelectByExampleWithoutBLOBsElementGenerated(
            XmlElement element, IntrospectedTable introspectedTable) {

        String limit = getLimitPropertyName();
        String offset = getOffsetPropertyName();

        element.addElement(new FluentXmlElement("isParameterPresent")
            .elem(new FluentXmlElement("isNotNull")
                .attr("property", "limit")
                .elem(new TextElement(" LIMIT "))
                .elem(new FluentXmlElement("isNotNull")
                    .attr("property", "offset")
                    .elem(new TextElement("#" + offset + ":DECIMAL#, "))
                    )
                .elem(new TextElement("#" + limit + ":DECIMAL#"))
                )
            );

        return true;
    }
}

あとは、ibatorConfig.xmlプラグイン指定で

    <ibatorPlugin type="example.ibatis.plugins.OracleSelectLimitPlugin">
      <property name="limitPropertyName" value="limit" />
      <property name="offsetPropertyName" value="offset" />
    </ibatorPlugin>

のように指定して自動生成行うと、全ての Example クラスに limit と offset という Integer 型のプロパティが追加され、

// IDの降順で並べ替えられた検索結果の41件目〜60件目までを取得する
UserExample example = new UserExample();
example.setLimit(20);
example.setOffset(40);
example.setOrderByClause("ID DESC");
List<User> users = getUserDAO().selectByExample(example);

のように絞り込みを行えるようになる。