MyBatis 如何权衡代码的易用性、性能和灵活性
Mybatis 和 ORM 框架介绍
因为 MyBatis 依赖 JDBC 驱动,所以,在项目中使用 MyBatis,除了需要引入 MyBatis 框架本身(mybatis.jar)之外,还需要引入 JDBC 驱动(比如,访问 MySQL 的 JDBC 驱动实现类库 mysql-connector-java.jar)。将两个 jar 包引入项目之后,我们就可以开始编程了。使用 MyBatis 来访问数据库中用户信息的代码如下所示:
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 public class UserDo { private long id; private String name; private String telephone; } public interface UserMapper { public UserDo selectById (long id) ; } <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="cn.xzg.cd.a87.repo.mapper.UserMapper" > <select id="selectById" resultType="cn.xzg.cd.a87.repo.UserDo" > select * from user where id=#{id} </select> </mapper> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <configuration> <environments default ="dev" > <environment id="dev" > <transactionManager type="JDBC" ></transactionManager> <dataSource type="POOLED" > <property name="driver" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8" /> <property name="username" value="root" /> <property name="password" value="..." /> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/UserMapper.xml" /> </mappers> </configuration> public class MyBatisDemo { public static void main (String[] args) throws IOException { Reader reader = Resources.getResourceAsReader("mybatis.xml" ); SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder ().build(reader); SqlSession session = sessionFactory.openSession(); UserMapper userMapper = session.getMapper(UserMapper.class); UserDo userDo = userMapper.selectById(8 ); } }
相对于使用 JdbcTemplate 的实现方式,使用 MyBatis 的实现方式更加灵活。在使用 JdbcTemplate 的实现方式中,对象与数据库中数据之间的转化代码、SQL 语句,是硬编码在业务代码中的。而在使用 MyBatis 的实现方式中,类字段与数据库字段之间的映射关系、接口与 SQL 之间的映射关系,是写在 XML 配置文件中的,是跟代码相分离的,这样会更加灵活、清晰,维护起来更加方便。
如何平衡易用性、性能和灵活性?
粗略地讲,有的时候,框架的易用性和性能成对立关系。追求易用性,那性能就差一些。相反,追求性能,易用性就差一些。除此之外,使用起来越简单,那灵活性就越差。这就好比我们用的照相机。傻瓜相机按下快门就能拍照,但没有复杂的单反灵活。
JdbcTemplate 提供的功能最简单,易用性最差,性能损耗最少,用它编程性能最好。Hibernate 提供的功能最完善,易用性最好,但相对来说性能损耗就最高了。MyBatis 介于两者中间,在易用性、性能、灵活性三个方面做到了权衡。它支撑程序员自己编写 SQL,能够延续程序员对 SQL 知识的积累。相对于完全黑盒子的 Hibernate,很多程序员反倒是更加喜欢 MyBatis 这种半透明的框架。这也提醒我们,过度封装,提供过于简化的开发方式,也会丧失开发的灵活性。
如何利用职责链与代理模式实现 MyBatis Plugin
MyBatis Plugin 功能介绍
MyBatis Plugin 使用起来比较简单,我们通过一个例子来快速看下。
假设我们需要统计应用中每个 SQL 的执行耗时,如果使用 MyBatis Plugin 来实现的话,我们只需要定义一个 SqlCostTimeInterceptor 类,让它实现 MyBatis 的 Interceptor 接口,并且,在 MyBatis 的全局配置文件中,简单声明一下这个插件就可以了。具体的代码和配置如下所示:
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 @Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})}) public class SqlCostTimeInterceptor implements Interceptor { private static Logger logger = LoggerFactory.getLogger(SqlCostTimeInterceptor.class); @Override public Object intercept (Invocation invocation) throws Throwable { Object target = invocation.getTarget(); long startTime = System.currentTimeMillis(); StatementHandler statementHandler = (StatementHandler) target; try { return invocation.proceed(); } finally { long costTime = System.currentTimeMillis() - startTime; BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); logger.info("执行 SQL:[ {} ]执行耗时[ {} ms]" , sql, costTime); } } @Override public Object plugin (Object target) { return Plugin.wrap(target, this ); } @Override public void setProperties (Properties properties) { System.out.println("插件配置的信息:" +properties); } } <!-- MyBatis全局配置文件:mybatis-config.xml --> <plugins> <plugin interceptor="com.xzg.cd.a88.SqlCostTimeInterceptor" > <property name="someProperty" value="100" /> </plugin> </plugins>
我们知道,不管是拦截器、过滤器还是插件,都需要明确地标明拦截的目标方法。@Intercepts 注解实际上就是起了这个作用。其中,@Intercepts 注解又可以嵌套 @Signature 注解。一个 @Signature 注解标明一个要拦截的目标方法。如果要拦截多个方法,我们可以像例子中那样,编写多条 @Signature 注解。
@Signature 注解包含三个元素:type、method、args。其中,type 指明要拦截的类、method 指明方法名、args 指明方法的参数列表。通过指定这三个元素,我们就能完全确定一个要拦截的方法。
默认情况下,MyBatis Plugin 允许拦截的方法有下面这样几个:
为什么默认允许拦截的是这样几个类的方法呢?
MyBatis 底层是通过 Executor 类来执行 SQL 的。Executor 类会创建 StatementHandler、ParameterHandler、ResultSetHandler 三个对象,并且,首先使用 ParameterHandler 设置 SQL 中的占位符参数,然后使用 StatementHandler 执行 SQL 语句,最后使用 ResultSetHandler 封装执行结果。所以,我们只需要拦截 Executor、ParameterHandler、ResultSetHandler、StatementHandler 这几个类的方法,基本上就能满足我们对整个 SQL 执行流程的拦截了。
实际上,除了统计 SQL 的执行耗时,利用 MyBatis Plugin,我们还可以做很多事情,比如分库分表、自动分页、数据脱敏、加密解密等等。如果感兴趣的话,你可以自己实现一下。
MyBatis Plugin 的设计与实现
我们已知职责链模式的实现一般包含处理器(Handler)和处理器链(HandlerChain)两部分。这两个部分对应到 Servlet Filter 的源码就是 Filter 和 FilterChain,对应到 Spring Interceptor 的源码就是 HandlerInterceptor 和 HandlerExecutionChain,对应到 MyBatis Plugin 的源码就是 Interceptor 和 InterceptorChain。除此之外,MyBatis Plugin 还包含另外一个非常重要的类:Plugin。它用来生成被拦截对象的动态代理。
集成了 MyBatis 的应用在启动的时候,MyBatis 框架会读取全局配置文件(前面例子中的 mybatis-config.xml 文件),解析出 Interceptor(也就是例子中的 SqlCostTimeInterceptor),并且将它注入到 Configuration 类的 InterceptorChain 对象中。这部分逻辑对应到源码如下所示:
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 public class XMLConfigBuilder extends BaseBuilder { private void parseConfiguration (XNode root) { try { pluginElement(root.evalNode("plugins" )); } catch (Exception e) { throw new BuilderException ("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } private void pluginElement (XNode parent) throws Exception { if (parent != null ) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor" ); Properties properties = child.getChildrenAsProperties(); Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); interceptorInstance.setProperties(properties); configuration.addInterceptor(interceptorInstance); } } } } public void addInterceptor (Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }
我们再来看 Interceptor 和 InterceptorChain 这两个类的代码,如下所示。Interceptor 的 setProperties() 方法就是一个单纯的 setter 方法,主要是为了方便通过配置文件配置 Interceptor 的一些属性值,没有其他作用。Interceptor 类中 intecept() 和 plugin() 函数,以及 InterceptorChain 类中的 pluginAll() 函数,是最核心的三个函数,我们待会再详细解释。
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 public class Invocation { private final Object target; private final Method method; private final Object[] args; public Object proceed () throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); } } public interface Interceptor { Object intercept (Invocation invocation) throws Throwable; Object plugin (Object target) ; void setProperties (Properties properties) ; } public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList <Interceptor>(); public Object pluginAll (Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } public void addInterceptor (Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors () { return Collections.unmodifiableList(interceptors); } }
解析完配置文件之后,所有的 Interceptor 都加载到了 InterceptorChain 中。接下来,我们再来看下,这些拦截器是在什么时候被触发执行的?又是如何被触发执行的呢?
前面我们提到,在执行 SQL 的过程中,MyBatis 会创建 Executor、StatementHandler、ParameterHandler、ResultSetHandler 这几个类的对象,对应的创建代码在 Configuration 类中,如下所示:
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 public Executor newExecutor (Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor (this , transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor (this , transaction); } else { executor = new SimpleExecutor (this , transaction); } if (cacheEnabled) { executor = new CachingExecutor (executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; } public ParameterHandler newParameterHandler (MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } public ResultSetHandler newResultSetHandler (Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { ResultSetHandler resultSetHandler = new DefaultResultSetHandler (executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } public StatementHandler newStatementHandler (Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler (executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
从上面的代码中,我们可以发现,这几个类对象的创建过程都调用了 InteceptorChain 的 pluginAll() 方法。这个方法的代码前面已经给出了。你可以回过头去再看一眼。它的代码实现很简单,嵌套调用 InterceptorChain 上每个 Interceptor 的 plugin() 方法。plugin() 是一个接口方法(不包含实现代码),需要由用户给出具体的实现代码。在之前的例子中,SQLTimeCostInterceptor 的 plugin() 方法通过直接调用 Plugin 的 wrap() 方法来实现。wrap() 方法的代码实现如下所示:
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 78 79 80 public class Plugin implements InvocationHandler { private final Object target; private final Interceptor interceptor; private final Map<Class<?>, Set<Method>> signatureMap; private Plugin (Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { this .target = target; this .interceptor = interceptor; this .signatureMap = signatureMap; } public static Object wrap (Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0 ) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin (target, interceptor, signatureMap)); } return target; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { return interceptor.intercept(new Invocation (target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); if (interceptsAnnotation == null ) { throw new PluginException ("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map<Class<?>, Set<Method>> signatureMap = new HashMap <Class<?>, Set<Method>>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.get(sig.type()); if (methods == null ) { methods = new HashSet <Method>(); signatureMap.put(sig.type(), methods); } try { Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException ("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet <Class<?>>(); while (type != null ) { for (Class<?> c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class <?>[interfaces.size()]); } }
实际上,Plugin 是借助 Java InvocationHandler 实现的动态代理类。用来代理给 target 对象添加 Interceptor 功能。其中,要代理的 target 对象就是 Executor、StatementHandler、ParameterHandler、ResultSetHandler 这四个类的对象。wrap() 静态方法是一个工具函数,用来生成 target 对象的动态代理对象。
当然,只有 interceptor 与 target 互相匹配的时候,wrap() 方法才会返回代理对象,否则就返回 target 对象本身。怎么才算是匹配呢?那就是 interceptor 通过 @Signature 注解要拦截的类包含 target 对象,具体可以参看 wrap() 函数的代码实现(上面一段代码中的第 16~19 行)。
MyBatis 中的职责链模式的实现方式比较特殊。它对同一个目标对象嵌套多次代理(也就是 InteceptorChain 中的 pluginAll() 函数要执行的任务)。每个代理对象(Plugin 对象)代理一个拦截器(Interceptor 对象)功能。为了方便你查看,我将 pluginAll() 函数的代码又拷贝到了下面。
1 2 3 4 5 6 7 8 9 10 11 12 13 public Object pluginAll (Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } Object target = interceptorChain.pluginAll(target);
当执行 Executor、StatementHandler、ParameterHandler、ResultSetHandler 这四个类上的某个方法的时候,MyBatis 会嵌套执行每层代理对象(Plugin 对象)上的 invoke() 方法。而 invoke() 方法会先执行代理对象中的 interceptor 的 intecept() 函数,然后再执行被代理对象上的方法。就这样,一层一层地把代理对象上的 intercept() 函数执行完之后,MyBatis 才最终执行那 4 个原始类对象上的方法。
MyBatis 框架中用到的十几种设计模式
SqlSessionFactoryBuilder:为什么要用建造者模式来创建 SqlSessionFactory?
为什么还要借助建造者模式创建 SqlSessionFactory 呢?
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 public class SqlSessionFactoryBuilder { public SqlSessionFactory build (Reader reader) { return build(reader, null , null ); } public SqlSessionFactory build (Reader reader, String environment) { return build(reader, environment, null ); } public SqlSessionFactory build (Reader reader, Properties properties) { return build(reader, null , properties); } public SqlSessionFactory build (Reader reader, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder (reader, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession." , e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { } } } public SqlSessionFactory build (InputStream inputStream) { return build(inputStream, null , null ); } public SqlSessionFactory build (InputStream inputStream, String environment) { return build(inputStream, environment, null ); } public SqlSessionFactory build (InputStream inputStream, Properties properties) { return build(inputStream, null , properties); } public SqlSessionFactory build (InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder (inputStream, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession." , e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { } } } public SqlSessionFactory build (Configuration config) { return new DefaultSqlSessionFactory (config); } }
我们知道,如果一个类包含很多成员变量,而构建对象并不需要设置所有的成员变量,只需要选择性地设置其中几个就可以。为了满足这样的构建需求,我们就要定义多个包含不同参数列表的构造函数。为了避免构造函数过多、参数列表过长,我们一般通过无参构造函数加 setter 方法或者通过建造者模式来解决。
从建造者模式的设计初衷上来看,SqlSessionFactoryBuilder 虽然带有 Builder 后缀,但不要被它的名字所迷惑,它并不是标准的建造者模式。一方面,原始类 SqlSessionFactory 的构建只需要一个参数,并不复杂。另一方面,Builder 类 SqlSessionFactoryBuilder 仍然定义了 n 多包含不同参数列表的构造函数。
实际上,SqlSessionFactoryBuilder 设计的初衷只不过是为了简化开发。因为构建 SqlSessionFactory 需要先构建 Configuration,而构建 Configuration 是非常复杂的,需要做很多工作,比如配置的读取、解析、创建 n 多对象等。为了将构建 SqlSessionFactory 的过程隐藏起来,对程序员透明,MyBatis 就设计了 SqlSessionFactoryBuilder 类封装这些构建细节。
SqlSessionFactory:到底属于工厂模式还是建造器模式?
我们先来看下 SqlSessionFactory 类的源码。
1 2 3 4 5 6 7 8 9 10 11 public interface SqlSessionFactory { SqlSession openSession () ; SqlSession openSession (boolean autoCommit) ; SqlSession openSession (Connection connection) ; SqlSession openSession (TransactionIsolationLevel level) ; SqlSession openSession (ExecutorType execType) ; SqlSession openSession (ExecutorType execType, boolean autoCommit) ; SqlSession openSession (ExecutorType execType, TransactionIsolationLevel level) ; SqlSession openSession (ExecutorType execType, Connection connection) ; Configuration getConfiguration () ; }
从 SqlSessionFactory 和 DefaultSqlSessionFactory 的源码来看,它的设计非常类似刚刚讲到的 SqlSessionFactoryBuilder,通过重载多个 openSession() 函数,支持通过组合 autoCommit、Executor、Transaction 等不同参数,来创建 SqlSession 对象。标准的工厂模式通过 type 来创建继承同一个父类的不同子类对象,而这里只不过是通过传递进不同的参数,来创建同一个类的对象。所以,它更像建造者模式。
BaseExecutor:模板模式跟普通的继承有什么区别?
如果去查阅 SqlSession 与 DefaultSqlSession 的源码,你会发现,SqlSession 执行 SQL 的业务逻辑,都是委托给了 Executor 来实现。Executor 相关的类主要是用来执行 SQL。其中,Executor 本身是一个接口;BaseExecutor 是一个抽象类,实现了 Executor 接口;而 BatchExecutor、SimpleExecutor、ReuseExecutor 三个类继承 BaseExecutor 抽象类。
那 BatchExecutor、SimpleExecutor、ReuseExecutor 三个类跟 BaseExecutor 是简单的继承关系,还是模板模式关系呢?怎么来判断呢?我们看一下 BaseExecutor 的源码就清楚了。
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 public abstract class BaseExecutor implements Executor { @Override public int update (MappedStatement ms, Object parameter) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing an update" ).object(ms.getId()); if (closed) { throw new ExecutorException ("Executor was closed." ); } clearLocalCache(); return doUpdate(ms, parameter); } public List<BatchResult> flushStatements (boolean isRollBack) throws SQLException { if (closed) { throw new ExecutorException ("Executor was closed." ); } return doFlushStatements(isRollBack); } private <E> List<E> queryFromDatabase (MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; } @Override public <E> Cursor<E> queryCursor (MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); return doQueryCursor(ms, parameter, rowBounds, boundSql); } protected abstract int doUpdate (MappedStatement ms, Object parameter) throws SQLException; protected abstract List<BatchResult> doFlushStatements (boolean isRollback) throws SQLException; protected abstract <E> List<E> doQuery (MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException; protected abstract <E> Cursor<E> doQueryCursor (MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException; }
模板模式基于继承来实现代码复用。如果抽象类中包含模板方法,模板方法调用有待子类实现的抽象方法,那这一般就是模板模式的代码实现。而且,在命名上,模板方法与抽象方法一般是一一对应的,抽象方法在模板方法前面多一个“do”,比如,在 BaseExecutor 类中,其中一个模板方法叫 update(),那对应的抽象方法就叫 doUpdate()。
SqlNode:如何利用解释器模式来解析动态 SQL?
支持配置文件中编写动态 SQL,是 MyBatis 一个非常强大的功能。所谓动态 SQL,就是在 SQL 中可以包含在 trim、if、#{}等语法标签,在运行时根据条件来生成不同的 SQL。这么说比较抽象,我举个例子解释一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <update id ="update" parameterType ="com.xzg.cd.a89.User" UPDATE user <trim prefix ="SET" prefixOverrides ="," > <if test ="name != null and name != ''" > name = #{name} </if > <if test ="age != null and age != ''" > , age = #{age} </if > <if test ="birthday != null and birthday != ''" > , birthday = #{birthday} </if > </trim > where id = ${id} </update >
显然,动态 SQL 的语法规则是 MyBatis 自定义的。如果想要根据语法规则,替换掉动态 SQL 中的动态元素,生成真正可以执行的 SQL 语句,MyBatis 还需要实现对应的解释器。这一部分功能就可以看做是解释器模式的应用。实际上,如果你去查看它的代码实现,你会发现,它跟我们在前面讲解释器模式时举的那两个例子的代码结构非常相似。
我们前面提到,解释器模式在解释语法规则的时候,一般会把规则分割成小的单元,特别是可以嵌套的小单元,针对每个小单元来解析,最终再把解析结果合并在一起。这里也不例外。MyBatis 把每个语法小单元叫 SqlNode。SqlNode 的定义如下所示:
1 2 3 public interface SqlNode { boolean apply (DynamicContext context) ; }
整个解释器的调用入口在 DynamicSqlSource.getBoundSql 方法中,它调用了 rootSqlNode.apply(context) 方法。因为整体的代码结构跟第 72 讲中的例子基本一致,所以每个 SqlNode 实现类的代码,我就不带你一块阅读了,感兴趣的话你可以自己去看下。