认真是一种态度
坚持是一种选择

0%

数据库访问框架 - Mybatis

Mybatis简介

MyBatis本是apache的一个开源项目iBatis,2010年这个项目由apache software foundation迁移到了google code,并且改名为Mybatis。2013年11月迁移到Github。
iBatis一词源于”internet”和”abatis”的组合,是一个基于Java的持久层框架。iBatis提供的持久层框架包括SQL Maps和Data Access Objects(DAOS)。
mybatis 内部封装了 jdbc,使开发者只需要关注 sql 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。
mybatis 通过 xml 或注解的方式将要执行的各种 statement 配置起来,并通过 java 对象和 statement 中 sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。
采用 ORM 思想解决了实体和数据库映射的问题,对 jdbc 进行了封装,屏蔽了 jdbc api 底层访问细节,使我们不用与 jdbc api 打交道,就可以完成对数据库的持久化操作。

Mybatis快速入门

基于dao的代理实现(默认方式)

1.创建mavan工程

2.添加Mybatis坐标
在项目的pom.xml文件中添加Mybatis3.4.5坐标:

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
<dependencies>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!-- mybatis依赖mysql驱动,必选 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version> <scope>runtime</scope>
</dependency>
<!-- 单元测试依赖,可选 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<!-- 日志依赖,可选 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
</dependencies>

3.添加实体类User
domain包下创建实体类User

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
public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;

public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username; }
public void setUsername(String username) {
this.username = username;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User [id=" + id + ", username=" + username + ", birthday=" + birthday
+ ", sex=" + sex + ", address="
+ address + "]";
}
}

4.编写持久层接口IUserDao
dao包下创建实体类IUserDao

1
2
3
4
5
6
7
8
//持久层接口(也可以写成 UserDao 或者 UserMapper)
public interface IUserDao {
/**
* 查询所有用户
* @return
*/
List<User> findAll();
}

5.编写持久层接口的映射文件IUserDao.xml

1
2
3
4
5
6
7
8
9
10
<?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.itheima.dao.IUserDao">
<!-- 配置查询所有操作 -->
<select id="findAll" resultType="com.itheima.domain.User">
select * from user
</select>
</mapper>

注意:

  1. 创建位置必须和持久层接口在相同的包中;
  2. 名称必须以持久层接口名称命名文件名,扩展名是.xml。

6.编写SqlMapConfig.xml配置文件

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
<?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>
<!-- 配置 mybatis 的环境 -->
<environments default="mysql">
<!-- 配置 mysql 的环境 -->
<environment id="mysql">
<!-- 配置事务的类型 -->
<transactionManager type="JDBC"></transactionManager>
<!-- 配置连接数据库的信息:用的是数据源(连接池) -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/ee50"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</dataSource>
</environment>
</environments>

<!-- 告知 mybatis 映射配置的位置 -->
<mappers>
<mapper resource="com/itheima/dao/IUserDao.xml"/>
</mappers>
</configuration>

7.编写测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MybatisTest {
public static void main(String[] args)throws Exception {
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建 SqlSessionFactory 的构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3.使用构建者创建工厂对象 SqlSessionFactory
SqlSessionFactory factory = builder.build(in);
//4.使用 SqlSessionFactory 生产 SqlSession 对象
SqlSession session = factory.openSession();
//5.使用 SqlSession 创建 dao 接口的代理对象
IUserDao userDao = session.getMapper(IUserDao.class);
//6.使用代理对象执行查询所有方法
List<User> users = userDao.findAll();
for(User user : users) {
System.out.println(user);
}
//7.释放资源
session.close();
in.close();
}
}

当满足了如下3点规则,就无须再编写dao的实现类,mybatis自动创建实现类:

  1. mybatis映射配置文件(mapper.xml)位置必须和dao接口的包结构相同;
  2. 映射配置文件的mapper标签namespace属性的取值必须是dao接口的全限定类名;
  3. 映射配置文件的操作配置(select)id属性的取值必须是dao接口的方法名;

基于注解的mybatis使用

1.在持久层接口IUserDao中添加注解

