前言

Mybatis Generator插件能够疾速的实现根底的数据库CRUD操作,它同时反对JAVA语言和Kotlin语言,将程序员从反复的Mapper和Dao层代码编写中释放出来。Mybatis Generator能够主动生成大部分的SQL代码,如update,updateSelectively,insert,insertSelectively,select语句等。然而,当程序中须要SQL不在主动生成的SQL范畴内时,就须要应用自定义Mapper来实现,即手动编写DAO层和Mapper文件(这里有一个小坑,当数据库实体减少字段时,对应的自定义Mapper也要及时手动更新)。抛开简单的定制化SQL如join,group by等,其实还是有一些比拟罕用的SQL在根底的Mybatis Generator工具中没有主动生成,比方分页能力,乐观锁,乐观锁等,而Mybatis Generator也为这些诉求提供了Plugin的能力。通过自定义实现Plugin能够扭转Mybatis Generator在生成Mapper和Dao文件时的行为。本文将从乐观锁为例,让你疾速理解如何实现Mybatis Generator Plugin。

实现背景:
数据库:MYSQL
mybatis generator runtime:MyBatis3

<!-- more -->

实现Mybatis乐观锁

当业务呈现须要保障强统一的场景时,能够通过在事务中对数据行上乐观锁后再进行操作来实现,这就是经典的”一锁二判三更新“。在交易或是领取零碎中,这种诉求十分广泛。Mysql提供了Select...For Update语句来实现对数据行上乐观锁。本文将不对Select...For Update进行具体的介绍,有趣味的同学能够查看其它文章深刻理解。

Mybatis Generator Plugin为这种具备通用性的SQL提供了很好的反对。通过继承org.mybatis.generator.api.PluginAdapter类即可自定义SQL生成逻辑并在在配置文件中应用。PluginAdapterPlugin接口的实现类,提供了Plugin的默认实现,本文将介绍其中比拟重要的几个办法:

public interface Plugin {    /**    * 将Mybatis Generator配置文件中的上下文信息传递到Plugin实现类中    * 这些信息包含数据库链接,类型映射配置等    */    void setContext(Context context);    /**    * 配置文件中的所有properties标签    **/    void setProperties(Properties properties);    /**    * 校验该Plugin是否执行,如果返回false,则该插件不会执行    **/    boolean validate(List<String> warnings);    /**    * 当DAO文件实现生成后会触发该办法,能够通过实现该办法在DAO文件中新增办法或属性    **/    boolean clientGenerated(Interface interfaze, TopLevelClass topLevelClass,            IntrospectedTable introspectedTable);    /**    * 当SQL XML 文件生成后会调用该办法,能够通过实现该办法在MAPPER XML文件中新增XML定义    **/    boolean sqlMapDocumentGenerated(Document document, IntrospectedTable introspectedTable);}

这里联合Mybatis Generator的配置文件和生成的DAO(也称为Client文件)和Mapper XML文件能够更好的了解。Mybatis Generator配置文件样例如下,其中蕴含了次要的一些配置信息,如用于形容数据库链接的<jdbcConnection>标签,用于定义数据库和Java类型转换的<javaTypeResolver>标签等。

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE generatorConfiguration  PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"  "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration>  <classPathEntry location="/Program Files/IBM/SQLLIB/java/db2java.zip" />  <context id="DB2Tables" targetRuntime="MyBatis3">    <jdbcConnection driverClass="COM.ibm.db2.jdbc.app.DB2Driver"        connectionURL="jdbc:db2:TEST"        userId="db2admin"        password="db2admin">    </jdbcConnection>    <javaTypeResolver >      <property name="forceBigDecimals" value="false" />    </javaTypeResolver>    <javaModelGenerator targetPackage="test.model" targetProject="\MBGTestProject\src">      <property name="enableSubPackages" value="true" />      <property name="trimStrings" value="true" />    </javaModelGenerator>    <sqlMapGenerator targetPackage="test.xml"  targetProject="\MBGTestProject\src">      <property name="enableSubPackages" value="true" />    </sqlMapGenerator>    <javaClientGenerator type="XMLMAPPER" targetPackage="test.dao"  targetProject="\MBGTestProject\src">      <property name="enableSubPackages" value="true" />    </javaClientGenerator>    <property name="printLog" value="true"/>    <table schema="DB2ADMIN" tableName="ALLTYPES" domainObjectName="Customer" >      <property name="useActualColumnNames" value="true"/>      <generatedKey column="ID" sqlStatement="DB2" identity="true" />      <columnOverride column="DATE_FIELD" property="startDate" />      <ignoreColumn column="FRED" />      <columnOverride column="LONG_VARCHAR_FIELD" jdbcType="VARCHAR" />    </table>  </context></generatorConfiguration>

