MyBatis面试题
什么是MyBatis
- MyBatis是一款基于Java的持久层框架,它提供了一种简单的方式来映射数据库操作到Java对象。它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态SQL,可以严格控制SQL执行性能,灵活度高
- MyBatis可以使用XML或注解来配置映射原生信息,将POJO映射成数据库中的记录,避免了几乎所有JDBC代码和手动设置参数以获取结果集
- 通过XML文件或注解的方式将要执行的各种statement配置起来,并通过Java对象和statement中的SQL的动态参数进行映射生成最终执行的SQL语句,最后由MyBatis框架执行SQL并将结果映射为Java对象并返回
说说MyBatis的优点和缺点
- 优点
- 基于SQL语句编程,相当灵活,不会对应用程序或数据库的现有设计造成任何影响,SQL写在XML里,解除SQL与程序代码的耦合(用注解写就另说了),便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用
- 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接
- 提供应设标签,支持对象与数据库的ORM字段关系映射,提供对象关系映射标签,支持对象关系组件维护
- 缺点
- SQL语句编写的工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求
- SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库
- XML配置较为繁琐,MyBatis的配置文件中需要编写大量的XML代码来描述SQL语句和映射关系,这可能会使配置文件显得较为繁琐
#{}
和${}
的区别是什么
- 在MyBatis中,
#{}
和${}
都是用来表示参数占位符的。不过它们的使用方式略有不同#{}
在SQL语句中表示一个占位符,它可以防止SQL注入攻击,并且可以自动进行参数类型转换。在执行SQL语句时,MyBatis会将参数值以安全的方式设置到SQL语句中。底层使用的是PreparedStatement
,#{}
占位符替换为?
${}
在SQL语句中也表示一个占位符,但它不会对参数进行任何处理,直接将参数值拼接到SQL语句中,因此容易引发SQL注入攻击。底层使用的是Statement
当实体类中的属性名和表中的字段名不一致时怎么办
- 通过在查询的SQL语句中定义字段名的别名,让其别名与实体类的属性名一致
1 | <select id=”selectorder” parametertype=”int” resultetype=”com.example.domain.order”> |
- 使用resultMap定义字段和属性的映射关系
1 | <select id="getOrder" parameterType="int" resultMap="orderresultmap"> |
MyBatis是如何进行分页的?分页插件的原理是什么
- MyBatis使用RowBounds对象进行分页,它是针对ResultSet结果及执行的内存分页,而非物理分页。可以在SQL内直接拼写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页,例如在原有SQL后面拼写limit
- 分页插件的基本原理是使用MyBatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的SQL,然后重写SQL,根据dialect方言,添加对应的物理分页语句和物理分页参数
MyBatis是如何将SQL执行结果封装为目标对象并返回的?都有哪些映射形式
- MyBatis将SQL查询结果封装为目标对象并返回,主要通过以下两个步骤完成
- 执行SQL查询语句,获取ResultSet结果集
- 根据目标对象的映射方式,将ResultSet结果集中的数据映射到目标对象中
- MyBatis支持以下几种映射方式
- 使用SQL列的别名功能,将列名的别名命名为对象的属性名
1
SELECT user_name as userName FROM user WHERE user_id = #{userId}
- resultMap映射:指定目标对象和ResultSet结果集中各列之间的映射关系
1
2
3
4
5<resultMap id="userMap" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<result property="age" column="user_age"/>
</resultMap>
如何执行批量插入
- 首先创建一个简单的insert语句
1 | <insert id=”insertname”> |
- Java代码中实现批量插入
1 | List<String> names = new ArrayList<>(); |
XML映射文件中,除了常见的select、insert、update、delete标签外,还有哪些标签
<resultMap>
:用于定义查询结果与Java对象的映射关系,可以自定义属性的映射关系,包括一对一、一对多等复杂映射关系<sql>
:用于定义可重用的SQL片段,可以将SQL片段抽象出来,供多个SQL语句使用<include>
:用于包含其他XML文件中定义的SQL片段,可以实现SQL的复用<if>
:用于在SQL语句中添加条件判断,可以根据参数动态生成SQL语句<where>
:用于将多个条件组合成一个WHERE子句,避免生成无用的WHERE子句<foreach>
:用于遍历集合或数组,并将元素拼接成SQL语句中的IN子句<choose>
、<when>
、<otherwise>
:用于实现复杂的条件判断,类似Java中的if-else语句<trim>
:用于对SQL语句进行字符串处理,如去除逗号、括号等
MyBatis实现一对一有几种方式?具体怎么操作的
- MyBatis实现一对一有以下两种方式
- 使用
<resultMap>
标签定义一对一映射关系- 例如,我们有一个Order类和一个User类,一个订单只属于一个用户,因此Order类中包含一个User对象作为属性。在XML映射问建中,我们可以定义一个
<resultMap>
标签,定义Order类与User类之间的映射关系
1
2
3
4
5
6<resultMap id="orderMap" type="Order">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="userId" column="user_id"/>
<association property="user" javaType="User" select="getUserById"/>
</resultMap>- 在上面的
标签中,我们使用 标签定义Order类中的user属性与User类之间的映射关系。其中,property属性指定Order类中的user属性,javaType属性指定User类的类型,select属性指定通过getUserById查询用户信息的SQL语句。
- 例如,我们有一个Order类和一个User类,一个订单只属于一个用户,因此Order类中包含一个User对象作为属性。在XML映射问建中,我们可以定义一个
- 使用嵌套查询
- 使用嵌套查询可以在查询订单的同时查询用户信息,将查询结果组装成一个Order对象,例如我们在XML映射文件中定义以下SQL语句
1
2
3
4
5
6<select id="getOrderById" resultMap="orderMap">
select o.id, o.name, o.user_id, u.username, u.phone, u.address
from orders o
join users u on o.user_id = u.id
where o.id = #{id}
</select>- 在上面的SQL语句中,我们通过join语句关联了
orders
表和users
表,并查询了订单和用户的信息。在<select>
标签中,我们通过resultMap
属性指定了将查询结果映射到Order对象上的<resultMap>
标签。在<resultMap>
标签中,我们通过<association>
标签定义了Order
对象和User
对象之间的映射关系。
- 使用
MyBatis是否支持延迟加载?如果支持,它的实现原理是什么
- MyBatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的是一对一,collection指的是一对多查询。在MyBatis配置文件中,可以配置是否启用延迟加载:
lazyLoadingEnable=true|false
- MyBatis实现延迟加载的原理是使用代理对象,代理对象在访问对象时触发延迟加载,从而实现按需加载
1 | <configuration> |
- 在MyBatis中,延迟加载的实现主要有两种方式
- 延迟加载嵌套查询:在XML映射文件中,我们可以通过使用
select
标签定义一个嵌套查询,当需要使用到延迟加载对象时,MyBatis会根据该嵌套查询获取相关的数据。例如,以下代码演示了如何通过嵌套查询实现延迟加载1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="name" column="order_name"/>
<association property="user" javaType="User" select="getUserById" fetchType="lazy"/>
</collection>
</resultMap>
<select id="getUserById" resultMap="userMap">
select u.id, u.name, o.id as order_id, o.name as order_name, o.user_id as user_id
from users u
left join orders o on o.user_id = u.id
where u.id = #{id}
</select>- 在上面的XML映射文件中,我们定义了一个
User
对象和一个Order
对象之间的一对多关系,使用collection
标签定义User
对象中的order
属性,其中通过<association>
标签的fetchType=lazy
属性实现了延迟加载
- 在上面的XML映射文件中,我们定义了一个
- 延迟加载代理对象:它的原理是,通过CGLIB动态代理生成了一个目标对象的代理对象。当我们调用代理对象的方法时,代理对象内部的拦截器方法会先被执行,这个拦截器会检查当前对象是否已经加载了关联对象。如果没有加载,拦截器会执行预先保存好的查询语句,从数据库中查询关联对象的数据,并将查询结果映射为Java对象。接着,代理对象的拦截器会将查询到的关联对象设置到目标对象中,然后继续执行我们最初调用的方法,以便返回正确的结果。
- 举个例子:假设我们有一个User对象,它包含一个Order对象的引用。我们在访问User对象的getOrder()方法时,如果关联的Order对象还没有被加载,拦截器会执行查询语句,从数据库中查询Order对象的数据,然后拦截器会将查询到的Order对象设置到User对象中的order属性中。最后拦截器返回Order对象,让我们可以正常地范文它的属性或方法
- 这样我们就可以通过延迟加载来优化数据库访问,只有需要时才回去查询关联对象,而不是每次访问对象时都进行查询
- 延迟加载嵌套查询:在XML映射文件中,我们可以通过使用
说说MyBatis的缓存机制
- MyBatis的缓存机制主要分为一级缓存和二级缓存
- 一级缓存指的是SqlSession级别的缓存,它默认是开启的,同一个SqlSession内部的多次查询操作,如果查询的参数和查询语句都相同,那么第一次查询时查询结果会被缓存到一级缓存中,后续的查询操作会直接从缓存中获取结果,而不会再去查询数据库。一级缓存的生命周期与SqlSession的生命周期相同,当SqlSession被关闭时,一级缓存也会被清空
- 二级缓存指的是Mapper级别的缓存,它需要手动开启。开启二级缓存后,查询操作的结果会被缓存到内存或者其他缓存存储设备中,当下次再次查询相同的数据时,会优先从缓存中获取数据,而不是再次查询数据库。不同的SqlSession之间可以共享二级缓存,因此当多个SqlSession操作同一个Mapper时,他们之间共享一个二级缓存,二级缓存的缓存时间是无限制的,但是它的缓存策略是基于LRU(最近最少使用)算法的
- 需要注意的是,二级缓存中的缓存数据是需要被序列化和反序列化的,因此当我们在Mapper中使用自定义类型时,需要确保这些类型支持序列化操作
- 为了提高缓存命中率,MyBatis还提供了缓存清空机制,可以通过在Mapper中配置
flushCache="true"
来清空缓存。此外,MyBatis还支持基于注解的缓存配置,可以通过在Mapper接口或方法上添加@CacheNamespace
或者@CacheNamespaceRef
注解来配置缓存
- 当开启缓存后,数据的查询执行流程为:二级缓存 -> 一级缓存 -> 数据库
- 但是MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻
- 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis等分布式缓存可能成本更低,安全性也更高
JDBC编程有哪些步骤
- JDBC是Java数据库连接的标准API,用于连接和操作关系型数据库。JDBC编程的基本步骤如下
- 加载数据库驱动程序。使用
Class.forName()
方法加载对应的数据库驱动程序,如Class.forName("com.mysql.jdbc.Driver")
加载MySQL数据库驱动程序 - 创建数据库连接,使用
DriverManager.getConnection()
方法创建数据库连接,需要指定数据库连接字符串、用户名、密码等连接信息,例如
1
Connection conn = DirverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "username", "password");
- 创建Statement或者PreparedStatement对象,用于执行SQL语句
1
2Connection.createStatement();
Connection.prepareStatement();- 执行SQL语句,使用Statement或PreparedStatement对象的
execute()
、executeQuery()
、executeUpdate()
等方法执行SQL语句 - 处理查询结果,如果执行的是查询语句,需要使用ResultSet对象获取查询结果
- 关闭ResultSet、Statement、Connection独享。释放占用的资源,防止出现内存泄漏
- 加载数据库驱动程序。使用
MyBatis中见过什么设计模式
- 工厂模式:MyBatis中的SqlSessionFactory和SqlSession都是通过工厂模式创建的,SqlSessionFactoryBuilder类的作用就是创建SqlSessionFactory对象。
- 代理模式:MyBatis中的Mapper接口实现类是通过JDK动态代理实现的。在执行Mapper接口中的方法时,实际上是通过动态代理生成一个代理对象,代理对象调用方法是会自动执行相应的SQL语句
- 装饰器模式:MyBatis中的插件就是通过装饰器模式实现的,插件可以在不修改原有代码的基础上,增强或修改原有功能
- 享元模式:MyBatis中的缓存就是使用了享元模式,通过缓存已经查询过的数据,避免重复查询数据库
- 观察者模式:MyBatis中的拦截器就是使用了观察者模式,拦截器可以在SQL执行前后进行一些额外操作
MyBatis中例如UserMapper.java是接口,为什么没有实现类还能调用
- 在MyBatis中,Mapper接口是由MyBatis框架动态生成的。当我们定义一个Mapper接口时,MyBatis会根据接口的定义和XML映射文件中的配置信息,动态生成一个接口的实现类,并将其注册到Spring容器中,供应用程序使用
- MyBatis使用了Java动态代理技术来生成Mapper接口的实现类。当应用程序调用Mapper接口中的方法时,实际上是调用了Mapper接口的代理对象的方法。代理对象会根据方法名和参数类型等信息,动态生成相应的SQL语句,并将其执行。由于动态代理是在运行时生成的,因此不需要Mapper接口的实现类
- 这种通过接口定义方法,而不需要手动实现的方式,成为面向接口编程。它可以降低代码的耦合度,使得应用程序更加灵活和可扩展
评论