前言
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); }
残缺代码
@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); }}