这些都被映射成Context对象,并通过setContext(Context context)办法传递到具体的Plugin实现中:

public class Context extends PropertyHolder{    /**    * <context>标签的id属性    */    private String id;    /**    * jdbc链接信息,对应<jdbcConnection>标签中的信息    */    private JDBCConnectionConfiguration jdbcConnectionConfiguration;    /**    * 类型映射配置,对应<javaTypeResolver>    */    private JavaTypeResolverConfiguration javaTypeResolverConfiguration;    /**    * ...其它标签对应的配置信息    */}

setProperties则将context下的<properties>标签收集起来并映射成Properties类,它实际上是一个Map容器,正如Properties类自身就继承了Hashtable。以上文中的配置文件为例,能够通过properties.get("printLog")取得值"true"。

validate办法则代表了这个Plugin是否执行,它通常进行一些十分根底的校验,比方是否兼容对应的数据库驱动或者是Mybatis版本:

    public boolean validate(List<String> warnings) {        if (StringUtility.stringHasValue(this.getContext().getTargetRuntime()) && !"MyBatis3".equalsIgnoreCase(this.getContext().getTargetRuntime())) {            logger.warn("itfsw:插件" + this.getClass().getTypeName() + "要求运行targetRuntime必须为MyBatis3!");            return false;        } else {            return true;        }    }

如果validate办法返回false,则无论什么场景下都不会运行这个Plugin。

接着是最重要的两个办法,别离是用于在DAO中生成新的办法clientGenerated和在XML文件中生成新的SQL sqlMapDocumentGenerated。

先说clientGenerated,这个办法共有三个参数,interfaze是以后曾经生成的客户端Dao接口,topLevelClass是指生成的实现类,这个类可能为空,introspectedTable是指以后解决的数据表,这里蕴含了从数据库中获取的对于表的各种信息,包含列名称,列类型等。这里能够看一下introspectedTable中几个比拟重要的办法:

public abstract class IntrospectedTable {    /**    * 该办法能够取得配置文件中该表对应<table>标签下的配置信息,包含映射成的Mapper名称,PO名称等    * 也能够在table标签下自定义<property>标签并通过getProperty办法取得值    */    public TableConfiguration getTableConfiguration() {        return tableConfiguration;    }    /**    * 这个办法中定义了默认的生成规定,能够通过calculateAllFieldsClass取得返回类型    */    public Rules getRules() {        return rules;    }}

乐观锁的clientGenerated办法如下:

    // Plugin配置,是否要生成selectForUpdate语句    private static final String CONFIG_XML_KEY = "implementSelectForUpdate";    @Override    public boolean clientGenerated(Interface interfaze, TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {        String implementUpdate = introspectedTable.getTableConfiguration().getProperty(CONFIG_XML_KEY);        if (StringUtility.isTrue(implementUpdate)) {            Method method = new Method(METHOD_NAME);            FullyQualifiedJavaType returnType = introspectedTable.getRules().calculateAllFieldsClass();            method.setReturnType(returnType);            method.addParameter(new Parameter(new FullyQualifiedJavaType("java.lang.Long"), "id"));            String docComment = "/**\n" +                    "      * 应用id对数据行上乐观锁\n" +                    "      */";            method.addJavaDocLine(docComment);            interfaze.addMethod(method);            log.debug("(乐观锁插件):" + interfaze.getType().getShortName() + "减少" + METHOD_NAME + "办法。");        }        return super.clientGenerated(interfaze, topLevelClass, introspectedTable);    }

