JDBC、MyBatis 、MyBatis-Plus面试总结(一)
以下为你整理了一些 MyBatis 和 MyBatis-Plus 中 mapper.xml
相关的常见面试问题及答案:
基础概念类
问题 1:什么是 mapper.xml
文件,它在 MyBatis 中有什么作用?
答案:mapper.xml
文件是 MyBatis 中用于定义 SQL 语句的配置文件。它将 SQL 语句与 Java 代码分离,使得 SQL 语句的管理和维护更加方便。在 mapper.xml
中可以定义各种 SQL 操作,如查询、插入、更新和删除等,通过映射关系将 SQL 执行结果映射到 Java 对象上。
问题 2:MyBatis-Plus 中为什么还需要 mapper.xml
文件?
答案:虽然 MyBatis-Plus 提供了很多内置的 CRUD 方法,可以减少编写 SQL 语句的工作量,但在以下情况下仍然需要使用 mapper.xml
文件:
- 复杂 SQL 场景:当需要编写复杂的 SQL 语句,如多表关联查询、复杂的嵌套查询、自定义的 SQL 逻辑等,MyBatis-Plus 内置方法无法满足需求时,就需要在
mapper.xml
中编写自定义 SQL。 - 性能优化:对于一些性能敏感的查询,开发人员可以在
mapper.xml
中手动优化 SQL 语句,以达到更好的性能。
配置与使用类
问题 3:如何在 mapper.xml
中定义一个简单的查询语句?
答案:以下是一个简单的查询示例,假设我们有一个 User
表,对应的 Java 实体类为 User
:
<mapper namespace="com.example.mapper.UserMapper">
<select id="selectUserById" resultType="com.example.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
在上述代码中,namespace
指定了该 mapper.xml
文件对应的 Mapper 接口的全限定名,select
标签用于定义查询语句,id
是该查询方法的唯一标识,resultType
指定了查询结果的映射类型。
问题 4:mapper.xml
中的 #{}
和 ${}
有什么区别?
答案:
#{}
:是预编译处理,MyBatis 在处理#{}
时,会将 SQL 中的#{}
替换为?
占位符,然后使用PreparedStatement
进行参数设置,这样可以有效防止 SQL 注入攻击。例如:SELECT * FROM user WHERE id = #{id}
。${}
:是字符串替换,MyBatis 在处理${}
时,会直接将${}
中的内容替换为传入的参数值,这种方式存在 SQL 注入风险。通常用于动态表名、动态列名等场景。例如:SELECT * FROM ${tableName}
。
问题 5:如何在 mapper.xml
中实现动态 SQL?
答案:MyBatis 提供了多种标签来实现动态 SQL,常见的有 <if>
、<choose>
、<when>
、<otherwise>
、<where>
、<set>
、<foreach>
等。
以下是一个使用 <if>
和 <where>
标签实现动态查询的示例:
<select id="selectUserByCondition" resultType="com.example.entity.User">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username = #{username}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
在上述代码中,<where>
标签会自动处理 SQL 语句中的 AND
和 OR
关键字,避免出现多余的 AND
或 OR
。<if>
标签用于根据条件判断是否拼接相应的 SQL 片段。
高级特性类
问题 6:mapper.xml
中如何实现关联查询和结果映射?
答案:可以使用 <resultMap>
标签来实现关联查询和结果映射。以下是一个简单的示例,假设我们有 User
和 Order
两个表,一个用户可以有多个订单:
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="UserResultMap" type="com.example.entity.User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="com.example.entity.Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="UserResultMap">
SELECT u.id AS user_id, u.username, o.id AS order_id, o.order_no
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
</select>
</mapper>
在上述代码中,<resultMap>
标签定义了查询结果的映射规则,<id>
标签用于映射主键,<result>
标签用于映射普通字段,<collection>
标签用于处理一对多的关联关系。
问题 7:如何在 mapper.xml
中使用存储过程?
答案:可以使用 <select>
或 <call>
标签来调用存储过程。以下是一个调用存储过程的示例:
<mapper namespace="com.example.mapper.UserMapper">
<select id="callProcedure" resultType="com.example.entity.User">
{call get_user_info(#{id, mode=IN, jdbcType=INTEGER})}
</select>
</mapper>
在上述代码中,{call get_user_info(...)}
表示调用名为 get_user_info
的存储过程,#{id, mode=IN, jdbcType=INTEGER}
表示传入一个输入参数 id
。
性能与优化类
问题 8:如何优化 mapper.xml
中的 SQL 语句以提高性能?
答案:可以从以下几个方面进行优化:
- 避免全表扫描:尽量使用索引,在 SQL 语句中避免使用
SELECT *
,只查询需要的字段。 - 合理使用分页:对于大数据量的查询,使用分页查询可以减少数据传输量,提高查询性能。
- 优化动态 SQL:避免在动态 SQL 中使用过多的条件判断,减少不必要的 SQL 拼接。
- 批量操作:对于插入、更新和删除操作,尽量使用批量操作,减少与数据库的交互次数。
问题 9:在 mapper.xml
中,如何处理大结果集以避免内存溢出?
答案:可以使用流式查询来处理大结果集。在 MyBatis 中,可以通过设置 fetchSize
属性来实现流式查询。例如:
<select id="selectLargeResult" resultType="com.example.entity.User" fetchSize="100">
SELECT * FROM user
</select>
在上述代码中,fetchSize="100"
表示每次从数据库中获取 100 条记录,避免一次性将所有记录加载到内存中。
缓存相关问题
问题 10:MyBatis 中 mapper.xml
如何配置一级缓存和二级缓存?它们有什么区别?
答案:
- 一级缓存配置:MyBatis 的一级缓存是默认开启的,不需要在
mapper.xml
中进行额外配置。一级缓存是基于 SqlSession 的,同一个 SqlSession 中执行相同的 SQL 查询时,会优先从缓存中获取结果,而不会再次执行 SQL 语句。 - 二级缓存配置:在
mapper.xml
中配置二级缓存,需要添加<cache>
标签,示例如下:
<mapper namespace="com.example.mapper.UserMapper">
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
<!-- 其他 SQL 语句 -->
</mapper>
其中,eviction
表示缓存的回收策略(如 LRU 最近最少使用),flushInterval
表示缓存刷新间隔,size
表示缓存对象的最大数量,readOnly
表示缓存是否只读。
区别:
- 作用域不同:一级缓存的作用域是 SqlSession,当 SqlSession 关闭时,一级缓存会被清空;二级缓存的作用域是 Mapper 接口,多个 SqlSession 可以共享同一个 Mapper 接口的二级缓存。
- 缓存范围不同:一级缓存只对同一个 SqlSession 内的相同查询有效;二级缓存对多个 SqlSession 内的相同查询都有效。
问题 11:如何在 mapper.xml
中手动控制缓存刷新?
答案:可以在 mapper.xml
中使用 <flushCache>
属性来手动控制缓存刷新。在 <select>
、<insert>
、<update>
、<delete>
标签中都可以使用该属性。例如:
<update id="updateUser" flushCache="true">
UPDATE user SET username = #{username} WHERE id = #{id}
</update>
当 flushCache="true"
时,执行该 SQL 语句后会清空一级缓存和二级缓存。
映射与类型转换问题
问题 12:在 mapper.xml
中,如何处理数据库字段类型与 Java 对象属性类型不匹配的情况?
答案:
- 使用
typeHandler
:MyBatis 提供了TypeHandler
接口来处理类型转换。可以自定义TypeHandler
类,实现该接口并重写相应的方法,然后在mapper.xml
中使用typeHandler
属性指定自定义的类型处理器。例如:
<resultMap id="UserResultMap" type="com.example.entity.User">
<result property="createTime" column="create_time" typeHandler="com.example.handler.DateTypeHandler"/>
</resultMap>
- 使用
<sql>
标签进行类型转换:在 SQL 语句中使用数据库的类型转换函数,如 MySQL 中的CAST
函数。例如:
<select id="selectUser" resultType="com.example.entity.User">
SELECT CAST(age AS SIGNED) AS age FROM user
</select>
问题 13:mapper.xml
中 <resultMap>
的 <association>
和 <collection>
标签在处理关联关系时有什么高级用法?
答案:
<association>
高级用法:除了基本的一对一关联映射,还可以使用fetchType
属性来控制关联对象的加载方式,有eager
(立即加载)和lazy
(延迟加载)两种模式。例如:
<resultMap id="UserResultMap" type="com.example.entity.User">
<id property="id" column="user_id"/>
<association property="department" javaType="com.example.entity.Department" fetchType="lazy">
<id property="id" column="dept_id"/>
<result property="deptName" column="dept_name"/>
</association>
</resultMap>
<collection>
高级用法:同样可以使用fetchType
属性控制集合的加载方式,还可以使用columnPrefix
属性为关联查询的列添加前缀,避免列名冲突。例如:
<resultMap id="UserResultMap" type="com.example.entity.User">
<id property="id" column="user_id"/>
<collection property="orders" ofType="com.example.entity.Order" fetchType="lazy" columnPrefix="order_">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
</collection>
</resultMap>
性能监控与调优问题
问题 14:如何在 mapper.xml
层面监控 SQL 执行性能?
答案:
- 使用日志框架:配置日志框架(如 Log4j、SLF4J 等),将 MyBatis 的日志级别设置为
DEBUG
,这样可以在日志中看到每条 SQL 语句的执行情况,包括执行时间、传入的参数等。例如,在log4j.properties
中配置:
log4j.logger.com.example.mapper=DEBUG
- 使用性能监控工具:可以使用一些第三方性能监控工具,如 P6Spy,它可以拦截和记录 SQL 语句的执行时间、参数等信息,方便进行性能分析。
问题 15:在 mapper.xml
中,如何优化批量插入操作的性能?
答案:
- 使用
<foreach>
标签:在mapper.xml
中使用<foreach>
标签将多个插入语句合并为一个批量插入语句。例如:
<insert id="batchInsertUsers">
INSERT INTO user (username, age) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.age})
</foreach>
</insert>
- 调整数据库连接池参数:适当增加数据库连接池的最大连接数、最小空闲连接数等参数,以提高并发插入的性能。
- 关闭自动提交:在代码中关闭自动提交,手动控制事务的提交,减少事务开销。例如:
SqlSession sqlSession = sqlSessionFactory.openSession(false);
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
userMapper.batchInsertUsers(users);
sqlSession.commit();
} catch (Exception e) {
sqlSession.rollback();
} finally {
sqlSession.close();
}
以下是一些关于MyBatis和MyBatis Plus中mapper.xml
的其他面试问题及答案:
动态SQL相关问题
问题16:MyBatis的mapper.xml
中<if>
、<choose>
、<when>
、<otherwise>
标签在动态SQL中有什么作用?如何使用?
答案:
<if>
标签:用于根据条件判断是否包含某个SQL片段。例如:
<select id="selectUsers" resultMap="UserResultMap">
SELECT * FROM user
<where>
<if test="username!= null">
AND username = #{username}
</if>
<if test="age!= null">
AND age = #{age}
</if>
</where>
</select>
上述代码根据username
和age
是否为null
来动态添加查询条件。
<choose>
、<when>
、<otherwise>
标签:类似于Java中的switch
语句,``是主标签,
是条件分支,
`是默认分支。例如:
<select id="selectUsersByCondition" resultMap="UserResultMap">
SELECT * FROM user
<where>
<choose>
<when test="condition == 1">
AND age > 18
</when>
<when test="condition == 2">
AND age <= 18
</when>
<otherwise>
AND gender = '男'
</otherwise>
</choose>
</where>
</select>
根据condition
的值来选择不同的查询条件,如果condition
既不等于1也不等于2,则使用otherwise
中的条件。
问题17:<foreach>
标签有哪些属性?在批量操作中如何使用?
答案:
<foreach>
标签的属性collection
:指定要遍历的集合或数组。item
:表示集合中每个元素的变量名。separator
:元素之间的分隔符。open
:在开始处添加的字符串。close
:在结束处添加的字符串。
- 批量操作示例 - 批量删除
<delete id="batchDeleteUsers">
DELETE FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
在上述代码中,ids
是一个包含用户ID的集合,通过<foreach>
标签将集合中的ID拼接成IN
子句中的参数列表,实现批量删除操作。
多表关联查询问题
问题18:在mapper.xml
中如何进行复杂的多表联合查询?
答案:可以使用JOIN
关键字进行多表联合查询,并通过resultMap
来映射结果。例如,查询用户及其所属部门,以及该部门下的所有员工:
<resultMap id="UserDeptEmpResultMap" type="com.example.entity.User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<association property="department" javaType="com.example.entity.Department">
<id property="id" column="dept_id"/>
<result property="deptName" column="dept_name"/>
<collection property="employees" ofType="com.example.entity.User">
<id property="id" column="emp_id"/>
<result property="username" column="emp_username"/>
</collection>
</association>
</resultMap>
<select id="selectUserDeptEmp" resultMap="UserDeptEmpResultMap">
SELECT
u.id AS user_id,
u.username,
d.id AS dept_id,
d.dept_name,
e.id AS emp_id,
e.username AS emp_username
FROM
user u
JOIN
department d ON u.dept_id = d.id
LEFT JOIN
user e ON d.id = e.dept_id
</select>
上述代码中,通过JOIN
和LEFT JOIN
实现了多表关联查询,并使用resultMap
对复杂的结果进行了映射。
问题19:在多表关联查询时,如何处理关联字段的别名冲突问题?
答案:可以在SQL查询中为关联字段指定不同的别名来避免冲突。例如:
<select id="selectUserAndOrders" resultMap="UserOrdersResultMap">
SELECT
u.id AS user_id,
u.username,
o.id AS order_id,
o.order_no,
o.order_amount
FROM
user u
JOIN
orders o ON u.id = o.user_id
</select>
在resultMap
中使用对应的别名进行映射:
<resultMap id="UserOrdersResultMap" type="com.example.entity.User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="com.example.entity.Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<result property="orderAmount" column="order_amount"/>
</collection>
</resultMap>
MyBatis Plus特有的mapper.xml
问题
问题20:MyBatis Plus中mapper.xml
与MyBatis的mapper.xml
有什么区别和联系?
答案:
- 联系:MyBatis Plus的
mapper.xml
在本质上和MyBatis的mapper.xml
作用是一样的,都是用于编写SQL语句和配置映射关系等,并且在很多用法上是相似的,比如都可以使用resultMap
、动态SQL标签等。 - 区别
- MyBatis Plus增强功能:MyBatis Plus的
mapper.xml
可以配合其提供的通用Mapper等功能,减少大量的基础SQL语句编写。例如,不需要编写简单的SELECT
、INSERT
、UPDATE
、DELETE
语句,通用Mapper已经提供了默认实现。 - 内置方法不同:MyBatis Plus的
mapper.xml
如果要使用其特有的方法,需要遵循其特定的命名规范和配置方式。比如selectPage
方法用于分页查询,在mapper.xml
中的配置和普通MyBatis的分页查询配置有所不同,它会结合Page
对象等进行分页操作。
- MyBatis Plus增强功能:MyBatis Plus的
问题21:在MyBatis Plus的mapper.xml
中如何实现自定义分页查询?
答案:首先在mapper.xml
中编写查询语句,使用${ew.customSqlSegment}
来接收MyBatis Plus自动生成的分页相关的SQL片段。例如:
<select id="selectUsersByConditionWithPage" resultMap="UserResultMap">
SELECT * FROM user
<where>
<!-- 自定义查询条件 -->
<if test="username!= null">
AND username = #{username}
</if>
</where>
${ew.customSqlSegment}
</select>
在Java代码中,使用Page
对象和QueryWrapper
来进行分页查询:
Page<User> page = new Page<>(1, 10);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("gender", "女");
userMapper.selectUsersByConditionWithPage(page, queryWrapper);
事务与并发控制问题
问题 22:在 mapper.xml
相关操作中,如何保证事务的一致性?
答案:
- 基于 Spring 管理事务:在 Spring 与 MyBatis 整合的项目中,通常使用 Spring 的声明式事务管理。通过在服务层方法上添加
@Transactional
注解,Spring 会自动管理事务的开启、提交和回滚。例如:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void updateUserAndOrder(User user, Order order) {
userMapper.updateUser(user);
userMapper.insertOrder(order);
}
}
- 手动控制事务(较少使用):在不使用 Spring 管理事务时,可通过
SqlSession
手动控制事务。例如:
SqlSession sqlSession = sqlSessionFactory.openSession(false);
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
userMapper.updateUser(user);
userMapper.insertOrder(order);
sqlSession.commit();
} catch (Exception e) {
sqlSession.rollback();
} finally {
sqlSession.close();
}
问题 23:在高并发场景下,mapper.xml
中的 SQL 操作可能会出现哪些问题?如何解决?
答案:
- 可能出现的问题
- 数据不一致:多个事务同时对同一数据进行读写操作,可能导致数据不一致,如脏读、不可重复读、幻读等问题。
- 死锁:多个事务相互等待对方释放锁,导致程序无法继续执行。
- 解决方法
- 使用合适的事务隔离级别:在 Spring 的
@Transactional
注解中设置合适的隔离级别,如@Transactional(isolation = Isolation.READ_COMMITTED)
可避免脏读问题。 - 优化 SQL 语句:减少锁的持有时间,避免长时间占用锁资源。例如,避免在事务中执行复杂的查询或耗时操作。
- 使用悲观锁或乐观锁:悲观锁通过
SELECT ... FOR UPDATE
语句在查询时加锁,确保数据在事务处理期间不被其他事务修改;乐观锁通过在表中添加版本号字段,在更新数据时检查版本号是否一致。
- 使用合适的事务隔离级别:在 Spring 的
代码维护与扩展性问题
问题 24:如何提高 mapper.xml
文件的可维护性和扩展性?
答案:
- 合理命名:
mapper.xml
中的id
要具有清晰的含义,能够准确反映该 SQL 操作的功能。例如,selectUserById
表示根据 ID 查询用户。 - 模块化设计:将常用的 SQL 片段提取到
<sql>
标签中,通过<include>
标签引用,提高代码的复用性。例如:
<sql id="userColumns">
id, username, age, gender
</sql>
<select id="selectUserById" resultType="com.example.entity.User">
SELECT <include refid="userColumns"/> FROM user WHERE id = #{id}
</select>
- 注释说明:在
mapper.xml
中添加详细的注释,解释 SQL 语句的功能、参数含义和返回值等信息,方便后续开发人员理解和维护。
问题 25:当业务需求变更时,如何修改 mapper.xml
中的 SQL 语句以最小化对现有代码的影响?
答案:
- 遵循开闭原则:尽量通过扩展而不是修改现有代码来满足新的需求。例如,当需要添加新的查询条件时,可以使用动态 SQL 标签
<if>
来扩展查询条件,而不是直接修改原有的 SQL 语句。 - 版本控制:使用版本控制系统(如 Git)对
mapper.xml
文件进行管理,方便回溯和比较不同版本的修改。在修改之前,先创建新的分支,确保修改不会影响到主分支的稳定性。 - 测试驱动开发:在修改
mapper.xml
中的 SQL 语句后,编写相应的单元测试和集成测试,确保修改后的代码仍然能够正常工作,并且不会引入新的问题。
性能调优深入问题
问题 26:在 mapper.xml
中,如何利用数据库索引来优化查询性能?
答案:
- 合理设计查询条件:确保 SQL 语句中的查询条件能够使用到数据库的索引。例如,在
WHERE
子句中使用索引列进行过滤,避免使用函数或表达式对索引列进行操作,因为这可能会导致索引失效。
<!-- 正确使用索引 -->
<select id="selectUserByUsername" resultType="com.example.entity.User">
SELECT * FROM user WHERE username = #{username}
</select>
<!-- 错误示例,可能导致索引失效 -->
<select id="selectUserByUsernameWrong" resultType="com.example.entity.User">
SELECT * FROM user WHERE UPPER(username) = UPPER(#{username})
</select>
- 复合索引的使用:如果查询条件涉及多个列,可以考虑创建复合索引。在编写 SQL 语句时,按照复合索引的列顺序进行条件过滤,以充分利用复合索引的性能优势。
问题 27:对于 mapper.xml
中的复杂 SQL 查询,如何进行性能分析和优化?
答案:
- 性能分析
- 数据库自带工具:使用数据库的性能分析工具,如 MySQL 的
EXPLAIN
关键字,分析 SQL 语句的执行计划,了解查询的索引使用情况、扫描行数等信息。 - 日志记录:通过日志记录 SQL 语句的执行时间,找出执行时间较长的 SQL 语句。
- 数据库自带工具:使用数据库的性能分析工具,如 MySQL 的
- 优化方法
- 索引优化:根据性能分析结果,添加或修改索引,提高查询效率。
- SQL 重构:将复杂的 SQL 查询拆分成多个简单的查询,或者使用存储过程来优化逻辑。
- 缓存使用:对于一些不经常变化的数据,可以使用缓存来减少数据库查询次数。
与其他框架集成问题
问题 28:当 MyBatis 的 mapper.xml
与 Spring Boot 集成时,有哪些常见的配置和注意事项?
答案:
- 常见配置
- 依赖添加:在
pom.xml
中添加 MyBatis 和 MyBatis - Spring - Boot - Starter 依赖,确保项目能正常使用 MyBatis 功能。
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>x.x.x</version> </dependency>
- 配置文件设置:在
application.properties
或application.yml
中配置数据库连接信息和mapper.xml
文件的位置。
spring.datasource.url=jdbc:mysql://localhost:3306/your_database spring.datasource.username=your_username spring.datasource.password=your_password mybatis.mapper-locations=classpath:mapper/*.xml
- 依赖添加:在
- 注意事项
- Mapper 接口扫描:确保 Spring Boot 能扫描到 Mapper 接口,可以使用
@MapperScan
注解指定 Mapper 接口所在的包。
@SpringBootApplication @MapperScan("com.example.mapper") public class YourApplication { public static void main(String[] args) { SpringApplication.run(YourApplication.class, args); } }
- 事务管理:Spring Boot 集成 MyBatis 时,要注意事务的配置和使用,确保数据操作的一致性。
- Mapper 接口扫描:确保 Spring Boot 能扫描到 Mapper 接口,可以使用
问题 29:如果要将 mapper.xml
与 Redis 集成实现缓存,该如何操作?
答案
- 添加依赖:在项目中添加 Redis 相关依赖,如 Spring Data Redis。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置 Redis:在
application.properties
或application.yml
中配置 Redis 连接信息。
spring.redis.host=localhost
spring.redis.port=6379
- 实现缓存逻辑:在服务层实现缓存逻辑,先从 Redis 中获取数据,如果缓存中不存在,则执行
mapper.xml
中的 SQL 查询,并将结果存入 Redis。
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
public User getUserById(Long id) {
String key = "user:" + id;
User user = (User) redisTemplate.opsForValue().get(key);
if (user == null) {
user = userMapper.selectUserById(id);
if (user != null) {
redisTemplate.opsForValue().set(key, user);
}
}
return user;
}
}
代码规范与最佳实践问题
问题 30:在编写 mapper.xml
时,有哪些代码规范和最佳实践?
答案
- 代码规范
- 命名规范:
mapper.xml
文件的命名应与对应的 Mapper 接口名称保持一致,id
属性命名要清晰反映 SQL 操作的功能,如selectUserByUsername
、updateUserInfo
等。 - 注释规范:为每个 SQL 语句添加详细的注释,包括功能描述、参数说明、返回值说明等,方便后续维护。
- 格式规范:保持 SQL 语句的格式整齐,合理使用缩进和换行,提高代码的可读性。
- 命名规范:
- 最佳实践
- 避免硬编码:尽量使用参数化查询,避免在 SQL 语句中硬编码常量,防止 SQL 注入攻击。
- 复用 SQL 片段:将常用的 SQL 片段提取到
<sql>
标签中,通过<include>
标签复用,减少代码冗余。 - 性能优化:编写高效的 SQL 语句,合理使用索引,避免全表扫描,提高查询性能。
问题 31:如何对 mapper.xml
中的 SQL 语句进行安全审查?
答案
- 防止 SQL 注入
- 使用参数化查询:确保在
mapper.xml
中使用#{}
占位符进行参数传递,MyBatis 会自动处理参数的预编译,防止 SQL 注入。 - 输入验证:在服务层对用户输入进行严格的验证和过滤,确保输入的数据符合预期。
- 使用参数化查询:确保在
- 权限审查
- 检查 SQL 操作权限:确保 SQL 语句所涉及的数据库操作在用户或系统的权限范围内,避免越权操作。
- 代码审查
- 人工审查:组织开发人员对
mapper.xml
中的 SQL 语句进行人工审查,检查是否存在潜在的安全风险。 - 工具辅助:使用静态代码分析工具,如 SonarQube 等,对
mapper.xml
文件进行扫描,发现潜在的安全问题。
- 人工审查:组织开发人员对
异常处理与调试问题
问题 32:在使用 mapper.xml
时,可能会遇到哪些异常?如何进行调试和解决?
答案
- 常见异常
- SQLSyntaxErrorException:SQL 语法错误,通常是由于 SQL 语句编写错误导致的,如关键字拼写错误、表名或列名错误等。
- SQLException:数据库连接异常、数据类型不匹配等问题都可能引发该异常。
- MapperException:MyBatis 映射异常,如
resultMap
配置错误、id
重复等。
- 调试和解决方法
- 日志调试:开启 MyBatis 的详细日志,通过日志信息查看具体的 SQL 语句和错误堆栈,定位问题所在。
- 数据库客户端测试:将
mapper.xml
中的 SQL 语句复制到数据库客户端中执行,检查是否能正常运行,排查 SQL 语法问题。 - 代码审查:仔细检查
mapper.xml
文件的配置,确保resultMap
、parameterType
等配置正确。
问题 33:当 mapper.xml
中的 SQL 执行超时,应该如何处理?
答案
- 优化 SQL 语句
- 索引优化:检查 SQL 语句是否使用了合适的索引,通过添加或修改索引来提高查询效率。
- 查询优化:避免复杂的嵌套查询和全表扫描,将复杂查询拆分成多个简单查询。
- 调整超时设置
- 数据库层面:在数据库中调整连接超时和查询超时的参数设置。
- 应用层面:在 MyBatis 配置中设置超时时间,例如在
DataSource
配置中设置connectionTimeout
和queryTimeout
。
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/your_database");
dataSource.setUsername("your_username");
dataSource.setPassword("your_password");
dataSource.setConnectionTimeout(30000); // 连接超时时间 30 秒
dataSource.setIdleTimeout(600000); // 空闲连接超时时间 10 分钟
dataSource.setMaxLifetime(1800000); // 最大连接生命周期 30 分钟
dataSource.setMaximumPoolSize(10); // 最大连接池大小
return dataSource;
}
}
分布式环境相关问题
问题 34:在分布式系统中,mapper.xml
里的 SQL 操作如何保证数据一致性?
答案:
- 使用分布式事务框架:如 Seata 等,Seata 提供了 AT、TCC、SAGA 等多种分布式事务模式。在服务层使用 Seata 的注解进行事务管理,例如在调用多个涉及
mapper.xml
中 SQL 操作的服务方法时,使用@GlobalTransactional
注解开启全局事务。
@Service
public class DistributedService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
@GlobalTransactional
public void createUserAndOrder(User user, Order order) {
userMapper.insertUser(user);
orderMapper.insertOrder(order);
}
}
- 消息队列与最终一致性:借助消息队列(如 Kafka、RabbitMQ)实现最终一致性。当一个 SQL 操作完成后,发送消息到消息队列,其他服务监听消息并执行相应的 SQL 操作。如果操作失败,可通过重试机制或人工干预来保证最终数据一致。
- 幂等性设计:确保
mapper.xml
中的 SQL 操作具有幂等性,即多次执行相同操作产生的结果与执行一次的结果相同。例如,在更新操作中使用UPDATE ... WHERE ...
语句,通过唯一标识进行更新,避免重复更新导致数据不一致。
问题 35:在分布式缓存场景下,mapper.xml
相关的 SQL 查询如何与缓存配合以提升性能?
答案:
- 缓存穿透处理:当查询的数据在缓存中不存在时,为避免大量请求直接穿透到数据库,可在缓存中设置空值或使用布隆过滤器。在
mapper.xml
查询时,先检查缓存,如果缓存为空,进行布隆过滤器判断,若可能存在则查询数据库并更新缓存。
public User getUserById(Long id) {
String key = "user:" + id;
User user = (User) redisTemplate.opsForValue().get(key);
if (user == null) {
if (bloomFilter.mightContain(id)) {
user = userMapper.selectUserById(id);
if (user != null) {
redisTemplate.opsForValue().set(key, user);
} else {
redisTemplate.opsForValue().set(key, null, 60, TimeUnit.SECONDS); // 设置空值缓存,避免缓存穿透
}
}
}
return user;
}
- 缓存更新策略:当
mapper.xml
中的 SQL 操作对数据进行更新时,需要更新缓存。可以采用先更新数据库,再删除缓存的策略,利用缓存失效机制保证下次查询时获取最新数据。
@Transactional
public void updateUser(User user) {
userMapper.updateUser(user);
String key = "user:" + user.getId();
redisTemplate.delete(key);
}
- 缓存预热:在系统启动时,将常用的查询结果预先加载到缓存中,减少首次查询时的数据库压力。可以通过定时任务或手动触发的方式,执行
mapper.xml
中的查询语句并将结果存入缓存。
动态数据源问题
问题 36:在使用动态数据源的情况下,mapper.xml
如何适配不同的数据源?
答案:
- 动态数据源配置:使用 Spring 的
AbstractRoutingDataSource
实现动态数据源切换。在配置类中定义多个数据源,并根据业务逻辑动态切换数据源。
@Configuration
public class DynamicDataSourceConfig {
@Bean
public DataSource dynamicDataSource() {
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("dataSource1", dataSource1());
targetDataSources.put("dataSource2", dataSource2());
dynamicRoutingDataSource.setDefaultTargetDataSource(dataSource1());
dynamicRoutingDataSource.setTargetDataSources(targetDataSources);
return dynamicRoutingDataSource;
}
@Bean
public DataSource dataSource1() {
// 配置数据源 1
}
@Bean
public DataSource dataSource2() {
// 配置数据源 2
}
}
mapper.xml
通用配置:mapper.xml
中的 SQL 语句应尽量保持通用性,避免使用特定数据库的语法。如果不同数据源使用的数据库类型不同,可通过动态 SQL 标签根据数据源类型进行适配。
<select id="selectUser" resultType="com.example.entity.User">
<choose>
<when test="@com.example.util.DataSourceContextHolder@getDataSource() == 'dataSource1'">
-- 数据源 1 的特定 SQL 语句
</when>
<when test="@com.example.util.DataSourceContextHolder@getDataSource() == 'dataSource2'">
-- 数据源 2 的特定 SQL 语句
</when>
<otherwise>
-- 默认 SQL 语句
</otherwise>
</choose>
</select>
问题 37:如何在 mapper.xml
中实现对不同数据库方言的支持?
答案:
- 动态 SQL 结合方言判断:在
mapper.xml
中使用动态 SQL 标签结合方言判断来编写不同数据库的 SQL 语句。可以通过配置文件或上下文信息获取当前使用的数据库方言。
<select id="selectUserCount" resultType="int">
<choose>
<when test="@com.example.util.DbDialectUtil@isMysql()">
SELECT COUNT(*) FROM user
</when>
<when test="@com.example.util.DbDialectUtil@isOracle()">
SELECT COUNT(*) FROM user_table
</when>
<otherwise>
-- 默认 SQL 语句
</otherwise>
</choose>
</select>
- 抽象 SQL 片段:将不同数据库中通用的 SQL 逻辑提取到
<sql>
标签中,然后在具体的 SQL 语句中通过<include>
标签引用,减少重复代码。对于不同数据库的差异部分,单独处理。
<sql id="userColumns">
id, username, age
</sql>
<select id="selectUser" resultType="com.example.entity.User">
<choose>
<when test="@com.example.util.DbDialectUtil@isMysql()">
SELECT <include refid="userColumns"/> FROM user
</when>
<when test="@com.example.util.DbDialectUtil@isOracle()">
SELECT <include refid="userColumns"/> FROM user_table
</when>
<otherwise>
-- 默认 SQL 语句
</otherwise>
</choose>
</select>
代码生成与自动化问题
问题 38:如何使用代码生成工具自动生成 mapper.xml
文件?
答案:
- MyBatis Generator:这是 MyBatis 官方提供的代码生成工具。
- 配置生成器:创建一个
generatorConfig.xml
配置文件,指定数据库连接信息、生成的表名、生成文件的路径等。
- 配置生成器:创建一个
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<commentGenerator>
<property name="suppressDate" value="true"/>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/your_database"
userId="your_username"
password="your_password">
</jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<javaModelGenerator targetPackage="com.example.entity"
targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<sqlMapGenerator targetPackage="com.example.mapper"
targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.example.mapper"
targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<table tableName="user"/>
</context>
</generatorConfiguration>
- **运行生成器**:在 Java 代码中运行生成器,执行代码生成操作。
public class Generator {
public static void main(String[] args) throws Exception {
List<String> warnings = new ArrayList<>();
boolean overwrite = true;
File configFile = new File("generatorConfig.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
}
}
- MyBatis-Plus Generator:MyBatis-Plus 提供的代码生成器,使用更便捷。
- 添加依赖:在
pom.xml
中添加 MyBatis-Plus Generator 依赖。
- 添加依赖:在
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>x.x.x</version>
</dependency>
- **编写生成代码**:通过 Java 代码配置生成器并执行生成操作。
public class CodeGenerator {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://localhost:3306/your_database", "your_username", "your_password")
.globalConfig(builder -> {
builder.author("your_name") // 设置作者
.outputDir("src/main/java"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.example") // 设置父包名
.moduleName("module") // 设置模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, "src/main/resources/mapper")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("user") // 设置需要生成的表名
.addTablePrefix("t_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
问题 39:自动生成的 mapper.xml
文件如何进行定制化修改和扩展?
答案:
- 备份与版本控制:在进行定制化修改之前,先对自动生成的
mapper.xml
文件进行备份,并使用版本控制系统(如 Git)进行管理,方便后续回溯和比较修改。 - 避免直接修改生成模板:尽量不要直接修改代码生成工具的模板文件,因为这样可能会影响后续的代码生成。可以在生成的基础上进行手动修改和扩展。
- 添加自定义 SQL 语句:在
mapper.xml
中添加自定义的 SQL 语句,如复杂的查询、存储过程调用等。同时,要注意保持命名规范和代码格式的一致性。
<mapper namespace="com.example.mapper.UserMapper">
<!-- 自动生成的 SQL 语句 -->
<select id="selectUserById" resultType="com.example.entity.User">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 自定义 SQL 语句 -->
<select id="selectUserByUsernameAndAge" resultType="com.example.entity.User">
SELECT * FROM user WHERE username = #{username} AND age = #{age}
</select>
</mapper>
- 扩展映射关系:如果需要扩展映射关系,可以在
resultMap
中添加新的映射字段或修改现有的映射规则。
<resultMap id="UserResultMap" type="com.example.entity.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="age" column="age"/>
<!-- 扩展映射字段 -->
<result property="email" column="email"/>
</resultMap>
日志与审计相关问题
问题 40:如何在 mapper.xml
相关操作中记录详细的日志,以便进行审计和问题排查?
答案:
- MyBatis 日志配置:MyBatis 本身支持多种日志框架,如 Log4j、Logback、SLF4J 等。可以在 MyBatis 配置文件或 Spring Boot 的配置文件中配置日志级别为
DEBUG
,这样会输出 SQL 语句及其参数。- 使用 Log4j 示例:在
log4j.properties
中添加以下配置:
- 使用 Log4j 示例:在
log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
log4j.logger.com.example.mapper=DEBUG
- **使用 Spring Boot 配置**:在 `application.properties` 中配置:
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
- 自定义日志记录:可以在服务层或拦截器中自定义日志记录逻辑,记录 SQL 操作的执行时间、影响行数等信息。例如,创建一个自定义的拦截器:
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class SqlLogInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(SqlLogInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String sqlId = mappedStatement.getId();
logger.info("SQL ID: {}, Execution Time: {} ms, Result: {}", sqlId, executionTime, result);
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可用于设置拦截器的属性
}
}
然后在 MyBatis 配置中注册该拦截器:
@Configuration
public class MyBatisConfig {
@Autowired
private SqlSessionFactory sqlSessionFactory;
@PostConstruct
public void addInterceptor() {
sqlSessionFactory.getConfiguration().addInterceptor(new SqlLogInterceptor());
}
}
问题 41:对于 mapper.xml
中的敏感数据操作,如何进行安全审计?
答案:
- 日志记录:对涉及敏感数据的 SQL 操作(如查询、修改、删除用户的身份证号、密码等)进行详细的日志记录,包括操作时间、操作人员、操作的 SQL 语句、影响的记录等信息。可以结合上述的日志配置和自定义拦截器来实现。
- 权限控制:在系统层面设置严格的权限控制,只有具有相应权限的用户才能执行涉及敏感数据的 SQL 操作。可以使用 Spring Security 等框架来实现权限管理。
- 审计表记录:创建专门的审计表,记录敏感数据操作的相关信息。在执行敏感数据操作时,同时向审计表中插入一条记录。例如:
<insert id="insertAuditLog">
INSERT INTO audit_log (operation_time, operator, sql_statement, affected_rows)
VALUES (#{operationTime}, #{operator}, #{sqlStatement}, #{affectedRows})
</insert>
在服务层调用该插入语句:
@Service
public class SensitiveDataService {
@Autowired
private AuditLogMapper auditLogMapper;
@Autowired
private SensitiveDataMapper sensitiveDataMapper;
public void updateSensitiveData(SensitiveData data) {
String sqlStatement = "UPDATE sensitive_data SET ...";
int affectedRows = sensitiveDataMapper.updateSensitiveData(data);
AuditLog auditLog = new AuditLog();
auditLog.setOperationTime(new Date());
auditLog.setOperator("user1");
auditLog.setSqlStatement(sqlStatement);
auditLog.setAffectedRows(affectedRows);
auditLogMapper.insertAuditLog(auditLog);
}
}
数据迁移与兼容性问题
问题 42:当数据库进行升级或迁移时,mapper.xml
中的 SQL 语句需要做哪些调整?
答案:
- 数据库语法差异:不同版本的数据库或不同类型的数据库(如从 MySQL 迁移到 PostgreSQL)可能存在语法差异。需要检查
mapper.xml
中的 SQL 语句,将不兼容的语法进行修改。例如,MySQL 中的LIMIT
关键字在 PostgreSQL 中也可以使用,但函数的使用方式可能有所不同。
<!-- MySQL 查询分页 -->
<select id="selectUsersByPage" resultType="com.example.entity.User">
SELECT * FROM user LIMIT #{offset}, #{limit}
</select>
<!-- PostgreSQL 查询分页 -->
<select id="selectUsersByPage" resultType="com.example.entity.User">
SELECT * FROM user OFFSET #{offset} LIMIT #{limit}
</select>
- 表结构变化:如果数据库升级或迁移后表结构发生了变化,如字段名更改、字段类型变更、新增或删除字段等,需要相应地修改
mapper.xml
中的 SQL 语句和映射关系。例如,原表中的user_name
字段改为username
,则需要修改 SQL 语句和resultMap
:
<resultMap id="UserResultMap" type="com.example.entity.User">
<!-- 修改前 -->
<!-- <result property="username" column="user_name"/> -->
<!-- 修改后 -->
<result property="username" column="username"/>
</resultMap>
<select id="selectUserById" resultType="com.example.entity.User">
<!-- 修改前 -->
<!-- SELECT user_name FROM user WHERE id = #{id} -->
<!-- 修改后 -->
SELECT username FROM user WHERE id = #{id}
</select>
- 存储过程和函数变化:如果使用了数据库的存储过程和函数,需要检查它们在新数据库中的定义是否发生了变化,如有变化则需要修改
mapper.xml
中调用这些存储过程和函数的 SQL 语句。
问题 43:如何确保 mapper.xml
中的 SQL 语句在不同数据库版本之间的兼容性?
答案:
- 使用标准 SQL 语法:尽量使用标准的 SQL 语法编写
mapper.xml
中的 SQL 语句,避免使用特定数据库版本的专有语法和函数。例如,使用WHERE
子句进行条件过滤,而不是依赖特定数据库的过滤方式。 - 动态 SQL 适配:利用 MyBatis 的动态 SQL 标签,根据不同的数据库版本或类型,动态生成不同的 SQL 语句。可以通过配置文件或上下文信息获取当前数据库的相关信息,然后在
mapper.xml
中进行判断。
<select id="selectUserCount" resultType="int">
<choose>
<when test="@com.example.util.DbVersionUtil@isOldVersion()">
-- 旧版本数据库的 SQL 语句
</when>
<otherwise>
-- 新版本数据库的 SQL 语句
</otherwise>
</choose>
</select>
- 测试与验证:在不同版本的数据库上进行充分的测试,确保
mapper.xml
中的 SQL 语句能够正常执行。可以使用测试框架(如 JUnit)编写单元测试和集成测试,模拟不同数据库环境下的操作。
代码优化与重构问题
问题 44:如何对已有的 mapper.xml
文件进行性能优化和代码重构?
答案:
- 性能优化
- 索引优化:分析 SQL 语句,确保查询条件能够使用到数据库的索引。可以通过数据库的
EXPLAIN
工具查看查询的执行计划,根据结果添加或调整索引。 - 减少全表扫描:避免在
WHERE
子句中使用函数或表达式对索引列进行操作,防止索引失效。尽量使用覆盖索引,减少数据库的 I/O 操作。 - 批量操作:对于插入、更新和删除操作,使用批量操作代替单条记录的操作,减少与数据库的交互次数。例如,使用
<foreach>
标签实现批量插入。
- 索引优化:分析 SQL 语句,确保查询条件能够使用到数据库的索引。可以通过数据库的
- 代码重构
- 提取公共 SQL 片段:将重复使用的 SQL 片段提取到
<sql>
标签中,通过<include>
标签引用,提高代码的复用性。 - 优化动态 SQL:检查动态 SQL 标签的使用,避免过多的嵌套和复杂的条件判断。可以将复杂的动态 SQL 拆分成多个简单的 SQL 语句,提高代码的可读性。
- 遵循命名规范:统一
mapper.xml
中id
、<sql>
标签的命名,使其具有清晰的含义,方便后续维护。
- 提取公共 SQL 片段:将重复使用的 SQL 片段提取到
问题 45:当 mapper.xml
文件变得非常庞大时,如何进行拆分和管理?
答案:
- 按功能模块拆分:根据业务功能将
mapper.xml
文件拆分成多个小文件。例如,将用户相关的 SQL 语句放在UserMapper.xml
中,订单相关的 SQL 语句放在OrderMapper.xml
中。 - 按操作类型拆分:将查询、插入、更新、删除等不同类型的 SQL 操作分别放在不同的
mapper.xml
文件中。例如,UserQueryMapper.xml
存放用户查询相关的 SQL 语句,UserUpdateMapper.xml
存放用户更新相关的 SQL 语句。 - 使用
<package>
配置:在 MyBatis 配置文件中使用<package>
标签指定mapper.xml
文件所在的包,让 MyBatis 自动扫描和加载这些文件。
<mappers>
<package name="com.example.mapper"/>
</mappers>
- 版本控制与管理:使用版本控制系统(如 Git)对拆分后的
mapper.xml
文件进行管理,方便团队协作和代码的版本追溯。同时,可以为每个文件添加详细的注释,说明其功能和用途。
MyBatis、MyBatis-Plus和JDBC之间存在一定的关系,它们的底层原理也各有特点,具体如下:
三者关系
- JDBC:Java Database Connectivity(Java数据库连接),是Java语言中用于与各种数据库进行交互的标准API。它提供了一套通用的接口,让Java程序能够连接到数据库、执行SQL语句、处理结果集等。MyBatis和MyBatis-Plus都是构建在JDBC之上的框架,它们最终都要通过JDBC来实现与数据库的交互。
- MyBatis:是一个持久层框架,它对JDBC进行了封装,简化了JDBC操作数据库的代码,使开发者可以更专注于SQL语句的编写和业务逻辑的实现。MyBatis通过配置文件或注解等方式来管理SQL语句,并提供了对象关系映射(ORM)等功能,将数据库中的数据映射为Java对象。
- MyBatis-Plus:是在MyBatis基础上进行扩展的增强工具,它在MyBatis的基础上封装了更多的通用操作,如通用的CRUD(增删改查)方法等,进一步简化了开发流程,提高了开发效率。MyBatis-Plus并不改变MyBatis的底层原理,而是在其基础上提供了更多的便捷功能和特性。
底层原理
- JDBC
- 连接管理:通过DriverManager类来管理数据库驱动,加载数据库驱动程序后,使用
DriverManager.getConnection()
方法建立与数据库的连接,这个过程会根据传入的数据库URL、用户名和密码等信息与数据库服务器进行通信,建立起物理连接。 - SQL执行:获取连接后,创建
Statement
或PreparedStatement
对象来执行SQL语句。Statement
用于执行静态SQL,PreparedStatement
用于执行预编译的SQL,它可以防止SQL注入攻击,并且在多次执行相同SQL但参数不同的情况下性能更好。 - 结果处理:执行SQL语句后,通过
ResultSet
对象来处理查询结果,它提供了一系列方法来获取结果集中的数据,如getInt()
、getString()
等,根据列的数据类型获取相应的值。
- 连接管理:通过DriverManager类来管理数据库驱动,加载数据库驱动程序后,使用
- MyBatis
- 配置解析:首先读取配置文件(如
mybatis-config.xml
),解析其中的数据源配置、映射文件路径等信息,创建Configuration
对象来存储这些配置信息。 - SQL映射:通过映射文件(如
.xml
文件或注解)将SQL语句与Java方法进行关联,在执行Java方法时,MyBatis会根据映射关系找到对应的SQL语句。 - 执行流程:当调用MyBatis的方法时,会创建
SqlSession
对象,它是MyBatis与数据库交互的核心对象。SqlSession
通过Executor
执行器来执行SQL语句,Executor
会根据配置和SQL语句生成StatementHandler
、ParameterHandler
和ResultSetHandler
等对象,分别负责处理SQL语句的创建、参数设置和结果集处理。 - ORM实现:利用反射机制,将结果集中的数据映射为Java对象,根据配置中的映射关系,将数据库列名与Java对象的属性进行匹配,通过调用对象的setter方法将数据设置到对象中。
- 配置解析:首先读取配置文件(如
- MyBatis-Plus
- 通用Mapper原理:基于MyBatis的插件机制,通过拦截器拦截MyBatis的SQL执行过程,在运行时根据实体类的信息和方法调用动态生成SQL语句。例如,对于通用的查询方法,它会根据实体类的字段信息生成相应的
SELECT
语句,无需开发者手动编写大量重复的SQL。 - 条件构造器原理:通过链式调用的方式构建查询条件,在底层将用户设置的条件转换为SQL中的
WHERE
子句条件。它利用了Java的函数式编程和反射等技术,对实体类的字段进行操作,生成准确的查询条件。 - 代码生成原理:根据数据库表结构和用户配置的模板,利用代码生成工具(如Freemarker、Velocity等)生成Java实体类、Mapper接口、Mapper XML文件等代码,减少了开发者手动编写基础代码的工作量。
- 通用Mapper原理:基于MyBatis的插件机制,通过拦截器拦截MyBatis的SQL执行过程,在运行时根据实体类的信息和方法调用动态生成SQL语句。例如,对于通用的查询方法,它会根据实体类的字段信息生成相应的
如何在MyBatis中使用注解配置SQL语句?
在MyBatis中,除了使用XML文件配置SQL语句外,还可以使用注解来配置,这样可以减少XML文件的使用,使代码更加简洁。以下为你详细介绍如何在MyBatis中使用注解配置SQL语句:
1. 环境准备
首先要确保项目中已经添加了MyBatis的依赖。如果你使用的是Maven项目,可以在pom.xml
中添加如下依赖:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
2. 定义实体类
定义一个简单的Java实体类,例如User
类:
public class User {
private Integer id;
private String username;
private String password;
// 构造方法、getter和setter方法
public User() {}
public User(Integer id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
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 String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
3. 定义Mapper接口并使用注解配置SQL
在Mapper接口中使用MyBatis提供的注解来配置SQL语句。常见的注解有@Select
、@Insert
、@Update
和@Delete
。
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface UserMapper {
// 查询所有用户
@Select("SELECT * FROM user")
List<User> getAllUsers();
// 根据ID查询用户
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(int id);
// 插入用户
@Insert("INSERT INTO user (username, password) VALUES (#{username}, #{password})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertUser(User user);
// 更新用户
@Update("UPDATE user SET username = #{username}, password = #{password} WHERE id = #{id}")
int updateUser(User user);
// 删除用户
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteUser(int id);
}
注解说明:
@Select
:用于定义查询语句。@Insert
:用于定义插入语句。@Options(useGeneratedKeys = true, keyProperty = "id")
表示使用数据库自动生成的主键,并将生成的主键值设置到User
对象的id
属性中。@Update
:用于定义更新语句。@Delete
:用于定义删除语句。
4. 配置MyBatis
创建MyBatis的配置文件mybatis-config.xml
,并在其中配置数据源和Mapper接口:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/your_database"/>
<property name="username" value="your_username"/>
<property name="password" value="your_password"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="com.example.mapper.UserMapper"/>
</mappers>
</configuration>
5. 测试代码
编写测试代码来验证注解配置的SQL语句是否正常工作:
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.List;
public class Main {
public static void main(String[] args) throws Exception {
// 加载MyBatis配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper userMapper = session.getMapper(UserMapper.class);
// 查询所有用户
List<User> users = userMapper.getAllUsers();
System.out.println("所有用户: " + users);
// 根据ID查询用户
User user = userMapper.getUserById(1);
System.out.println("ID为1的用户: " + user);
// 插入用户
User newUser = new User(null, "testUser", "testPassword");
int rows = userMapper.insertUser(newUser);
System.out.println("插入用户影响的行数: " + rows);
System.out.println("新插入用户的ID: " + newUser.getId());
// 更新用户
newUser.setUsername("updatedUser");
rows = userMapper.updateUser(newUser);
System.out.println("更新用户影响的行数: " + rows);
// 删除用户
rows = userMapper.deleteUser(newUser.getId());
System.out.println("删除用户影响的行数: " + rows);
session.commit();
}
}
}
其他复杂注解使用
动态SQL注解
MyBatis还提供了@SelectProvider
、@InsertProvider
、@UpdateProvider
和@DeleteProvider
注解,用于实现动态SQL。以下是一个使用@SelectProvider
的示例:
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.SelectProvider;
import java.util.List;
public interface UserMapper {
@SelectProvider(type = UserSqlProvider.class, method = "getUsersByCondition")
List<User> getUsersByCondition(@Param("username") String username, @Param("password") String password);
class UserSqlProvider {
public String getUsersByCondition(String username, String password) {
StringBuilder sql = new StringBuilder("SELECT * FROM user WHERE 1 = 1");
if (username != null && !username.isEmpty()) {
sql.append(" AND username = #{username}");
}
if (password != null && !password.isEmpty()) {
sql.append(" AND password = #{password}");
}
return sql.toString();
}
}
}
通过上述步骤,你就可以在MyBatis中使用注解来配置SQL语句了。注解配置适用于简单的SQL场景,而对于复杂的SQL,使用XML配置可能会更加清晰和易于维护。
MyBatis-Plus和MyBatis的注解有哪些区别?
MyBatis-Plus 是基于 MyBatis 开发的增强工具,它在保留 MyBatis 注解的基础上,新增了一些注解以提供更便捷的功能。以下为你详细介绍两者注解的区别:
共同使用的基础注解
MyBatis-Plus 完全兼容 MyBatis 的基础注解,这些注解在两者中功能和使用方式基本一致:
@Select
:用于定义查询语句。
// MyBatis 和 MyBatis-Plus 中使用方式相同
import org.apache.ibatis.annotations.Select;
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User selectUserById(Long id);
}
@Insert
:用于定义插入语句。
import org.apache.ibatis.annotations.Insert;
public interface UserMapper {
@Insert("INSERT INTO user (username, age) VALUES (#{username}, #{age})")
int insertUser(User user);
}
@Update
:用于定义更新语句。
import org.apache.ibatis.annotations.Update;
public interface UserMapper {
@Update("UPDATE user SET username = #{username} WHERE id = #{id}")
int updateUser(User user);
}
@Delete
:用于定义删除语句。
import org.apache.ibatis.annotations.Delete;
public interface UserMapper {
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteUserById(Long id);
}
MyBatis-Plus 新增的注解
实体类相关注解
@TableName
:用于指定实体类对应的数据库表名。当实体类名与数据库表名不一致时,可以使用该注解进行映射。
import com.baomidou.mybatisplus.annotation.TableName;
@TableName("t_user")
public class User {
// 类的属性和方法
}
@TableId
:用于指定实体类中哪个属性为主键,并且可以设置主键的生成策略。
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.IdType;
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
// 其他属性和方法
}
@TableField
:用于指定实体类属性与数据库表字段的映射关系,还可以设置字段的一些特性,如是否为数据库表字段、字段的填充策略等。
import com.baomidou.mybatisplus.annotation.TableField;
public class User {
private String username;
@TableField(value = "user_age", fill = FieldFill.INSERT)
private Integer age;
// 其他属性和方法
}
@TableLogic
:用于实现逻辑删除功能,标记实体类中的某个字段为逻辑删除字段。
import com.baomidou.mybatisplus.annotation.TableLogic;
public class User {
private Long id;
@TableLogic
private Integer deleted;
// 其他属性和方法
}
代码生成相关注解
@Mapper
:MyBatis-Plus 建议在 Mapper 接口上添加该注解,这样可以让 Spring 自动扫描并将其注册为 Bean。
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 可以自定义扩展方法
}
注解使用场景差异
- MyBatis:其注解更侧重于手动编写 SQL 语句,适用于需要精细控制 SQL 逻辑、处理复杂业务需求的场景。开发人员需要自己编写完整的 SQL 语句,对 SQL 有较高的掌控度。
- MyBatis-Plus:注解主要用于简化开发流程,提供了大量的通用 CRUD 操作,减少了手动编写 SQL 的工作量。对于简单的增删改查操作,使用 MyBatis-Plus 的注解和通用方法可以快速实现;而对于复杂的业务逻辑,仍然可以结合 MyBatis 的注解手动编写 SQL。
如何在MyBatis中使用注解配置一对多和多对多的关联查询?
在MyBatis中,可以使用注解来配置一对多和多对多的关联查询。以下分别介绍这两种关联查询的实现方式:
一对多关联查询
1. 数据库表结构和实体类定义
假设我们有两个表:user
(用户表)和order
(订单表),一个用户可以有多个订单,这是典型的一对多关系。
- 数据库表结构
-- 用户表
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50)
);
-- 订单表
CREATE TABLE `order` (
id INT PRIMARY KEY AUTO_INCREMENT,
order_number VARCHAR(50),
user_id INT,
FOREIGN KEY (user_id) REFERENCES user(id)
);
- 实体类定义
import java.util.List;
// 用户实体类
public class User {
private Integer id;
private String username;
private List<Order> orders;
// 构造方法、getter和setter方法
public User() {}
public User(Integer id, String username) {
this.id = id;
this.username = username;
}
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 List<Order> getOrders() {
return orders;
}
public void setOrders(List<Order> orders) {
this.orders = orders;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", orders=" + orders +
'}';
}
}
// 订单实体类
public class Order {
private Integer id;
private String orderNumber;
private Integer userId;
// 构造方法、getter和setter方法
public Order() {}
public Order(Integer id, String orderNumber, Integer userId) {
this.id = id;
this.orderNumber = orderNumber;
this.userId = userId;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
@Override
public String toString() {
return "Order{" +
"id=" + id +
", orderNumber='" + orderNumber + '\'' +
", userId=" + userId +
'}';
}
}
2. Mapper 接口定义及注解配置
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface UserMapper {
@Results({
@Result(property = "id", column = "id"),
@Result(property = "username", column = "username"),
@Result(property = "orders", column = "id", many = @Many(select = "com.example.mapper.OrderMapper.getOrdersByUserId"))
})
@Select("SELECT * FROM user")
List<User> getAllUsers();
}
public interface OrderMapper {
@Select("SELECT * FROM `order` WHERE user_id = #{userId}")
List<Order> getOrdersByUserId(Integer userId);
}
注解解释
@Results
:用于定义结果映射关系。@Result
:具体的映射规则,property
表示实体类的属性名,column
表示数据库表的列名。@Many
:用于处理一对多关系,select
属性指定调用的另一个 Mapper 方法来查询关联的订单数据。
多对多关联查询
1. 数据库表结构和实体类定义
假设我们有三个表:student
(学生表)、course
(课程表)和student_course
(学生 - 课程关联表),一个学生可以选择多门课程,一门课程也可以被多个学生选择,这是典型的多对多关系。
- 数据库表结构
-- 学生表
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50)
);
-- 课程表
CREATE TABLE course (
id INT PRIMARY KEY AUTO_INCREMENT,
course_name VARCHAR(50)
);
-- 学生 - 课程关联表
CREATE TABLE student_course (
student_id INT,
course_id INT,
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES student(id),
FOREIGN KEY (course_id) REFERENCES course(id)
);
- 实体类定义
import java.util.List;
// 学生实体类
public class Student {
private Integer id;
private String name;
private List<Course> courses;
// 构造方法、getter和setter方法
public Student() {}
public Student(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Course> getCourses() {
return courses;
}
public void setCourses(List<Course> courses) {
this.courses = courses;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", courses=" + courses +
'}';
}
}
// 课程实体类
public class Course {
private Integer id;
private String courseName;
// 构造方法、getter和setter方法
public Course() {}
public Course(Integer id, String courseName) {
this.id = id;
this.courseName = courseName;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
@Override
public String toString() {
return "Course{" +
"id=" + id +
", courseName='" + courseName + '\'' +
'}';
}
}
2. Mapper 接口定义及注解配置
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface StudentMapper {
@Results({
@Result(property = "id", column = "id"),
@Result(property = "name", column = "name"),
@Result(property = "courses", column = "id", many = @Many(select = "com.example.mapper.CourseMapper.getCoursesByStudentId"))
})
@Select("SELECT * FROM student")
List<Student> getAllStudents();
}
public interface CourseMapper {
@Select("SELECT c.* FROM course c " +
"JOIN student_course sc ON c.id = sc.course_id " +
"WHERE sc.student_id = #{studentId}")
List<Course> getCoursesByStudentId(Integer studentId);
}
注解解释
同样使用 @Results
、@Result
和 @Many
注解来处理多对多关系,@Many
中的 select
属性指定调用的另一个 Mapper 方法来查询关联的课程数据。
测试代码
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.List;
public class Main {
public static void main(String[] args) throws Exception {
// 加载MyBatis配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {
// 一对多查询测试
UserMapper userMapper = session.getMapper(UserMapper.class);
List<User> users = userMapper.getAllUsers();
System.out.println("一对多查询结果:");
for (User user : users) {
System.out.println(user);
}
// 多对多查询测试
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
List<Student> students = studentMapper.getAllStudents();
System.out.println("多对多查询结果:");
for (Student student : students) {
System.out.println(student);
}
}
}
}
通过以上步骤,你可以在 MyBatis 中使用注解配置一对多和多对多的关联查询。
MyBatis-Plus的注解在使用时有哪些需要注意的地方?
1. 实体类注解
@TableName
- 当实体类名与数据库表名不一致时,使用该注解指定表名。要确保表名正确,包括大小写(某些数据库区分大小写)。
- 若表名有特殊字符(如
-
),需要使用反引号(`)包裹。
@TableId
- 要正确设置主键生成策略(
IdType
),如IdType.AUTO
适用于自增主键,IdType.INPUT
表示手动输入主键值等。 - 确保
value
属性指定的列名与数据库中主键列名一致。
- 要正确设置主键生成策略(
@TableField
exist
属性设置为false
时,该属性不会参与 SQL 语句的生成,常用于实体类中不需要映射到数据库字段的属性。fill
属性用于设置字段的自动填充策略,使用时要确保配置了相应的自动填充处理器。
2. 逻辑删除注解 @TableLogic
- 要在数据库中设计好逻辑删除字段(通常为 `int` 类型),并设置好默认值和逻辑删除值。
- 配置好全局的逻辑删除字段值,否则可能导致查询结果不符合预期。
3. 其他注解
- 在使用自定义 SQL 注解(如 `@SqlParser`)时,要了解其作用和使用场景,避免滥用导致 SQL 执行异常。
二、JDBC、MyBatis、MyBatis-Plus 实现 CURD
1. JDBC 实现 CURD
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
// 假设数据库中有一个 user 表,包含 id, name, age 字段
class User {
private int id;
private String name;
private int age;
public User() {}
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
// getters and setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', age=" + age + "}";
}
}
public class JdbcExample {
private static final String URL = "jdbc:mysql://localhost:3306/test";
private static final String USER = "root";
private static final String PASSWORD = "password";
public static void main(String[] args) {
// 创建
createUser(new User(0, "John", 25));
// 查询
List<User> users = readUsers();
for (User user : users) {
System.out.println(user);
}
// 更新
updateUser(new User(1, "UpdatedJohn", 26));
// 删除
deleteUser(1);
}
public static void createUser(User user) {
String sql = "INSERT INTO user (name, age) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, user.getName());
pstmt.setInt(2, user.getAge());
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static List<User> readUsers() {
List<User> users = new ArrayList<>();
String sql = "SELECT * FROM user";
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
users.add(new User(id, name, age));
}
} catch (SQLException e) {
e.printStackTrace();
}
return users;
}
public static void updateUser(User user) {
String sql = "UPDATE user SET name = ?, age = ? WHERE id = ?";
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, user.getName());
pstmt.setInt(2, user.getAge());
pstmt.setInt(3, user.getId());
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void deleteUser(int id) {
String sql = "DELETE FROM user WHERE id = ?";
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
2. MyBatis 实现 CURD
实体类
public class User {
private int id;
private String name;
private int age;
// getters and setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', age=" + age + "}";
}
}
Mapper 接口
public interface UserMapper {
void insertUser(User user);
List<User> selectAllUsers();
void updateUser(User user);
void deleteUser(int id);
}
Mapper XML 文件(UserMapper.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.example.mapper.UserMapper">
<insert id="insertUser" parameterType="com.example.entity.User">
INSERT INTO user (name, age) VALUES (#{name}, #{age})
</insert>
<select id="selectAllUsers" resultType="com.example.entity.User">
SELECT * FROM user
</select>
<update id="updateUser" parameterType="com.example.entity.User">
UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}
</update>
<delete id="deleteUser" parameterType="int">
DELETE FROM user WHERE id = #{id}
</delete>
</mapper>
测试代码
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.List;
public class MyBatisExample {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper userMapper = session.getMapper(UserMapper.class);
// 创建
User user = new User(0, "John", 25);
userMapper.insertUser(user);
// 查询
List<User> users = userMapper.selectAllUsers();
for (User u : users) {
System.out.println(u);
}
// 更新
user.setName("UpdatedJohn");
user.setAge(26);
userMapper.updateUser(user);
// 删除
userMapper.deleteUser(user.getId());
session.commit();
}
}
}
3. MyBatis-Plus 实现 CURD
实体类
import com.baomidou.mybatisplus.annotation.TableName;
@TableName("user")
public class User {
private int id;
private String name;
private int age;
// getters and setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', age=" + age + "}";
}
}
Mapper 接口
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface UserMapper extends BaseMapper<User> {
}
测试代码
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.List;
@SpringBootApplication
public class MyBatisPlusExample implements CommandLineRunner {
@Autowired
private UserMapper userMapper;
public static void main(String[] args) {
SpringApplication.run(MyBatisPlusExample.class, args);
}
@Override
public void run(String... args) throws Exception {
// 创建
User user = new User();
user.setName("John");
user.setAge(25);
userMapper.insert(user);
// 查询
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
List<User> users = userMapper.selectList(queryWrapper);
for (User u : users) {
System.out.println(u);
}
// 更新
user.setName("UpdatedJohn");
user.setAge(26);
userMapper.updateById(user);
// 删除
userMapper.deleteById(user.getId());
}
}
三、MyBatis 实现一对多查询
数据库表结构
假设存在 user
表和 order
表,一个用户可以有多个订单,表结构如下:
-- 用户表
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50)
);
-- 订单表
CREATE TABLE `order` (
id INT PRIMARY KEY AUTO_INCREMENT,
order_number VARCHAR(50),
user_id INT,
FOREIGN KEY (user_id) REFERENCES user(id)
);
实体类
import java.util.List;
public class User {
private int id;
private String name;
private List<Order> orders;
// getters and setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Order> getOrders() {
return orders;
}
public void setOrders(List<Order> orders) {
this.orders = orders;
}
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', orders=" + orders + "}";
}
}
public class Order {
private int id;
private String orderNumber;
private int userId;
// getters and setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
@Override
public String toString() {
return "Order{id=" + id + ", orderNumber='" + orderNumber + "', userId=" + userId + "}";
}
}
Mapper 接口
public interface UserMapper {
List<User> selectAllUsersWithOrders();
}
Mapper XML 文件(UserMapper.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.example.mapper.UserMapper">
<resultMap id="UserWithOrdersResultMap" type="com.example.entity.User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<collection property="orders" ofType="com.example.entity.Order">
<id property="id" column="order_id"/>
<result property="orderNumber" column="order_number"/>
<result property="userId" column="user_id"/>
</collection>
</resultMap>
<select id="selectAllUsersWithOrders" resultMap="UserWithOrdersResultMap">
SELECT u.id AS user_id, u.name AS user_name, o.id AS order_id, o.order_number
FROM user u
LEFT JOIN `order` o ON u.id = o.user_id
</select>
</mapper>
测试代码
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.List;
public class OneToManyExample {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper userMapper = session.getMapper(UserMapper.class);
List<User> users = userMapper.selectAllUsersWithOrders();
for (User user : users) {
System.out.println(user);
}
}
}
}
以上代码展示了 JDBC、MyBatis、MyBatis-Plus 实现 CURD 以及 MyBatis 实现一对多查询的详细过程。注意,运行代码前需要确保数据库连接配置正确,并且添加相应的依赖(如 MyBatis、MyBatis-Plus、JDBC 驱动等)。
MyBatis-Plus 实现一对多
以下将详细介绍如何使用 MyBatis-Plus 结合 mapper.xml
实现一对多查询,仍以用户(User
)和订单(Order
)的一对多关系为例。
1. 数据库表结构
-- 用户表
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50)
);
-- 订单表
CREATE TABLE `order` (
id INT PRIMARY KEY AUTO_INCREMENT,
order_number VARCHAR(50),
user_id INT,
FOREIGN KEY (user_id) REFERENCES user(id)
);
2. 实体类定义
// User.java
public class User {
private Integer id;
private String username;
private List<Order> orders;
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 List<Order> getOrders() {
return orders;
}
public void setOrders(List<Order> orders) {
this.orders = orders;
}
}
// Order.java
public class Order {
private Integer id;
private String orderNumber;
private Integer userId;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
}
3. Mapper 接口定义
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
// UserMapper.java
public interface UserMapper extends BaseMapper<User> {
/**
* 查询所有用户及其关联的订单
* @return 用户列表
*/
List<User> getAllUsersWithOrders();
}
4. Mapper XML 文件(UserMapper.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.example.mapper.UserMapper">
<!-- 定义结果映射 -->
<resultMap id="UserWithOrdersResultMap" type="com.example.entity.User">
<id property="id" column="user_id"/>
<result property="username" column="username"/>
<!-- 一对多关联,使用 collection 标签 -->
<collection property="orders" ofType="com.example.entity.Order">
<id property="id" column="order_id"/>
<result property="orderNumber" column="order_number"/>
<result property="userId" column="user_id"/>
</collection>
</resultMap>
<!-- 查询所有用户及其关联的订单 -->
<select id="getAllUsersWithOrders" resultMap="UserWithOrdersResultMap">
SELECT
u.id AS user_id,
u.username,
o.id AS order_id,
o.order_number
FROM
user u
LEFT JOIN
`order` o ON u.id = o.user_id
</select>
</mapper>
5. Service 层实现
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
// UserService.java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> getUsersWithOrders() {
return userMapper.getAllUsersWithOrders();
}
}
6. 测试代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.List;
// Application.java
@SpringBootApplication
public class Application implements CommandLineRunner {
@Autowired
private UserService userService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
public void run(String... args) throws Exception {
List<User> users = userService.getUsersWithOrders();
for (User user : users) {
System.out.println("User: " + user.getUsername());
List<Order> orders = user.getOrders();
if (orders != null) {
for (Order order : orders) {
System.out.println(" Order: " + order.getOrderNumber());
}
}
}
}
}
代码解释
- 实体类:
User
类中包含一个List<Order>
类型的orders
属性,用于存储该用户关联的所有订单。 - Mapper 接口:
UserMapper
继承自BaseMapper<User>
,并定义了getAllUsersWithOrders
方法用于查询所有用户及其关联的订单。 - Mapper XML 文件:
resultMap
标签定义了结果映射规则,id
标签用于映射主键,result
标签用于映射普通字段。collection
标签用于处理一对多关系,ofType
属性指定集合中元素的类型。select
语句使用LEFT JOIN
关联user
表和order
表,确保即使某个用户没有订单,该用户信息也会被查询出来。
- Service 层:
UserService
中的getUsersWithOrders
方法调用UserMapper
的getAllUsersWithOrders
方法获取用户及其关联的订单信息。 - 测试代码:在
Application
类中调用UserService
的getUsersWithOrders
方法,并打印查询结果。
通过以上步骤,你可以使用 MyBatis-Plus 结合 mapper.xml
实现一对多查询。
JDBC、MyBatis、MyBatis-Plus是Java开发中常用的数据库操作技术,以下是它们的架构图、底层原理和底层实现的相关介绍:
JDBC
- 架构图
- JDBC架构主要包括Java应用程序、JDBC API、JDBC驱动管理器、JDBC驱动程序和数据库。Java应用程序通过JDBC API与JDBC驱动管理器交互,驱动管理器根据不同的数据库类型选择合适的JDBC驱动程序,然后通过驱动程序与数据库建立连接并执行SQL操作。
- 底层原理
- 基于Java的接口和类实现对各种数据库的统一操作。它定义了一系列标准的接口,如
Connection
、Statement
、ResultSet
等,这些接口提供了与数据库交互的方法。不同数据库厂商提供各自的JDBC驱动实现这些接口,使得Java程序能够以统一的方式操作不同类型的数据库。
- 基于Java的接口和类实现对各种数据库的统一操作。它定义了一系列标准的接口,如
- 底层实现
- 当Java程序加载JDBC驱动时,会通过
Class.forName()
方法将驱动类加载到内存中。驱动类实现了java.sql.Driver
接口,在加载过程中会向DriverManager
注册自己。DriverManager
负责管理和维护已注册的驱动程序列表。当需要建立数据库连接时,DriverManager
会根据连接字符串中的数据库标识,选择合适的驱动程序来建立连接。在连接建立后,通过Statement
或PreparedStatement
接口来执行SQL语句,将SQL语句发送到数据库服务器,数据库服务器执行完后将结果以ResultSet
的形式返回给Java程序。
- 当Java程序加载JDBC驱动时,会通过
MyBatis
- 架构图
- MyBatis的架构主要涉及应用程序、SQL映射配置文件(XML或注解)、MyBatis核心组件(如SqlSessionFactory、SqlSession等)、JDBC和数据库。应用程序通过MyBatis核心组件与数据库交互,核心组件根据配置文件中的信息来构建SQL语句并执行。
- 底层原理
- 核心是将SQL语句与Java对象进行映射。通过配置文件或注解定义SQL语句以及参数和结果的映射关系,在运行时,MyBatis根据这些配置信息,将Java方法的参数转换为SQL语句中的参数值,执行SQL后再将结果集映射为Java对象返回给应用程序。
- 底层实现
- 利用Java的反射机制和动态代理技术。在启动时,MyBatis会解析配置文件或扫描注解,将SQL语句、参数映射、结果映射等信息加载到内存中,构建
Configuration
对象。通过SqlSessionFactory
创建SqlSession
,SqlSession
是与数据库交互的核心接口。当执行SQL操作时,MyBatis会根据配置信息创建StatementHandler
、ParameterHandler
、ResultSetHandler
等对象来处理SQL语句的执行、参数设置和结果处理。对于接口的映射,MyBatis使用动态代理为接口生成代理对象,在代理对象中拦截方法调用,根据方法名和参数等信息构建并执行SQL语句。
- 利用Java的反射机制和动态代理技术。在启动时,MyBatis会解析配置文件或扫描注解,将SQL语句、参数映射、结果映射等信息加载到内存中,构建
MyBatis-Plus
- 架构图
- MyBatis-Plus在MyBatis的基础上进行了扩展,架构上增加了代码生成器、分页插件、性能分析插件等组件。应用程序通过MyBatis-Plus提供的增强API与数据库交互,这些API在MyBatis核心组件的基础上进行了封装和扩展。
- 底层原理
- 基于MyBatis框架,对MyBatis的功能进行增强和扩展。它提供了通用的CRUD操作接口和方法,通过约定大于配置的方式,减少了开发人员编写SQL语句的工作量。同时,支持自动生成代码、分页插件、逻辑删除等功能。
- 底层实现
- 继承了MyBatis的底层实现机制,通过自定义插件和拦截器来实现功能扩展。例如,在启动时,MyBatis-Plus的代码生成器可以根据数据库表结构和配置信息自动生成实体类、Mapper接口、Mapper XML等文件。分页插件通过拦截
StatementHandler
,在SQL语句执行前对其进行改写,添加分页相关的逻辑。对于通用的CRUD操作,MyBatis-Plus利用MyBatis的Configuration
和SqlSession
等核心对象,根据实体类的信息和方法调用,自动构建SQL语句并执行。
- 继承了MyBatis的底层实现机制,通过自定义插件和拦截器来实现功能扩展。例如,在启动时,MyBatis-Plus的代码生成器可以根据数据库表结构和配置信息自动生成实体类、Mapper接口、Mapper XML等文件。分页插件通过拦截
JDBC、MyBatis、MyBatis-Plus 中的事务分别是什么?怎么实现的给出代码或者sql脚本? 底层原理是什么?
1. JDBC 中的事务
事务概念
在 JDBC 中,事务是一组不可分割的数据库操作序列,这些操作要么全部成功执行并持久化到数据库(提交),要么在出现错误时全部撤销(回滚),以此保证数据的一致性和完整性。
实现代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class JDBCTransactionExample {
private static final String URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASSWORD = "your_password";
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
try {
// 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接
connection = DriverManager.getConnection(URL, USER, PASSWORD);
// 关闭自动提交,开启事务
connection.setAutoCommit(false);
statement = connection.createStatement();
// 执行一系列 SQL 操作
String sql1 = "INSERT INTO account (id, balance) VALUES (1, 1000)";
statement.executeUpdate(sql1);
String sql2 = "UPDATE account SET balance = balance - 500 WHERE id = 1";
statement.executeUpdate(sql2);
// 提交事务
connection.commit();
System.out.println("事务提交成功");
} catch (Exception e) {
// 出现异常,回滚事务
if (connection != null) {
try {
connection.rollback();
System.out.println("事务回滚成功");
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 关闭资源
try {
if (statement != null) statement.close();
if (connection != null) connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
底层原理
JDBC 事务的底层原理基于数据库本身的事务管理机制。当调用 connection.setAutoCommit(false)
时,JDBC 驱动会通知数据库服务器开始一个新的事务。之后执行的 SQL 语句都在这个事务的上下文中进行。当调用 connection.commit()
时,JDBC 驱动会向数据库服务器发送提交事务的指令,数据库服务器将事务中所有操作的结果持久化到磁盘;当调用 connection.rollback()
时,数据库服务器会撤销事务中已经执行的操作。
2. MyBatis 中的事务
事务概念
MyBatis 中的事务和 JDBC 事务本质相同,是为了确保一组数据库操作的原子性、一致性、隔离性和持久性(ACID 特性)。
实现代码
以下是使用 MyBatis 手动控制事务的示例:
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MyBatisTransactionExample {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
// 加载 MyBatis 配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取 SqlSession,关闭自动提交,开启事务
sqlSession = sqlSessionFactory.openSession(false);
// 执行数据库操作,假设存在一个 UserMapper 接口和对应的方法
// UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// userMapper.insertUser(new User());
// userMapper.updateUser(new User());
// 提交事务
sqlSession.commit();
System.out.println("事务提交成功");
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
// 出现异常,回滚事务
if (sqlSession != null) {
sqlSession.rollback();
System.out.println("事务回滚成功");
}
e.printStackTrace();
} finally {
// 关闭 SqlSession
if (sqlSession != null) {
sqlSession.close();
}
}
}
}
底层原理
MyBatis 事务底层依赖于 JDBC 事务。MyBatis 的 SqlSession
持有一个 Connection
对象,当调用 sqlSessionFactory.openSession(false)
时,会将 Connection
的自动提交模式关闭,开启一个新的事务。在事务执行过程中,MyBatis 通过 StatementHandler
、ParameterHandler
等组件将 SQL 语句发送到数据库执行。当调用 sqlSession.commit()
或 sqlSession.rollback()
时,实际上是调用 Connection
的 commit()
或 rollback()
方法来提交或回滚事务。
3. MyBatis-Plus 中的事务
事务概念
MyBatis-Plus 是基于 MyBatis 的增强工具,其事务概念和 MyBatis 一致,用于保证数据库操作的一致性和完整性。
实现代码
通常结合 Spring Boot 和 Spring 的事务管理,使用 @Transactional
注解来实现事务控制。
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void doTransaction() {
// 执行数据库操作
User user = new User();
user.setName("John");
userMapper.insert(user);
// 模拟异常
int result = 1 / 0;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "John");
List<User> users = userMapper.selectList(queryWrapper);
for (User u : users) {
u.setName("Updated John");
userMapper.updateById(u);
}
}
}
底层原理
MyBatis-Plus 借助 Spring 的事务管理机制,通过 Spring AOP(面向切面编程)实现事务控制。当方法上添加了 @Transactional
注解时,Spring 在方法执行前会创建一个事务上下文,获取 DataSourceTransactionManager
管理的 Connection
,并将其绑定到当前线程。在方法执行过程中,MyBatis-Plus 的数据库操作会在这个事务上下文中进行。如果方法正常执行完毕,Spring 的事务管理器会调用 Connection
的 commit()
方法提交事务;如果方法抛出异常,事务管理器会根据异常类型和 @Transactional
注解的配置,调用 Connection
的 rollback()
方法回滚事务。
JDBC、MyBatis、MyBatis-Plus怎么保证幂等性
你想问的可能是“JDBC、MyBatis、MyBatis - Plus 怎么保证幂等性”。幂等性是指一个操作无论执行多少次,所产生的影响都和执行一次的影响相同。下面分别介绍这三种技术保证幂等性的方法:
JDBC 保证幂等性
1. 唯一索引
在数据库表中创建唯一索引,例如在插入操作时,如果存在唯一索引,重复插入相同的数据会导致数据库抛出唯一约束冲突异常,从而避免重复插入。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class JDBCIdempotencyExample {
private static final String URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASSWORD = "your_password";
public static void main(String[] args) {
try (Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
// 假设表中有一个唯一索引在 username 字段上
String sql = "INSERT INTO users (username, email) VALUES (?, ?)";
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
preparedStatement.setString(1, "unique_user");
preparedStatement.setString(2, "user@example.com");
try {
preparedStatement.executeUpdate();
System.out.println("插入成功");
} catch (SQLException e) {
if (e.getSQLState().equals("23000")) {
System.out.println("由于唯一约束,插入失败,数据可能已存在");
} else {
e.printStackTrace();
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
2. 查询 - 插入模式
在执行插入操作之前,先查询数据是否已经存在,如果存在则不进行插入操作。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JDBCQueryInsertExample {
private static final String URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASSWORD = "your_password";
public static void main(String[] args) {
try (Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
// 查询数据是否存在
String querySql = "SELECT id FROM users WHERE username = ?";
try (PreparedStatement queryStatement = connection.prepareStatement(querySql)) {
queryStatement.setString(1, "unique_user");
try (ResultSet resultSet = queryStatement.executeQuery()) {
if (!resultSet.next()) {
// 数据不存在,进行插入操作
String insertSql = "INSERT INTO users (username, email) VALUES (?, ?)";
try (PreparedStatement insertStatement = connection.prepareStatement(insertSql)) {
insertStatement.setString(1, "unique_user");
insertStatement.setString(2, "user@example.com");
insertStatement.executeUpdate();
System.out.println("插入成功");
}
} else {
System.out.println("数据已存在,不进行插入");
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
MyBatis 保证幂等性
1. 使用唯一索引和异常处理
和 JDBC 类似,利用数据库的唯一索引,在 MyBatis 的 Mapper 接口方法中捕获唯一约束冲突异常进行处理。
import org.apache.ibatis.exceptions.PersistenceException;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
public class MyBatisIdempotencyExample {
public static void main(String[] args) {
String resource = "mybatis-config.xml";
try (InputStream inputStream = org.apache.ibatis.io.Resources.getResourceAsStream(resource)) {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setUsername("unique_user");
user.setEmail("user@example.com");
try {
userMapper.insertUser(user);
sqlSession.commit();
System.out.println("插入成功");
} catch (PersistenceException e) {
if (e.getCause() instanceof java.sql.SQLIntegrityConstraintViolationException) {
System.out.println("由于唯一约束,插入失败,数据可能已存在");
} else {
e.printStackTrace();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
2. 查询 - 插入逻辑
在 Service 层实现查询 - 插入的逻辑。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void insertUserIfNotExists(User user) {
User existingUser = userMapper.selectUserByUsername(user.getUsername());
if (existingUser == null) {
userMapper.insertUser(user);
}
}
}
MyBatis - Plus 保证幂等性
1. 利用唯一索引和异常处理
MyBatis - Plus 基于 MyBatis,同样可以利用数据库的唯一索引,在调用 MyBatis - Plus 提供的插入方法时捕获唯一约束冲突异常。
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void insertUser(User user) {
try {
userMapper.insert(user);
} catch (MybatisPlusException e) {
if (e.getCause() instanceof java.sql.SQLIntegrityConstraintViolationException) {
System.out.println("由于唯一约束,插入失败,数据可能已存在");
} else {
e.printStackTrace();
}
}
}
}
2. 使用 saveOrUpdate
方法
MyBatis - Plus 提供了 saveOrUpdate
方法,该方法会根据实体的主键是否存在来决定是执行插入还是更新操作,一定程度上保证了幂等性。
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> {
public void saveOrUpdateUser(User user) {
this.saveOrUpdate(user);
}
}
3. 分布式锁和令牌机制(适用于高并发场景)
在高并发场景下,可以使用分布式锁(如 Redis 分布式锁)或令牌机制来保证操作的幂等性。例如,在执行操作前,先从 Redis 中获取一个唯一的令牌,操作完成后将令牌存储在 Redis 中,后续再次请求时先检查令牌是否已存在,如果存在则不执行操作。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void insertUserWithIdempotency(User user) {
String token = UUID.randomUUID().toString();
if (redisTemplate.opsForValue().setIfAbsent(token, "processed")) {
try {
userMapper.insert(user);
} finally {
redisTemplate.delete(token);
}
} else {
System.out.println("操作已执行,避免重复操作");
}
}
}
以上就是 JDBC、MyBatis、MyBatis - Plus 保证幂等性的常见方法,不同的场景可以选择合适的方式来确保操作的幂等性。
JDBC、MyBatis 和 MyBatis - Plus 的事务操作本质上是基于数据库事务
是的,你的理解是正确的。JDBC、MyBatis 和 MyBatis - Plus 的事务操作本质上是基于数据库事务的,若数据库本身不支持事务,这些框架即便有操作事务的方法和功能,也无法保证事务的特性。下面为你详细解释:
本质依赖数据库事务
- JDBC:作为 Java 与数据库交互的基础 API,JDBC 本身并没有实现事务机制,而是通过调用数据库驱动提供的方法来管理事务。例如,
Connection
类中的setAutoCommit
、commit
和rollback
方法,实际上是委托给数据库驱动与数据库进行交互,依赖数据库自身的事务处理能力来实现事务的开启、提交和回滚。 - MyBatis:MyBatis 是一个持久层框架,它在 JDBC 的基础上进行了封装和抽象。MyBatis 的事务管理器(如
JdbcTransaction
和ManagedTransaction
)底层还是借助 JDBC 与数据库进行通信,依赖数据库的事务功能来保证数据操作的一致性和完整性。 - MyBatis - Plus:MyBatis - Plus 是 MyBatis 的增强工具,它继承了 MyBatis 的事务管理机制。在使用 Spring 集成时,虽然通过
@Transactional
注解等方式简化了事务的使用,但本质上依然依赖数据库的事务支持,只是将事务管理的配置和使用进行了封装。
数据库不支持事务时的情况
- 无法保证原子性:事务的原子性要求一组操作要么全部成功,要么全部失败。如果数据库不支持事务,当执行一系列操作时,可能会出现部分操作成功,部分操作失败的情况,无法实现原子性。例如,在一个转账操作中,从一个账户扣款成功,但向另一个账户存款失败,由于没有事务的支持,无法回滚扣款操作,导致数据不一致。
- 无法保证一致性:一致性是指事务执行前后数据库的状态保持一致。没有数据库事务的支持,在并发操作或异常情况下,数据可能会处于不一致的状态。例如,多个线程同时对同一数据进行修改,由于没有事务的隔离机制,可能会导致数据的错误更新。
- 无法保证隔离性:隔离性是指多个事务之间相互隔离,互不干扰。数据库不支持事务时,无法提供不同的隔离级别来控制事务之间的可见性,可能会出现脏读、不可重复读、幻读等问题。
- 无法保证持久性:持久性要求事务一旦提交,其对数据库的修改就永久保存。若数据库不支持事务,在系统崩溃或出现其他异常时,已经执行的操作可能会丢失,无法保证数据的持久性。
综上所述,JDBC、MyBatis 和 MyBatis - Plus 的事务操作依赖于数据库的事务支持,数据库的事务功能是实现这些框架事务特性的基础。
JDBC、MyBatis 和 MyBatis - Plus 怎么实现逻辑删除?
逻辑删除是指在数据库中并不真正删除数据,而是通过设置一个标志位来表示该数据已被删除,这样可以避免物理删除带来的数据丢失风险,方便后续的数据恢复和审计。以下分别介绍 JDBC、MyBatis 和 MyBatis - Plus 实现逻辑删除的方法:
JDBC 实现逻辑删除
1. 数据库表设计
在需要进行逻辑删除的表中添加一个逻辑删除标志字段,例如 is_deleted
,通常使用 0
表示未删除,1
表示已删除。
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
is_deleted TINYINT(1) DEFAULT 0
);
2. Java 代码实现
在执行删除操作时,将 is_deleted
字段更新为 1
,而不是真正删除记录。在查询数据时,过滤掉 is_deleted
为 1
的记录。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JDBCLogicDeleteExample {
private static final String URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASSWORD = "your_password";
public static void main(String[] args) {
// 逻辑删除操作
try (Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
String deleteSql = "UPDATE users SET is_deleted = 1 WHERE id = ?";
try (PreparedStatement preparedStatement = connection.prepareStatement(deleteSql)) {
preparedStatement.setInt(1, 1);
preparedStatement.executeUpdate();
System.out.println("逻辑删除成功");
}
} catch (SQLException e) {
e.printStackTrace();
}
// 查询未删除的数据
try (Connection connection = DriverManager.getConnection(URL, USER, PASSWORD)) {
String selectSql = "SELECT * FROM users WHERE is_deleted = 0";
try (PreparedStatement preparedStatement = connection.prepareStatement(selectSql);
ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
System.out.println("ID: " + resultSet.getInt("id") + ", Username: " + resultSet.getString("username"));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
MyBatis 实现逻辑删除
1. 数据库表设计
同样在表中添加 is_deleted
字段。
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
is_deleted TINYINT(1) DEFAULT 0
);
2. 实体类定义
在实体类中添加 isDeleted
属性,并使用 @TableLogic
注解(如果使用 MyBatis - Plus 注解支持)或在 SQL 中手动处理。
import lombok.Data;
@Data
public class User {
private Integer id;
private String username;
private Integer isDeleted;
}
3. Mapper 接口和 XML 文件
在 Mapper 接口中定义逻辑删除和查询方法,在 XML 文件中编写相应的 SQL 语句。
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
int logicDeleteById(Integer id);
}
<?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.example.mapper.UserMapper">
<update id="logicDeleteById">
UPDATE users
SET is_deleted = 1
WHERE id = #{id}
</update>
<select id="selectAllUsers" resultType="com.example.entity.User">
SELECT * FROM users WHERE is_deleted = 0
</select>
</mapper>
MyBatis - Plus 实现逻辑删除
1. 数据库表设计
添加 is_deleted
字段。
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50),
is_deleted TINYINT(1) DEFAULT 0
);
2. 实体类定义
使用 @TableLogic
注解标记逻辑删除字段。
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;
@Data
public class User {
private Integer id;
private String username;
@TableLogic
private Integer isDeleted;
}
3. 配置逻辑删除规则
在配置类中配置逻辑删除的规则。
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
dbConfig.setLogicDeleteField("is_deleted");
dbConfig.setLogicNotDeleteValue("0");
dbConfig.setLogicDeleteValue("1");
globalConfig.setDbConfig(dbConfig);
return globalConfig;
}
}
4. 使用 MyBatis - Plus 进行逻辑删除和查询
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
public void logicDeleteUser(Integer id) {
this.removeById(id); // 调用 MyBatis - Plus 的 removeById 方法进行逻辑删除
}
public void selectAllUsers() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
List<User> users = this.list(queryWrapper); // 查询未删除的数据
for (User user : users) {
System.out.println("ID: " + user.getId() + ", Username: " + user.getUsername());
}
}
}
以上就是 JDBC、MyBatis 和 MyBatis - Plus 实现逻辑删除的方法,其中 MyBatis - Plus 提供了更便捷的方式来实现逻辑删除,减少了开发人员的工作量。