一、简介

MyBatis是一个支持自定义SQL、存储过程和高级映射的一流持久化框架;它几乎消除了所有JDBC代码、手动设置参数和结果检索。

MyBatis可以使用简单的XML或注释进行配置将Java POJOS(Plain Old Java Objects)等映射到数据库记录。

二、安装

下载mybatis-x.x.x.jar并添加到classpath中或使用Maven配置:

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.6</version>
</dependency>

三、入门

1、样例

每个MyBatis应用程序都围绕着一个SqlSessionFactory实例,SqlSessionFactory实例可以通过SqlSessionFactoryBuilder获取:SqlSessionFactoryBuilder可以从XML配置文件或自定义的Configuration类实例构建 SqlSessionFactory实例。

  • 从XML构建SqlSessionFactory

mybatis-config.xml

<?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="development">
		<environment id="development">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<property name="driver" value="com.mysql.jdbc.Driver" />
				<property name="url" value="jdbc:mysql://localhost:3306/test" />
				<property name="username" value="root" />
				<property name="password" value="password" />
			</dataSource>
		</environment>
	</environments>
	<mappers>
		<mapper resource="com/daily/study/mybatis/FruitMapper.xml" />
	</mappers>
</configuration>

FruitMapper.xml

<?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="com.daily.study.mybatis.FruitMapper">
	<select id="selectFruit" resultType="com.daily.study.mybatis.model.Fruit">
		select * from fruit where id = #{id}
	</select>
</mapper>

模型Fruit.java

package com.daily.study.mybatis.model;

public class Fruit implements Serializable{

	private static final long serialVersionUID = -3558129281973069819L;

	private int id;
	private String name;
	private String description;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getDescription() {
		return description;
	}
	public void setDescription(String description) {
		this.description = description;
	}
	
	@Override
	public String toString() {
		return String.format("id: %s, name: %s, description: %s", id, name, description);
	}
}

fruit表:

id name description
1 apple 苹果
2 avocado 南美梨
3 banana 香蕉

测试程序:

public static void main(String[] args) throws Exception {
	String resource = "com/daily/study/mybatis/mybatis-config.xml";
	InputStream inputStream = Resources.getResourceAsStream(resource);
	SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
	try(SqlSession session = sessionFactory.openSession()){
		Fruit fruit = session.selectOne("com.daily.study.mybatis.FruitMapper.selectFruit", 1);
		System.out.println(fruit);
	}
}

输出:

id: 1, name: apple, description: 苹果

查询多条数据:

<select id="selectFruits" resultType="Fruit">
	select * from fruit
</select>
List<Fruit> fruits = session.selectList("com.daily.study.mybatis.FruitMapper.selectFruits");
  • 不使用XML构建SqlSessionFactory

FruitDataSourceFactory.java

public class FruitDataSourceFactory implements DataSourceFactory{

	@Override
	public void setProperties(Properties props) {
		
	}

	@Override
	public DataSource getDataSource() {
		PooledDataSource pds = new PooledDataSource();
		pds.setDriver("com.mysql.jdbc.Driver");
		pds.setUrl("jdbc:mysql://localhost:3306/test");
		pds.setUsername("root");
		pds.setPassword("password");
		return pds;
	}

}

FruitMapper.java

public interface FruitMapper {

	@Select("SELECT * FROM fruit WHERE id = #{id}")
	Fruit selectFruit(int id);
}

测试程序:

public static void main(String[] args) {
	DataSource dataSource = new FruitDataSourceFactory().getDataSource();
	TransactionFactory transactionFactory = new JdbcTransactionFactory();
	Environment environment = new Environment("development", transactionFactory, dataSource);
	Configuration configuration = new Configuration(environment);
	configuration.addMapper(FruitMapper.class);
	SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(configuration);
	try(SqlSession session = sessionFactory.openSession()){
		FruitMapper mapper = session.getMapper(FruitMapper.class);
		Fruit fruit = mapper.selectFruit(3);
		System.out.println(fruit);
	}
}

输出:

id: 3, name: banana, description: 香蕉

查询多条数据:

//FruitMapper.java
@Select("SELECT * FROM fruit")
List<Fruit> selectFruits();
List<Fruit> fruits = mapper.selectFruits();