这里能够通过在对应table下新增property标签来决定是否要为这张表生成对应的乐观锁办法,配置样例如下:

 <table tableName="demo" domainObjectName="DemoPO" mapperName="DemoMapper"               enableCountByExample="true"               enableUpdateByExample="true"               enableDeleteByExample="true"               enableSelectByExample="true"               enableInsert="true"               selectByExampleQueryId="true">    <property name="implementUpdateWithCAS" value="true"/> </table>

代码中通过mybatis提供的Method办法,定义了办法的名称,参数,返回类型等,并应用interfaze.addMethod办法将办法增加到客户端的接口中。

再到sqlMapDocumentGenerated这个办法,这个办法中传入了Document对象,它对应生成的XML文件,并通过XmlElement来映射XML文件中的元素。通过document.getRootElement().addElement能够将自定义的XML元素插入到Mapper文件中。自定义XML元素就是指拼接XmlElement,XmlElement的addAttribute办法能够为XML元素设置属性,addElement则能够为XML标签增加子元素。有两种类型的子元素,别离是TextElement和XmlElement自身,TextElement则间接填充标签中的内容,而XmlElement则对应新的标签,如<where> <include>等。乐观锁的SQL生成逻辑如下:

    // Plugin配置,是否要生成selectForUpdate语句    private static final String CONFIG_XML_KEY = "implementSelectForUpdate";    @Override    public boolean sqlMapDocumentGenerated(Document document, IntrospectedTable introspectedTable) {        String implementUpdate = introspectedTable.getTableConfiguration().getProperty(CONFIG_XML_KEY);        if (!StringUtility.isTrue(implementUpdate)) {            return super.sqlMapDocumentGenerated(document, introspectedTable);        }        XmlElement selectForUpdate = new XmlElement("select");        selectForUpdate.addAttribute(new Attribute("id", METHOD_NAME));        StringBuilder sb;        String resultMapId = introspectedTable.hasBLOBColumns() ? introspectedTable.getResultMapWithBLOBsId() : introspectedTable.getBaseResultMapId();        selectForUpdate.addAttribute(new Attribute("resultMap", resultMapId));        selectForUpdate.addAttribute(new Attribute("parameterType", introspectedTable.getExampleType()));        selectForUpdate.addElement(new TextElement("select"));        sb = new StringBuilder();        if (StringUtility.stringHasValue(introspectedTable.getSelectByExampleQueryId())) {            sb.append('\'');            sb.append(introspectedTable.getSelectByExampleQueryId());            sb.append("' as QUERYID,");            selectForUpdate.addElement(new TextElement(sb.toString()));        }        XmlElement baseColumn = new XmlElement("include");        baseColumn.addAttribute(new Attribute("refid", introspectedTable.getBaseColumnListId()));        selectForUpdate.addElement(baseColumn);        if (introspectedTable.hasBLOBColumns()) {            selectForUpdate.addElement(new TextElement(","));            XmlElement blobColumns = new XmlElement("include");            blobColumns.addAttribute(new Attribute("refid", introspectedTable.getBaseColumnListId()));            selectForUpdate.addElement(blobColumns);        }        sb.setLength(0);        sb.append("from ");        sb.append(introspectedTable.getAliasedFullyQualifiedTableNameAtRuntime());        selectForUpdate.addElement(new TextElement(sb.toString()));        TextElement whereXml = new TextElement("where id = #{id} for update");        selectForUpdate.addElement(whereXml);        document.getRootElement().addElement(selectForUpdate);        log.debug("(乐观锁插件):" + introspectedTable.getMyBatis3XmlMapperFileName() + "减少" + METHOD_NAME + "办法(" + (introspectedTable.hasBLOBColumns() ? "有" : "无") + "Blob类型))。");        return super.sqlMapDocumentGenerated(document, introspectedTable);    }

