前言
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 生成逻辑并在在配置文件中应用。PluginAdapter
是 Plugin
接口的实现类,提供了 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);
}
残缺代码
@Slf4j
public 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);
}
}