2、说明

  • 映射SQL语句

在上面的示例中,语句都可以由XML或Annotation定义。可以根据需要在单个映射(mapper)XML文件中定义任意多个映射语句,通过使用基于XML的映射语言,可以实现MyBatis提供的全部特性。

  • 两种方式对比
Fruit fruit = session.selectOne("com.daily.study.mybatis.FruitMapper.selectFruit", 1);
FruitMapper mapper = session.getMapper(FruitMapper.class);
Fruit fruit = mapper.selectFruit(3);

第二种方法有很多优点:它不依赖于字符串文字,因此它要安全得多;还可以通过IDE直接导航映射到SQL语句。

public interface FruitMapper {

	@Select("SELECT * FROM fruit WHERE id = #{id}")
	Fruit selectFruit(int id);
}

对于Mapper类来说,当SQL语句比较简单时,这种方式简单明了,然而当SQL比较复杂时,注解比较有限和混乱。因此,如果必须执行任何复杂的操作,最好使用XML映射语句。

3、范围和生命周期

理解各种类的范围和生命周期是非常重要的,不正确地使用它们会导致严重的并发性问题。

  • SqlSessionFactoryBuilder

SqlSessionFactoryBuilder实例的最佳作用域是方法作用域(即局部方法变量);可以重用SqlSessionFactoryBuilder来构建多个SqlSessionFactory实例,但是仍然最好不要保留它,以确保释放所有XML解析资源(parsing resources)用于更重要的事情。

  • SqlSessionFactory

SqlSessionFactory应该在应用程序执行期间存在,很少或根本没有理由处理或重新创建它。在应用程序运行过程中,最好不要多次重新创建SqlSessionFactory。因此,SqlSessionFactory的最佳范围是应用程序范围;这可以通过多种方式实现,最简单的方法是使用单例模式或静态单例模式。

  • SqlSession

每个线程都应该有自己的SqlSession实例,SqlSession的实例不能共享,并且不是线程安全的。因此,最好的范围是请求或方法范围。不要在静态字段或者甚至类的实例字段中保留对SqlSession实例的引用,并且应该始终确保它在 finally块内被关闭。

try (SqlSession session = sqlSessionFactory.openSession()) {
  // do work
}
  • Mapper Instances

Mapper是用于绑定到映射语句的接口,它的实例是从SqlSession中获取的,因此,任何Mapper的实例最广泛的作用域与请求它们的SqlSession相同;尽可能保持简单,使映射器在方法范围内。

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // do work
}

四、配置

简单介绍Mybatis的配置,更具体的请参考Configuration

1、properties

这些属性是可外部化的、可替换的属性,可以在Java Properties文件中配置,或使用Properties标签。例如:

<configuration>
	<properties>
		<property name="username" value="root"/>
		<property name="password" value="password" />
	</properties>

	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<property name="driver" value="com.mysql.jdbc.Driver" />
				<property name="url" value="jdbc:mysql://localhost:3306/test" />
				<property name="username" value="${username}" />
				<property name="password" value="${password}" />
			</dataSource>
		</environment>
	</environments>
</configuration>

2、settings

修改settings的设置可以修改MyBatis在运行时的行为方式,例如:

<configuration>
	<settings>
		<setting name="cacheEnabled" value="true"/>
		<setting name="lazyLoadingEnabled" value="true"/>
		...
	</settings>
</configuration>
  • cacheEnabled

全局启用或禁用任何mapper中配置的任何缓存

  • lazyLoadingEnabled

全局启用或禁用延迟加载

3、typeAliases

类型别名是Java类型的短名称,用于减少完全限定类名的冗余。例如:

<typeAliases>
	<typeAlias alias="Fruit" type="com.daily.study.mybatis.model.Fruit"/>
</typeAliases>

mapper:

<select id="selectFruit" resultType="Fruit">
	select * from fruit where id = #{id}
</select>

4、typeHandlers

当MyBatis在PreparedStatement上设置参数或从ResultSet中检索值时,就会使用TypeHandler以适合Java类型的方式检索值。部分默认的TypeHandlers如下:

类型处理程序 Java 类型 JDBC 类型
BooleanTypeHandler java.lang.Boolean, boolean BOOLEAN
StringTypeHandler java.lang.String CHAR, VARCHAR
IntegerTypeHandler java.lang.Integer, int NUMERIC or INTEGER
DateTypeHandler java.util.Date TIMESTAMP

可以通过重写类型处理程序(例如org.apache.ibatis.type.BaseTypeHandler)或实现org.apache.ibatis.type.TypeHandler创建自己的程序来处理不支持的或非标准的类型:

@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i,
    String parameter, JdbcType jdbcType) throws SQLException {
    ps.setString(i, parameter);
  }

  ...
}
<typeHandlers>
	<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
  • 枚举(Enums)

如果需要映射枚举,则需要使用EnumTypeHandler或EnumOrdinalTypeHandler。例如,假设我们需要存储数字舍入模式(rounding mode),如果需要舍入的话,这个舍入模式应该与某个数字一起使用。默认情况下,MyBatis使用 EnumTypeHandler将Enum值转换为它们的名称。

如果希望存储整数代码,则需要将EnumOrdinalTypeHandler添加到配置文件中的typeHandlers中;例如:

<typeHandlers>
	<typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/>
</typeHandlers>

5、objectFactory

每次MyBatis创建结果对象的新实例时,都会使用ObjectFactory实例来完成。ObjectFactory默认使用缺省构造函数实例化目标类,如果存在参数映射,则使用参数化的构造函数。可以通过重写ObjectFactory类来修改默认行为。

public class ExampleObjectFactory extends DefaultObjectFactory {
  @Override
  public <T> T create(Class<T> type) {
	//处理缺省构造函数
    return super.create(type);
  }

  @Override
  public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
	//处理参数化构造函数
    return super.create(type, constructorArgTypes, constructorArgs);
  }

  @Override
  public void setProperties(Properties properties) {
	//配置文件objectFactory元素中定义的属性将在初始化objectFactory实例后传递给此方法
    super.setProperties(properties);
  }

  @Override
  public <T> boolean isCollection(Class<T> type) {
    return Collection.class.isAssignableFrom(type);
  }
}
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
	<property name="someProperty" value="100"/>
</objectFactory>

6、environments

可以用多个environments配置MyBatis来实现将SQL映射应用于多个数据库。例如:开发、测试和生产环境可能有不同的配置。(虽然可以配置多个环境,但是每个SqlSessionFactory实例只能选择一个)

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC">
            <property name="..." value="..." />
        </transactionManager>
        <dataSource type="POOLED">
            ...
        </dataSource>
    </environment>
</environments>

environments的default属性指定默认的环境ID;transactionManager和dataSource分别指定事务管理和数据源的配置。

  • transactionManager

    MyBatis包含两种TransactionManager类型:

    • JDBC

    直接使用JDBC提交和回滚工具,它依赖于从dataSource检索到的连接来管理事务的范围。

    • MANAGED

    这个配置几乎什么都不做,它从不提交或回滚连接;相反,它允许容器管理事务的整个生命周期。

  • dataSource

    dataSource元素使用标准JDBC DataSource接口来配置JDBC Connection对象的源;有三种内置的dataSource类型:

    • UNPOOLED

    这种DataSource的实现在每次请求数据源时,只是简单的打开和关闭一个连接,速度稍慢。

    • POOLED

    这种实现了DataSource池的JDBC Connection对象可以避免创建新Connection实例所需的初始连接和身份验证时间。这是一种流行的并发Web应用程序实现最快响应的方法。

    • JNDI

    这种DataSource实现用于EJB或Application Servers等容器,这些容器可以集中或外部配置DataSource,并在JNDI 上下文中放置对它的引用。

7、mappers

MyBatis使用mappers配置了映射文件的位置,可以使用类路径相对资源、完全限定url、类名或包名。例如:

<!-- Using classpath relative resources -->
<mappers>
	<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
</mappers>
<!-- Using url fully qualified paths -->
<mappers>
	<mapper url="file:///var/mappers/BlogMapper.xml"/>
</mappers>
<!-- Using mapper interface classes -->
<mappers>
	<mapper class="org.mybatis.builder.BlogMapper"/>
</mappers>
<!-- Register all interfaces in a package as mappers -->
<mappers>
	<package name="org.mybatis.builder"/>