残缺代码

@Slf4jpublic class SelectForUpdatePlugin extends PluginAdapter {    private static final String CONFIG_XML_KEY = "implementSelectForUpdate";    private static final String METHOD_NAME = "selectByIdForUpdate";    @Override    public boolean validate(List<String> list) {        return true;    }    @Override    public boolean clientGenerated(Interface interfaze, TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {        String implementUpdate = introspectedTable.getTableConfiguration().getProperty(CONFIG_XML_KEY);        if (StringUtility.isTrue(implementUpdate)) {            Method method = new Method(METHOD_NAME);            FullyQualifiedJavaType returnType = introspectedTable.getRules().calculateAllFieldsClass();            method.setReturnType(returnType);            method.addParameter(new Parameter(new FullyQualifiedJavaType("java.lang.Long"), "id"));            String docComment = "/**\n" +                    "      * 应用id对数据行上乐观锁\n" +                    "      */";            method.addJavaDocLine(docComment);            interfaze.addMethod(method);            log.debug("(乐观锁插件):" + interfaze.getType().getShortName() + "减少" + METHOD_NAME + "办法。");        }        return super.clientGenerated(interfaze, topLevelClass, introspectedTable);    }    @Override    public boolean sqlMapDocumentGenerated(Document document, IntrospectedTable introspectedTable) {        String implementUpdate = introspectedTable.getTableConfiguration().getProperty(CONFIG_XML_KEY);        if (!StringUtility.isTrue(implementUpdate)) {            return super.sqlMapDocumentGenerated(document, introspectedTable);        }        XmlElement selectForUpdate = new XmlElement("select");        selectForUpdate.addAttribute(new Attribute("id", METHOD_NAME));        StringBuilder sb;        String resultMapId = introspectedTable.hasBLOBColumns() ? introspectedTable.getResultMapWithBLOBsId() : introspectedTable.getBaseResultMapId();        selectForUpdate.addAttribute(new Attribute("resultMap", resultMapId));        selectForUpdate.addAttribute(new Attribute("parameterType", introspectedTable.getExampleType()));        selectForUpdate.addElement(new TextElement("select"));        sb = new StringBuilder();        if (StringUtility.stringHasValue(introspectedTable.getSelectByExampleQueryId())) {            sb.append('\'');            sb.append(introspectedTable.getSelectByExampleQueryId());            sb.append("' as QUERYID,");            selectForUpdate.addElement(new TextElement(sb.toString()));        }        XmlElement baseColumn = new XmlElement("include");        baseColumn.addAttribute(new Attribute("refid", introspectedTable.getBaseColumnListId()));        selectForUpdate.addElement(baseColumn);        if (introspectedTable.hasBLOBColumns()) {            selectForUpdate.addElement(new TextElement(","));            XmlElement blobColumns = new XmlElement("include");            blobColumns.addAttribute(new Attribute("refid", introspectedTable.getBaseColumnListId()));            selectForUpdate.addElement(blobColumns);        }        sb.setLength(0);        sb.append("from ");        sb.append(introspectedTable.getAliasedFullyQualifiedTableNameAtRuntime());        selectForUpdate.addElement(new TextElement(sb.toString()));        TextElement whereXml = new TextElement("where id = #{id} for update");        selectForUpdate.addElement(whereXml);        document.getRootElement().addElement(selectForUpdate);        log.debug("(乐观锁插件):" + introspectedTable.getMyBatis3XmlMapperFileName() + "减少" + METHOD_NAME + "办法(" + (introspectedTable.hasBLOBColumns() ? "有" : "无") + "Blob类型))。");        return super.sqlMapDocumentGenerated(document, introspectedTable);    }}