动态SQL是Mybatis的强大特性之一,可以简化根据不同条件使用不同SQL的操作,并自动去除不需要的关键字。在Mybatis3中引入了OGNL表达式,使学习元素种类的过程变得简易。
<select id="findActivePersonLike" resultType="Person">
SELECT * FROM Person WHERE state = ‘ACTIVE’
<if test="name != null">
AND name like #{name}
if>
<if test="age != null">
AND age = #{age}
if>
select>
<select id="findActivePersonLike" resultType="Person">
SELECT * FROM Person WHERE state = ‘ACTIVE’
<choose>
<when test="name != null">
AND name like #{name}
when>
<when test="age != null">
AND age = #{age}
when>
<otherwise>
AND phone_number != null
otherwise>
choose>
select>
<select id="findActivePersonLike" resultType="Person">
SELECT * FROM Person
<where>
<if test="name != null">
name = #{name}
if>
<if test="age != null">
AND age = #{age}
if>
<if test="birthdayYear != null">
AND YEAR(birthday) = #{birthdayYear}
if>
where>
select>
where元素只会在子元素返回任何内容的情况下才插入“WHERE”子句。而且,若子句的开头为“AND”或“OR”,where元素也会将它们去除。
如where元素无法满足要求,可以用trim定制where元素的功能,使之更加灵活。
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
trim>
以上trim元素的定义,等同于where元素的效果。prefixOverrides属性会忽略通过管道符分隔的文本序列,该属性中的值会被移除,并且插入prefix属性中指定的内容。
set语句用于动态生成数据库更新SQL,如下:
<update id="updatePersonIfNecessary">
update Person
<set>
<if test="name != null">name=#{name},if>
<if test="age != null">age=#{age},if>
<if test="phoneNumber != null">phone_number=#{phoneNumber},if>
<if test="birthday != null">birthday=#{birthday}if>
set>
where id=#{id}
update>
set元素会动态在首行加入set关键字,并去除if标签不满足要求时的尾部逗号。同样,set元素也可以被trim元素等价替换,如下:
<trim prefix="SET" suffixOverrides=",">
...
trim>
<select id="selectPersonNameIn" resultType="Person">
SELECT * FROM Person
<where>
<foreach item="item" index="index" collection="list"
open="name in (" separator="," close=")" nullable="true">
#{item}
foreach>
where>
select>
foreach元素中集合参数支持任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数,当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
bind 元素允许你在OGNL表达式以外创建一个变量,并将其绑定到当前的上下文。如:
<select id="selectPersonsLike" resultType="Person">
<bind name="pattern" value="'%' + _parameter.getName() + '%'" />
SELECT * FROM Person
WHERE name LIKE #{pattern}
select>
script 元素用于在带注解的映射器接口类中使用动态 SQL。
@Update({""})
void updatePersonValues(Person person);
在项目中Spring和Mybatis整合时,需要在xml配置文件中配置如下类似内容:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="multipleDataSource"/>
<property name="configLocation">
<value>classpath:sqlMapConfig.xmlvalue>
property>
bean>
上述配置中引入了org.mybatis.spring.SqlSessionFactoryBean类,通过如下源码可知:
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent>
该类继承了FactoryBean(工厂类接口)、InitializingBean(为bean提供属性初始化后的处理方法)及ApplicationListener(观察者设计模式中监听作用),主要关注初始化bean的方式,即InitializingBean中afterPropertiesSet()方法的实现。
@Override
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = buildSqlSessionFactory();
}
该方法除了进行一些数据的判断外,主要是调用了buildSqlSessionFactory()方法,用于返回SqlSessionFactory实例。buildSqlSessionFactory()方法内部会使用XMLConfigBuilder解析属性configLocation中配置的路径,还会使用XMLMapperBuilder属性解析mapperLocations属性中的各个xml文件。部分源码如下:
if (this.configLocation != null) {
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
configuration = xmlConfigBuilder.getConfiguration();
}
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
configuration, mapperLocation.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
因XMLConfigBuilder最后会调用parse()方法,且方法内部解析mappers的方法mapperElement(root.evalNode("mappers"))
最终还是使用XMLMapperBuilder类中configurationElement(XNode context)
方法进行操作,所以直接关注该方法的操作即可。
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
此方法主要是处理缓存、查询参数、返回结果、sql语句及增删改查节点。本次主要关注的是增删改查节点的处理,如下:
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
和动态SQL相关的源码如下:
// Parse the SQL (pre: and were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
其中,通过xml构建SQL的实现类是XMLLanguageDriver。
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
可见,动态SQL的生成主要是通过XMLScriptBuilder类实现的。其中对于动态SQL语句脚本节点的生成源码如下:
public SqlSource parseScriptNode() {
List<SqlNode> contents = parseDynamicTags(context);
MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
SqlSource sqlSource = null;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
List<SqlNode> parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlers(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return contents;
}
XMLScriptBuilder类为每一个标签关键字创建了一个Handler,如下:
NodeHandler nodeHandlers(String nodeName) {
Map<String, NodeHandler> map = new HashMap<String, NodeHandler>();
map.put("trim", new TrimHandler());
map.put("where", new WhereHandler());
map.put("set", new SetHandler());
map.put("foreach", new ForEachHandler());
map.put("if", new IfHandler());
map.put("choose", new ChooseHandler());
map.put("when", new IfHandler());
map.put("otherwise", new OtherwiseHandler());
map.put("bind", new BindHandler());
return map.get(nodeName);
}
以if为例,查看IfHandler()。
private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
List<SqlNode> contents = parseDynamicTags(nodeToHandle);
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
在此方法中创建了IfSqlNode对象,后由该对象中的apply(DynamicContext context)
方法处理SQL的动态拼接,如下:
@Override
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}
public boolean evaluateBoolean(String expression, Object parameterObject) {
Object value = OgnlCache.getValue(expression, parameterObject);
if (value instanceof Boolean) {
return (Boolean) value;
}
if (value instanceof Number) {
return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);
}
return value != null;
}
在调用链的最后,动态SQL的拼接基于的是OGNL表达式的判断结果,与Mybatis官方文档中内容一致。
OGNL (Object Graph Navigation Language) 是一个开源的表达式引擎。通过使用OGNL,我们能够通过表达式存取Java对象树中的任意属性和调用Java对象树的方法等。
OGNL所在类的源码如下:
public abstract class Ognl {
/**
* 通过传入的OGNL表达式,在给定的上下文环境中,从root对象里取值
*/
public static Object getValue(String expression, Map context, Object root) throws OgnlException {
return getValue(expression, context, root, null);
}
/**
* 通过传入的OGNL表达式,在给定的上下文环境中,往root对象里写值
*/
public static void setValue(String expression, Map context, Object root, Object value) throws OgnlException {
setValue(parseExpression(expression), context, root, value);
}
}
OGNL三要素:
表达式(Expression)
表达式是整个OGNL的核心,所有的OGNL操作都是针对表达式的解析后进行的。表达式会规定此次OGNL操作到底要干什么。因此,表达式其实是一个带有语法含义的字符串,这个字符串将规定操作的类型和操作的内容。
Root对象(Root Object)
OGNL的Root对象可以理解为OGNL的操作对象。当OGNL表达式规定了“干什么”以后,我们还需要指定对谁干。OGNL的Root对象实际上是一个Java对象,是所有OGNL操作的实际载体。这就意味着,如果我们有一个OGNL的表达式,那么我们实际上需要针对Root对象去进行OGNL表达式的计算并返回结果。
上下文环境(Context)
有了表达式和Root对象,我们已经可以使用OGNL的基本功能。例如,根据表达式针对OGNL中的Root对象进行“取值”或者“写值”操作。
不过,事实上,在OGNL的内部,所有的操作都会在一个特定的数据环境中运行,这个数据环境就是OGNL的上下文环境(Context)。说得再明白一些,就是这个上下文环境(Context)将规定OGNL的操作在哪里干。
OGNL的上下文环境是一个Map结构,称之为OgnlContext。之前我们所提到的Root对象(Root Object),事实上也会被添加到上下文环境中去,并且将被作为一个特殊的变量进行处理。
Q:以下Mapper.xml中,如参数传递
{
"state": 0
}
时,是否会拼接SQL?
<if test="req.state != null and req.state != ''">
AND md.state = #{req.state}
if>
state字段定义如下:
/**
* 数据状态
*/
@Range(min = 0, max = 1, message = "状态只能为0(未处理),1(已处理)")
private Integer state;
A:不会拼接,因为在OGNL表达式中,0 == ‘’ 会返回true。