</mappers>

五、Mapper XML

通过MyBatis的映射语句,可以减少代码专注于SQL。以下简单介绍Mapper XML的配置,更具体的请参考Mapper XML

1、select

select语句是在MyBatis中使用最多的元素之一,因为大多数应用程序中的查询远多于修改。select元素非常简单,例如:

<select id="selectPerson" parameterType="int" resultType="hashmap">
	SELECT * FROM PERSON WHERE ID = #{id}
</select>

这个查询语句的ID为selectPerson,接受int(或Integer)类型的参数,并返回一个由列名映射到行值的键值构成的HashMap。#{id}表示需要创建一个PreparedStatement参数。与上面查询等价的JDBC代码如下:

String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);

select元素可以配置多个属性:

<select
	id="selectPerson"
	parameterType="int"
	parameterMap="deprecated"
	resultType="hashmap"
	resultMap="personResultMap"
	flushCache="false"
	useCache="true"
	timeout="10"
	fetchSize="256"
	statementType="PREPARED"
	resultSetType="FORWARD_ONLY">

例如:parameterType可以指定传到语句中参数的完全限定类名或别名,该属性可选,因为MyBatis可以计算传递给语句的实际参数中要使用的TypeHandler。

2、insert

示例:

<insert id="insertAuthor">
	insert into Author (id,username,password,email,bio)
	values (#{id},#{username},#{password},#{email},#{bio})
</insert>

insert元素也可以配置多个属性:

<insert
	id="insertAuthor"
	parameterType="domain.blog.Author"
	flushCache="true"
	statementType="PREPARED"
	keyProperty=""
	keyColumn=""
	useGeneratedKeys=""
	timeout="20">
  • 自动生成主键
<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
    insert into Author (username,password,email,bio) 
	values (#{username},#{password},#{email},#{bio})
</insert>
  • 多行插入
<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
    insert into Author (username, password, email, bio) values
    <foreach item="item" collection="list" separator=",">
        (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
    </foreach>
</insert>

3、update、delete

示例:

<update id="updateAuthor">
	update Author set
	username = #{username},
	password = #{password},
	email = #{email},
	bio = #{bio}
	where id = #{id}
</update>

<delete id="deleteAuthor">
	delete from Author where id = #{id}
</delete>

与insert类似,也有多个属性可配置:

<update
	id="updateAuthor"
	parameterType="domain.blog.Author"
	flushCache="true"
	statementType="PREPARED"
	timeout="20">

<delete
	id="deleteAuthor"
	parameterType="domain.blog.Author"
	flushCache="true"
	statementType="PREPARED"
	timeout="20">

4、sql

此元素可用于定义可包含在其他语句中的可重用SQL代码片段,例如:

SQL代码片段:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

在另一个语句中使用:

<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

属性值也可用于include的refid属性或include子句中的property value,例如:

<sql id="sometable">
    ${prefix}Table
</sql>
<sql id="someinclude">
    from
    <include refid="${include_target}" />
</sql>
<select id="select" resultType="map">
    select field1, field2, field3
    <include refid="someinclude">
        <property name="prefix" value="Some" />
        <property name="include_target" value="sometable" />
    </include>
</select>

5、parameters

SQL语句中基础数据类型的参数可以完全替换,如果是复杂类型的参数,例如User类型,则将查找id、username和password属性,并将其值传递到PreparedStatement参数:

<insert id="insertUser" parameterType="User">
    insert into users (id, username, password) 
	values (#{id}, #{username}, #{password})
</insert>

SQL语句的参数中也可以指定更具体的数据类型或自定义类型处理类(TypeHandler类或别名):

#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
  • 字符串替换(Substitution)

默认情况下,使用#{}语法MyBatis将生成PreparedStatement属性并根据PreparedStatement参数安全地设置值;但如果只想将未修改的字符串注入到SQL语句中,可以使用类似如下的代码:

ORDER BY ${columnName}

同样,如果SQL语句中的元数据(表名或列名)是动态的时候,也可以使用字符串替换;例如:

可以使用

@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);

代替多个findByXxx方法:

@Select("select * from user where id = #{id}")
User findById(@Param("id") long id);

@Select("select * from user where name = #{name}")
User findByName(@Param("name") String name);

@Select("select * from user where email = #{email}")
User findByEmail(@Param("email") String email);

需要注意的是:以这种方式接受用户的输入并将其提供给未经处理的语句是不安全的,这会导致潜在的SQL注入攻击,因此要么禁止用户在这些字段中输入,要么总是执行自己的转义和检查。

6、Result Maps

resultMap是MyBatis中最重要最强大的元素,使用它可以去掉JDBC从ResultSets检索数据所需的90%的代码。ResultMaps 的设计是这样的:简单的语句不需要显式的结果映射,而更复杂的语句只需要绝对必要的关系描述。

如下简单的语句没有显示的resultMap,这种语句会自动将所有列映射到HashMap中。

<select id="selectUsers" resultType="map">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

也可以映射到JavaBean,只需要修改配置中的resultType:resultType="com.someapp.model.User"

package com.someapp.model;
public class User {
  private int id;
  private String username;
  private String hashedPassword;

  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getUsername() {
    return username;
  }
  public void setUsername(String username) {
    this.username = username;
  }
  public String getHashedPassword() {
    return hashedPassword;
  }
  public void setHashedPassword(String hashedPassword) {
    this.hashedPassword = hashedPassword;
  }
}
<!-- In Config XML file -->
<typeAlias type="com.someapp.model.User" alias="User"/>

<!-- In SQL Mapping XML file -->
<select id="selectUsers" resultType="User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

如果列名和属性名不匹配,则可以使用select子句的别名来匹配:

<select id="selectUsers" resultType="User">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password     as "hashedPassword"
  from some_table
  where id = #{id}
</select>

或者通过resultMap来解决列名不匹配的问题:

<resultMap id="userResultMap" type="User">
  <id property="id" column="user_id" />
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>

<select id="selectUsers" resultMap="userResultMap">
  select user_id, user_name, hashed_password
  from some_table
  where id = #{id}
</select>
  • constructor

可以使用constructor元素指定构造函数:

<resultMap type="userResultMap" id="User">
	<constructor>
		<idArg column="id" javaType="int" />
		<arg column="username" javaType="String" />
		<arg column="age" javaType="_int" />
	</constructor>
</resultMap>
  • association

    使用association元素可以处理关联类型关系。例如,一个博客(Blog)有一个作者(Author):

      <association property="author" javaType="Author">
          <id property="id" column="author_id"/>
          <result property="username" column="author_username"/>
      </association>
    

    有两种关联方式:

    • 嵌套查询

      通过执行另一个映射的SQL语句来返回所需的复杂类型:

        <resultMap id="blogResult" type="Blog">
            <association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
        </resultMap>
        <select id="selectBlog" resultMap="blogResult">
            SELECT * FROM BLOG WHERE ID = #{id}
        </select>
        <select id="selectAuthor" resultType="Author">
            SELECT * FROM AUTHOR WHERE ID = #{id}
        </select>
      

      这种方法虽然简单,但对大型数据集或列表来说,它不会有很好的效果。优点是MyBatis可以延迟加载这些查询,因此可以节省这些语句的开销。但是,如果加载这样一个列表,然后立即遍历该列表以访问嵌套数据,就会调用所有的延迟加载,因此性能可能会非常糟糕。

    • 嵌套结果

      通过使用嵌套结果映射(result mappings)来处理连接结果:

        <select id="selectBlog" resultMap="blogResult">
          select
            B.id            as blog_id,
            B.title         as blog_title,
            B.author_id     as blog_author_id,
            A.id            as author_id,
            A.username      as author_username,
            A.password      as author_password,
            A.email         as author_email,
            A.bio           as author_bio
          from Blog B left outer join Author A on B.author_id = A.id
          where B.id = #{id}
        </select>
      
        <resultMap id="blogResult" type="Blog">
          <id property="id" column="blog_id" />
          <result property="title" column="blog_title"/>
          <association property="author" resultMap="authorResult" />
        </resultMap>
      
        <resultMap id="authorResult" type="Author">
          <id property="id" column="author_id"/>
          <result property="username" column="author_username"/>
          <result property="password" column="author_password"/>
          <result property="email" column="author_email"/>
          <result property="bio" column="author_bio"/>
        </resultMap>
      

      如果不需要重用Author resultMap,也可以使用以下方式:

        <resultMap id="blogResult" type="Blog">
          <id property="id" column="blog_id" />
          <result property="title" column="blog_title"/>
          <association property="author" javaType="Author">
            <id property="id" column="author_id"/>
            <result property="username" column="author_username"/>
            <result property="password" column="author_password"/>
            <result property="email" column="author_email"/>
            <result property="bio" column="author_bio"/>
          </association>
        </resultMap>
      
  • collection

    一个博客(Blog)有一个作者(Author),但一个博客会有多个帖子(Posts)。要将一组嵌套结果映射到类似这样的 List时需要使用collection元素。与association元素一样,也有两种方式:

    • 嵌套查询

        <resultMap id="blogResult" type="Blog">
          <collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>
        </resultMap>
      
        <select id="selectBlog" resultMap="blogResult">
          SELECT * FROM BLOG WHERE ID = #{id}
        </select>
      
        <select id="selectPostsForBlog" resultType="Post">
          SELECT * FROM POST WHERE BLOG_ID = #{id}
        </select>
      

      其中collection的javaType="ArrayList"属性是非必须的,大多数情况MyBatis会自动处理。

    • 嵌套结果

        <select id="selectBlog" resultMap="blogResult">
          select
          B.id as blog_id,
          B.title as blog_title,
          B.author_id as blog_author_id,
          P.id as post_id,
          P.subject as post_subject,
          P.body as post_body,
          from Blog B
          left outer join Post P on B.id = P.blog_id
          where B.id = #{id}
        </select>
      
        <resultMap id="blogResult" type="Blog">
          <id property="id" column="blog_id" />
          <result property="title" column="blog_title"/>
          <collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
        </resultMap>
      
        <resultMap id="blogPostResult" type="Post">
          <id property="id" column="id"/>
          <result property="subject" column="subject"/>
          <result property="body" column="body"/>
        </resultMap
      

      如果不需要重用resultMap,也可以写为:

        <resultMap id="blogResult" type="Blog">
          <id property="id" column="blog_id" />
          <result property="title" column="blog_title"/>
          <collection property="posts" ofType="Post">
            <id property="id" column="post_id"/>
            <result property="subject" column="post_subject"/>
            <result property="body" column="post_body"/>
          </collection>
        </resultMap>
      

7、Auto-mapping

MyBatis在自动映射结果时,查找与列名具有相同名称的属性(忽略大小写),如果找到则设置属性值为查询出数据列的值。

如果要开启自动映射,需要将mapUnderscoreToCamelCase参数设置为true。下面的例子中,id和userName列将被自动映射:

<select id="selectUsers" resultMap="userResultMap">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password
  from some_table
  where id = #{id}
</select>

<resultMap id="userResultMap" type="User">
  <result property="password" column="hashed_password"/>
</resultMap>
public class User {
	private int id;
	private String username;
	private String hashedPassword;
	...
}

8、cache

MyBatis包括一个功能强大的事务性查询缓存特性,它是可配置和可定制的。默认情况下,只启用本地会话(session)缓存,该缓存仅用于在会话期间缓存数据。如果要启用全局二级缓存,只需在SQL Mapping文件(XxxMapper.xml)中添加一行:

<cache/>

作用如下:

  • 将缓存Mapper文件中select语句的所有结果

  • Mapper文件中的所有insert、update和delete语句都将刷新缓存

  • 缓存将使用最近最少使用(LRU)算法进行淘汰

  • 缓存没有刷新间隔

  • 缓存将存储对列表或对象的1024个引用

  • 缓存将被视为读/写缓存,这意味着检索到的对象不是共享的,调用方可以安全地修改它而不会干扰其他调用方

也可以通过设置cache元素的属性来修改默认配置:

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

缓存配置只作用于特定命名空间,如果需要共享相同的缓存配置,则可通过cache-ref引用另一个缓存:

<cache-ref namespace="com.someone.application.data.SomeMapper"/>

六、动态SQL

动态SQL一直是MyBatis最强大的特性之一,在MyBatis3中,使用了强大的基于OGNL的表达式来消除大多数其他Dynamic SQL元素。

  • if

当标题和作者不为空时会作为查询条件:

<select id="findActiveBlogLike" 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>
  • choose、when、otherwise

与Java中的switch语句类似,MyBatis提供了一个choose元素用于在多个选项中只选择一种情况:

<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>
  • trim、where、set
<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>

对于上面的SQL,根据条件的不同可能会得到以下错误的SQL:

SELECT * FROM BLOG WHERE
SELECT * FROM BLOG WHERE AND title like 'someTitle'

类似上面的问题,大部分可以使用where元素解决:

<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元素的行为不完全符合要求,则可以通过trim元素对其进行自定义;与上面where元素等价的trim元素的写法是:

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

trim元素属性的含义是删除prefixOverrides属性中指定的任何内容,并插入prefix属性中的任何内容。prefixOverrides属性接受以管道符|分隔的文本列表,其中的空白字符是有意义的。

对于动态更新语句有一个类似的元素set;此元素可用于动态地包含要更新的列,而省略其他无关的逗号。

<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>

等价的trim元素的写法如下:

<trim prefix="SET" suffixOverrides=",">
	...
</trim>
  • foreach

动态SQL的另一个常用场景是遍历一个集合,通常是为了构建IN条件。例如:

<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>

可以将任何可迭代对象以及任何Map或Array对象作为集合参数传递给foreach。当使用Iterable或Array时,index是当前迭代次数,item是在此次迭代中元素的值。当使用Map时,index是Map的key,item是Map的value。

  • script

注解中也可以通过script元素使用动态SQL:

@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表达式创建变量并将其绑定到上下文:

<select id="selectBlogsLike" resultType="Blog">
    <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
    SELECT * FROM BLOG WHERE title LIKE #{pattern}
</select>
  • 多数据库支持

如果databaseIdProvider中配置了一个’_databaseId’变量则可用于动态代码中根据数据库供应商来构建不同的语句。

<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>

七、日志

MyBatis通过使用内部日志工厂(log factory)提供日志信息,内部日志工厂将把日志信息委托给SLF4JApache Commons LoggingLog4j等日志实现。

MyBatis可以对包、mapper(完全限定类名称)和语句(完全限定命名空间)启用日志记录。

  • 添加日志实现jar

添加日志实现jar到应用程序classpath中。

  • 配置日志
public interface FruitMapper {
	@Select("SELECT * FROM fruit WHERE id = #{id}")
	Fruit selectFruit(int id);

	@Select("SELECT * FROM fruit")
	List<Fruit> selectFruits();
}

如果要对上面的mapper启用日志记录,只需在日志配置文件中添加一行:

log4j.logger.com.daily.study.mybatis.FruitMapper=TRACE

完整的log4j.properties文件:

# Global logging configuration
log4j.rootLogger=ERROR, stdout
# MyBatis logging configuration...
log4j.logger.com.daily.study.mybatis.FruitMapper=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

执行查询:

Fruit fruit = mapper.selectFruit(3);

日志输出:

DEBUG [main] - ==>  Preparing: SELECT * FROM fruit WHERE id = ?
DEBUG [main] - ==> Parameters: 3(Integer)
TRACE [main] - <==    Columns: id, name, description
TRACE [main] - <==        Row: 3, banana, 香蕉
DEBUG [main] - <==      Total: 1

也可以做更精细的控制,对特定语句启用日志记录:

log4j.logger.com.daily.study.mybatis.FruitMapper.selectFruit=TRACE

或者对某一组mapper启用日志记录:

log4j.logger.com.daily.study.mybatis=TRACE

有些查询可以返回大量结果集,在这种情况下可能希望只看到SQL语句,但不希望看到结果,此时可以将日志级别设置为DEBUG:

log4j.logger.com.daily.study.mybatis=DEBUG

如果程序中使用的是XML配置方式,则使用命名空间即可:

<mapper namespace="org.mybatis.example.BlogMapper">
    <select id="selectBlog" resultType="Blog">
        select * from Blog where id = #{id}
    </select>
</mapper>

对应的mapper和语句的日志配置分别如下:

log4j.logger.org.mybatis.example.BlogMapper=TRACE
log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE
参考资料:

MyBatis Introduction