Spring 集成 MyBatis

除了配置文件,查询只需要下面的内容
  1. 可选:domain 类(也可以不要,直接用 Map)
  2. 必要:mapper xml
  3. 必要:mapper interface

1. 添加 pom.xml 依赖

添加 MySQL, MyBatis, MyBatis-Spring 的依赖到 pom.xml

<!-- For MyBatis -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.2.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.21</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.2.1</version>
</dependency>

2. 创建 MyBatis for Spring 的配置

保存在 resources/spring-mybatis.xml

参考文档MyBatis - MyBatis-Spring | 简介

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 1. Date source config -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=UTF-8" />
        <property name="username" value="root" />
        <property name="password" value="root" />
    </bean>

    <!-- 2. SQL session factory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="mapperLocations" value="classpath:mybatis-mapper/**/*.xml" /> <!-- Mapper xml -->
    </bean>

    <!-- 3. Instantiate Mapper -->
    <!--<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">-->
        <!--<property name="mapperInterface" value="mapper.UserMapper" />-->
        <!--<property name="sqlSessionFactory" ref="sqlSessionFactory" />-->
    <!--</bean>-->
    <!--没有必要在Spring的XML配置文件中注册所有的映射器。
        相反, 你可以使用一个MapperScannerConfigurer,
        它将会查找类路径下的映射器并自动将它们创建成MapperFactoryBean。
    -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="mapper" />
    </bean>
</beans>

3. 加载 spring-mybatis.xml

web.xml 中用 ContextLoaderListener 加载 spring-mybatis.xml
ContextLoaderListener 对应的容器是其他 Spring 容器的父容器,所以在里面创建的 MyBatis 的 mapper 在 springmvc 这个容器即以后要使用的 Shiro filter 中都能访问。

<web-app>
    ...
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:spring-mybatis.xml
        </param-value>
    </context-param>
    ...
</web-app>

4. 创建 domain 类

用于把数据库查询到的结果映射为对象,方便使用。和查询结果的列对应就可以了,不需要和数据库的所有列一一对应。

domain.User 在前面的例子里已经创建好了,如果没有,则创建。

package domain;

import org.hibernate.validator.constraints.NotBlank;

import javax.validation.constraints.NotNull;

public class User {
    private int id;
    private String username;
    private String password;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @NotBlank(message="用户名不能为空") // 进行参数验证
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @NotNull(message="密码不能为null") // 进行参数验证
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

5. 创建 Mapper xml

保存在 resources/mybatis-mapper/User.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">

<!--namespace 非常重要:必须是 Mapper 类的全路径-->
<mapper namespace="mapper.UserMapper">
    <!-- [1] 简单的 JavaBean,直接使用 resultType: 数据库表的列与 JavaBean 的属性对应 -->
    <select id="findUserById" parameterType="int" resultType="domain.User">
        SELECT id, username, password FROM user WHERE id = #{id}
    </select>

    <select id="findUsers" resultType="domain.User">
        SELECT id, username, password FROM user LIMIT ${offset}, ${count}
    </select>
</mapper>

6. 创建 Mapper interface

创建接口 mapper.UserMapper,只是定义一下接口,不需要我们提供它的实现,这个接口的实现会由 MyBatis-Spring 的框架创建。

package mapper;

import domain.User;
import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface UserMapper {
    public User findUserById(int id);

    // 使用 @Param 的方式传参数
    public List<User> findUsers(@Param("offset") int offset, @Param("count") int count);
}

7. 创建 Controller

类 controller.MyBatisController 用于测试使用 MyBatis 访问数据库。

package controller;

import domain.User;
import mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;

@Controller
public class MyBatisController {
    @Autowired
    private UserMapper userMapper;

