Mybatis generator plugin pessimistic lock implementation

Time:2021-11-27

preface

Mybatis generator plug-in can quickly realize basic database crud operation. It supports Java language and kotlin language at the same time, freeing programmers from repeated mapper and Dao layer coding. Mybatis generator can automatically generate most SQL codes, such as update, updateselectively, insert, insertselectively, select statements, etc. However, when the required SQL in the program is not within the scope of automatically generated SQL, it needs to be implemented by using custom mapper, that is, manually writing Dao layer and mapper files (there is a small pit here. When the database entity adds fields, the corresponding custom mapper should also be updated manually in time). Despite the complex customized SQL, such as join, group by, etc., there are still some commonly used SQL that are not automatically generated in the basic mybatis generator tool, such as paging capability, pessimistic lock, optimistic lock, etc., and mybatis generator also provides plugin capability for these demands. The custom implementation of plugin can change the behavior of mybatis generator when generating mapper and Dao files. 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 –>

Implement mybatis pessimistic lock

When the business needs to ensure strong consistency, it can be realized by pessimistic locking the data row in the transaction and then operating. This is the classic “one lock, two judgments and three updates” “. this demand is very common in transaction or payment systems. MySQL provides a select… For UPDATE statement to implement pessimistic locking on data rows. This article will not introduce select… For update in detail. Interested students can see other articles for in-depth understanding.

Mybatis generator plugin provides good support for this general SQL. By inheritanceorg.mybatis.generator.api.PluginAdapterClass to customize the SQL generation logic and use it in the configuration file.PluginAdapteryesPluginThe implementation class of the interface provides the default implementation of plugin. This article will introduce several important methods:

public interface Plugin {
    /**
    *Pass the context information in the mybatis generator configuration file to the plugin implementation class
    *This information includes database links, type mapping, configuration, etc
    */
    void setContext(Context context);

    /**
    *All properties tags in the configuration file
    **/
    void setProperties(Properties properties);

    /**
    *Verify whether the plugin is executed. If false is returned, the plugin will not be executed
    **/
    boolean validate(List<String> warnings);

    /**
    *This method will be triggered after the Dao file is generated. You can add methods or attributes in the Dao file by implementing this method
    **/
    boolean clientGenerated(Interface interfaze, TopLevelClass topLevelClass,
            IntrospectedTable introspectedTable);

    /**
    *This method will be called after the SQL XML file is generated. You can add an XML definition in the mapper XML file by implementing this method
    **/
    boolean sqlMapDocumentGenerated(Document document, IntrospectedTable introspectedTable);
}

It can be better understood here by combining the configuration file of mybatis generator with the generated Dao (also known as client file) and mapper XML file. The sample mybatis generator configuration file is as follows, which contains some main configuration information, such as the < JDBC connection > tag used to describe database links, and the < javatyperesolver > tag used to define database and Java type conversion.

<?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 mapped into context objects and passed throughsetContext(Context context)Method is passed to the specific plugin implementation:

public class Context extends PropertyHolder{

    /**
    *ID attribute of < context > tag
    */
    private String id;

    /**
    *JDBC link information, corresponding to the information in the < JDBC connection > tag
    */
    private JDBCConnectionConfiguration jdbcConnectionConfiguration;

    /**
    *Type mapping configuration, corresponding to < javatyperesolver >
    */
    private JavaTypeResolverConfiguration javaTypeResolverConfiguration;

    /**
    *... configuration information corresponding to other tags
    */
}

setPropertiesThe < Properties > tag under the context is collected and mapped into the properties class, which is actually a map container, just as the properties class itself inherits the hashtable. Taking the configuration file in the above article as an example, you can obtain the value “true” through properties.get (“printlog”).

validateMethod represents whether the 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: plugin" + this. Getclass(). Gettypename() + "targetruntime must be mybatis3!");
            return false;
        } else {
            return true;
        }
    }

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

Then there are the two most important methods: clientgenerated for generating a new method in Dao and sqlmapdocumentgenerated for generating a new SQL method in XML file.

Let’s talk about clientgenerated. This method has three parameters. Interfaze is the currently generated client Dao interface. Toplevelclass refers to the generated implementation class, which may be empty. Introspectedtable refers to the currently processed data table, which contains various information about the table obtained from the database, including column name, column type, etc. Here are some important methods in the introspectedtable:

public abstract class IntrospectedTable {
    /**
    *This method can obtain the configuration information under the < Table > tag corresponding to the table in the configuration file, including mapper name, Po name, etc
    *You can also customize the < property > tag under the table tag and obtain the value through the getproperty method
    */
    public TableConfiguration getTableConfiguration() {
        return tableConfiguration;
    }

    /**
    *The default generation rule is defined in this method, and the return type can be obtained through calculateallfieldsclass
    */
    public Rules getRules() {
        return rules;
    }
}

The clientgenerated method for pessimistic locks is as follows:

//Plugin configuration, do you want to generate a selectforupdate statement
    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" +
                    "* lock data rows with ID \ n"+
                    "      */";
            method.addJavaDocLine(docComment);
            interfaze.addMethod(method);
            Log. Debug ("(pessimistic lock plug-in):" + interfaze. Gettype(). Getshortname() + "add" + method_name + "method");
        }

        return super.clientGenerated(interfaze, topLevelClass, introspectedTable);
    }

Here, you can decide whether to generate the 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>

In the code, the method method provided by mybatis defines the name, parameters, return type, etc. of the method, and uses the interfaze.addmethod method method to add the method to the client interface.

Then go to the sqlmapdocumentgenerated method, which passes in the document object, which corresponds to the generated XML file, and maps the elements in the XML file through xmlelement. adoptdocument.getRootElement().addElementYou can insert custom XML elements into mapper files. Custom XML elements refer to splicing xmlelements. The addAttribute method of xmlelement can set attributes for XML elements, and addelement can add child elements for XML tags. There are two types of child elements, textelement and xmlelement itself. Textelement directly fills the content in the label, while xmlelement corresponds to a new label, such as < where > < include >. The SQL generation logic of pessimistic lock is as follows:

//Plugin configuration, do you want to generate a selectforupdate statement
    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 ("(pessimistic lock plug-in):" + introspectedtable. Getmybatis3xmlmapperfilename() + "add" + method_name + "method (" + (introspectedtable. Hasblobcolumns()? "Yes": "None") + "blob type"));
        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" +
                    "* lock data rows with ID \ n"+
                    "      */";
            method.addJavaDocLine(docComment);
            interfaze.addMethod(method);
            Log. Debug ("(pessimistic lock plug-in):" + interfaze. Gettype(). Getshortname() + "add" + method_name + "method");
        }

        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 ("(pessimistic lock plug-in):" + introspectedtable. Getmybatis3xmlmapperfilename() + "add" + method_name + "method (" + (introspectedtable. Hasblobcolumns()? "Yes": "None") + "blob type"));
        return super.sqlMapDocumentGenerated(document, introspectedTable);
    }

}