一、基于代理Dao实现CRUD操作
1. 根据ID查询用户信息
(1)在持久层接口中添加 findById 方法:
User findById(Integer userId);
(2)在映射文件中配置:
<!-- 根据 id 查询 -->
<select id="findById" resultType="com.itheima.domain.User" parameterType="int">
select * from user where id = #{uid}
</select>
resultType 属性:用于指定结果集的类型。
parameterType 属性:用于指定传入参数的类型。
#{uid}:用来接收传入的参数。
(3)在测试类中添加测试方法:
public class MybastisCRUDTest {
private InputStream in ;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;
@Test
public void testFindOne() {
//6.执行操作
User user = userDao.findById(41);
System.out.println(user);
}
@Before//在测试方法执行之前执行
public void init()throws Exception {
//1.读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3.创建 SqlSession 工厂对象
factory = builder.build(in);
//4.创建 SqlSession 对象
session = factory.openSession();
//5.创建 Dao 的代理对象
userDao = session.getMapper(IUserDao.class);
}
@After//在测试方法执行完成之后执行
public void destroy() throws Exception{
session.commit();
//7.释放资源
session.close();
in.close();
}
}
2. 新增用户信息
(1)在持久层接口中添加插入方法:
/**
* 保存用户
* @param user
* @return 影响数据库记录的行数
*/
int insertUser(User user);
(2)在映射文件中配置:
<!-- 新增用户-->
<insert id="insertUser" parameterType="com.itheima.domain.User">
insert into user(username,birthday,sex,address) values(#{username},#{birthday},# {sex},#{address})
</insert>
(3)在测试类中添加测试方法:
@Test
public void testSave(){
User user = new User();
user.setUsername("张三");
user.setAddress("北京市顺义区");
user.setSex("男");
user.setBirthday(new Date());
System.out.println("插入操作之前:"+user);
// 执行插入方法
userDao.insertUser(user);
System.out.println("插入操作之后:"+user);
}
/*
打开 MySQL 数据库发现并没有添加任何记录,原因是什么?这一点和 JDBC 是一样的,我们在实现增删改时一 定要去控制事务的提交,那么在 MyBatis 中如何控制事务提交呢?可以使用:session.commit();来实现事务提交。
*/
@After//在测试方法执行完成之后执行
public void destroy() throws Exception{
session.commit();
// 释放资源
session.close();
in.close();
}
3. 更新用户信息
(1)在持久层接口中添加更新方法:
/**
* 更新用户
* @param user
* @return 影响数据库记录的行数
*/
int updateUser(User user);
(2)在映射文件中配置:
<!-- 更新用户 -->
<update id="updateUser" parameterType="com.itheima.domain.User">
update user set username=#{username},birthday=#{birthday},sex=#{sex},address=# {address} where id=#{id}
</update>
(3)在测试类中添加测试方法:
@Test
public void testUpdateUser() throws Exception{
//1.根据 id 查询
User user = userDao.findById(52);
//2.更新操作
user.setAddress("北京市顺义区");
int res = userDao.updateUser(user);
System.out.println(res);
}
4. 删除用户信息
(1)在持久层接口中添加删除方法:
/**
* 根据 id 删除用户
* @param userId
* @return
*/
int deleteUser(Integer userId);
(2)在映射文件中配置:
<!-- 删除用户 -->
<delete id="deleteUser" parameterType="java.lang.Integer">
delete from user where id = #{uid}
</delete>
(3)在测试类中添加测试方法:
@Test
public void testDeleteUser() throws Exception {
int res = userDao.deleteUser(52);
System.out.println(res);
}
5. 模糊查询
(1)在持久层接口中添加模糊查询方法:
/**
* 根据名称模糊查询
* @param username
* @return
*/
List<User> findByName(String username);
(2)在映射文件中配置:
<!-- 根据名称模糊查询 -->
<select id="findByName" resultType="com.itheima.domain.User" parameterType="String">
select * from user where username like #{username}
</select>
(3)在测试类中添加测试方法:
@Test
public void testFindByName(){
List<User> users = userDao.findByName("%王%");
for(User user : users){
System.out.println(user);
}
}
6. 使用聚合函数查询
(1)在持久层接口中添加查询方法:
/**
* 查询总记录条数
* @return
*/
int findTotal();
(2)在映射文件中配置:
<!-- 查询总记录条数 -->
<select id="findTotal" resultType="int">
select count(*) from user;
</select>
(3)在测试类中添加测试方法:
@Test
public void testFindTotal() throws Exception {
int res = userDao.findTotal();
System.out.println(res);
}
二、MyBatis的动态 SQL 语句
1. < if >标签
我们根据实体类的不同取值,使用不同的 SQL 语句来进行查询。比如在 username 不为空时可以根据 username 查询,如果 address 不为空时还要加入 address 作为查询条件。这种情况在我们的多条件组合查询中经常会碰到。
(1)持久层Dao接口:
/**
* 根据用户信息,查询用户列表
* @param user
* @return
*/
List<User> findByUser(User user);
(2)映射配置文件:
<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>
【注】< if >标签的 test 属性中写的是对象的属性名。
(3)测试:
@Test
public void testFindByUser() {
User u = new User();
u.setUsername("%王%");
u.setAddress("%顺义%");
List<User> users = userDao.findByUser(u);
for(User user : users) {
System.out.println(user);
}
}
2. < where >标签
为了简化上面 where 1=1 的条件拼装,我们可以采用< where >标签来简化开发。
映射配置文件:
<!-- 根据用户信息查询 -->
<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>
3. < foreach >标签
(0)需求:
传入多个 id 查询用户信息,用下边两个 SQL 实现:
SELECT * FROM USERS WHERE username LIKE '%张%' AND (id =10 OR id =89 OR id=16)
SELECT * FROM USERS WHERE username LIKE '%张%' AND id IN (10,89,16)
这样我们在进行范围查询时,就要将一个集合中的值,作为参数动态添加进来。那么我们将如何进行参数的传递呢?
(1)在 QueryVo 中加入一个 List 集合用于封装参数:
public class QueryVo implements Serializable {
private List<Integer> ids;
public List<Integer> getIds() {
return ids;
}
public void setIds(List<Integer> ids) {
this.ids = ids;
}
}
(2)持久层Dao接口:
/**
* 根据 id 集合查询用户
* @param vo
* @return
*/
List<User> findInIds(QueryVo vo);
(3)映射配置文件:
<!-- 查询所有 id 在 id 集合之中的用户 -->
<select id="findInIds" resultType="user" parameterType="queryvo">
<include refid="defaultSql"></include>
<where>
<if test="ids != null and ids.size() > 0">
<foreach collection="ids" open="id in (" close=")" item="uid" separator=",">
#{uid}
</foreach>
</if>
</where>
</select>
< foreach >标签用于遍历集合,它的属性:
collection:代表要遍历的集合
open:代表where子句的开始部分
close:代表where子句的结束部分
item:代表遍历集合的每个元素,生成的变量名
sperator:代表分隔符
(4)测试:
@Test
public void testFindInIds() {
QueryVo vo = new QueryVo();
List<Integer> ids = new ArrayList<>();
ids.add(41);
ids.add(42);
ids.add(43);
ids.add(46);
ids.add(57);
vo.setIds(ids);
List<User> users = userDao.findInIds(vo);
for(User user : users) {
System.out.println(user);
}
}
4. < include >标签
SQL 中可将重复的 SQL 提取出来,使用时用 < include > 标签引用即可,最终达到 SQL 重用的目的。
(1)定义代码片段:
<!-- 抽取重复的代码片段 -->
<sql id="defaultSql">
select * from user
</sql>
(2)引用代码片段:
<!-- 查询所有用户信息 -->
<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>
三、MyBatis的多表查询
本次案例主要以最为简单的用户和账户的模型来分析 MyBatis 的多表关系。用户为 User 表,账户为Account表。一个用户(User)可以有多个账户(Account)。
注意:
因为一个账户信息只能供某个用户使用,所以从查询账户信息出发关联查询用户信息为一对一查询。如果从用户信息出发查询用户下的账户信息则为一对多查询,因为一个用户可以有多个账户。
1. 一对一查询
(0)需求:
查询所有账户信息,关联查询用户信息。
(1)修改 Account 类:
在 Account 类中加入 User 类的对象作为 Account 类的一个属性。
public class Account implements Serializable {
private Integer id;
private Integer uid;
private Double money;
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.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;
}
@Override
public String toString() {
return "Account [id=" + id + ", uid=" + uid + ", money=" + money + "]";
}
}
(2)持久层Dao接口:
public interface IAccountDao {
/**
* 查询所有账户,同时获取账户的所属用户名称以及他的地址信息
* @return
*/
List<Account> findAll();
}
(3)映射配置文件:
<?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.IAccountDao">
<!-- 建立对应关系 -->
<resultMap type="account" id="accountMap">
<id column="aid" property="id"/>
<result column="uid" property="uid"/>
<result column="money" property="money"/>
<!-- 它是用于指定从表方的引用实体属性的 -->
<association property="user" javaType="user">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="sex" property="sex"/>
<result column="birthday" property="birthday"/>
<result column="address" property="address"/>
</association>
</resultMap>
<select id="findAll" resultMap="accountMap">
select u.*,a.id as aid,a.uid,a.money from account a,user u where a.uid =u.id;
</select>
</mapper>
(4)测试:
@Test
public void testFindAll() {
List<Account> accounts = userDao.findAll();
for(Account au : accounts) {
System.out.println(au);
System.out.println(au.getUser());
}
}
2. 一对多查询
(0)需求:
查询所有用户信息及用户关联的账户信息。
(1)分析:
用户信息和他的账户信息为一对多关系,并且查询过程中如果用户没有账户信息,此时也要将用户信息查询出来,我们想到了用左外连接查询比较合适。
(2)编写SQL语句:
SELECT
u.*, acc.id, acc.uid, acc.money
FROM
user u
LEFT OUTER JOIN account acc ON u.id = acc.uid
(3)在User 类中加入 List< Account >:
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 List<Account> getAccounts() {
return accounts;
}
public void setAccounts(List<Account> accounts) {
this.accounts = 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;
}
@Override
public String toString() {
return "User [id=" + id + ", username=" + username + ", birthday=" + birthday+ ", sex=" + sex + ", address="+ address + "]";
}
}
(4)持久层Dao接口:
/**
* 查询所有用户,同时获取出每个用户下的所有账户信息
* @return
*/
List<User> findAll();
(5)映射配置文件:
<?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">
<resultMap type="user" id="userMap">
<id column="id" property="id"></id>
<result column="username" property="username"/>
<result column="address" property="address"/>
<result column="sex" property="sex"/>
<result column="birthday" property="birthday"/>
<!-- collection 是用于建立一对多中集合属性的对应关系
ofType 用于指定集合元素的数据类型
-->
<collection property="accounts" ofType="account">
<id column="aid" property="id"/>
<result column="uid" property="uid"/>
<result column="money" property="money"/>
</collection>
</resultMap>
<!-- 配置查询所有操作 -->
<select id="findAll" resultMap="userMap">
select u.*, a.id as aid , a.uid, a.money from user u left outer join account a on u.id = a.uid
</select>
</mapper>
collection部分定义了用户关联的账户信息,表示关联查询结果集。
属性 property 定义了关联查询的结果集存储在 User 对象的上哪个属性。
属性 ofType 定义了关联查询的结果集中的对象类型,即List中的对象类型。
(6)测试:
public class UserTest {
private InputStream in ;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;
@Test
public void testFindAll() {
//6.执行操作
List<User> users = userDao.findAll();
for(User user : users) {
System.out.println("-------每个用户的内容---------");
System.out.println(user);
System.out.println(user.getAccounts());
}
}
@Before//在测试方法执行之前执行
public void init() throws Exception {
//1.读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3.创建 SqlSession 工厂对象
factory = builder.build(in);
//4.创建 SqlSession 对象
session = factory.openSession();
//5.创建 Dao 的代理对象
userDao = session.getMapper(IUserDao.class);
}
@After//在测试方法执行完成之后执行
public void destroy() throws Exception{
session.commit();
//7.释放资源
session.close();
in.close();
}
}
四、MyBatis的延迟加载策略
1. 概念
延迟加载:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载.
好处:
先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度快。
坏处:
因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。
2. 需求
查询账户(Account)信息并且关联查询用户(User)信息。先查询账户(Account)信息即可满足要求,当我们需要查询用户(User)信息时再查询用户(User)信息。对用户(User)信息的按需查询就是延迟加载。
在使用MyBatis进行多表查询时,我们通过association、collection 实现一对一及一对多映射。association、collection 具备延迟加载功能。
3. 使用 assocation 实现延迟加载(一对一)
(0)需求:
查询账户信息的同时关联查询用户信息。
(1)账户的持久层Dao接口:
public interface IAccountDao {
/**
* 查询所有账户,同时获取账户的所属用户名称以及他的地址信息
* @return
*/
List<Account> findAll();
}
(2)账户的持久层映射文件:
<?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.IAccountDao">
<!-- 建立对应关系 -->
<resultMap type="account" id="accountMap">
<id column="aid" property="id"/>
<result column="uid" property="uid"/>
<result column="money" property="money"/>
<!--
select: 填写我们要调用的 select 映射的 id
column: 填写我们要传递给 select 映射的参数
-->
<association property="user" javaType="user"
select="com.itheima.dao.IUserDao.findById"
column="uid">
</association>
</resultMap>
<select id="findAll" resultMap="accountMap">
select * from account
</select>
</mapper>
(4)用户的持久层接口和映射文件:
public interface IUserDao {
/**
* 根据 id 查询
* @param userId
* @return
*/
User findById(Integer userId);
}
<?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">
<!-- 根据 id 查询 -->
<select id="findById" resultType="user" parameterType="int" >
select * from user where id = #{uid}
</select>
</mapper>
(5)开启 Mybatis 的延迟加载策略:
我们需要在 MyBatis 的配置文件 SqlMapConfig.xml 文件中添加延迟加载的配置。
<!-- 开启延迟加载的支持 -->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
(6)编写测试只查账户信息不查用户信息:
public class AccountTest {
private InputStream in ;
private SqlSessionFactory factory;
private SqlSession session;
private IAccountDao accountDao;
@Test
public void testFindAll() {
//6.执行操作
List<Account> accounts = accountDao.findAll();
}
@Before//在测试方法执行之前执行
public void init()throws Exception {
//1.读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3.创建 SqlSession 工厂对象
factory = builder.build(in);
//4.创建 SqlSession 对象
session = factory.openSession();
//5.创建 Dao 的代理对象
accountDao = session.getMapper(IAccountDao.class);
}
@After//在测试方法执行完成之后执行
public void destroy() throws Exception{
//7.释放资源
session.close();
in.close();
}
}
4. 使用 collection 实现延迟加载(一对多)
同样我们也可以在一对多关系配置的< collection >标签中配置延迟加载策略。< collection >结点中也有 select 属性和 column 属性。
(0)需求:
完成加载用户对象时,查询该用户所拥有的账户信息。
(1)在 User 实体类中加入 List< Account >属性:
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 List<Account> getAccounts() {
return accounts;
}
public void setAccounts(List<Account> accounts) {
this.accounts = 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;
}
@Override
public String toString() {
return "User [id=" + id + ", username=" + username + ", birthday=" + birthday
+ ", sex=" + sex + ", address="
+ address + "]";
}
}
(2)编写用户和账户持久层接口的方法:
/**
* 查询所有用户,同时获取出每个用户下的所有账户信息
* @return
*/
List<User> findAll();
/**
* 根据用户 id 查询账户信息
* @param uid
* @return
*/
List<Account> findByUid(Integer uid);
(3)用户的持久层映射文件:
<resultMap type="user" id="userMap">
<id column="id" property="id"></id>
<result column="username" property="username"/>
<result column="address" property="address"/>
<result column="sex" property="sex"/>
<result column="birthday" property="birthday"/>
<!-- collection 是用于建立一对多中集合属性的对应关系
ofType 用于指定集合元素的数据类型
select 是用于指定查询账户的唯一标识(账户的 dao 全限定类名加上方法名称)
column 是用于指定使用哪个字段的值作为条件查询
-->
<collection property="accounts" ofType="account"
select="com.itheima.dao.IAccountDao.findByUid"
column="id">
</collection>
</resultMap>
<!-- 配置查询所有操作 -->
<select id="findAll" resultMap="userMap">
select * from user
</select>
(4)账户的持久层映射文件:
<!-- 根据用户 id 查询账户信息 -->
<select id="findByUid" resultType="account" parameterType="int">
select * from account where uid = #{uid}
</select>
(5)测试只加载用户信息:
public class UserTest {
private InputStream in ;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;
@Test
public void testFindAll() {
//6.执行操作
List<User> users = userDao.findAll();
}
@Before//在测试方法执行之前执行
public void init() throws Exception {
//1.读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3.创建 SqlSession 工厂对象
factory = builder.build(in);
//4.创建 SqlSession 对象
session = factory.openSession();
//5.创建 Dao 的代理对象
userDao = session.getMapper(IUserDao.class);
}
@After//在测试方法执行完成之后执行
public void destroy() throws Exception{
session.commit();
//7.释放资源
session.close();
in.close();
}
}
五、MyBatis的缓存
像大多数的持久层框架一样,MyBatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。MyBatis 中的缓存分为一级缓存和二级缓存。
1. MyBatis的一级缓存
一级缓存是 SqlSession 范围的缓存,当调用 SqlSession 的修改,添加,删除,commit(),close()等方法时,就会清空一级缓存。
第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息,得到用户信息,将用户信息存储到一级缓存中。
第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。
如果 SqlSession 去执行 commit 操作(执行插入、更新、删除),则清空 SqlSession 中的一级缓存。这样做的目的为了让缓存中存储的都是最新的信息,避免脏读。
2. MyBatis的二级缓存
二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 SQL 语句,多个SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。
sqlSession1 去查询用户信息,查询到用户信息会将查询数据存储到二级缓存中。
sqlSession2 去查询与 sqlSession1 相同的用户信息,首先会去缓存中找是否存在数据,如果存在则直接从缓存中取出数据。
如果 sqlSession3 去执行相同 mapper 映射下的 SQL,执行 commit 提交,将会清空该 mapper 映射下的二级缓存区域的数据。