目录
0. 现实背景
1. 方案调研
1.1 自动添加mapper接口方法
1.2 自动生成xml文件
2. 方案实现
2.1 批量插入插件BatchInsertPlugin实现
2.2 AdditionalXMLMapperGenerator Document生成器实现
2.3 BatchInsertElementGenerator 动态SQL生成
在项目开发中,业务数据落库时,为了减少和数据库之间的通信,频繁占用数据库链接资源,我们一般都会需要一个批量插入的mapper接口方法;但遗憾的是,mybatis generator框架中没有提供这样的方法,目前一般的解决方案是,我们手动在mapper接口中添加批量插入的方法,然后再手工添加一个批量插入的动态SQL与之关联,但这样的问题在于:
每个table手写批量插入的动态SQL要花费一定的时间,手写完也不能保证是没有问题的,一次通过;
每次table 新增字段时,其他的mapper接口,都可以重新编译生成,然后覆盖,但批量插入的动态SQL还需要我们手动去添加维护,有时也很容易遗漏,出现问题;
为了解决上述问题,批量插入mapper接口方法以及动态SQL xml文件 也需要做到一次编译,自动生成;
在上篇文章中Mybatis Generator源码:Generator自动生成框架,我们知道在JavaClientGenerator配置中有一个rootInterface的属性配置,该配置可以使生成的Domain对象自动实现某个接口方法,这里的实现源码如下(JavaMapperGenerator方法getCompilationUnits中):
@Override
public List getCompilationUnits() {
progressCallback.startTask(getString("Progress.17", //$NON-NLS-1$
introspectedTable.getFullyQualifiedTable().toString()));
CommentGenerator commentGenerator = context.getCommentGenerator();
FullyQualifiedJavaType type = new FullyQualifiedJavaType(
introspectedTable.getMyBatis3JavaMapperType());
Interface interfaze = new Interface(type);
interfaze.setVisibility(JavaVisibility.PUBLIC);
commentGenerator.addJavaFileComment(interfaze);
String rootInterface = introspectedTable
.getTableConfigurationProperty(PropertyRegistry.ANY_ROOT_INTERFACE);
if (!stringHasValue(rootInterface)) {
rootInterface = context.getJavaClientGeneratorConfiguration()
.getProperty(PropertyRegistry.ANY_ROOT_INTERFACE);
}
if (stringHasValue(rootInterface)) {
FullyQualifiedJavaType fqjt = new FullyQualifiedJavaType(
rootInterface);
interfaze.addSuperInterface(fqjt);
interfaze.addImportedType(fqjt);
}
addCountByExampleMethod(interfaze);
addDeleteByExampleMethod(interfaze);
addDeleteByPrimaryKeyMethod(interfaze);
addInsertMethod(interfaze);
addInsertSelectiveMethod(interfaze);
addSelectByExampleWithBLOBsMethod(interfaze);
addSelectByExampleWithoutBLOBsMethod(interfaze);
addSelectByPrimaryKeyMethod(interfaze);
addUpdateByExampleSelectiveMethod(interfaze);
addUpdateByExampleWithBLOBsMethod(interfaze);
addUpdateByExampleWithoutBLOBsMethod(interfaze);
addUpdateByPrimaryKeySelectiveMethod(interfaze);
addUpdateByPrimaryKeyWithBLOBsMethod(interfaze);
addUpdateByPrimaryKeyWithoutBLOBsMethod(interfaze);
List answer = new ArrayList();
if (context.getPlugins().clientGenerated(interfaze, null,
introspectedTable)) {
answer.add(interfaze);
}
List extraCompilationUnits = getExtraCompilationUnits();
if (extraCompilationUnits != null) {
answer.addAll(extraCompilationUnits);
}
return answer;
}
因此,可以把batchInsert方法加入到某个接口中,然后rootInterface指定该接口即可;
在上篇文章Mybatis Generator源码:Generator自动生成框架中生成xml文件阶段调用了context的generateFiles方法,下面看一下该方法的内部实现:
public void generateFiles(ProgressCallback callback,
List generatedJavaFiles,
List generatedXmlFiles, List warnings)
throws InterruptedException {
pluginAggregator = new PluginAggregator();
for (PluginConfiguration pluginConfiguration : pluginConfigurations) {
Plugin plugin = ObjectFactory.createPlugin(this,
pluginConfiguration);
if (plugin.validate(warnings)) {
pluginAggregator.addPlugin(plugin);
} else {
warnings.add(getString("Warning.24", //$NON-NLS-1$
pluginConfiguration.getConfigurationType(), id));
}
}
if (introspectedTables != null) {
for (IntrospectedTable introspectedTable : introspectedTables) {
callback.checkCancel();
introspectedTable.initialize();
introspectedTable.calculateGenerators(warnings, callback);
generatedJavaFiles.addAll(introspectedTable
.getGeneratedJavaFiles());
generatedXmlFiles.addAll(introspectedTable
.getGeneratedXmlFiles());
generatedJavaFiles.addAll(pluginAggregator
.contextGenerateAdditionalJavaFiles(introspectedTable));
generatedXmlFiles.addAll(pluginAggregator
.contextGenerateAdditionalXmlFiles(introspectedTable));
}
}
generatedJavaFiles.addAll(pluginAggregator
.contextGenerateAdditionalJavaFiles());
generatedXmlFiles.addAll(pluginAggregator
.contextGenerateAdditionalXmlFiles());
}
这里同时完成了java文件和xml文件的生成,其中pluginAggregator.contextGenerateAdditionalXmlFiles(introspectedTable)提供了应用generator插件生成其它xml文件的扩展接口,这样,我们就可以自定义一个generator插件来完成 batchInsert xml动态Sql的自动生成;
自动添加mapper接口batchInsert方法比较简单,这里不再进行说明,下面主要介绍xml动态SQL自动生成的设计实现:
public class BatchInsertPlugin extends PluginAdapter {
@Override
public boolean validate(List warnings) {
return true;
}
@Override
public List contextGenerateAdditionalXmlFiles(IntrospectedTable introspectedTable) {
AbstractXmlGenerator xmlGenerator = new AdditionalXMLMapperGenerator();
xmlGenerator.setContext(introspectedTable.getContext());
xmlGenerator.setIntrospectedTable(introspectedTable);
Document document = xmlGenerator.getDocument();
List answer = Lists.newArrayListWithCapacity(1);
if (document != null) {
GeneratedXmlFile gxf = new GeneratedXmlFile(document, introspectedTable.getMyBatis3XmlMapperFileName(),
introspectedTable.getMyBatis3XmlMapperPackage(),
context.getSqlMapGeneratorConfiguration().getTargetProject(), true, context.getXmlFormatter());
answer.add(gxf);
}
return answer;
}
@Override
public boolean sqlMapGenerated(GeneratedXmlFile sqlMap, IntrospectedTable introspectedTable) {
return true;
}
}
这里通过自定义的AdditionalXMLMapperGenerator完成Document对象的构建,进而完成了批量插入xml动态SQL文件GeneratedXmlFile的自动生成,下面看下AdditionalXMLMapperGenerator Document生成器的实现:
AdditionalXMLMapperGenerator具体实现代码如下,借助BatchInsertElementGenerator完成了Document文档对象的构建:
public class AdditionalXMLMapperGenerator extends AbstractXmlGenerator {
public AdditionalXMLMapperGenerator() {
super();
}
@Override
public Document getDocument() {
Document document =
new Document(XmlConstants.MYBATIS3_MAPPER_PUBLIC_ID, XmlConstants.MYBATIS3_MAPPER_SYSTEM_ID);
document.setRootElement(getSqlMapElement());
if (!context.getPlugins().sqlMapDocumentGenerated(document, introspectedTable)) {
document = null;
}
return document;
}
private XmlElement getSqlMapElement() {
XmlElement root = new XmlElement("mapper"); //$NON-NLS-1$
String namespace = introspectedTable.getMyBatis3SqlMapNamespace();
root.addAttribute(new Attribute("namespace", //$NON-NLS-1$
namespace));
context.getCommentGenerator().addRootComment(root);
root.addElement(new TextElement("")); //$NON-NLS-1$
addBatchInsertElement(root);
return root;
}
private void addBatchInsertElement(XmlElement parentElement) {
if (introspectedTable.getRules().generateInsert()) {
AbstractXmlElementGenerator elementGenerator = new BatchInsertElementGenerator();
initializeAndExecuteGenerator(elementGenerator, parentElement);
}
}
private void initializeAndExecuteGenerator(AbstractXmlElementGenerator elementGenerator, XmlElement parentElement) {
elementGenerator.setContext(context);
elementGenerator.setIntrospectedTable(introspectedTable);
elementGenerator.addElements(parentElement);
}
}
如下,BatchInsertElementGenerator完成了批量插入动态SQL的构造,这里就是我们比较熟悉的动态sql语句了;
public class BatchInsertElementGenerator extends AbstractXmlElementGenerator {
private static final String BATCH_INSERT_STATEMENT_ID = "batchInsert";
private static final int NEW_LINE_CHARACTER_LIMIT = 80;
BatchInsertElementGenerator() {
super();
}
@Override
public void addElements(XmlElement parentElement) {
XmlElement answer = new XmlElement("insert");
answer.addAttribute(new Attribute("id", BATCH_INSERT_STATEMENT_ID));
answer.addAttribute(
new Attribute("parameterType", FullyQualifiedJavaType.getNewListInstance().getFullyQualifiedName()));
context.getCommentGenerator().addComment(answer);
GeneratedKey gk = introspectedTable.getGeneratedKey();
if (gk != null) {
IntrospectedColumn introspectedColumn = introspectedTable.getColumn(gk.getColumn());
if (introspectedColumn != null) {
if (gk.isJdbcStandard()) {
answer.addAttribute(new Attribute("useGeneratedKeys", "true"));
answer.addAttribute(new Attribute("keyProperty", introspectedColumn.getJavaProperty()));
} else {
answer.addElement(getSelectKey(introspectedColumn, gk));
}
}
}
StringBuilder insertClause = new StringBuilder();
StringBuilder valuesClause = new StringBuilder();
insertClause.append("insert into ");
insertClause.append(introspectedTable.getFullyQualifiedTableNameAtRuntime());
insertClause.append(" (");
List valuesClauses = Lists.newArrayList();
valuesClauses.add("values ");
valuesClauses.add("");
valuesClauses.add(" (");
Iterator iter = introspectedTable.getAllColumns().iterator();
while (iter.hasNext()) {
IntrospectedColumn introspectedColumn = iter.next();
if (introspectedColumn.isIdentity()) {
// cannot set values on identity fields
continue;
}
insertClause.append(MyBatis3FormattingUtilities.getEscapedColumnName(introspectedColumn));
if (introspectedColumn.getDefaultValue() == null) {
getValueClauseWhenNoDefaultValue(valuesClause, valuesClauses, iter, introspectedColumn, 1);
} else {
getChooseString(valuesClause, valuesClauses, introspectedColumn, iter);
}
appendCommaWhenHasNext(insertClause, iter);
if (insertClause.length() > NEW_LINE_CHARACTER_LIMIT) {
answer.addElement(new TextElement(insertClause.toString()));
insertClause.setLength(0);
OutputUtilities.xmlIndent(insertClause, 1);
}
}
insertClause.append(')');
answer.addElement(new TextElement(insertClause.toString()));
valuesClause.append(')');
valuesClauses.add(valuesClause.toString());
valuesClause.setLength(0);
valuesClause.append(" ");
valuesClauses.add(valuesClause.toString());
for (String clause : valuesClauses) {
answer.addElement(new TextElement(clause));
}
if (context.getPlugins().sqlMapInsertElementGenerated(answer, introspectedTable)) {
parentElement.addElement(answer);
}
}
private void getValueClauseWhenNoDefaultValue(StringBuilder valuesClause, List valuesClauses,
Iterator iter, IntrospectedColumn introspectedColumn, int indentLevel) {
OutputUtilities.xmlIndent(valuesClause, indentLevel);
valuesClause.append(MyBatis3FormattingUtilities.getParameterClause(introspectedColumn));
insertItemAfterLeftBrace(valuesClause);
appendCommaWhenHasNext(valuesClause, iter);
valuesClauses.add(valuesClause.toString());
valuesClause.setLength(0);
}
private void getChooseString(StringBuilder valuesClause, List valuesClauses,
IntrospectedColumn introspectedColumn, Iterator iter) {
OutputUtilities.xmlIndent(valuesClause, 1);
valuesClause.append("");
valuesClauses.add(valuesClause.toString());
valuesClause.setLength(0);
OutputUtilities.xmlIndent(valuesClause, 2);
valuesClause.append("");
valuesClauses.add(valuesClause.toString());
valuesClause.setLength(0);
getValueClauseWhenNoDefaultValue(valuesClause, valuesClauses, iter, introspectedColumn, 3);
OutputUtilities.xmlIndent(valuesClause, 2);
valuesClause.append(" ");
valuesClauses.add(valuesClause.toString());
valuesClause.setLength(0);
OutputUtilities.xmlIndent(valuesClause, 2);
valuesClause.append("");
valuesClauses.add(valuesClause.toString());
valuesClause.setLength(0);
OutputUtilities.xmlIndent(valuesClause, 3);
valuesClause.append(wrapQuotaPair(introspectedColumn.getDefaultValue()));
appendCommaWhenHasNext(valuesClause, iter);
valuesClauses.add(valuesClause.toString());
valuesClause.setLength(0);
OutputUtilities.xmlIndent(valuesClause, 2);
valuesClause.append(" ");
valuesClauses.add(valuesClause.toString());
valuesClause.setLength(0);
OutputUtilities.xmlIndent(valuesClause, 1);
valuesClause.append(" ");
valuesClauses.add(valuesClause.toString());
valuesClause.setLength(0);
}
private void appendCommaWhenHasNext(StringBuilder insertClause, Iterator iter) {
if (iter.hasNext()) {
insertClause.append(", ");
}
}
private void insertItemAfterLeftBrace(StringBuilder valuesClause) {
int index = valuesClause.indexOf("{");
if (index != -1) {
valuesClause.insert(index + 1, "item.");
}
}
private String wrapQuotaPair(String defaultValue) {
return "'" + defaultValue + "'";
}
}