Preface

The Mybatis Generator plug-in can quickly implement basic database CRUD operations. It supports both JAVA and Kotlin languages, freeing programmers from repeated Mapper and Dao layer code writing. Mybatis Generator can automatically generate most of the SQL code, such as update, updateSelectively, insert, insertSelectively, select statements, etc. However, when the SQL required in the program is not within the scope of the automatically generated SQL, you need to use a custom Mapper to implement it, that is, manually write the DAO layer and the Mapper file (there is a small pit, when the database entity adds fields, the corresponding self The definition of Mapper should also be updated manually in time). Aside from complex customized SQL such as join, group by, etc., there are actually some commonly used SQLs that are not automatically generated in the basic Mybatis Generator tool, such as paging capabilities, pessimistic locking, optimistic locking, etc., and Mybatis Generator is also for these The appeal provides the ability of Plugin. By customizing the implementation of Plugin, the behavior of Mybatis Generator when generating Mapper and Dao files can be changed. This article will take pessimistic lock as an example to let you quickly understand how to implement Mybatis Generator Plugin.

Implementation background:
Database: MYSQL
mybatis generator runtime:MyBatis3

<!-- more -->

Realize Mybatis pessimistic lock

When the business needs to ensure strong consistency, it can be realized by pessimistically locking the data row in the transaction and then performing the operation. This is the classic "one lock, two judgments and three updates". In transaction or payment systems, this kind of appeal is very common. Mysql provides Select...For Update statement to achieve pessimistic locking on data rows. This article will not give a detailed introduction to Select...For Update, and interested students can check other articles for in-depth understanding.

Mybatis Generator Plugin provides good support for this kind of universal SQL. By inheriting the org.mybatis.generator.api.PluginAdapter class, you can customize the SQL generation logic and use it in the configuration file. PluginAdapter is the implementation class of the Plugin interface, which provides the default implementation of Plugin. This article will introduce several of the more important methods:

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);
}

Here, it can be better understood by combining the configuration file of Mybatis Generator and the generated DAO (also called Client file) and Mapper XML file. A sample of the Mybatis Generator configuration file is as follows, which contains some main configuration information, such as the <jdbcConnection> tag used to describe the database connection, the <javaTypeResolver> tag used to define the database and Java type conversion, etc.

<?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>

These are all mapped into Context objects and passed to the specific Plugin implementation setContext(Context context)

public class Context extends PropertyHolder{

    /**
    * <context>标签的id属性
    */
    private String id;

    /**
    * jdbc链接信息,对应<jdbcConnection>标签中的信息
    */
    private JDBCConnectionConfiguration jdbcConnectionConfiguration;

    /**
    * 类型映射配置,对应<javaTypeResolver>
    */
    private JavaTypeResolverConfiguration javaTypeResolverConfiguration;

    /**
    * ...其它标签对应的配置信息
    */
}

setProperties the <properties> tags under the context and maps them into the Properties class, which is actually a Map container, just as the Properties class itself inherits Hashtable. Take the configuration file in the above article as an example, you can get the value "true" through properties.get("printLog").

validate method represents whether this Plugin is executed. It usually performs some very basic checks, such as whether it is compatible with the corresponding database driver or Mybatis version:

    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;
        }
    }

If the validate method returns false, the Plugin will not run in any scenario.

Next are the two most important methods, which are used to generate a new method clientGenerated in DAO and generate a new SQL sqlMapDocumentGenerated in an XML file.

Let’s talk about clientGenerated first. This method has three parameters. Interfaze is the currently generated Dao interface of the client. topLevelClass refers to the generated implementation class. This class may be empty. IntrospectedTable refers to the currently processed data table, which includes the slave database. Various information about the table obtained in the, including column names, column types, etc. Here you can take a look at several more important methods in the introspectedTable:

public abstract class IntrospectedTable {
    /**
    * 该方法可以获得配置文件中该表对应<table>标签下的配置信息,包括映射成的Mapper名称,PO名称等
    * 也可以在table标签下自定义<property>标签并通过getProperty方法获得值
    */
    public TableConfiguration getTableConfiguration() {
        return tableConfiguration;
    }

    /**
    * 这个方法中定义了默认的生成规则,可以通过calculateAllFieldsClass获得返回类型
    */
    public Rules getRules() {
        return rules;
    }
}

The clientGenerated method of pessimistic lock is as follows:

    // 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);
    }

Here you can determine whether to generate a corresponding pessimistic lock method for this table by adding a property tag under the corresponding table. The configuration example is as follows:

 <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>

The code defines the name, parameters, return type, etc. of the method through the Method method provided by mybatis, and uses the interfaze.addMethod method to add the method to the client interface.

Then to the method sqlMapDocumentGenerated, this method passes in the Document object, which corresponds to the generated XML file, and maps the elements in the XML file through XmlElement. document.getRootElement().addElement can insert custom XML elements into the Mapper file through 06151dc816487e. Custom XML element refers to splicing XmlElement. The addAttribute method of XmlElement can set attributes for XML elements, and addElement can add sub-elements to XML tags. There are two types of sub-elements, namely TextElement and XmlElement itself. TextElement directly fills the content in the tag, while XmlElement corresponds to the new tag, such as <where> <include> and so on. The SQL generation logic of pessimistic locking is as follows:

    // 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);
    }

Complete code

@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);
    }

}

raledong
2.7k 声望2k 粉丝

心怀远方,负重前行