MyBatis(二)

  • 动态SQL
  • Java API
  • 日志

动态SQL

MyBatis 可以根据参数来动态拼接SQL, 以免为了不同业务, 而写出大量近乎雷同的SQL, 极大简化了开发

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

if

1
2
3
4
5
6
7
8
9
10
11
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>

如果传递了title 那么 AND title like #{title} 将会拼接到SQL中, 如果传递了author, 并且author包含了name属性, AND author_name like #{author.name} 将会被拼接到SQL.

没有 else 这点有点不习惯, 不过可以使用 choose, when, otherwise 来实现 if-else

choose, when, otherwise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>

choose, when, otherwise 类似于 Java 里面的 switch 语句, 上述表示, 如果 title 不为空, 则拼接 AND title like #{title}; 否则如果 author不为空, author.name不为空, 则拼接 AND author_name like #{author.name}; 如果都不满足, 则拼接 AND featured = 1;

trim, where, set

使用 if 有一个弊端, 拼接出的SQL可能会出现问题, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>

如果都不满足, 将会拼接出:

1
2
SELECT * FROM BLOG
WHERE

如果第一个不满足, 会拼接出:

1
2
3
SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

查询必定失败, 为了解决这个问题, 出现了 where

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>

where 会在至少有一个元素被拼接时插入 where 语言, 但是这样还会有 SELECT * FROM BLOG WHERE AND title like ‘someTitle’, 这是就有了 trim, 例如:

1
2
3
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>

trim 是一个代替 where的存在, 他能配合 if 拼接出正确的语句, 同理 set 也会出现 where 一样的问题, 这样的问题可以通过 trim 来解决.

1
2
3
4
5
6
7
8
9
10
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>

异常情况下会拼接出:

1
2
3
4
update Author
set
username='haha',
where id = 1

set 后的 , 号, 会导致执行失败, 通过 trim 可写为:

1
2
3
<trim prefix="SET" suffixOverrides=",">
...
</trim>

合理使用 prefixprefixOverridessuffixsuffixOverrides 这4个标签, 可以解决这个问题

  • prefix : 如果里面有字符串, 它会拼接到前面
  • suffix : 如果里面有字符串, 它会拼接到后面
  • prefixOverrides: 如果里面有字符串, 前面的这个字符串会被删掉
  • suffixOverrides: 如果里面有字符串, 后面的这个字符串会被删掉

foreach

1
2
3
4
5
6
7
8
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list" open="(" separator="," close=")">
#{item}
</foreach>
</select>

经常会出现根据id来查东西的情况, id数量不固定, 可能有多个可能有一个, 针对这种情况, 使用 foreach 可以利用变量来生成语句, 有下面的属性:

  • item 类似于 for(int a in list) 中的 a
  • index 索引
  • collection 传入的集合
  • open 开头的字符
  • separator 分割字符
  • close 结尾的字符

其他语句

script

用于注解方式的SQL编写

1
2
3
4
5
6
7
8
9
10
11
@Update({"<script>",
"update Author",
" <set>",
" <if test='username != null'>username=#{username},</if>",
" <if test='password != null'>password=#{password},</if>",
" <if test='email != null'>email=#{email},</if>",
" <if test='bio != null'>bio=#{bio}</if>",
" </set>",
"where id=#{id}",
"</script>"})
void updateAuthorValues(Author author);

bind

bind 元素可以从 OGNL 表达式中创建一个变量并将其绑定到上下文。比如:

1
2
3
4
5
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>

当sql比较复杂, 重复的比较多的使用, 用这个应该会很方便.

多数据库支持

针对不同数据库的语句不同的情况使用, 不过一般情况下, 数据库选定后不会轻易的改变, 所以这种比较少用哦.

