MyBatis(二)
一、MyBatis 连接池技术详解
1.1 连接池基础概念
连接池是存储数据库连接的容器,通过复用连接避免频繁创建/销毁连接的开销,显著提升程序性能。
MyBatis 内置三种连接池类型,通过 dataSource
标签的 type
属性配置:
UNPOOLED
:不使用连接池,每次请求创建新连接(性能较差,适用于简单场景)。POOLED
:使用内置连接池,自动管理连接的创建、复用和销毁(推荐生产环境使用)。JNDI
:通过 JNDI(Java 命名目录接口)获取容器管理的连接池(适用于 Java EE 容器环境)。
1.2 核心配置示例(POOLED 连接池)
<environments default="mysql"><environment id="mysql"><transactionManager type="JDBC"/><dataSource type="POOLED"><!-- 数据库驱动与连接信息 --><property name="driver" value="${jdbc.driver}"/><property name="url" value="${jdbc.url}"/><property name="username" value="${jdbc.username}"/><property name="password" value="${jdbc.password}"/></dataSource></environment>
</environments>
二、动态 SQL 实战:从条件拼接 to 复杂查询
2.1 if 标签:条件动态拼接
场景:根据用户输入的条件动态拼接 SQL 查询语句,避免硬编码。
示例:根据用户名和性别筛选用户:
<select id="findByWhere" parameterType="User" resultType="User">SELECT * FROM user<where><if test="username != null and username != ''">AND username LIKE #{username}</if><if test="sex != null and sex != ''">AND sex = #{sex}</if></where>
</select>
where
标签作用:自动处理首个AND
关键字,避免WHERE 1=1
冗余写法。
测试
public interface UserMapper {// 条件查询public List<User> findByWhere(User user);}
/*** 条件查询* @throws IOException*/@Testpublic void testFindByWhere() throws IOException {// 先加载主配置文件,加载到输入流中InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");// 创建SqlSessionFactory对象,创建SqlSession对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);// 创建SqlSession对象SqlSession session = factory.openSession();
User user = new User();user.setUsername("%熊%");user.setSex("女");// 条件查询UserMapper mapper = session.getMapper(UserMapper.class);List<User> list = mapper.findByWhere(user);for (User user1 : list) {System.out.println(user1);}// 关闭资源session.close();inputStream.close();}
2.2 foreach 标签:批量操作与集合查询
场景:处理 IN
条件、批量插入/删除等集合操作。
2.2.1 场景一:IN 条件查询
<select id="findByIds" parameterType="User" resultType="User">SELECT * FROM user<where><foreach collection="ids" open="id in ( " separator="," close=")" item="i">#{i}</foreach></where>
</select>
- 参数传递:通过
User
对象的List<Integer> ids
属性传递待查询的 ID 集合。 - 关键属性:
collection
:集合属性名(如ids
)。item
:集合元素别名(如i
)。open/close
:拼接 SQL 的前缀和后缀(如IN (
和)
)。separator
:元素间分隔符(如,
)。
2.2.2 场景二:批量插入
<insert id="batchInsert" parameterType="List<User>">INSERT INTO user (username, sex)VALUES<foreach collection="list" item="u" separator=",">(#{u.username}, #{u.sex})</foreach>
</insert>
2.3 公用 SQL 提取:sql 与 include 标签
场景:重复 SQL 片段提取,提升代码复用性。
示例:提取基础查询语句:
<!--提取公共的SQL-->
<sql id="findAllSql">select * from user
</sql><select id="findAll" resultType="User"><include refid="findAllSql"/>
</select><select id="findById" parameterType="int" resultType="User"><include refid="findAllSql"/> WHERE id = #{id}
</select>
三、高级映射:从一对一到多对多
搭建开发环境
CREATE TABLE `user` (`id` int(11) NOT NULL auto_increment,`username` varchar(32) NOT NULL COMMENT '用户名称',`birthday` datetime default NULL COMMENT '生日',`sex` char(1) default NULL COMMENT '性别',`address` varchar(256) default NULL COMMENT '地址',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;insert into `user`(`id`,`username`,`birthday`,`sex`,`address`) values (1,'老王','2018-02-27 17:47:08','男','北京'),(2,'熊大','2018-03-02 15:09:37','女','上海'),(3,'熊二','2018-03-04 11:34:34','女','深圳'),(4,'光头强','2018-03-04 12:04:06','男','广州');
CREATE TABLE `account` (`ID` int(11) NOT NULL COMMENT '编号',`UID` int(11) default NULL COMMENT '用户编号',`MONEY` double default NULL COMMENT '金额',PRIMARY KEY (`ID`),KEY `FK_Reference_8` (`UID`),CONSTRAINT `FK_Reference_8` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into `account`(`ID`,`UID`,`MONEY`) values (1,1,1000),(2,2,1000),(3,2,2000);
3.1 多对一查询(Account → User)
场景:查询账户信息时关联用户数据(如账户所属用户的名称和地址)。
3.1.1 实体类定义
public class Account implements Serializable{private Integer id;private Integer uid; // 关联用户 IDprivate Double money;private User user; // 多对一关联的用户对象public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public Integer getUid() {return uid;}public void setUid(Integer uid) {this.uid = uid;}public Double getMoney() {return money;}public void setMoney(Double money) {this.money = money;}
public User getUser() {return user;}
public void setUser(User user) {this.user = user;}
}
3.1.2 AccountMapper 接口
public interface AccountMapper {
public List<Account> findAll();
}
3.1.3 Mapper 配置(使用 association 标签)
<mapper namespace="AccountMapper"><!--内连接查询--><select id="findAll" resultMap="accountMap">select a.*,u.username,u.address from account a,user u where a.uid = u.id</select><!--进行数据封装--><resultMap id="accountMap" type="Account"><result property="id" column="id"/><result property="uid" column="uid"/><result property="money" column="money"/><!-- 配置多对一关联 --><association property="user" javaType="User"><result property="username" column="username"/><result property="address" column="address"/></association></resultMap></mapper>
3.1.4 测试方法
/*** 多对一查询* @throws IOException*/@Testpublic void testFindAll() throws IOException {// 先加载主配置文件,加载到输入流中InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");// 创建SqlSessionFactory对象,创建SqlSession对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);// 创建SqlSession对象SqlSession session = factory.openSession();// 获取代理对象AccountMapper mapper = session.getMapper(AccountMapper.class);// 查询List<Account> list = mapper.findAll();// 遍历for (Account account : list) {System.out.println(account);}// 关闭资源session.close();inputStream.close();}
3.2 一对多查询(User → Account)
场景:查询用户信息时关联其所有账户数据。
3.2.1 实体类定义
public class User implements Serializable{// 主键private Integer id;// 用户名private String username;// 生日private Date birthday;// 性别private String sex;// 地址private String address;// 一对多关联的账户列表private List<Account> accounts; 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;}
@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + '\'' +", birthday=" + birthday +", sex='" + sex + '\'' +", address='" + address + '\'' +", accounts=" + accounts +'}';}
}
3.2.2 UserMapper 接口
public interface UserMapper {//查询一对多public List<User> findOneToMany();}
3.2.3 Mapper 配置(使用 collection 标签)
<mapper namespace="UserMapper"><select id="findOneToMany" resultMap="userMap">select u.*,a.money from user u left join account a on u.id = a.uid</select><resultMap id="userMap" type="User"><result property="id" column="id"/><result property="username" column="username"/><result property="birthday" column="birthday"/><result property="sex" column="sex"/><result property="address" column="address"/><!-- 配置一对多关联 --><collection property="accounts" ofType="Account"><result property="money" column="money"/></collection></resultMap>
</mapper>
3.2.4 测试方法
/*** 一对多查询*/@Testpublic void testOneToMany() throws IOException {// 先加载主配置文件,加载到输入流中InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");// 创建SqlSessionFactory对象,创建SqlSession对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);// 创建SqlSession对象SqlSession session = factory.openSession();UserMapper mapper = session.getMapper(UserMapper.class);List<User> list = mapper.findOneToMany();// 遍历list集合for (User user : list) {System.out.println(user);}// 关闭资源session.close();inputStream.close();}
3.3 多对多查询(User ↔ Role)
场景:用户与角色为多对多关系(如用户可拥有多个角色,角色可分配给多个用户)。
3.3.1 表结构
user
表、role
表、中间表user_role
(存储用户与角色关联关系)。
CREATE TABLE `role` (`ID` int(11) NOT NULL COMMENT '编号',`ROLE_NAME` varchar(30) default NULL COMMENT '角色名称',`ROLE_DESC` varchar(60) default NULL COMMENT '角色描述',PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into `role`(`ID`,`ROLE_NAME`,`ROLE_DESC`) values (1,'组长','管理整个组'),(2,'班主任','管理整个班级'),(3,'校长','管理整个学校');
CREATE TABLE `user_role` (`UID` int(11) NOT NULL COMMENT '用户编号',`RID` int(11) NOT NULL COMMENT '角色编号',PRIMARY KEY (`UID`,`RID`),KEY `FK_Reference_10` (`RID`),CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `role` (`ID`),CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into `user_role`(`UID`,`RID`) values (1,1),(1,2),(2,2);
3.3.2 实体类定义
package com.qcby.domain;import java.io.Serializable;
import java.util.Date;
import java.util.List;public class User implements Serializable{private static final long serialVersionUID = 525400707336671154L;private Integer id;private String username;private Date birthday;private String sex;private String address;// 定义ids属性,用来存储所有的idprivate List<Integer> ids;// 演示一对多查询private List<Account> accounts;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;}public List<Integer> getIds() {return ids;}public void setIds(List<Integer> ids) {this.ids = ids;}public List<Account> getAccounts() {return accounts;}public void setAccounts(List<Account> accounts) {this.accounts = accounts;}
}public class Role implements Serializable{
private static final long serialVersionUID = 4836306672907553166L;private Integer id;private String role_name;private String role_desc;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getRole_name() {return role_name;}public void setRole_name(String role_name) {this.role_name = role_name;}public String getRole_desc() {return role_desc;}public void setRole_desc(String role_desc) {this.role_desc = role_desc;}
}
3.3.3 RoleMapper接口
public interface RoleMapper {public List<Role> findAll();
}
3.3.4 RoleMapper 配置
<!-- RoleMapper配置 --><select id="findAll" resultMap="roleMap">SELECT r.*,u.username FROM USER u,user_role ur,role r WHERE u.id = ur.UID AND ur.RID = r.ID</select><resultMap type="role" id="roleMap"><id property="id" column="id"/><result property="role_name" column="role_name"/><result property="role_desc" column="role_desc"/><collection property="users" ofType="user"><result property="username" column="username"/> </collection></resultMap>
3.3.5 测试方法
/*** 多对多查询*/@Testpublic void testRole() throws IOException{// 先加载主配置文件,加载到输入流中InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");// 创建SqlSessionFactory对象,创建SqlSession对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);// 创建SqlSession对象SqlSession session = factory.openSession();RoleDao dao=session.getMapper(RoleDao.class);List<Role> list = dao.findAll();for(Role role:list){System.out.println(role);}//关闭资源session.close();inputStream.close();}
四、延迟加载策略:按需加载关联数据
4.1 延迟加载 vs 立即加载
- 立即加载:主查询时立即加载关联数据(如查询用户时同时查询其所有账户)。
- 延迟加载:主查询时不加载关联数据,仅在访问关联数据时触发查询(提升初始查询性能)。
4.2 配置延迟加载
- 全局配置(
SqlMapConfig.xml
):
<settings><setting name="lazyLoadingEnabled" value="true"/> <!-- 开启延迟加载 --><setting name="aggressiveLazyLoading" value="false"/> <!-- 关闭积极加载(按需加载) -->
</settings>
- 关联查询配置(以多对一为例):
<mapper namespace="AccountMapper"><select id="findAll" resultMap="accountMap">SELECT * FROM account <!-- 主查询仅查账户数据 --></select><resultMap id="accountMap" type="Account"><!-- 延迟加载用户数据,通过 select 指定关联查询方法,column 传递关联字段 --><association property="user" javaType="User" select="UserMapper.findById" column="uid"/></resultMap>
</mapper><mapper namespace="UserMapper"><select id="findById" parameterType="int" resultType="User">SELECT * FROM user WHERE id = #{id} <!-- 延迟加载时触发此查询 --></select>
</mapper>
注:
<configuration><!-- 注意:以下元素必须按此顺序排列 --><properties .../><settings .../><typeAliases .../><typeHandlers .../><objectFactory .../><objectWrapperFactory .../><reflectorFactory .../><plugins .../><environments .../><databaseIdProvider .../><mappers .../>
</configuration>
4.3 测试
4.3.1 多对一的延迟加载查询演示
- 在AccountMapper接口中编写方法
public List<Account> findAll();
- 编写配置文件和SQL语句
<?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.tx.mapper.AccountMapper"><!-- 内连接的查询 --><select id="findAll" resultMap="accountMap">SELECT * from account</select><!-- 通过用户的id查询账户信息 --><select id="findByUid" parameterType="int" resultType="account">select * from account where uid = #{uid}</select><!-- 配置映射 --><resultMap type="Account" id="accountMap"><id column="id" property="id"/><result column="uid" property="uid"/><result column="money" property="money"/><!-- 配置延迟加载 --><association property="user" javaType="User" select="com.qcbyjy.mapper.UserMapper.findById" column="uid"><id column="id" property="id"/><result column="username" property="username"/><result column="birthday" property="birthday"/><result column="sex" property="sex"/><result column="addresss" property="addresss"/></association></resultMap></mapper>
- 在UserMapper接口中编写方法
public User findById(Integer uid);
- 编写配置文件
<select id="findById" parameterType="int" resultType="User">select * from user where id = #{id}</select>
- 在SqlMapConfig.xml配置文件中开启延迟加载的配置
<settings><!-- 开启延迟加载 --><setting name="lazyLoadingEnabled" value="true"/><!-- 将积极加载改为消极加载及按需加载 --><setting name="aggressiveLazyLoading" value="false"/></settings>
- 编写测试方法
/*** 测试延迟加载的测试方法*/// 1.测试多对一的延迟加载的测试方法@Testpublic void testfindAccountlazyALL(){//加载配置文件try {InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");//创建sqlSessionFactory工厂对象 使用对象创建SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);//通过工厂对象在获得sqlsession对象SqlSession session = factory.openSession();//SqlSession session = factory.openSession(true);//获取到代理对象,MyBatis框架生成的代理对象AccountMapper mapper = session.getMapper(AccountMapper.class);//mapper指向的就是代理对象List<Account> list = mapper.findAll();session.commit();for (Account account : list) {System.out.println(account.getMoney());System.out.println(account.getUser().getUsername());System.out.println("=============================");System.out.println("");}//关闭资源session.close();inputStream.close();} catch (Exception e) {e.printStackTrace();}}
4.3.2 一对多的延迟加载查询演示
- 在UserMapper中编写方法
public interface UserMapper {public List<User> findAll();}
- 在UserMapper.xml配置文件中编写配置和SQL语句
<?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.tx.mapper.UserMapper"><!-- 一对多的查询 --><select id="findAll" resultMap="userMap">select * from user</select><!-- 数据封装 --><resultMap type="user" id="userMap"><id column="id" property="id"/><result column="username" property="username"/><result column="birthday" property="birthday"/><result column="sex" property="sex"/><result column="addresss" property="addresss"/><!-- select="" 使用帐户的方法查询column="id" 使用id值去查询账户--><collection property="accounts" ofType="Account" select="cn.tx.mapper.AccountMapper.findByUid" column="id" ><id column="id" property="id"/><result column="uid" property="uid"/><result column="money" property="money"/></collection></resultMap></mapper>
- 在AccountMapper接口中编写方法
public List<Account> findByUid(Integer uid);
- 在AccountMapper.xml配置文件中编写配置和SQL语句
<!-- 通过用户的id查询账户信息 --><select id="findByUid" parameterType="int" resultType="account">select * from account where uid = #{uid}</select>
- 在SqlMapConfig.xml配置文件中开启延迟加载的配置
<settings><!-- 开启延迟加载 --><setting name="lazyLoadingEnabled" value="true"/><!-- 将积极加载改为消极加载及按需加载 --><setting name="aggressiveLazyLoading" value="false"/></settings>
- 编写测试方法
// 2.测试一对多的延迟加载的测试方法@Testpublic void testfindUserlazyALL(){//加载配置文件try {InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");//创建sqlSessionFactory工厂对象 使用对象创建SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);//通过工厂对象在获得sqlsession对象SqlSession session = factory.openSession();//SqlSession session = factory.openSession(true);//获取到代理对象,MyBatis框架生成的代理对象UserMapper mapper = session.getMapper(UserMapper.class);//mapper指向的就是代理对象List<User> list = mapper.findAll();session.commit();for (User user : list) {System.out.println(user.getUsername());System.out.println(user.getAccounts().size());System.out.println("=============================");System.out.println("");}//关闭资源session.close();inputStream.close();} catch (Exception e) {e.printStackTrace();}}
五、MyBatis 缓存机制:提升查询性能
缓存在内存中临时存储数据,速度快,可以减少数据库的访问次数;经常需要查询,不经常修改的数据,不是特别重要的数据都适合于存储到缓存中。
5.1 一级缓存(SqlSession 缓存)
- 作用域:基于
SqlSession
对象,默认开启。 - 原理:首次查询后将结果存入
SqlSession
的本地缓存,相同查询(相同 SQL 与参数)再次执行时直接从缓存获取。 - 触发更新:调用
update
/insert
/delete
或commit
/close
时清空缓存。
示例:验证一级缓存
/*** 演示一级缓存的存在*/
public class Test03 {/*** 证明一级缓存的存在* 一级缓存底层是Map集合* Map<k,v> k指SQL语句 v指查询出来的值*/@Testpublic void run1() throws IOException {//加载配置文件InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");//通过流创建工厂对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);//获取到sqlsession对象,没有设置事务的方式,默认手动提交SqlSession sqlSession = factory.openSession();//获取代理对象UserMapper mapper = sqlSession.getMapper(UserMapper.class);//调用方法 通过主键查询//先查询一级缓存,没有数据。会查数据库,都会有SQL语句,把查询出的数据存储到一级缓存中User user = mapper.findById(1);//打印地址System.out.println(user);System.out.println("==================================");//手动清空缓存//如果没有清空缓存,在执行第一条结束后,sql语句将会存放到mybatis提供的缓存中,两条查询语句将会执行一条;//若清空缓存,则将执行两条sql语句sqlSession.clearCache();//再查询一次//先查询一级缓存,存在数据。从缓存中把数据返回,就没有SQL语句User user1=mapper.findById(1);//这样两个user对象地址一样System.out.println(user1);sqlSession.close();inputStream.close();}
}
5.2 二级缓存(全局缓存)
- 作用域:基于
SqlSessionFactory
,跨SqlSession
共享缓存。 - 配置步骤:
- 开启全局缓存(
SqlMapConfig.xml
):<settings><setting name="cacheEnabled" value="true"/> <!-- 开启二级缓存 --> </settings>
- 在 Mapper 中配置缓存:
<mapper namespace="UserMapper"><!-- 开启二级缓存使用 --><cache/><!-- 配置缓存策略 <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/> --><select id="findById" resultType="User" useCache="true">SELECT * FROM user WHERE id = #{id}</select> </mapper>
- 实体类实现序列化接口:
public class User implements Serializable { /* ... */ }
- 开启全局缓存(
5.3 缓存策略对比
缓存类型 | 作用域 | 时效性 | 适用场景 |
---|---|---|---|
一级缓存 | SqlSession | 会话周期内有效 | 单会话内重复查询 |
二级缓存 | SqlSessionFactory | 全局有效(需手动配置) | 跨会话重复查询、读多写少场景 |
六、总结:MyBatis 核心能力与最佳实践
- 连接池选择:生产环境优先使用
POOLED
连接池,提升连接复用效率。 - 动态 SQL 原则:
- 复杂条件拼接用
if
+where
,集合查询用foreach
。 - 提取公用 SQL 片段,避免重复编码。
- 复杂条件拼接用
- 关联映射策略:
- 多对一/一对一用
association
,一对多/多对多用collection
。 - 大数据量场景优先使用延迟加载,减少初始查询压力。
- 多对一/一对一用
- 缓存优化:
- 合理使用一级缓存(默认开启,无需额外配置)。
- 二级缓存适用于高频查询、低频更新场景,需注意实体类序列化与缓存策略配置。
通过掌握上述核心技术,可在实际项目中高效使用 MyBatis 完成复杂数据操作,平衡性能与开发效率。