1
2
3
4
5
6
7
8
public interface IUserDao {
/**
* 查询所有用户
* @return
*/
@Select("select * from user")
List<User> findAll();
}

2.不再需要持久层接口的映射文件IUserDao.xml,直接删除

3.修改SqlMapConfig.xml,xml路径改为类名

1
2
3
4
<!-- 告知 mybatis 映射配置的位置 --> 
<mappers>
<mapper class="com.itheima.dao.IUserDao"/>
</mappers>

自己实现dao的实现类

1.创建持久层接口IUserDao的实现类impl.UserDaoImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserDaoImpl implements IUserDao{
private SqlSessionFactory factory;

public UserDaoImpl(SqlSessionFactory factory){
this.factory = factory;
}

public List<User> findAll(){
//1.使用工厂创建SqlSession对象
SqlSession session = factory.openSession();
//2.使用session执行查询所有方法,这里一定要是包含完整包名的接口全名方法,才能找到最终要执行的sql语句
List<User> users = session.selectList("com.itheima.dao.IUserDao.findAll");
session.close();
//3.返回查询结果
return users;
}

}

2.修改测试类的调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MybatisTest {
public static void main(String[] args)throws Exception {
//1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建 SqlSessionFactory 的构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3.使用构建者创建工厂对象 SqlSessionFactory
SqlSessionFactory factory = builder.build(in);
//4.使用工厂创建dao对象
IUserDao userDao = new UserDaoImpl(factory);
//5.使用dao对象执行查询所有方法
List<User> users = userDao.findAll();
for(User user : users) {
System.out.println(user);
}
//6.释放资源
in.close();
}
}

Mybatis基本操作

在上面的快速入门中介绍了最基本的查询操作,接下来介绍mybatis对数据库的增删查改操作

基于代理的增删查改CRUD

1.在持久层接口IUserDao中添加接口方法

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
/**
* 根据id查询用户信息
* @param userId
* @return
*/
User findById(Integer userId);

/**
* 保存用户
* @param user
*/
void saveUser(User user);

/**
* 更新用户
* @param user
*/
void updateUser(User user);

/**
* 根据Id删除用户
* @param userId
*/
void deleteUser(Integer userId);

/**
* 根据名称模糊查询用户信息
* @param username
* @return
*/
List<User> findByName(String username);

2.在用户的映射配置文件SqlMapConfig.xml中增加配置

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
<!-- 根据id查询用户 -->
<!-- 内置类型可以用简称int,也可以用全限定名java.lang.Integer,但自定义类型必须用全限定名 -->
<select id="findById" parameterType="int" resultType="com.itheima.domain.User">
select * from user where id = #{uid}
</select>

<!-- 保存用户 -->
<insert id="saveUser" parameterType="com.itheima.domain.User">
<!-- 配置插入操作后,获取插入数据的id -->
<!-- keyProperty实体类属性名 -->
<!-- keyColumn数据库列名 -->
<!-- order="AFTER"语句执行完成后再执行 -->
<selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
select last_insert_id();
</selectKey>
insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday});
</insert>

<!-- 更新用户 -->
<update id="updateUser" parameterType="com.itheima.domain.User">
update user set username=#{username},address=#{address},sex=#{sex},birthday=#{birthday} where id=#{id}
</update>

<!-- 删除用户-->
<delete id="deleteUser" parameterType="java.lang.Integer">
delete from user where id = #{uid}
</delete>

