前言
Mybatis Generator插件可以快速的實(shí)現(xiàn)基礎(chǔ)的數(shù)據(jù)庫(kù)CRUD操作,它同時(shí)支持JAVA語(yǔ)言和Kotlin語(yǔ)言,將程序員從重復(fù)的Mapper和Dao層代碼編寫中釋放出來(lái)。Mybatis Generator可以自動(dòng)生成大部分的SQL代碼,如update,updateSelectively,insert,insertSelectively,select語(yǔ)句等。但是,當(dāng)程序中需要SQL不在自動(dòng)生成的SQL范圍內(nèi)時(shí),就需要使用自定義Mapper來(lái)實(shí)現(xiàn),即手動(dòng)編寫DAO層和Mapper文件(這里有一個(gè)小坑,當(dāng)數(shù)據(jù)庫(kù)實(shí)體增加字段時(shí),對(duì)應(yīng)的自定義Mapper也要及時(shí)手動(dòng)更新)。拋開(kāi)復(fù)雜的定制化SQL如join,group by等,其實(shí)還是有一些比較常用的SQL在基礎(chǔ)的Mybatis Generator工具中沒(méi)有自動(dòng)生成,比如分頁(yè)能力,悲觀鎖,樂(lè)觀鎖等,而Mybatis Generator也為這些訴求提供了Plugin的能力。通過(guò)自定義實(shí)現(xiàn)Plugin可以改變Mybatis Generator在生成Mapper和Dao文件時(shí)的行為。本文將從悲觀鎖為例,讓你快速了解如何實(shí)現(xiàn)Mybatis Generator Plugin。
實(shí)現(xiàn)背景:
- 數(shù)據(jù)庫(kù):MYSQL
- mybatis generator runtime:MyBatis3
實(shí)現(xiàn)Mybatis悲觀鎖
當(dāng)業(yè)務(wù)出現(xiàn)需要保證強(qiáng)一致的場(chǎng)景時(shí),可以通過(guò)在事務(wù)中對(duì)數(shù)據(jù)行上悲觀鎖后再進(jìn)行操作來(lái)實(shí)現(xiàn),這就是經(jīng)典的”一鎖二判三更新“。在交易或是支付系統(tǒng)中,這種訴求非常普遍。Mysql提供了Select...For Update語(yǔ)句來(lái)實(shí)現(xiàn)對(duì)數(shù)據(jù)行上悲觀鎖。本文將不對(duì)Select...For Update進(jìn)行詳細(xì)的介紹,有興趣的同學(xué)可以查看其它文章深入了解。
Mybatis Generator Plugin為這種具有通用性的SQL提供了很好的支持。通過(guò)繼承org.mybatis.generator.api.PluginAdapter類即可自定義SQL生成邏輯并在在配置文件中使用。PluginAdapter是Plugin接口的實(shí)現(xiàn)類,提供了Plugin的默認(rèn)實(shí)現(xiàn),本文將介紹其中比較重要的幾個(gè)方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public interface Plugin { /** * 將Mybatis Generator配置文件中的上下文信息傳遞到Plugin實(shí)現(xiàn)類中 * 這些信息包括數(shù)據(jù)庫(kù)鏈接,類型映射配置等 */ void setContext(Context context); /** * 配置文件中的所有properties標(biāo)簽 **/ void setProperties(Properties properties); /** * 校驗(yàn)該P(yáng)lugin是否執(zhí)行,如果返回false,則該插件不會(huì)執(zhí)行 **/ boolean validate(List<String> warnings); /** * 當(dāng)DAO文件完成生成后會(huì)觸發(fā)該方法,可以通過(guò)實(shí)現(xiàn)該方法在DAO文件中新增方法或?qū)傩?/code> **/ boolean clientGenerated(Interface interfaze, TopLevelClass topLevelClass, IntrospectedTable introspectedTable); /** * 當(dāng)SQL XML 文件生成后會(huì)調(diào)用該方法,可以通過(guò)實(shí)現(xiàn)該方法在MAPPER XML文件中新增XML定義 **/ boolean sqlMapDocumentGenerated(Document document, IntrospectedTable introspectedTable); } |
這里結(jié)合Mybatis Generator的配置文件和生成的DAO(也稱為Client文件)和Mapper XML文件可以更好的理解。Mybatis Generator配置文件樣例如下,其中包含了主要的一些配置信息,如用于描述數(shù)據(jù)庫(kù)鏈接的<jdbcConnection>標(biāo)簽,用于定義數(shù)據(jù)庫(kù)和Java類型轉(zhuǎn)換的<javaTypeResolver>標(biāo)簽等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
<? 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對(duì)象,并通過(guò)setContext(Context context)方法傳遞到具體的Plugin實(shí)現(xiàn)中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class Context extends PropertyHolder{ /** * <context>標(biāo)簽的id屬性 */ private String id; /** * jdbc鏈接信息,對(duì)應(yīng)<jdbcConnection>標(biāo)簽中的信息 */ private JDBCConnectionConfiguration jdbcConnectionConfiguration; /** * 類型映射配置,對(duì)應(yīng)<javaTypeResolver> */ private JavaTypeResolverConfiguration javaTypeResolverConfiguration; /** * ...其它標(biāo)簽對(duì)應(yīng)的配置信息 */ } |
setProperties則將context下的<properties>標(biāo)簽收集起來(lái)并映射成Properties類,它實(shí)際上是一個(gè)Map容器,正如Properties類本身就繼承了Hashtable。以上文中的配置文件為例,可以通過(guò)properties.get("printLog")獲得值"true"。
validate方法則代表了這個(gè)Plugin是否執(zhí)行,它通常進(jìn)行一些非常基礎(chǔ)的校驗(yàn),比如是否兼容對(duì)應(yīng)的數(shù)據(jù)庫(kù)驅(qū)動(dòng)或者是Mybatis版本:
1
2
3
4
5
6
7
8
|
public boolean validate(List<String> warnings) { if (StringUtility.stringHasValue( this .getContext().getTargetRuntime()) && ! "MyBatis3" .equalsIgnoreCase( this .getContext().getTargetRuntime())) { logger.warn( "itfsw:插件" + this .getClass().getTypeName() + "要求運(yùn)行targetRuntime必須為MyBatis3!" ); return false ; } else { return true ; } } |
如果validate方法返回false,則無(wú)論什么場(chǎng)景下都不會(huì)運(yùn)行這個(gè)Plugin。
接著是最重要的兩個(gè)方法,分別是用于在DAO中生成新的方法clientGenerated和在XML文件中生成新的SQL sqlMapDocumentGenerated。
先說(shuō)clientGenerated,這個(gè)方法共有三個(gè)參數(shù),interfaze是當(dāng)前已經(jīng)生成的客戶端Dao接口,topLevelClass是指生成的實(shí)現(xiàn)類,這個(gè)類可能為空,introspectedTable是指當(dāng)前處理的數(shù)據(jù)表,這里包含了從數(shù)據(jù)庫(kù)中獲取的關(guān)于表的各種信息,包括列名稱,列類型等。這里可以看一下introspectedTable中幾個(gè)比較重要的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public abstract class IntrospectedTable { /** * 該方法可以獲得配置文件中該表對(duì)應(yīng)<table>標(biāo)簽下的配置信息,包括映射成的Mapper名稱,PO名稱等 * 也可以在table標(biāo)簽下自定義<property>標(biāo)簽并通過(guò)getProperty方法獲得值 */ public TableConfiguration getTableConfiguration() { return tableConfiguration; } /** * 這個(gè)方法中定義了默認(rèn)的生成規(guī)則,可以通過(guò)calculateAllFieldsClass獲得返回類型 */ public Rules getRules() { return rules; } } |
悲觀鎖的clientGenerated方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// Plugin配置,是否要生成selectForUpdate語(yǔ)句 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對(duì)數(shù)據(jù)行上悲觀鎖\n" + " */" ; method.addJavaDocLine(docComment); interfaze.addMethod(method); log.debug( "(悲觀鎖插件):" + interfaze.getType().getShortName() + "增加" + METHOD_NAME + "方法。" ); } return super .clientGenerated(interfaze, topLevelClass, introspectedTable); } |
這里可以通過(guò)在對(duì)應(yīng)table下新增property標(biāo)簽來(lái)決定是否要為這張表生成對(duì)應(yīng)的悲觀鎖方法,配置樣例如下:
1
2
3
4
5
6
7
8
9
|
< 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 > |
代碼中通過(guò)mybatis提供的Method方法,定義了方法的名稱,參數(shù),返回類型等,并使用interfaze.addMethod方法將方法添加到客戶端的接口中。
再到sqlMapDocumentGenerated這個(gè)方法,這個(gè)方法中傳入了Document對(duì)象,它對(duì)應(yīng)生成的XML文件,并通過(guò)XmlElement來(lái)映射XML文件中的元素。通過(guò)document.getRootElement().addElement可以將自定義的XML元素插入到Mapper文件中。自定義XML元素就是指拼接X(jué)mlElement,XmlElement的addAttribute方法可以為XML元素設(shè)置屬性,addElement則可以為XML標(biāo)簽添加子元素。有兩種類型的子元素,分別是TextElement和XmlElement本身,TextElement則直接填充標(biāo)簽中的內(nèi)容,而XmlElement則對(duì)應(yīng)新的標(biāo)簽,如<where> <include>等。悲觀鎖的SQL生成邏輯如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
// Plugin配置,是否要生成selectForUpdate語(yǔ)句 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() ? "有" : "無(wú)" ) + "Blob類型))。" ); return super .sqlMapDocumentGenerated(document, introspectedTable); } |
完整代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
@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對(duì)數(shù)據(jù)行上悲觀鎖\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() ? "有" : "無(wú)" ) + "Blob類型))。" ); return super .sqlMapDocumentGenerated(document, introspectedTable); } } |
到此這篇關(guān)于Mybatis Generator Plugin悲觀鎖實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)Mybatis Generator Plugin悲觀鎖 內(nèi)容請(qǐng)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://segmentfault.com/a/1190000040747121