前言
在使用 MyBatis 进行数据库操作时,很多开发者会依赖返回的行数来判断操作是否成功。然而,默认配置下,MySQL 数据库返回的行数可能会导致一些误判。这篇文章将详细探讨 MyBatis 返回行数的原理、常见问题及如何通过配置来避免这些陷阱,从而确保应用程序的可靠性和准确性。此外,我们还将介绍在无法配置 useAffectedRows=true
的情况下,其他可行的解决方案。
MyBatis 默认行为
匹配行数 vs. 受影响行数
MySQL 默认返回的是匹配的行数(matched rows),而不是受影响的行数(affected rows)。这两者的区别在于:
- 匹配行数:表示查询条件匹配到的行数,无论这些行是否实际被修改。
- 受影响行数:表示实际被修改的行数,只有在数据发生改变时才会增加。
默认行为的潜在问题
假设我们有以下更新操作:
UPDATE users SET name = 'John Doe' WHERE id = 1;
如果用户的名字已经是 ‘John Doe’,匹配的行数是 1,但受影响的行数是 0。如果我们依赖于返回的行数来判断操作是否成功,这种行为会导致误判。
配置 useAffectedRows=true
为了确保返回的行数是实际受影响的行数,我们需要在 JDBC URL 中配置 useAffectedRows=true
。
JDBC URL 配置示例
jdbc:mysql://localhost:3306/mydatabase?useSSL=false&useAffectedRows=true
配置 MyBatis
MyBatis 配置文件(mybatis-config.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//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/mydatabase?useSSL=false&useAffectedRows=true"/><property name="username" value="root"/><property name="password" value="password"/></dataSource></environment></environments><mappers><mapper resource="org/mybatis/example/BlogMapper.xml"/></mappers>
</configuration>
Spring Boot 配置
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase?useSSL=false&useAffectedRows=true
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
application.yml
spring:datasource:url: jdbc:mysql://localhost:3306/mydatabase?useSSL=false&useAffectedRows=trueusername: rootpassword: passworddriver-class-name: com.mysql.cj.jdbc.Driver
使用示例
假设我们有一个更新用户信息的方法,我们希望在没有行被更新时抛出异常:
Mapper 接口
public interface UserMapper {@Update("UPDATE users SET name = #{name} WHERE id = #{id}")int updateUser(User user);@Select("SELECT * FROM users WHERE id = #{id}")User findUserById(int id);
}
服务层代码
public void updateUser(User user) throws Exception {// 查询当前版本号User currentUser = userMapper.findUserById(user.getId());user.setVersion(currentUser.getVersion());// 执行更新操作int affectedRows = userMapper.updateUser(user);// 检查是否更新成功if (affectedRows == 0) {throw new Exception("User update failed, no rows affected.");}
}
在这个示例中,通过配置 useAffectedRows=true
,我们确保 affectedRows
的值是实际受影响的行数。如果没有行被更新,affectedRows
将是零,从而抛出异常。
无法配置 useAffectedRows=true
的解决方案
如果无法配置 useAffectedRows=true
,仍有其他方法可以判断更新操作是否成功。这些方法包括:
方法一:手动校验数据的变化
在执行更新操作前后,手动查询并比较数据是否发生变化。这种方法适合在数据变化不频繁的情况下使用。
示例代码
假设我们有一个 User
表,需要更新用户的名字。我们可以先查询当前名字,再执行更新操作,最后比较更新前后的名字。
public void updateUser(User user) throws Exception {// 查询当前数据User currentUser = userMapper.findUserById(user.getId());String currentName = currentUser.getName();// 执行更新操作int affectedRows = userMapper.updateUser(user);// 更新后再查询一次User updatedUser = userMapper.findUserById(user.getId());String updatedName = updatedUser.getName();// 比较更新前后的数据if (affectedRows == 0 || currentName.equals(updatedName)) {throw new Exception("User update failed, no rows affected.");}
}
方法二:使用触发器
可以在数据库层面创建触发器,当数据发生变化时记录日志或进行其他操作。
示例
假设我们有一个 User
表,我们可以创建一个触发器来记录每次更新操作。
CREATE TABLE UserUpdateLog (id INT AUTO_INCREMENT PRIMARY KEY,userId INT,oldName VARCHAR(255),newName VARCHAR(255),updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);DELIMITER $$
CREATE TRIGGER before_user_update
BEFORE UPDATE ON users
FOR EACH ROW
BEGININSERT INTO UserUpdateLog(userId, oldName, newName)VALUES (OLD.id, OLD.name, NEW.name);
END$$
DELIMITER ;
然后在应用程序中检查触发器日志来判断是否更新成功。
方法三:使用乐观锁
通过在表中添加一个版本号或时间戳字段,每次更新时检查并更新版本号或时间戳,以确保数据的一致性。
示例
假设我们有一个 User
表,包含 version
字段。
Mapper 接口
public interface UserMapper {@Update("UPDATE users SET name = #{name}, version = version + 1 WHERE id = #{id} AND version = #{version}")int updateUser(User user);
}
服务层代码
public void updateUser(User user) throws Exception {// 查询当前版本号User currentUser = userMapper.findUserById(user.getId());user.setVersion(currentUser.getVersion());// 执行更新操作int affectedRows = userMapper.updateUser(user);// 检查是否更新成功if (affectedRows == 0) {throw new Exception("User update failed, possibly due to concurrent modification.");}
}
总结
尽管配置 useAffectedRows=true
是最直接、最简便的方法,但在无法配置的情况下,仍有其他方法可以确保更新操作的成功:
- 手动校验数据的变化:适合数据变化不频繁的场景。
- 使用触发器:在数据库层记录变化,适合需要详细记录数据变化的场景。
- 乐观锁:基于版本号或时间戳来确保数据一致性,适合并发更新的场景。
这些方法可以根据实际业务需求和系统架构选择使用,以确保数据更新操作的可靠性和准确性。通过上述配置和方法,你的应用程序可以更可靠地判断更新操作的成功与否,从而开发出更加健壮和可靠的应用程序。