1. Mybatis概述
MyBatis 是一个优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects, 普通的 Java 对象) 映射成数据库中的记录。
特点
-
SQL 编写的灵活性:MyBatis 允许开发者编写原生 SQL 语句,提供了极大的灵活性。
-
支持映射复杂类型:MyBatis 支持将数据库结果集的一行或多行映射到 Java 对象。
-
支持延迟加载:MyBatis 支持延迟加载,可以配置懒加载,提高应用程序性能。
-
支持注解和 XML 配置:MyBatis 支持使用注解或 XML 配置 SQL 映射。
-
支持多数据库:MyBatis 可以与多种数据库一起使用,例如 MySQL、PostgreSQL、Oracle 等。
-
支持事务管理:MyBatis 提供了对事务的细粒度控制。
-
支持缓存:MyBatis 支持内置的一级缓存和可以配置的二级缓存。
-
支持动态 SQL:MyBatis 支持动态 SQL 语句,可以根据条件构建 SQL 语句。
-
支持批量操作:MyBatis 支持批量插入、更新和删除操作。
-
支持映射文件的复用:MyBatis 允许开发者在多个映射文件中复用 SQL 语句。
-
支持插件:MyBatis 允许开发者通过插件扩展其功能。
-
支持分页查询:MyBatis 可以很容易地实现分页查询。
2. Mybatis执行流程
-
初始化:
加载配置文件,创建SqlSessionFactory
。 -
获取 SqlSession:
通过SqlSessionFactory
获取SqlSession
对象,该对象包含了对数据库的操作方法。 -
执行操作:
通过SqlSession
执行增删改查操作。MyBatis 会将接口和 XML 映射文件中的 SQL 语句进行动态代理,调用对应的方法时,会执行相应的 SQL。 -
提交事务:
如果是自动提交事务,MyBatis 会在每次操作后自动提交事务。如果不是自动提交事务,需要手动调用SqlSession
的commit
方法提交事务。 -
获取结果:
操作数据库后,可以通过SqlSession
获取操作结果。 -
关闭 SqlSession:
操作完成后,需要关闭SqlSession
以释放数据库资源。 -
错误处理:
在执行过程中如果发生异常,MyBatis 会回滚事务。
3. Mybatis基础操作
3.1 快速开始
-
创建SpringBoot项目
-
引入依赖
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
-
在
application.properties
中配置数据库连接参数spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/db01 spring.datasource.username=root spring.datasource.password=root1234
-
创建实体类
Person
,并创建相应的数据库表public class Person { private Integer id; private String name; private Integer age; // getters and setters }
-
创建
PersonController
@Controller public class PersonController { @Autowired private PersonMapper personMapper; @GetMapping("/find") @ResponseBody public List<Person> find() { List<Person> list = personMapper.findAll(); System.out.println(list); return list; } }
-
创建
com.codingfuture.mapper.PersonMapper
接口@Mapper public interface PersonMapper { List<Person> find(); }
-
resources
中添加映射文件com.codingfuture.mapper.PersonMapper.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.codingfuture.mapper.PersonMapper"> <select id="find" resultType="com.codingfuture.entity.Person"> select * from person; </select> </mapper>
-
测试
3.2 statement详解
Mybatis的核心是SQL,一个statement代表着一个SQL,因此,statement的配置即是我们通过Mybatis操作数据库的核心。
statement分为四个标签:<insert>
、<delete>
、<update>
、<select>
,分别代表对数据的增删改查。
标签中定义的便是原生的SQL语句,需要新掌握的是标签上的属性:
-
id
每个statement都有,且为必选属性,
id
为statement提供唯一标识,以保证该statement可以被成功定位并执行。不能重复。 -
resultType
只有select语句有该属性,代表SQL返回结果的类型,查询用户可以指定为
entity.Person
类型比如查询数据总条数,可以指定
resultType
为long类型,<select id="findAll" resultType="com.codingfuture.entity.Person"> select * from person </select> <select id="total" resultType="long"> select count(*) from person </select>
-
parameterType
如果SQL中需要传入参数,则可以通过该属性指定参数类型,如果不指定,Mybatis会自动判断,因此该属性没有实质作用。
<select id="findAllById" parameterType="int" resultType="com.codingfuture.entity.Person"> select * from person where id = #{id} </select>
-
resultMap
只有select语句有,当SQL结果集不能自动映射到实体类属性时使用,
比如数据库字段为
person_id
,而Person
类中属性为personId
,此时Mybatis不能自动映射,需要手动映射。以下为实例,
id
标签指定主键属性,result
标签指定普通属性,column
属性对应表中字段名,property
属性对应类中属性名,autoMapping
自动映射。可选<resultMap id="personResultMap" type="com.codingfuture.entity.Person"> <id column="id" property="id"/> <!-- <result column="name" property="name"/>--> <!-- <result column="age" property="age"/>--> <result column="love_color" property="loveColor"/> </resultMap> <select id="findAll3" resultMap="personResultMap"> select * from person </select>
-
insert插入
<insert id="insert"> insert into person(id,name,age,love_color) values (null,"zl",26,"紫色") </insert>
-
update更新
<update id="updateById"> update person set name ='kobe12' where id = 203 </update>
-
delete删除
<delete id="deleteById"> delete from person where id = 204 </delete>
3.3 注解
常见注解
- @Select: 用于查询操作。
- @Insert: 用于插入操作。
- @Update: 用于更新操作。
- @Delete: 用于删除操作。
- @Results: 用于定义结果映射。
- @Result: 用于定义单列映射。
- @Param: 用于指定参数名称。
- @SelectKey: 用于生成主键。
- @Options: 用于设置一些全局性的操作选项。
参数占位符
在 MyBatis 中,参数占位符通常用于 SQL 语句中,以便在执行时动态替换为实际的参数值。以下是一些常见的参数占位符:
#{}
: 预处理参数,防止 SQL 注入。MyBatis 会将参数转义,确保 SQL 的安全性。${}
: 直接将参数内容拼接到 SQL 中,可能会导致 SQL 注入风险。@Param
: 用于指定参数名称,可以在 SQL 中通过名称引用参数。
以下是直接在Mapper层编写sql,适用于简单操作,实际使用多在xml映射文件中编写 :
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User selectUserById(int id);
@Insert("INSERT INTO users(name, email) VALUES(#{name}, #{email})")
int insertUser(@Param("name") String name, @Param("email") String email);
@Update("UPDATE users SET name = #{name} WHERE id = #{id}")
int updateUser(@Param("name") String name, @Param("id") int id);
@Delete("DELETE FROM users WHERE id = #{id}")
int deleteUser(int id);
@Select("SELECT * FROM users")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "name", column = "name"),
@Result(property = "email", column = "email")
})
List<User> findAllUsers();
}
4. 动态SQL
MyBatis 是一个优秀的持久层框架,它支持动态 SQL,允许开发者在 SQL 语句中根据条件动态地拼接不同的 SQL 片段。
-
<where>:
<where>
标签用于动态地构建 SQL 的 WHERE 条件部分。它会自动处理语句的开头(不添加 "WHERE"),并且会智能地处理 AND 或 OR 的添加,以避免在生成的 SQL 中出现多余的 AND 或 OR。 -
<if>:
<if>
标签用于条件判断,只有当测试表达式的值为 true 时,包含的 SQL 片段才会被包含在最终的 SQL 中。它常用于简单的条件判断。 -
<foreach>:
<foreach>
标签用于遍历一个集合,并为每个元素生成 SQL 片段。它通常用于 IN 子句或批量操作(如批量插入、更新、删除)。 -
<set>:
<set>
标签用于动态构建 UPDATE 语句中的 SET 部分。它会智能地处理逗号和生成的 SQL,确保不会出现多余的逗号。 -
<choose>、<when>、<otherwise>:
这三个标签一起使用,提供了类似于编程语言中的 if-else 语句的功能。<choose>
标签表示一个条件选择块的开始。<when>
标签表示一个条件分支,只有当测试表达式的值为 true 时,包含的 SQL 片段才会被包含在最终的 SQL 中。<otherwise>
标签表示其他情况下的 SQL 片段,类似于编程中的 else 分支。 -
<sql>:
<sql>
标签用于定义可重用的 SQL 片段,可以在不同的映射器中通过<include>
标签引用。 -
<include>:
<include>
标签用于引入定义在其他 XML 文件中的 SQL 片段,这有助于代码的重用和模块化。 -
<bind>:
<bind>
标签用于创建一个变量,通常用于复杂的表达式,以提高 SQL 的可读性和维护性。 -
<trim>:
<trim>
标签用于去除 SQL 片段前后的指定字符,如空格,或者用于拼接多个 SQL 片段,同时去除前后的分隔符。
代码示例
-
<where> 示例:
<select id="selectUsers" resultType="User"> SELECT * FROM users <where> <if test="id != null"> id = #{id} </if> <if test="username != null and username != ''"> AND username = #{username} </if> <if test="status != null"> AND status = #{status} </if> </where> </select>
-
<if> 示例:
<select id="selectUsersIf" resultType="User"> SELECT * FROM users WHERE <if test="id != null"> id = #{id} </if> <if test="username != null and username != ''"> AND username = #{username} </if> </select>
-
<foreach> 示例:
<select id="selectUsersIn" resultType="User"> SELECT * FROM users WHERE id IN <foreach item="id" collection="list" open="(" separator="," close=")"> #{id} </foreach> </select>
-
<set> 示例:
<update id="updateUser"> UPDATE users <set> <if test="username != null">username = #{username},</if> <if test="password != null">password = #{password},</if> <if test="email != null">email = #{email}</if> </set> WHERE id = #{id} </update>
-
<choose>、<when>、<otherwise> 示例:
<select id="selectUser" resultType="User"> SELECT * FROM users WHERE <choose> <when test="id != null"> id = #{id} </when> <when test="username != null and username != ''"> username = #{username} </when> <otherwise> active = 1 </otherwise> </choose> </select>
-
<sql> 和 <include> 示例:
<!-- UserMapper.xml --> <sql id="userColumns"> id, username, password, email </sql> <select id="selectUsers" resultType="User"> SELECT <include refid="userColumns"/> FROM users </select>
-
<bind> 示例:
<select id="selectUsersBind" resultType="User"> SELECT * FROM users WHERE <bind name="sortedUsername" value="'%' + username + '%'"/> username LIKE #{sortedUsername} </select>
-
<trim> 示例:
<select id="selectUsersTrim" resultType="User"> SELECT * FROM users WHERE <trim suffixOverrides="AND"> <if test="id != null"> id = #{id} AND </if> <if test="username != null and username != ''"> username = #{username} AND </if> </trim> </select>
5. 多表映射
5.1 需求一
需求:查询订单信息,关联查询购买该订单的用户信息。
需求:查询订单信息,关联查询购买该订单的用户信息。
SELECT * FROM `order` o, `user` u WHERE o.user_id = u.id;
多表查询涉及到重名的字段,可以通过sql中的别名解决:
SELECT
o.id order_id,
o.create_time create_time,
o.user_id user_id,
u.username username,
u.PASSWORD PASSWORD
FROM
`order` o,
`user` u
WHERE o.user_id = u.id
查询出正确且合适的结果集后,就可以进行映射的配置了:
User
类。用户信息
public class User {
private Integer id;
private String username;
private String password;
//getter setter
}
Order
类。订单
public class Order {
private Integer id;
private Date createTime;
private Integer userId;
//private User user;//后加 代表订单所属用户
//getter setter
}
Item
类。商品
public class Item {
private Integer id;
private String name;
private Double price;
//getter setter
}
OrderItem
类。订单明细
public class OrderItem {
private Integer id;
private Integer orderId;
private Integer itemId;
private Integer num;
//getter setter
}
创建OderMapper接口和OrderMapper.xml
public interface OrderMapper {
List<Order> findOrdersWithUser();
}
<resultMap id="OrderUserMap" type="order" autoMapping="true">
<id column="order_id" property="id"/>
<result column="create_time" property="createTime"/>
<result column="user_id" property="userId"/>
<association property="user" javaType="user" autoMapping="true">
<id column="user_id" property="id"/>
<!-- <result column="username" property="username"/>-->
<!-- <result column="password" property="password"/>-->
</association>
</resultMap>
<select id="findOrdersWithUser" resultMap="OrderUserMap">
SELECT
o.id order_id,
o.create_time create_time,
o.user_id user_id,
u.username username,
u.PASSWORD PASSWORD
FROM
`order` o,
`user` u
WHERE
o.user_id = u.id
</select>
<association>
代表单一的关联,在多对一. 或. 一对一的关系中使用,注意需要指定javaType
属性。
测试类:
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
List<Order> ordersWithUser = mapper.findOrdersWithUser();
for (Order order : ordersWithUser) {
System.out.println(order);
}
注意:
我们想偷个懒,标注红线的地方的映射我们想省略,
那就需要加一个配置,并添加autoMapping属性设置为true
mybatis.configuration.map-underscore-to-camel-case=true # 下划线转驼峰
5.2 需求二
需求:查询订单信息,关联查询它的订单明细信息。
SELECT * FROM`order` o,order_item oi WHERE o.id = oi.order_id
整理之后的sql语句:
SELECT
o.id order_id,
o.create_time create_time,
o.user_id user_id,
oi.id order_item_id,
oi.item_id item_id,
oi.num num
FROM
`order` o,
order_item oi
WHERE
o.id = oi.order_id
Order
类
public class Order {
private Integer id;
private Date createTime;
private Integer userId;
private User user;// 代表订单所属用户
private List<OrderItem> orderItems;//代表订单明细信息
//getter setter toString
}
OrderMapper.xml映射文件配置
<resultMap id="OrderOrderItemMap" type="order" autoMapping="true">
<id column="order_id" property="id"/>
<!-- <result column="create_time" property="createTime"/>-->
<!-- <result column="user_id" property="userId"/>-->
<collection property="orderItems" ofType="orderItem" autoMapping="true">
<id column="order_item_id" property="id"/>
<!-- <result column="order_id" property="orderId"/>-->
<!-- <result column="item_id" property="itemId"/>-->
<!-- <result column="num" property="num"/>-->
</collection>
</resultMap>
<select id="findOrdersWithOrderItems" resultMap="OrderOrderItemMap">
SELECT
o.id order_id,
o.create_time create_time,
o.user_id user_id,
oi.id order_item_id,
oi.item_id item_id,
oi.num num
FROM
`order` o,
order_item oi
WHERE
o.id = oi.order_id
</select>
public interface OrderMapper {
List<Order> findOrdersWithOrderItems();
}
测试:
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
List<Order> ordersWithUser = mapper.findOrdersWithOrderItems();
for (Order order : ordersWithUser) {
System.out.println(order);
}
<collection>
代表集合的关联,在一对多的关系中使用,注意需要通过ofType
属性指定集合泛型的类型。
6. 缓存
6.1 缓存简介
Mybatis包含一个非常强大的查询缓存特性,它可以非常方便配置和定制。缓存可以极大的提升查询效率。
例如:每个用户登入的页面的菜单功能选项都是固定的,点击每个选项都需要去数据库中查询数据,那么对于所有用户来说,数据都是一样的,那么我们就没必要每次点击菜单功能选项都去查询数据库,那样效率会很低,用户很多的时候,数据库服务器负担就会很严重。
所以我们就需要用到缓存。
Mybatis的查询分为:
-
一级缓存指的是sqlsession级别的。(本地缓存)
-
一级缓存只会在同一个sqlsession之间共享,多个sqlsession之间的一级缓存是互相独立的,默认一直开启,没法关闭。
-
与数据库同一次会话期间查询到的数据会放在本地缓存。以后如果需要获取相同数据,直接从缓存中拿,没必要再去查数据库。
-
-
二级缓存指的是mapper(namespace)级别的。(全局缓存)
-
二级缓存只会在同一个namespace下的mapper映射文件之间共享。
-
6.2 一级缓存
Mybatis默认支持一级缓存。
-
第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
-
如果sqlSession去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
-
第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。
@Configuration
public class SqlSessionConfig {
@Autowired
private DataSource dataSource;
@Bean
@Primary
public SqlSessionFactory sqlSession() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
SqlSessionFactory factory = null;
try {
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().
getResources("classpath:mapper/*Mapper.xml"));//设置对应的xml文件地址
factory = sqlSessionFactoryBean.getObject();
} catch (Exception e1) {
e1.printStackTrace();
}
return factory;
}
}
//一级缓存
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
Order order1 = mapper.findByIdWithUserLazy(1);//查询用户id为1的用户信息
System.out.println(order1.getUser());//获取User的用户信息
Order order2 = mapper.findByIdWithUserLazy(1);
System.out.println(order2.getUser());
结果:
一级缓存失效:
-
sqlSession不同:相同数据在sqlSession域中会被共享,不同sqlSession域之间数据不会共享
-
sqlSession相同:查询条件不同,(一级缓存中还没有这个要查询的数据)
-
sqlSession相同:两次查询之间,增加了 CRUD操作(有可能CRUD会影响当前数据)
-
sqlSession相同:手动清除了一级缓存数据,(缓存清空)
代码:
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Test
public void test4() throws IOException {
SqlSession sqlSession = sqlSessionFactory.openSession(true);//true自动提交
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);//true自动提交
//1.分别获取不同的sqlSession对象
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
Order order1 = mapper.findByIdWithUserLazy(1);
System.out.println(order1.getUser());//获取User的用户信息
OrderMapper mapper2 = sqlSession1.getMapper(OrderMapper.class);
Order order2 = mapper.findByIdWithUserLazy(1);
System.out.println(order2.getUser());//获取User的用户信息
//2.
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
Order order1 = mapper.findByIdWithUserLazy(1);//sql语句相同,但是 查询条件不同,
Order order2 = mapper.findByIdWithUserLazy(3);//域中没有id为3的 用户信息
System.out.println(order1.getUser());//获取User的用户信息
System.out.println(order2.getUser());//获取User的用户信息
//3.
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
Order order1 = mapper.findByIdWithUserLazy(1);//执行操作是一样的
System.out.println(order1.getUser());//获取User的用户信息
//执行修改操作
//mapper.updateOrdersWithUser(1);
Order order2 = mapper.findByIdWithUserLazy(1);//执行操作是一样的
System.out.println(order2.getUser());//获取User的用户信息 就是修改之后的
//4
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
Order order1 = mapper.findByIdWithUserLazy(1);//执行操作是一样的
System.out.println(order1.getUser());//获取User的用户信息
sqlSession.clearCache();//清空一级缓存
Order order2 = mapper.findByIdWithUserLazy(1);//执行操作是一样的
System.out.println(order2.getUser());//获取User的用户信息
}
1.
2.
3.
4.
6.3 二级缓存
二级缓存指的是mapper(namespace)级别的。一个namespace对应一个二级缓存
工作机制:
-
一个会话,查询一条数据,这个数据会被放在当前会话的一级缓存中
-
如果会话关闭,一级缓存中的数据会被保存到二级缓存中,新的会话查询信息,可以参考二级缓存
-
sqlSession--->
不同namespace查出的数据会放在自己的对应缓存中(map)
使用:
-
开启全局二级缓存配置:
<!--默认是开启的--> mybatis.configuration.cache-enabled=true
-
开启mapper级别的缓存开关,在对应的
OrderMapper.xml
中添加缓存开关<cache></cache> <!-- 里面的参数属性 作为了解,如果什么都不配置,就是默认如下配置--> <!-- <cache eviction="FIFO" flushInterval="60000" readOnly="false" size="5"></cache> --> <!-- eviction:缓存的回收策略: • LRU – 最近最少使用的:移除最长时间不被使用的对象。 • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。 • SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。 • WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。 • 默认的是 LRU。 flushInterval:缓存刷新间隔 缓存多长时间清空一次,默认不清空,设置一个毫秒值 readOnly:是否只读: true:只读;mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。 mybatis为了加快获取速度,直接就会将数据在缓存中的引用交给用户。不安全,速度快 false:非只读:mybatis觉得获取的数据可能会被修改。 mybatis会利用序列化&反序列的技术克隆一份新的数据给你。安全,速度慢 size:缓存存放多少元素; -->
-
Pojo类需要序列化,并定义UID
不是说因为学习了缓存我们才实现序列化,是因为POJO类都是实现数据持久化交互的
public class Order implements Serializable { private static final long serialVersionUID = 4829831994525772316L; }
-
测试:我们把二级缓存配置注释。测试
@Test public void test5() throws IOException { InputStream inputStream = Resources.getResourceAsStream("mybatis.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(true);// SqlSession sqlSession1 = sqlSessionFactory.openSession(true);// OrderMapper mapper = sqlSession.getMapper(OrderMapper.class); OrderMapper mapper1 = sqlSession1.getMapper(OrderMapper.class); Order order = mapper.findByIdWithUserLazy(1); System.out.println(order.getUser());//获取User的用户信息 sqlSession.close();//关闭会话 Order order1 = mapper1.findByIdWithUserLazy(1); System.out.println(order1.getUser());//获取User的用户信息 sqlSession1.close();//关闭会话 }
结果:会发现我们当关闭一个sqlSession时,另一个sqlSession就需要在此请求sql语句,发送2次请求
-
我们把二级缓存配置 激活,测试
会发现提示缓存命中率。Cache Hit Ratio。
证明我们发送二次请求的时候,是从二级缓存中拿的数据,并没有再次发送sql
注意:只有会话关闭了,该sqlSession数据才会跑到二级缓存中。