1
2
3
4
5
6
7
8
9
10
11
<insert id="insert">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
<if test="_databaseId == 'oracle'">
select seq_users.nextval from dual
</if>
<if test="_databaseId == 'db2'">
select nextval for seq_users from sysibm.sysdummy1"
</if>
</selectKey>
insert into users values (#{id}, #{name})
</insert>

动态 SQL 中的可插拔脚本语言

MyBatis 从 3.2 开始支持可插拔脚本语言,这允许你插入一种脚本语言驱动,并基于这种语言来编写动态 SQL 查询语句, MyBatis 提供了极大的可定制空间.

  1. 实现如下接口
    1
    2
    3
    4
    5
    public interface LanguageDriver {
    ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
    SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
    SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
    }
  2. 在 mybatis-config.xml 文件中将它设置为默认语言, 或者针对某一个语句设置特定语言.
    1. 设置为默认语言

      1
      2
      3
      4
      5
      6
      <typeAliases>
      <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
      </typeAliases>
      <settings>
      <setting name="defaultScriptingLanguage" value="myLanguage"/>
      </settings>
    2. 针对某一个语句设置特定语言

      1
      2
      3
      <select id="selectBlog" lang="myLanguage">
      SELECT * FROM BLOG
      </select>

      or

      1
      2
      3
      4
      5
      public interface Mapper {
      @Lang(MyLanguageDriver.class)
      @Select("SELECT * FROM BLOG")
      List<Blog> selectBlog();
      }

Java API

这里主要介绍下 SqlSessions, 他是MyBatis中比较重要的接口.

SqlSessionFactoryBuilder

SqlSessions 由 SqlSessionFactory 实例创建, SqlSessionFactory 由 SqlSessionFactoryBuilder 构建, 也可以由 XML, 注解 创建

有5个方法, 用来创建 SqlSessionFactory

1
2
3
4
5
SqlSessionFactory build(InputStream inputStream);
SqlSessionFactory build(InputStream inputStream, String environment);
SqlSessionFactory build(InputStream inputStream, Properties properties);
SqlSessionFactory build(InputStream inputStream, String env, Properties props);
SqlSessionFactory build(Configuration config);

属性可以从 mybatis-config.xml 中配置, 也可以方法传入, 还可以resource 或 url 指定, 对于相同的配置在多个地方配置, 是有优先级的, 对于重复属性, 前面加载的会被后面加载的所覆盖, 加载顺序是:

  1. 首先读取在 mybatis-config.xml 中的 properties 元素体中指定的属性;
  2. 其次,读取从 properties 元素的类路径 resource 或 url 指定的属性,且会覆盖已经指定了的重复属性;
  3. 最后,读取作为方法参数传递的属性,且会覆盖已经从 properties 元素体和 resource 或 url 属性中加载了的重复属性。

SqlSessionFactory

SqlSessionFactory 有六个方法创建 SqlSession 实例。通常来说,当你选择这些方法时你需要考虑以下几点:

  • 事务处理:我需要在 session 使用事务或者使用自动提交功能(auto-commit)吗?(通常意味着很多数据库和/或 JDBC 驱动没有事务)
  • 连接:我需要依赖 MyBatis 获得来自数据源的配置吗?还是使用自己提供的配置?
  • 执行语句:我需要 MyBatis 复用预处理语句和/或批量更新语句(包括插入和删除)吗?
1
2
3
4
5
6
7
8
9
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();

SqlSession

MyBatis 是通过 SqlSession 执行语句, 提交事务, 回滚事务 等操作的.

执行语句

这些方法被用来执行定义在 SQL 映射的 XML 文件中的 增, 删, 改, 查 语句。它们都会自行解释,每一句都使用语句的 ID 属性和参数对象,参数可以是原生类型(自动装箱或包装类)、JavaBean、POJO 或 Map。

事务控制方法

控制事务作用域有四个方法。 但是使用场景有限,如果已经设置了自动提交或你正在使用外部事务管理器,这就没有任何效果了。然而,如果你正在使用 JDBC 事务管理器,由Connection 实例来控制,那么这四个方法就会派上用场:

Mybatis 一般会结合 Spring 使用, 一般会使用Spring来管理事务

1
2
3
4
void commit();
void commit(boolean force);
void rollback();
void rollback(boolean force);

本地缓存

有两种: 本地缓存 和 二级缓存

每创建一个新的 session, MyBatis 会创建一个关联的本地缓存, 保证不会重复查询数据库, 本地缓存默认能在整个Session周期内使用, 也可以设置 localCacheScope=STATEMENT 表示缓存仅在语句执行时有效。

注意: 如果 localCacheScope 被设置为 SESSION,那么 MyBatis 所返回的引用将传递给保存在本地缓存里的相同对象。对返回的对象(例如 list)做出任何更新将会影响本地缓存的内容,进而影响存活在 session 生命周期中的缓存所返回的值。因此,不要对 MyBatis 所返回的对象作出更改,以防后患。

使用映射器

MyBatis的sql是一般情况写到xml中的, 所以我们需要一个映射器, 来关联这语句, 调用这些映射器方法, 即可执行相应的SQL.

1
2
3
4
5
6
7
8
9
public interface AuthorMapper {
Author selectAuthor(@Param("id") int id);
List<Author> selectAuthors();
@MapKey("id")
Map<Integer, Author> selectAuthors();
int insertAuthor(Author author);
int updateAuthor(Author author);
int deleteAuthor(int id);
}

可以通过 @Param 来映射参数.

映射器注解

不仅仅可以使用xml来写sql, 注解也可以使用, 注解提供了一种简单的方式来实现简单映射语句,而不会引入大量的开销.

仅仅列举我用过的啊, 感觉还是xml用着方便, 注解方式会让 XXXMapper.java 变的很难看, 我更喜欢干净的 XXXMapper.java

| 注解 | 使用对象 | 相对应的 XML | 描述 |
| — | — | — | — |
| @Param | 参数 | N/A | 如果你的映射方法的形参有多个,这个注解使用在映射方法的参数上就能为它们取自定义名字。若不给出自定义名字,多参数(不包括 RowBounds 参数)则先以 “param” 作前缀,再加上它们的参数位置作为参数别名。例如 #{param1}, #{param2},这个是默认值。如果注解是 @Param("person"),那么参数就会被命名为 #{person}。 |
| — | — | — | — |

日志^1

Mybatis 的内置日志工厂提供日志功能,内置日志工厂将日志交给以下其中一种工具作代理:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j
  • JDK logging

MyBatis 会运行时查找并选择合适的日志工具, 从上到下依次查找, 如果没有找到日志将被禁用.

注意: 很多应用服务器会自带 commons logging, 这种情况下MyBatis会默认其为日志工具, 如果你想用其他的日志工具, 可以在 mybatis-config.xml 中指定.

1
2
3
4
5
6
7
<configuration>
<settings>
...
<setting name="logImpl" value="LOG4J"/>
...
</settings>
</configuration>

logImpl 可选的值有:SLF4JLOG4JLOG4J2JDK_LOGGINGCOMMONS_LOGGINGSTDOUT_LOGGINGNO_LOGGING,或者是实现了接口 org.apache.ibatis.logging.Log 的,且构造方法是以字符串为参数的类的完全限定名。

你也可以调用如下任一方法来使用日志工具:

1
2
3
4
5
org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
org.apache.ibatis.logging.LogFactory.useJdkLogging();
org.apache.ibatis.logging.LogFactory.useCommonsLogging();
org.apache.ibatis.logging.LogFactory.useStdOutLogging();

如果你决定要调用以上某个方法,请在调用其它 MyBatis 方法之前调用它。另外,仅当运行时类路径中存在该日志工具时,调用与该日志工具对应的方法才会生效,否则 MyBatis 一概忽略。如你环境中并不存在 Log4J,你却调用了相应的方法,MyBatis 就会忽略这一调用,转而以默认的查找顺序查找日志工具。

日志配置(Log4J)

  1. 引入Log4J
  2. 配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # Global logging configuration
    log4j.rootLogger=ERROR, stdout
    # MyBatis logging configuration...
    log4j.logger.org.mybatis.example.BlogMapper=TRACE
    # Console output...
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

    # 指定某个sql
    log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE
    # 指定某个mapper
    log4j.logger.org.mybatis.example.BlogMapper=TRACE
    # 指定某个包下
    log4j.logger.org.mybatis.example=TRACE
  3. mybatis-config.xml 中指定 LOG4J.