    @RequestMapping("/user/{userId}")
    @ResponseBody
    public String findUser(@PathVariable Integer userId) {
        // 查找一个 User
        User user = userMapper.findUserById(userId);

        List<User> users = userMapper.findUsers(1, 2);
        System.out.println(users);

        return user.toString();
    }
}

8. 访问 http://localhost/user/1

控制台输出的 MyBatis 的 Debug 信息

[2015-04-06 14:09:15] [DEBUG] [BaseJdbcLogger.java-debug:132] - ooo Using Connection [jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8, UserName=root@localhost, MySQL-AB JDBC Driver]
[2015-04-06 14:09:15] [DEBUG] [BaseJdbcLogger.java-debug:132] - ==> Preparing: SELECT id, username, password FROM user WHERE id = ?
[2015-04-06 14:09:15] [DEBUG] [BaseJdbcLogger.java-debug:132] - ==> Parameters: 1(Integer)
[2015-04-06 14:09:16] [DEBUG] [BaseJdbcLogger.java-debug:132] - ooo Using Connection [jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8, UserName=root@localhost, MySQL-AB JDBC Driver]
[2015-04-06 14:09:16] [DEBUG] [BaseJdbcLogger.java-debug:132] - ==> Preparing: SELECT id, username, password FROM user LIMIT 1, 2
[2015-04-06 14:09:16] [DEBUG] [BaseJdbcLogger.java-debug:132] - ==> Parameters:
[User{id=2, username='黄彪', password='Pa88w0rd'}, User{id=3, username='Alice', password='xxxxxxxx'}]

可以看到查询的 SQL 语句,查询用的参数等,可以在 Logback 的配置中设置 MyBatis 的日志级别,如 <logger name="org.mybatis" level="debug"/>,以便看到更多信息。


到此看看工程的目录结构

9. MyBatis 里使用 LIKE 语句

MySql:
SELECT * FROM user WHERE name like CONCAT('%',#{name},'%')

Oracle:
SELECT * FROM user WHERE name like CONCAT('%',#{name},'%') 或 
SELECT * FROM user WHERE name like '%'||#{name}||'%'

SQLServer:  
SELECT * FROM user WHERE name like '%'+#{name}+'%'

DB2:
SELECT * FROM user WHERE name like CONCAT('%',#{name},'%') 或  
SELECT * FROM user WHERE name like '%'||#{name}||'%'

10. 使用 @Param 传参数

查看 mapper.UserMapper

public List<User> findUsers(@Param("offset") int offset, @Param("count") int count);

11. #{name} 与 ${name} 的区别

#{name} 会根据传进来的参数的类型自动加上相应的信息,例如字符串两边会加上 '',日期对象会自动的转化成 SQL 识别的内容,可以防止 SQL 注入攻击

${name} 直接替换,例如传进来的是字符串,不会在字符串两边加上 '',比较适合 int 等类型,例如分页时的 offset and count。

参考 8 中控制台输出的 SQL 语句。

12. 使用数据库连接池

MyBatis 推荐使用连接池 DBCP,Hibernate 推荐使用连接池 C3P0

12.1 在 pom.xml 加入 DBCP 依赖

<dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>1.4</version>
</dependency>

12.2 把 Data Source 的配置换成

修改 resources/spring-mybatis.xml 里的 <bean id="dataSource" ...> 为下面的内容

<!-- 1. Data Source using DBCP. -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=UTF-8" />
    <property name="username" value="root" />
    <property name="password" value="root" />

    <!-- 连接池启动时的初始值 -->
    <property name="initialSize" value="10" />
    <!-- 连接池的最大值 -->
    <property name="maxActive" value="100" />
    <!-- 最大空闲值.当经过一个高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放一部分,一直减少到maxIdle为止 -->
    <property name="maxIdle" value="50" />
    <!-- 最小空闲值.当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰来时来不及申请 -->
    <property name="minIdle" value="5" />
    <!--#给出一条简单的sql语句进行验证-->
    <property name="validationQuery" value="select NOW()" />
    <!--#在取出连接时进行有效验证-->
    <property name="testOnBorrow" value="false" />
    <property name="testWhileIdle" value="true" />
    <property name="logAbandoned" value="true" />
    <property name="removeAbandoned" value="true" />
    <property name="removeAbandonedTimeout" value="120" />
    <!-- #运行判断连接超时任务的时间间隔,单位为毫秒,默认为-1,即不执行任务。 -->
    <property name="timeBetweenEvictionRunsMillis" value="3600000" />
    <!-- #连接的超时时间,默认为半小时。 -->
    <property name="minEvictableIdleTimeMillis" value="3600000" />
</bean>

用命令 SHOW FULL PROCESSLIST 可以看到 MySQL 里的连接数

从上图可以看到我们的程序创建了 10 个连接(15到24),和配置里初始化时创建 10 个连接正好匹配。多次访问 http://localhost/user/1 可以发现某些连接的 Time 会变小(不活动时间),说明连接被重复的使用,而不是新创建的。

13. Mapper 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.tur.mapper.UserMapper" >
    <sql id="columns" > id, age, name</sql>

    <!-- [[1]] 简单的JavaBean,直接使用resultType: 数据库表的列与JavaBean的属性对应 -->
    <select id="selectUserById" parameterType="int" resultType="com.tur.domain.User" >
        SELECT <include refid="columns"/>
        FROM user WHERE id = #{id}
    </select>

    <select id="selectUsersByName" parameterType="string" resultType="com.tur.domain.User" >
        SELECT <include refid="columns"/>
        FROM user WHERE name = #{name}
    </select>

    <!-- [[2]] 可以使用resultMap映射自己的类: 例如多表查询时 -->
    <select id="selectUserById" parameterType="int" resultMap="userResultMap" >
        SELECT <include refid="columns"/>
        FROM user WHERE id = #{id}
    </select>
    <resultMap id="userResultMap" type="com.tur.domain.User" >
        <id property="id" column="id"/>
        <result property="age" column="age"/>
        <result property="name" column="name"/>
    </resultMap>


    <!-- [[3]] 使用resultMap映射,属性是另一个类的对象: association -->
    <select id="selectFullUserById" parameterType="int" resultMap="userAssociationResultMap" >
        SELECT
            user.id         as id, <!-- 重命名列非常有用 -->
            user.age        as age,
            user.name       as name,
            ui.id           as user_info_id,
            ui.user_id      as user_info_user_id,
            ui.telephone    as user_info_telephone,
            ui.address      as user_info_address
        <!--FROM user, user_info ui-->
        FROM user
            INNER JOIN user_info ui ON user.id=ui.user_id
        WHERE user.id=#{id}
            <!--AND user.id=ui.user_id-->
    </select>
    <resultMap id="userAssociationResultMap" type="com.tur.domain.User" >
        <id property="id" column="id"/>
        <result property="age" column="age"/>
        <result property="name" column="name"/>
        <!--嵌套映射中还可以使用resultMap: association, collection
        还可以使用嵌套查询,但是会产生N+1问题,在大数量的数据库里会有很大的性能问题-->
        <!--<association property="userInfo" column="user_info_id" javaType="domain.UserInfo">
            <id     property="id"        column="user_info_id"/>
            <result property="userId"    column="user_info_user_id"/>
            <result property="telephone" column="user_info_telephone"/>
            <result property="address"   column="user_info_address"/>
        </association>-->
        <!--association是一对一关系,collection是一对多关系-->
        <!--使用columnPrefix可以使result map重用-->
        <association property="userInfo" column="user_info_id" columnPrefix="user_info_" resultMap="userInfoResultMap"/>
    </resultMap>
    <resultMap id="userInfoResultMap" type="com.tur.domain.UserInfo" >
        <id     property="id"        column="id"/>
        <result property="userId"    column="user_id"/>
        <result property="telephone" column="telephone"/>
        <result property="address"   column="address"/>
    </resultMap>

    <select id="selectUsersWithName" parameterType="list" resultType="com.tur.domain.User" >
        SELECT  <include refid="columns"/>
        FROM    user
        WHERE   name in
        <foreach item="item" index="index" open="("separator=","close=")" collection="list" >
            #{item}
        </foreach>
    </select>
</mapper>