Mybatis框架专辑
在学习 MyBatis 程序之前,需要了解一下 MyBatis 工作原理,以便于理解程序。MyBatis 的工作 原理如下图:
- 1. 读取 MyBatis 配置文件:mybatis-confifig.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。
- 2. 加载映射文件:映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-confifig.xml 中加载。mybatis-confifig.xml 文件可以加载多个映射文 件,每个文件对应数据库中的一张表。
- 3. 构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
- 4. 创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
- 5. Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
- 6. MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参 数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
- 7. 输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。
- 8. 输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。
一、Mybatis能做啥?
(1)Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。
(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
(3)通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。
二、Mybatis的优点
(1)基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
(2)与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
(3)很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
(4)能够与Spring很好的集成;
(5)提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
Mybatis动态sql有什么用?执行原理?有哪些动态sql?
Mybatis动态sql可以在Xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值完成逻辑判断并动态拼接sql的功能。
Mybatis提供了9种动态sql标签:trim | where | set | foreach | if | choose | when | otherwise |bind。
三、Mybatis的缺点
(1)SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
(2)SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
四、MyBatis与Hibernate有哪些不同?
Hibernate和Mybatis都是orm对象关系映射框架,都是用于将数据持久化的框架技术。
hibernate是全自动,Hiberante较深度的封装了jdbc,对开发者写sql的能力要求的不是那么的高,我们只要通过hql语句操作对象即可完成对数据持久化的操作了。另外hibernate可移植性好,如一个项目开始使用的是mysql数据库,但是随着业务的发展,现mysql数据库已经无法满足当前的绣球了,现在决定使用Oracle数据库,虽然sql标准定义的数据库间的sql语句差距不大,但是不同的数据库sql标准还是有差距的,那么我们手动修改起来会存在很大的困难,使用hibernate只需改变一下数据库方言即可搞定。用hibernate框架,数据库的移植变的非常方便。但是hibernate也存在着诸多的不足,比如在实际开发过程中会生成很多不必要的sql语句耗费程序资源,优化起来也不是很方便,且对存储过程支持的也不够太强大。但是针对于hibernate它也提供了一些优化策略,比如说懒加载、缓存、策略模式等都是针对于它的优化方案。
而mybatis是半自动,Mybatis 也是对jdbc的封装,但是封装的没有hibernate那么深,我们可以再配置文件中写sql语句,可以根据需求定制sql语句,数据优化起来较hibernate容易很多。
Mybatis要求程序员写sql的能力要相对使用hibernate的开发人员要高的多,且可移植性也不是很好。
涉及到大数据的系统使用Mybatis比较好,因为优化较方便。涉及的数据量不是很大且对优化没有那么高,可以使用hibernate。
hibernate拥有完整的日志系统,mybatis则欠缺一些。
五、当实体类中的属性名和表中的字段名不一样 ,怎么办 ?
第1种: 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
<select id=”selectorder” parametertype=”int” resultetype=”me.gacl.domain.order”>
select order_id id, order_no orderno,order_price price form orders where order_id=#{id};
</select>
第2种: 通过<resultMap>来映射字段名和实体类属性名的一一对应的关系。
<select id="getOrder" parameterType="int" resultMap="orderresultmap">
select * from orders where order_id=#{id}
</select>
<resultMap type=”me.gacl.domain.order” id=”orderresultmap”>
<!–用id属性来映射主键字段–>
<id property=”id” column=”order_id”>
<!–用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性–>
<result property = “orderno” column =”order_no”/>
<result property=”price” column=”order_price” />
</reslutMap>
六、通常一个Xml映射文件,都会写一个Dao接口与之对应,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?
<?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.gefeng.dao.UserDao">
<!-- 查询用户 -->
<select id="getUserList" resultType="user">
SELECT * FROM user
</select>
<!-- 查询所有用户 -->
<select id="getUserById" parameterType="int" resultType="user">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 新增用户 -->
<insert id="addUser" parameterType="user">
INSERT INTO user (id, name, phone) VALUES (#{id}, #{name}, #{phone})
</insert>
<!-- 修改用户 -->
<update id="updateUser" parameterType="user">
UPDATE user SET name=#{name}, phone=#{phone} WHERE id=#{id}
</update>
<!-- 删除用户 -->
<delete id="deleteUserById" parameterType="int">
DELETE FROM user WHERE id=#{id}
</delete>
</mapper>
public interface UserDao {
List<User> getUserList();
User getUserById(int id);
void addUser(User user);
void updateUser(User user);
void deleteUserById(int id);
}
Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。
Mapper接口没有实现类,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个<select>、<insert>、<update>、<delete>标签,都会被解析为一个MapperStatement对象。
举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面 id 为 findStudentById 的 MapperStatement。
Mapper接口里的方法不能重载,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,转而执行MapperStatement所代表的sql,然后将sql执行结果返回。
Statement 和 PreparedStatement 有什么区别?
与 Statement 相比,①PreparedStatement 接口代表预编译的语句,它主要的优
势在于可以减少 SQL 的编译错误并增加 SQL 的安全性(减少 SQL 注射攻击的可
能性);②PreparedStatement 中的 SQL 语句是可以带参数的,避免了用字符串
连接拼接 SQL 语句的麻烦和不安全;③当批量处理 SQL 或频繁执行相同的查询时,
PreparedStatement 有明显的性能上的优势,由于数据库可以将编译优化后的
SQL 语句缓存起来,下次执行相同结构的语句时就会很快(不用再次编译和生成
执行计划)。
七、Mybatis是如何进行分页的?分页插件的原理是什么?
有两种分页方法:
- 使用Map来进行包装数据实现分页功能
- 使用RowBounds来实现分页
7.1 使用Map来进行包装数据实现分页功能
SQL映射
<!--查询所有的用户信息,用map分页实现-->
<select id="getAllMap" resultType="User" parameterType="Map">
SELECT * FROM user limit #{startIndex},#{pageSize}
</select>
DAO实现类
//这个是实现分页查询功能(用map来实现的第一种方式)
public List<User> getAll(int currentPage,int pageSize) throws IOException {
SqlSession sqlSession = MybatisUtil.getSession();
Map<String,Integer> map = new HashMap<String, Integer>();
map.put("startIndex",(currentPage-1)*pageSize);
map.put("pageSize",pageSize);
List<User> list = sqlSession.selectList("UserMapper.getAllMap",map);
sqlSession.close();
return list;
}
测试类
public static void main(String[] args) throws IOException {
UserDao userDao = new UserDao();
//这个传进来的第一个参数是你要显示第几页的数据,第二是你需要没页显示几条记录
List<User> list = userDao.getAll(2, 3);
for (User user : list) {
System.out.println(user.toString());
}
}
7.2 使用RowBounds来实现分页
SQL的xml映射
<!--查询所有用户的信息,用RowBounds来实现-->
<select id="getAllRowBounds" resultType="User">
SELECT *FROM user
</select>
DAO实现类
//这个是通过RowBounds来实现查询功能的分页操作
public List<User> getAllRowBounds(int currentPage,int pageSize) throws IOException {
SqlSession sqlSession = MybatisUtil.getSession();
/*rowBounds需要的第一个参数就是从数据的哪个下标开始开始查,第二个就是你需要查询的条数*/
RowBounds rowBounds= new RowBounds((currentPage-1)*pageSize,pageSize);
List<User> list = sqlSession.selectList("UserMapper.getAllRowBounds",
null, rowBounds);
sqlSession.close();
return list;
}
测试类
public class TestRowBounds {
public static void main(String[] args) throws IOException {
UserDao userDao = new UserDao();
List<User> list = userDao.getAllRowBounds(1, 3);
for (User user : list) {
System.out.println(user.toString());
}
}
}
逻辑分页(RowBounds)实现原理:目前 MyBatis 提供了基于逻辑分页实现机制,其实现原理是在执行分页查询时会将所有的记录都查询出来,然后根据 RowBounds 设置的 limit 和 offset 参数从记录中提取想要的数据,这样存在的弊端是,一次查询所有的数据对于数据库的性能是有影响的。
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。mybatis 逻辑分页和物理分页的区别:物理分页速度上并不一定快于逻辑分页,逻辑分页速度上也并不一定快于物理分页。物理分页总是优于逻辑分页:没有必要将属于数据库端的压力加诸到应用端来。
【分页插件的基本原理:】使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
不修改对象的代码,怎么对对象的行为进行修改,比如说在原来的方法前面做一点事情,在原来的方法后面做一点事情?大家很容易能想到用代理模式,这个也确实是MyBatis 插件的原理。我们可以定义很多的插件,那么这种所有的插件会形成一个链路,比如我们提交一个休假申请,先是项目经理审批,然后是部门经理审批,再是HR 审批,再到总经理审批,怎么实现层层的拦截?插件是层层拦截的,我们又需要用到另一种设计模式——责任链模式。
八、Mybatis是如何将sql执行结果封装为目标对象并返回的?
第一种是使用<resultMap>标签,逐一定义数据库列名和对象属性名之间的映射关系。
第二种是使用sql列的别名功能,将列的别名书写为对象属性名。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
九、如何执行批量插入?
方法一:xml配置。
最基础的是用mapping.xml配置的方式,包括以下两种具体方式:
(1)mapping.xml中insert语句可以写成单条插入,在调用方循环1000次
<!-- 在外部for循环调用1000次 -->
<insert id="insert" parameterType="com.xxp.mybatis.Person">
insert into person (id, name,sex,address)
values
(#{id,jdbcType=INTEGER},#{name,jdbcType=VARCHAR},
#{sex,jdbcType=VARCHAR},#{address,jdbcType=VARCHAR})
</insert>
(2)mapping.xml中insert语句写成一次性插入一个1000的list
<insert id="insertBatch" >
insert into person ( <include refid="Base_Column_List" /> )
values
<foreach collection="list" item="item" index="index" separator=",">
(null,#{item.name},#{item.sex},#{item.address})
</foreach>
</insert>
mapper接口中的使用:
public interface TabMapper {
public List<Tab> getTabsByConditionLike(@Param("list")List<Integer> ids);
}
方法二:注解
注解说明:
MyBatis提供用于插入数据的注解有两个:@insert,@InsertProvider,类似还有:@DeleteProvider@UpdateProvider,和@SelectProvider,
作用:
用来在实体类的Mapper类里注解保存方法的SQL语句
区别:
@Insert是直接配置SQL语句,而@InsertProvider则是通过SQL工厂类及对应的方法生产SQL语句,这种方法的好处在于,我们可以根据不同的需求生产出不同的SQL,适用性更好。
使用:
@Insert("insert into blog(blogId,title,author) values(#blogId,#title,#author)")
public boolean saveBlog(Blog blog);
@InsertProvider(type = SqlFactory.class,method = "insertBlog")
public boolean saveBlog(@Param("bean")Blog blog);
@InsertProvider(type = UrlBlackDAOProvider.class, method = "insertAll")
void batchSaveBlackList(@Param("list") List<UrlBlackInfo> blacklists);
说明:type指明SQL工厂类,method是工厂类里对应的方法
参数解释:
type为工厂类的类对象,method为对应的工厂类中的方法,方法中的@Param(“list”)是因为批量插入传入的是一个list,但是Mybatis会将其包装成一个map。其中map的key为“list”,value为传入的list。
十、如何获取自动生成的(主)键值?
在向数据库插入数据时,常常需要保留插入数据的id,以便进行后续的update操作或者将id存入其他表作为关联。
但 insert 方法总是返回一个int值 ,这个值代表的是插入的行数,并非表示主键id。
要返回ID有两种情况:
10.1 使用@Insert 注解时
@Insert("insert into file(create_at,update_at,file_name,file_url,file_md5) VALUES (#{create_at},#{update_at},#{file_name},#{file_url},#{file_md5})")
@Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id") //该注解用于返回主键 void save(File file);
@Options注解:
- useGeneratedKeys 指对于支持自动生成记录主键的数据库,如:MySQL,此时设置useGeneratedKeys参数值为true,在执行添加记录之后可以获取到数据库自动生成的主键ID
- keyProperty 指传入对象的成员变量的id属性
- keyColumn 指定数据库table中的主键
在调用了插入方法之后,@Options注解会自动为表的主键字段设置自增的值,并把它赋值给作为入参的POJO,进而可以直接从这个对象中获取新生成记录的主键。
10.2 使用 xml 时
<insert id="insert" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
insert into file (create_at, update_at, file_name, file_url, file_md5) values (#{create_at,jdbcType=BIGINT}, #{update_at,jdbcType=BIGINT}, #{file_name,jdbcType=VARCHAR}, #{file_url,jdbcType=VARCHAR}, #{file_md5,jdbcType=VARCHAR})
</insert>
说明就和 @Options注解 几乎一样了。
十一、 在mapper中如何传递多个参数?
(1)第一种:
//DAO层的函数
Public UserselectUser(String name,String area);
//对应的xml,#{0}代表接收的是dao层中的第一个参数,#{1}代表dao层中第二参数,更多参数一致往后加即可。
<select id="selectUser"resultMap="BaseResultMap">
select * fromuser_user_t whereuser_name = #{0} anduser_area=#{1}
</select>
(2)第二种: 使用 @param 注解:
public interface usermapper {
user selectuser(@param(“username”) string username,@param(“hashedpassword”) string hashedpassword);
}
然后,就可以在xml像下面这样使用(推荐封装为一个map,作为单个参数传递给mapper):
<select id=”selectuser” resulttype=”user”>
select id, username, hashedpassword
from some_table
where username = #{username}
and hashedpassword = #{hashedpassword}
</select>
(3)第三种:多个参数封装成map
try{
//映射文件的命名空间.SQL片段的ID,就可以调用对应的映射文件中的SQL
//由于我们的参数超过了两个,而方法中只有一个Object参数收集,因此我们使用Map集合来装载我们的参数
Map<String, Object> map = new HashMap();
map.put("start", start);
map.put("end", end);
return sqlSession.selectList("StudentID.pagination", map);
}catch(Exception e){
e.printStackTrace();
sqlSession.rollback();
throw e; }
finally{
MybatisUtil.closeSqlSession();
}
十二、Mybatis动态sql有什么用?执行原理?有哪些动态sql?
Mybatis动态sql可以在Xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值 完成逻辑判断并动态拼接sql的功能。Mybatis提供了9种动态sql标签:trim | where | set | foreach | if | choose | when | otherwise | bind。
十三、Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?
<resultMap>、<parameterMap>、<sql>、<include>、<selectKey>,加上动态sql的9个标签,其中<sql>为sql片段标签,通过<include>标签引入sql片段,<selectKey>为不支持自增的主键生成策略标签。
十四、Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?
不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;
原因就是namespace+id是作为Map<String, MapperStatement>的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。
但是,在以前的Mybatis版本的namespace是可选的,不过新版本的namespace已经是必须的了。
十五、Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
mybatis的延迟加载就是按需查询,在需要的时候进行查询。
有两张表:
图书表(book):
图书类型表(category):
他们之间通过类型id进行关联,现在我要显示图书类型名,点击类型名再显示该类型下的所有图书。一次性的把图书类型和图书查询出来,Sql语句如下:
SELECT book.*,cname FROM book,category WHERE book.cid = category.cid
这样做可以完成功能,但是我们只是需要显示图书类型,点击的时候才显示该类型的图书,如果能做到开始只查询类型,点击类型的时候再查询该类型的图书,就不需要进行两表联查了,可以提高查询的效率,也比较节省内存,这就是延迟加载。
延迟加载如何实现?
1. Category实体类同上
public class Category {
private int cid;
private String cname;
private List<Book> books;
//省略get set
}
2. UserDao.xml
<mapper namespace="cn.xh.dao.UserDao">
<select id="findCategoryWithLazingload" resultMap="categoryMap">
select * from category
</select>
<resultMap id="categoryMap" type="cn.xh.pojo.Category">
<id column="cid" property="cid"></id>
<result column="cname" property="cname"></result>
<collection property="books" column="cid" select="findBookWithLazy"></collection>
</resultMap>
<select id="findBookWithLazy" parameterType="int" resultType="cn.xh.pojo.Book">
select * from book where cid = #{cid}
</select>
</mapper>
只有我们点击类型的时候才需要查询该类型下的图书,所以这里我们没有用两表联查,而是将类型表的查询语句和图书表的查询语句分开。重点来看下这个配置:
<collection property="books" column="cid" select="findBookWithLazy"></collection>
collection,association是支持延迟加载的,这里的select属性表示要执行的sql语句,column表示执行sql语句要传的参数,该参数为select * from category查询出来的字段cid,property=”books”表示查询出来的结果给到books属性。
3. 在mybatis的核心配置文件中配置
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"></setting>
</settings>
注意,这个配置必须写在properties配置的后面,typeAliases的前面。将lazyLoadingEnabled设置为true表示开启延迟加载,默认为false;将aggressiveLazyLoading设置为false表示按需加载,默认为true。
4. 测试
@Test
public void testDo(){
SqlSession session = sqlSessionFactory.openSession();
UserDao u = session.getMapper(UserDao.class);//用动态代理的方式自动生成接口的实现类
List<Category> lst = u.findCategoryWithLazingload();
for (Category c : lst) {
System.out.println(c.getCname());
}
session.close();
}
执行看看日志:
这段代码只涉及到了图书类型,并未涉及到图书部分,所以只执行了select * from category从类型表中查询出类型信息。再来测试这段代码:
@Test
public void testDo1(){
SqlSession session = sqlSessionFactory.openSession();
UserDao u = session.getMapper(UserDao.class);//用动态代理的方式自动生成接口的实现类
List<Category> lst = u.findCategoryWithLazingload();
for (Category c : lst) {
System.out.println(c.getCname());
}
List<Book> lstBook = lst.get(0).getBooks();
session.close();
}
这里执行了两个sql语句:
Select * from category
Select * from book where cid = ?
对比这两段代码,可以看到, 只有当执行List<Book> lstBook = lst.get(0).getBooks();这行代码的时候才会去执行sql语句Select * from book where cid = ?。
这就是延迟加载里的按需执行sql语句,只有在需要的时候才会去执行。
回顾一下第三个步骤中的配置:
此时 lazyLoadingEnabled设置为true, aggressiveLazyLoading设置为false,表示延迟加载开启,按需加载也开启。分析一下在这种配置下代码的每一步都做了什么:
1.List<Category> lst = u.findCategoryWithLazingload();执行到这行代码的时候从数据库中查询图书类型的信息。
2.System.out.println(c.getCname());执行这行代码的时候因为图书类型信息已经被查询出来,所以不需要再和数据库交互。
3.List<Book> lstBook = lst.get(0).getBooks();执行这行代码的时候从数据库中查询该类型下图书的信息。
如果将lazyLoadingEnabled设置为true, aggressiveLazyLoading设置为true,表示延迟加载开启,按需加载关闭,代码每一步都做了什么呢?
1.List<Category> lst = u.findCategoryWithLazingload();执行到这行代码的时候从数据库中查询图书类型的信息。
2.System.out.println(c.getCname());执行这行代码的时候,需要加载图书类型的属性“类型名”,因为将按需加载关闭,所以此时会把Category的所有属性都加载进来,包括List<Book> books,会去数据库中查询图书的信息。
3.List<Bosok> lstBook = lst.get(0).getBooks();执行这行代码的时候因为图书的信息已经被加载进来,不需要查询数据库。
如果将lazyLoadingEnabled设置为false,相当于关闭了延迟加载,此时无论aggressiveLazyLoading是true还是false都会在执行,List<Category> lst = u.findCategoryWithLazingload();的时候将类型和图书的信息都查询出来。
总结一下:
一:延迟加载就是按需加载,在需要查询的时候再去查询,使用延迟加载可以避免表连接查询,表连接查询比单表查询的效率低,但是它需要多次与数据库进行交互,所以延迟加载并不是银弹,使用需谨慎。
二:关于延迟加载有两个重要的设置:lazyLoadingEnabled表示延迟加载是否开启,如果设置为true表示开启,此时还需要设置aggressiveLazyLoading为false,才能做到按需加载,如果aggressiveLazyLoading设置为true则按需加载关闭,此时只要加载了某个属性就会将所有属性都加载。
lazyLoadingEnabled的默认值为false
aggressiveLazyLoading的默认值为true
原理:
延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定方法,执行数据加载。
生成代理对象,对象方法调用时执行查询语句。
使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。
十六、Mybatis组件的生命周期
1、 SqlSessionFactoryBuilder
2、 SqlSessionFactory
3、 SqlSession
4、 Mapper
十六、Mybatis的一级、二级缓存
先说缓存,合理使用缓存是优化中最常见的,将从数据库中查询出来的数据放入缓存中,下次使用时不必从数据库查询,而是直接从缓存中读取,避免频繁操作数据库,减轻数据库的压力,同时提高系统性能。
一级缓存
一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构用于存储缓存数据。不同的sqlSession之间的缓存数据区域是互相不影响的。也就是他只能作用在同一个sqlSession中,不同的sqlSession中的缓存是互相不能读取的。
一级缓存的工作原理:
1)用户发起查询请求,查找某条数据,sqlSession先去缓存中查找,是否有该数据,如果有,读取;
2)如果没有,从数据库中查询,并将查询到的数据放入一级缓存区域,供下次查找使用。
但sqlSession执行commit,即增删改操作时会清空缓存。这么做的目的是避免脏读。如果commit不清空缓存,会有以下场景:A查询了某商品库存为10件,并将10件库存的数据存入缓存中,之后被客户买走了10件,数据被delete了,但是下次查询这件商品时,并不从数据库中查询,而是从缓存中查询,就会出现错误。
二级缓存
既然有了一级缓存,那么为什么要提供二级缓存呢?
二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。二级缓存的作用范围更大。
还有一个原因,实际开发中,MyBatis通常和Spring进行整合开发。Spring将事务放到Service中管理,每个service中的sqlsession是不同的,这是通过mybatis-spring中的org.mybatis.spring.mapper.MapperScannerConfigurer创建sqlsession自动注入到service中的。 每次查询之后都要进行关闭sqlSession,关闭之后数据被清空。所以spring整合之后,如果没有事务,一级缓存是没有意义的。
二级缓存原理:
每个namespace的mapper都有一个二级缓存区域,两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同的二级缓存区域中。
开启二级缓存:
1,打开总开关,在MyBatis的配置文件中加入:
<settings>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
2,在需要开启二级缓存的mapper.xml中加入caceh标签
<cache/>
3,让使用二级缓存的POJO类实现Serializable接口
public class User implements Serializable {}
测试一下:
@Test
public void testCache2() throws Exception {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = userMapper1.findUserById(1);
System.out.println(user1);
sqlSession1.close();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.findUserById(1);
System.out.println(user2);
sqlSession2.close();
}
DEBUG [main] - Cache Hit Ratio [com.iot.mybatis.mapper.UserMapper]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 103887628.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@631330c]
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [id=1, username=张三, sex=1, birthday=null, address=null]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@631330c]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@631330c]
DEBUG [main] - Returned connection 103887628 to pool.
DEBUG [main] - Cache Hit Ratio [com.iot.mybatis.mapper.UserMapper]: 0.5
User [id=1, username=张三, sex=1, birthday=null, address=null]
从打印的信息看出,两个sqlSession,去查询同一条数据,只发起一次select查询语句,第二次直接从Cache中读取。
但不能滥用二级缓存,二级缓存也有很多弊端,从MyBatis默认二级缓存是关闭的就可以看出来。二级缓存是建立在同一个namespace下的,如果对表的操作查询可能有多个namespace,那么得到的数据就是错误的。
举个简单的例子:订单和订单详情,orderMapper、orderDetailMapper。在查询订单详情时我们需要把订单信息也查询出来,那么这个订单详情的信息被二级缓存在orderDetailMapper的namespace中,这个时候有人要修改订单的基本信息,那就是在orderMapper的namespace下修改,他是不会影响到orderDetailMapper的缓存的,那么你再次查找订单详情时,拿到的是缓存的数据,这个数据其实已经是过时的。
根据以上,想要使用二级缓存时需要想好两个问题:
- 1)对该表的操作与查询都在同一个namespace下,其他的namespace如果有操作,就会发生数据的脏读。
- 2)对关联表的查询,关联的所有表的操作都必须在同一个namespace。
MyBatis 的二级缓存是和命名空间绑定的,所以通常情况下每一个 Mapper 映射文件都拥有 自己的二级缓存,不同 Mapper 的二级缓存互不影响。在常见的数据库操作中,多表联合查询非常常见,由于关系型数据库的设计, 使得很多时候需要关联多个表才能获得想要的数据。在关联多表查询时肯定会将该查询放到某个命名空间下的映射文件中,这样一个多表的查询就会缓 存在该命名空间的二级缓存中。涉及这些表的增、删、改操作通常不在一个映射文件中,它们 的命名空间不同, 因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生 脏数据。
在以下场景中,推荐使用二级缓存。
1. 以查询为主的应用中,只有尽可能少的增、删、改操作;
2. 绝大多数以单表操作存在时,由于很少存在互相关联的情况,因此不会出现脏数据。
十七、什么是MyBatis的接口绑定?有哪些实现方式?
接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。
接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定;另外一种就是通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。
十八、Mybatis的插件运行原理,以及如何编写一个插件。
Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,当然,只会拦截那些你指定需要拦截的方法。(动态代理参见:代理模式(静态代理模式、动态代理模式、cgLib代理模式、拦截器)_沙滩的流沙520的博客-CSDN博客)
写一个插件,分三步完成
1.编写Intercepror接口的实现类
2.设置插件的签名,告诉mybatis拦截哪个对象的哪个方法
3.最后将插件注册到全局配置文件中
// 插件签名,告诉mybatis当前插件拦截哪个对象的哪个方法
// type表示要拦截的目标对象,method表示要拦截的方法,args表示要拦截方法的参数
@Intercepts({
@Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
})
public class MySecondPlugin implements Interceptor {
// 拦截目标对象的目标方法执行
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MySecondPlugin拦截目标对象:"+invocation.getTarget()+"的目标方法:"+invocation.getMethod());
/*
* 插件的主要功能:在执行目标方法之前,可以对sql进行修改已完成特定的功能
* 例如增加分页功能,实际就是给sql语句添加limit;还有其他等等操作都可以
* */
// 执行目标方法
Object proceed = invocation.proceed();
// 返回执行后的返回值
return proceed;
}
// 包装目标对象:为目标对象创建代理对象
@Override
public Object plugin(Object target) {
System.out.println("MySecondPlugin为目标对象"+target+"创建代理对象");
// this表示当前拦截器,target表示目标对象,wrap方法利用mybatis封装的方法为目标对象创建代理对象(没有拦截的对象会直接返回,不会创建代理对象)
Object wrap = Plugin.wrap(target, this);
return wrap;
}
// 设置插件在配置文件中配置的参数值
@Override
public void setProperties(Properties properties) {
System.out.println("MySecondPlugin配置的参数:"+properties);
}
}
编写插件:实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住在配置文件中配置你编写的插件。
<plugins>
<plugin interceptor="com.mybatis_demo.plugin.MySecondPlugin"></plugin>
</plugins>
十九、Mybatis都有哪些Executor执行器?它们之间的区别是什么?
Statement对象用于执行不带参数的简单SQL语句。
Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。
SimpleExecutor:简单执行器,是 MyBatis 中默认使用的执行器,每执行一次 update 或 select,就开启一个 Statement 对象,用完就直接关闭 Statement 对象(可以是 Statement 或者是 PreparedStatment 对象)
ReuseExecutor:可重用执行器,这里的重用指的是重复使用 Statement,它会在内部使用一个 Map 把创建的 Statement 都缓存起来,每次执行 SQL 命令的时候,都会去判断是否存在基于该 SQL 的 Statement 对象,如果存在 Statement 对象并且对应的 connection 还没有关闭的情况下就继续使用之前的 Statement 对象,并将其缓存起来。每个SqlSession 都有一个新的 Executor 对象,所以我们缓存在 ReuseExecutor 上的Statement 作用域是同一个 SqlSession。简言之,就是重复使用Statement对象。
BatchExecutor:批处理执行器,用于将多个SQL一次性输出到数据库,它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。
作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。
Mybatis中如何指定使用哪一种Executor执行器?
答:在Mybatis配置文件中,可以指定默认的ExecutorType执行器类型,也可以手动给DefaultSqlSessionFactory的创建SqlSession的方法传递ExecutorType类型参数。
二十、Mybatis 的编程步骤
A.创建 SqlSessionFactory
B.通过 SqlSessionFactory 创建 SqlSession
C.通过 sqlsession 执行数据库操作
D.调用 session.commit()提交事务
E.调用 session.close()关闭会话
二十一、Mybatis为什么只有mapper接口没有实现类?
mybatis的mapper接口主要应用了JDK 动态代理技术。在Service层,通过@Autowired xxxUserMapper xxxDao注入属性时,返回的就是它的代理类。执行 xxxDao.findById 的方法的时候,实际调用的是代理类的invoke方法,invoke方法中会加载xml中的sql完成操作数据库,再返回结果。
二十二、同一个方法,Mybatis 多次请求数据库,是否要创建多个 SqlSession 会话?
同一个方法,Mybatis 多次请求数据库且没有事务的情况下,创建了多个 SqlSession 会话!
在 testSqlSession 方法上加上 @Transactional 注解:
在有事务的情况下,同一个方法,Mybatis 多次请求数据库,只创建了一个 SqlSession 会话!
如果有事务,并且方法内存在多个线程的情况下:
在有事务的情况下,同一个方法内,有多个线程 Mybatis 多次请求数据库的情况下,创建了两个 SqlSession 会话
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!