<!-- 根据名称模糊查询 -->
<select id="findByName" parameterType="string" resultType="com.itheima.domain.User">
select * from user where username like #{name}
<!-- select * from user where username like '%${value}%'-->
</select>
  • resultType 属性:用于指定结果集的类型。

  • parameterType 属性:用于指定传入参数的类型。

  • sql语句中的#{}表示占位符,相当于原来jdbc的?,都是用于执行语句时替换实际的数据,具体的数据由#{}里面的内容决定的,如果数据类型是基本类型,里面可以填写任意名字,比如findById中的uid可以对应接口方法中的实际参数名userId。如果数据类型是复杂类型,语法格式就是使用 #{对象.对象}的方式,比如#{user.username}它会先去找 user 对象,然后在 user 对象中找到 username 属性,并调用getUsername()方法把值取出来,我们在 parameterType 属性上指定了实体类名称,所以可以省略user.,而直接写 username。

  • #{}${}的区别:

    • #{}表示一个占位符号,通过#{}可以实现 preparedStatement 向占位符中设置值,自动进行 java 类型和 jdbc 类型转换,#{}可以有效防止 sql 注入。 #{}可以接收简单类型值或 pojo 属性值。 如果 parameterType 传输单个简单类型值,#{}括号中可以是 value 或其它任意名称。

    • ${}表示拼接 sql 串,通过${}可以将 parameterType 传入的内容拼接在 sql 中且不进行 jdbc 类型转换, ${}可以接收简单类型值或 pojo 属性值,如果 parameterType 传输单个简单类型值,${}括号中只能是 value,或者在接口方法中声明参数名:List<User> findByName(@Param(value="name") String username);

3.编写测试类

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
public class MybatisTest {
private InputStream in;
private SqlSession sqlSession;
private IUserDao userDao;

@Before//用于在测试方法执行之前执行
public void init()throws Exception{
//1.读取配置文件,生成字节输入流
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.获取SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//3.获取SqlSession对象
sqlSession = factory.openSession();
//4.获取dao的代理对象
userDao = sqlSession.getMapper(IUserDao.class);
}

@After//用于在测试方法执行之后执行
public void destroy()throws Exception{
//提交事务
sqlSession.commit();
//6.释放资源
sqlSession.close();
in.close();
}

@Test
public void testFindOne(){
//5.执行查询一个方法
User user = userDao.findById(50);
System.out.println(user);
}

@Test
public void testFindByName(){
//5.执行查询一个方法
List<User> users = userDao.findByName("%王%");
//List<User> users = userDao.findByName("王");
for(User user : users){
System.out.println(user);
}
}
}

4.参数中如果需要传递多个Pojo(Plain Ordinary Java Object)对象,可以用一个pojo类QueryVo包含多个pojo

1
2
3
4
5
6
7
8
9
public class QueryVo {
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
1
2
3
4
<!-- 根据用户名称模糊查询,参数变成一个 QueryVo 对象了 -->
<select id="findByVo" resultType="com.itheima.domain.User" parameterType="com.itheima.domain.QueryVo">
select * from user where username like #{user.username};
</select>

配置文件的主要标签说明

SqlMapConfig.xml文件配置的内容和层级关系:

-properties(属性)
—property
-settings(全局配置参数)
—setting
-typeAliases(类型别名)
—typeAliase
—package
-typeHandlers(类型处理器)
-objectFactory(对象工厂)
-plugins(插件)
-environments(环境集合属性对象)
—environment(环境子属性对象)
—-transactionManager(事务管理)
—-dataSource(数据源)
-mappers(映射器)
—mapper
—package

返回值映射resultMap

sql语句中的查询返回列必须和resultType类型中的属性名对应,否则会赋值为null,如果仅大小写不同,仍可以赋值成功,因为mysql 在 windows 系统中不区分大小写!
如果要匹配,可以在sql语句中设置别名强制对应上,此方法要可能要修改大量的sql语句,工作量较大,可以考虑另一种方法,对返回值做映射:
1.在IUserDao.xml中定义resultMap

1
2
3
4
5
6
7
8
9
10
<!-- 配置 查询结果的列名和实体类的属性名的对应关系 -->
<resultMap id="userMap" type="com.itheima.domain.User">
<!-- 主键字段的对应 -->
<id property="userId" column="id"></id>
<!--非主键字段的对应-->
<result property="userName" column="username"></result>
<result property="userAddress" column="address"></result>
<result property="userSex" column="sex"></result>
<result property="userBirthday" column="birthday"></result>
</resultMap>

2.在需要映射的地方将返回类型由resultType改为resultMap

1
2
3
4
<!-- 根据id查询用户 -->
<select id="findById" parameterType="int" resultMap="userMap">
select * from user where id = #{uid}
</select>

属性标签配置properties

可将一些配置信息抽离出来保存在properties标签中,实现复用,修改SqlMapConfig.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    <properties>
<property name="driver" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/eesy_mybatis"></property>
<property name="username" value="root"></property>
<property name="password" value="1234"></property>
</properties>
...
<!--配置连接池-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>

也可将properties抽离出来,放在单独的文件,在 classpath 下定义jdbcConfig.properties 文件

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/eesy
jdbc.username=root
jdbc.password=1234

SqlMapConfig.xmlproperties标签设置url或者resources属性,用于引用jdbcConfig.properties 文件,两者区别如下:

  • resource属性:常用的用于指定配置文件的位置,是按照类路径的写法来写,并且必须存在于类路径下。

    1
    <properties resource="jdbcConfig.properties"></properties>
  • url属性:要求按照Url的写法来写地址。

    1
    <properties url="file:///D:/xxx/xxx/jdbcConfig.properties"></properties>

类名别名标签typeAliases

1
2
3
4
5
6
7
8
9
10
<!--使用typeAliases配置别名,它只能配置domain中类的别名 -->
<typeAliases>
<!--typeAlias用于配置别名。type属性指定的是实体类全限定类名。alias属性指定别名,当指定了别名就再区分大小写 -->
<!-- 单个别名定义 -->
<-- <typeAlias type="com.itheima.domain.User" alias="user"></typeAlias> -->

<!-- 用于指定要配置别名的包,当指定之后,该包下的实体类都会注册别名,并且类名就是别名,不再区分大小写-->
<!-- 批量别名定义,扫描整个包下的类,别名为类名(首字母大写或小写都可以) -->
<package name="com.itheima.domain"></package>
</typeAliases>

映射器标签mappers

1
2
3
4
5
6
7
8
9
10
11
<!-- 配置映射文件的位置 -->
<mappers>
<!-- 使用相对于类路径的资源 -->
<!--<mapper resource="com/itheima/dao/IUserDao.xml"></mapper>-->

<!-- 使用mapper接口类路径 -->
<!--<mapper class="com.itheima.dao.UserDao"/>-->

<!-- package标签是用于指定dao接口所在的包,当指定了之后就不需要在写mapper以及resource或者class了 -->
<package name="com.itheima.dao"></package>
</mappers>

配置连接池

在 Mybatis 的SqlMapConfig.xml配置文件中,通过<dataSource type="pooled">来实现 Mybatis 中连接池的配置。
Mybatis 将它自己的数据源分为三类:

  • POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现
  • UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想
  • JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样的

MyBatis内部定义了实现了java.sql.DataSource接口的UnpooledDataSource类和PooledDataSource类分别表示UNPOOLED、POOLED类型的数据源。

Mysql的数据库事务

数据库事务(transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

事务的四大特性(ACID)

  • 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
  • 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
  • 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
  • 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

事务并发带来的三个问题

  • 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
  • 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
  • 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行(Repeatable read),解决幻读需要锁表。

MySQL定义的四种事务隔离级别

事务隔离级别 脏读 不可重复读 幻读 解释
读未提交(read-uncommitted) 可以读到未提交的事物
不可重复读(read-committed) 只能读提交的事物
可重复读(repeatable-read) 事务提交前后都能读【MySql默认】
串行化(serializable) serializable时会锁表,是最安全的,也是日常开发基本不会用

Mysql事务设置命令

  • 读未提交
    set session transaction isolation level read uncommitted;
  • 不可重复读(读已提交的)
    set session transaction isolation level read committed;
  • 可重复读
    set session transaction isolation level repeatable read;
  • 串行化
    set session transaction isolation level serializable;

Mybatis中使用事务

Mybatis是基于jdbc的封装,在 JDBC 中可通过setAutoCommit()将事务提交设置为自动或手动的方式。

setAutoCommit官方定义:

将此连接的自动提交模式设置为给定状态。如果连接处于自动提交模式下,则它的所有SQL语句将被执行并作为单个事务提交。否则,它的SQL语句将聚集到事务中,直到调用commit 方法或 rollback 方法为止。默认情况下,新连接处于自动提交模式。

在mybatis的session = factory.openSession();方法中,默认参数是false,即手动提交事务的方式,在执行完sql语句后,需手动执行session.commitsession.rollback,这也是常用的方式,因为我们可以根据业务情况来决定是否进行提交。
如果设置`session = factory.openSession(true);,则mybatis会设置jdbc为自动提交事务的方法,无须显示执行session.commit

Mybatis动态Sql语句

代码片段

可将重复的sql语句抽离出来,使用时用include引用即可,达到 sql 重用的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 抽取重复的语句代码片段 --> 
<sql id="defaultSql">
select * from user
</sql>

<!-- 配置查询所有操作 -->
<select id="findAll" resultType="user">
<include refid="defaultSql"></include>
</select>

<!-- 根据 id 查询 -->
<select id="findById" resultType="user" parameterType="int">
<include refid="defaultSql"></include>
where id = #{uid}
</select>

动态语句<if>

可以根据条件来判断是否需要拼接sql语句,标签中的 test 属性中写的是对象的属性名,如果是包装类的对象要使用 OGNL 表达式的写法。
为保证拼接顺利,主语句上要加上where 1=1

1
2
3
4
5
6
7
8
9
<select id="findByUser" resultType="user" parameterType="user">
select * from user where 1=1
<if test="username!=null and username != '' ">
and username like #{username}
</if>
<if test="address != null">
and address like #{address}
</if>
</select>

动态语句<where>

使用<where>标签可以简化where 1=1的条件封装,同时可以自动处理条件掉表达式开头的and或者or字符,使生成正确的sql语句。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 根据用户信息查询 --> 
<select id="findByUser" resultType="user" parameterType="user">
<include refid="defaultSql"></include>
<where>
<if test="username!=null and username != '' ">
and username like #{username}
</if>
<if test="address != null">
and address like #{address}
</if>
</where>
</select>

动态语句<foreach>

在拼接sql语句时,有时需要传递一个集合,比如这样的sql语句select 字段 from user where id in (?),可以写这样的语句:

1
2
3
4
5
6
7
8
9
10
11
<!-- 根据queryvo中的Id集合实现查询用户列表 -->
<select id="findUserInIds" resultMap="userMap" parameterType="queryvo">
<include refid="defaultUser"></include>
<where>
<if test="ids != null and ids.size()>0">
<foreach collection="ids" open="and id in (" close=")" item="uid" separator=",">
#{uid}
</foreach>
</if>
</where>
</select>

<foreach>标签用于遍历集合,它的属性含义为:

  • collection:代表要遍历的集合元素,注意编写时不要写#{}
  • open:代表语句的开始部分
  • close:代表结束部分
  • item:代表遍历集合的每个元素,生成的变量名
  • sperator:代表分隔符

测试方法,构造一个ids的集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 测试foreach标签的使用
*/
@Test
public void testFindInIds(){
QueryVo vo = new QueryVo();
List<Integer> list = new ArrayList<Integer>();
list.add(41);
list.add(42);
list.add(46);
vo.setIds(list);

List<User> users = userDao.findUserInIds(vo);
for(User user : users){
System.out.println(user);
}
}

多表查询

一对一的association标签

如果2个表关联查询,比如Account表和User表,一个Account只能对应一个User,Account对User是1对1的关系,可以采用继承的方式让Account继承User的全部字段,sql语句返回的数据列分别和各字段绑定。另一个更好的办法是采用组合的方式,为Account定义一个user的属性,使之将和user有关的数据封装起来。
Mybatis中可以使用association标签定义,IAccountDao.xml的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 定义封装account和user的resultMap -->
<resultMap id="accountUserMap" type="account">
<id property="id" column="aid"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<!-- 一对一的关系映射:配置封装user的内容-->
<association property="user" column="uid" javaType="user">
<id property="id" column="id"></id>
<result column="username" property="username"></result>
<result column="address" property="address"></result>
<result column="sex" property="sex"></result>
<result column="birthday" property="birthday"></result>
</association>
</resultMap>

<!-- 查询所有 -->
<select id="findAll" resultMap="accountUserMap">
select u.*,a.id as aid,a.uid,a.money from account a , user u where u.id = a.uid;
</select>

Account实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Account implements Serializable {
...

//从表实体应该包含一个主表实体的对象引用
private User user;

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}
}

一对多的collection标签

一个User可以对应多个Account,User对Account是1对多的关系。
在Mybatis中可以使用collection标签定义,IUserDao.xml的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 定义User的resultMap-->
<resultMap id="userAccountMap" type="user">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="address" column="address"></result>
<result property="sex" column="sex"></result>
<result property="birthday" column="birthday"></result>
<!-- 配置user对象中accounts集合的映射 -->
<!-- collection 是用于建立一对多中集合属性的对应关系
ofType 用于指定集合元素的数据类型 -->
<collection property="accounts" ofType="account">
<id column="aid" property="id"></id>
<result column="uid" property="uid"></result>
<result column="money" property="money"></result>
</collection>
</resultMap>

<!-- 查询所有 -->
<select id="findAll" resultMap="userAccountMap">
select u.*,a.id as aid ,a.uid,a.money from user u left outer join account a on u.id =a.uid
</select>

User实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User implements Serializable {
...

//一对多关系映射:主表实体应该包含从表实体的集合引用
private List<Account> accounts;

public List<Account> getAccounts() {
return accounts;
}

public void setAccounts(List<Account> accounts) {
this.accounts = accounts;
}
}

延迟加载策略

延迟加载就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据,也称懒加载。

  • 好处:先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
  • 坏处:因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。

使用 assocation 实现延迟加载

1.修改IAccountDao.xml,将关联用户设置为延迟加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 定义封装account和user的resultMap -->
<resultMap id="accountUserMap" type="account">
<id property="id" column="id"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<!-- 一对一的关系映射:配置封装user的内容
select属性指定的内容:查询用户的唯一标识,方法名,必填
column属性指定的内容:用户根据id查询时,所需要的参数名(sql语句返回的列名),必填
-->
<association property="user" column="uid" javaType="user" select="com.itheima.dao.IUserDao.findById">
</association>
</resultMap>

<!-- 查询所有 -->
<select id="findAll" resultMap="accountUserMap">
select * from account
</select>

2.修改IUserDao.xml,添加查询单个用户的方法:

1
2
3
4
<!-- 根据id查询用户 -->
<select id="findById" parameterType="int" resultType="user">
select * from user where id = #{uid}
</select>

3.开启 Mybatis 的延迟加载策略,修改SqlMapConfig.xml

1
2
3
4
5
6
7
<!--配置参数-->
<settings>
<!--延迟加载的全局开关,开启Mybatis支持延迟加载,默认值false-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载,默认值false(true in <=3.4.1)-->
<setting name="aggressiveLazyLoading" value="false"></setting>
</settings>

设置完毕后,当只查询Account而没用到User属性时,不会执行User对象的查询sql语句。

使用 collection 实现延迟加载

1.修改IUserDao.xml,将关联账号设置为延迟加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 定义User的resultMap-->
<resultMap id="userAccountMap" type="user">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="address" column="address"></result>
<result property="sex" column="sex"></result>
<result property="birthday" column="birthday"></result>
<!-- 配置user对象中accounts集合的映射 -->
<!-- 用于指定查询account列表的sql语句,所以填写的是该sql映射的id,必填
column属性指定的内容:用于指定select属性的sql语句的参数来源,上面的参数来自于user的id列,所以就写成id这一个字段名了,必填-->
<collection property="accounts" ofType="account" select="com.itheima.dao.IAccountDao.findAccountByUid" column="id">
</collection>
</resultMap>

<!-- 查询所有 -->
<select id="findAll" resultMap="userAccountMap">
select * from user
</select>

2.修改IAccountDao.xml,添加根据用户id查询账号的方法:

1
2
3
4
<!-- 根据用户id查询账户列表 -->
<select id="findAccountByUid" resultType="account">
select * from account where uid = #{uid}
</select>

3.开启mybatis延迟加载全局开关

Mybatis缓存

一级缓存

一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flush 或 close,它就存在。
比如在同一个SqlSession下对用户查询2次User user = userDao.findById(41);,只会执行一次sql语句,2次返回的User对象是同一个对象。
一级缓存是 SqlSession 默认的缓存,当调用 SqlSession 的修改,添加,删除,commit(),close(),clearCache()等方法时,就会清空一级缓存。

  1. 第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
  2. 如果 sqlSession 去执行 commit 操作(执行插入、更新、删除),清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
  3. 第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。

二级缓存

二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个SqlSession可以共用二级缓存,二级缓存是跨 SqlSession 的。

二级缓存的开启与关闭

1.在SqlMapConfig.xml文件开启二级缓存

1
2
3
4
5
<settings>
<!-- 开启二级缓存的支持 -->
<!-- cacheEnabled 的默认取值就是true,所以这一步其实可以省略不配置 -->
<setting name="cacheEnabled" value="true"/>
</settings>

2.配置需要二级缓存的Mapper映射文件

1
2
3
4
5
6
7
8
9
10
<?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.itheima.dao.IUserDao">
<!--开启user支持二级缓存-->
<!--标签表示当前这个 mapper 映射将使用二级缓存,区分的标准就看mapper的namespace值-->
<cache></cache>
...
</mapper>

3.配置 statement 上面的 useCache 属性

1
2
3
4
<!-- 根据id查询用户 -->
<select id="findById" parameterType="INT" resultType="user" useCache="true">
select * from user where id = #{uid}
</select>

IUserDao.xml映射文件中的<select>标签中设置useCache="true"代表当前这个 statement 要使用二级缓存,如果不使用二级缓存可以设置为 false。
注意:

  • 在select的statement中useCache默认为true,所以针对每次查询都需要最新的数据sql,要设置成 useCache=false,禁用二级缓存。
  • 二级缓存是将查询对象序列化后缓存起来的,当再次查询时不会执行sql,而是从缓存中反序列化成对象,所以当使用二级缓存时,所缓存的类一定要实现java.io.Serializable接口,这样才可以使用序列化方式来保存对象。

注解开发

使用Mybatis提供的注解方式,就可以不用创建Mapper映射文件了。

mybatis常用注解说明

  • @Insert:实现新增
  • @Update:实现更新
  • @Delete:实现删除
  • @Select:实现查询
  • @Result:实现结果集封装
  • @Results:可以与@Result 一起使用,封装多个结果集
  • @ResultMap:实现引用@Results 定义的封装
  • @One:实现一对一结果集封装
  • @Many:实现一对多结果集封装
  • @SelectProvider: 实现动态 SQL 映射
  • @CacheNamespace:实现注解二级缓存的使用

注解实现基本CRUD

1.编写SqlMapConfig.xml配置文件

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
<?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>
<!-- 配置properties文件的位置 -->
<properties resource="jdbcConfig.properties"></properties>
<!-- 配置别名的注册 -->
<typeAliases>
<package name="com.itheima.domain"></package>
</typeAliases>
<!-- 配置环境-->
<environments default="mysql">
<environment id="mysql">
<!-- 配置事务的类型是 JDBC -->
<transactionManager type="JDBC"></transactionManager>
<!-- 配置数据源 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>
<!-- 配置映射信息 -->
<!-- 指定带有注解的dao接口所在位置 -->
<mappers>
<!-- 配置 dao 接口的位置,它有两种方式
第一种:使用 mapper 标签配置 class 属性
第二种:使用 package 标签,直接指定 dao 接口所在的包
-->
<mapper class="com.itheima.dao.IUserDao"></mapper>
</mappers>
</configuration>

2.使用注解方式开发持久层接口IUserDao

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
public interface IUserDao {

/**
* 查询所有用户
* @return
*/
@Select("select * from user")
@Results(id="userMap",
value= {
@Result(id=true,column="id",property="userId"),
@Result(column="username",property="userName"),
@Result(column="sex",property="userSex"),
@Result(column="address",property="userAddress"),
@Result(column="birthday",property="userBirthday")
})
List<User> findAll();

/**
* 根据 id 查询一个用户
* @param userId
* @return
*/
@Select("select * from user where id=#{id} ")
@ResultMap("userMap")
User findById(Integer userId);

/**
* 保存操作
* @param user
*/
@Insert("insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday})")
@SelectKey(keyColumn="id",keyProperty="id",resultType=Integer.class,before =
false, statement = { "select last_insert_id()" })
int saveUser(User user);

/**
* 更新操作
* @param user
*/
@Update("update user set username=#{username},sex=#{sex},birthday=#{birthday},address=#{address} where id=#{id}")
int updateUser(User user);

/**
* 删除用户
* @param userId
*/
@Delete("delete from user where id=#{uid} ")
int deleteUser(Integer userId);

/**
* 查询使用聚合函数
* @return
*/
@Select("select count(*) from user ")
int findTotalUser();

/**
* 根据用户名称模糊查询
* @param username
* @return
*/
//@Select("select * from user where username like #{username} ")
@Select("select * from user where username like '%${value}%' ")
List<User> findUserByName(String username);
}

Results指定了id后,接下来的ResultMap可以直接引用这个Results,比如查询所有用户和查询单个用户的方法。

注解实现复杂关系映射

注解对应的xml映射字段:

注解 xml 说明
@Results <resultMap> 可以使用单个@Result 注解,也可以使用@Result 集合<br>@Results({@Result(),@Result()})@Results(@Result())
@Result <id><result> 属性介绍:
id 是否是主键字段
column 数据库的列名
property 需要装配的属性名
one 需要使用的@One注解
many 需要使用的@Many注解
@One <assocation> 用来指定子查询返回单一对象,属性介绍:
select 指定用来多表查询的 sqlmapper
fetchType 查询方式,懒加载或立即加载,会覆盖全局的配置参数 lazyLoadingEnabled
使用格式:<br>@Result(column="",property="",one=@One(select=""))
@Many <collection> 用来指定子查询返回对象集合,属性介绍:
映射文件中的javaType 一般为 ArrayList,注解中可以不定义
使用格式:<br>@Result(column="",property="",many=@Many(select=""))

使用@One注解实现一对一关系映射

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 查询所有账户,采用延迟加载的方式查询账户的所属用户
* @return
*/
@Select("select * from account")
@Results(id="accountMap",
value= {
@Result(id=true,column="id",property="id"),
@Result(column="uid",property="uid"),
@Result(column="money",property="money"),
@Result(column="uid",property="user",one=@One(select="com.itheima.dao.IUserDao.findById",fetchType=FetchType.LAZY))
})
List<Account> findAll();

使用@Many注解实现一对多关系映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 查询所有用户
* @return
*/
@Select("select * from user")
@Results(id="userMap",
value= {
@Result(id=true,column="id",property="userId"),
@Result(column="username",property="userName"),
@Result(column="sex",property="userSex"),
@Result(column="address",property="userAddress"),
@Result(column="birthday",property="userBirthday"),
@Result(column="id",property="accounts",many=@Many(select="com.itheima.dao.IAccountDao.findByUid",fetchType=FetchType.LAZY))
})
List<User> findAll();

注解实现二级缓存

1.在SqlMapConfig.xml中开启二级缓存支持

1
2
3
4
5
<!-- 配置二级缓存 --> 
<settings>
<!-- 开启二级缓存的支持 -->
<setting name="cacheEnabled" value="true"/>
</settings>

2.在持久层接口中使用注解配置二级缓存

1
2
3
4
@CacheNamespace(blocking=true)  //mybatis 基于注解方式实现配置二级缓存
public interface IUserDao {
...
}

参考资料:

传智播客SSM框架之MyBatis
深入浅出Mybatis系列(五)Mybatis事务篇