MyBatis 查詢結果與 MySQL 執行結果不一致?

1. 碎碎念

最近在業務中遇到一個問題,業務是這樣的:在插入新用戶時須要校驗用戶的某些信息是否惟一,而在程序中校驗結果永遠是不惟一的。而後我把 MyBatis 打印的執行 SQL 語句拿了出來在數據庫中執行,發現沒有數據。java

而後我就奇怪了,數據庫是同一個啊、SQL 是同一個啊、查詢結果都沒有變啊,爲何執行的結果在程序裏面是 1,而在數據庫中是0。mysql

難道是由於 MyBatis 和數據庫執行的結果不同?ios

image.png

後來我才明白不一致的緣由。web

我編寫了一個與實際業務相似的代碼,用來模擬上述的問題。spring

2. 復現問題

2.1. 表結構

MySQL 數據庫中建立了一張用戶表,只有4個字段。sql

CREATE TABLE `user`  (
  `user_id` varchar(36) NOT NULL COMMENT '用戶主鍵id',
  `user_name` varchar(55) NULL DEFAULT NULL COMMENT '帳號',
  `password` varchar(55) NULL DEFAULT NULL COMMENT '密碼',
  `email` varchar(55) NULL DEFAULT NULL COMMENT '郵箱',
  PRIMARY KEY (`user_id`) USING BTREE
);
複製代碼

2.2. 項目依賴

示例項目是一個 SpringBoot 工程,pom 文件中除了 web 依賴還有 mysql 的驅動、MyBatis 和 lombok。數據庫

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.2.6.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.8</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.0</version>
    </dependency>
</dependencies>
複製代碼

2.2. 業務

業務流程是這樣:新建一個用戶,在新建用戶以前首先校驗郵箱是否已在數據庫中存在,而後執行一些其餘的業務,而後執行 insert 方法插入數據庫,而後執行一些其餘的業務,最後再校驗 user_name 是否已存在。瀏覽器

@Slf4j
@RestController
public class TestController {

    @Resource
    private UserMapper userMapper;

    /** * springboot Dao層查詢結果與數據庫實際執行結果不一致? */
    @GetMapping("test")
    @Transactional(rollbackFor = RuntimeException.class)
    public void transactionalDemo() {
        // 要插入的數據
        User user = new User();
        user.setUserId("userId");
        user.setUserName("planeswalker");
        user.setPassword("password");
        user.setEmail("123@gmail.com");
        // 校驗郵箱
        if (userMapper.countByEmail(user.getEmail())>0) {
            throw new RuntimeException("插入失敗,user_id 重複");
        }
        // 執行插入用戶操做
        userMapper.insert(user);
        // 校驗 user_name
        if (userMapper.countByName(user.getUserName())>0) {
            throw new RuntimeException("插入失敗,user_name 重複");
        }
        log.info("do something others...");
    }
}
複製代碼

userMapper 接口類的代碼以下:springboot

@Repository
public interface UserMapper {

    /** * 查詢 email 是否重複 * @param email * @return */
    @Select("select count(*) from user where email=#{email}")
    int countByEmail(String email);

    /** * 查詢 name 是否重複 * @param userName * @return */
    @Select("select count(*) from user where user_name=#{userName}")
    int countByName(String userName);
}
複製代碼

我認可這個方法確實可能不是特別好,好比校驗重複的方法爲何有兩次,好比 user_id 校驗重複方法的合理性。但爲了與我在項目中遇到的問題作模擬,這是很相似的,在項目中就是在插入後又校驗了一次(由於公用的校驗方法會查詢兩張表而其中一張表的數據是在校驗以前插入的)。mybatis

可能不少同窗已經知道這個業務的問題所在了,先很少說,執行就好了。

2.3. 測試

當我在瀏覽器上訪問這個接口http://127.0.0.1:8080/test後,控制檯輸出了以下的內容:

2020-05-27 14:07:09.183 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByEmail    : ==>  Preparing: select count(*) from user where email=? 
2020-05-27 14:07:09.208 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByEmail    : ==> Parameters: 123@gmail.com(String)
2020-05-27 14:07:09.218 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByEmail    : <==      Total: 1
2020-05-27 14:07:09.233 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.mapper.UserMapper.insert     : ==>  Preparing: INSERT INTO user ( user_id,user_name,password,email ) VALUES( ?,?,?,? ) 2020-05-27 14:07:09.234 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.mapper.UserMapper.insert : ==> Parameters: userId(String), planeswalker(String), password(String), 123@gmail.com(String)
2020-05-27 14:07:09.237 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.mapper.UserMapper.insert     : <==    Updates: 1
2020-05-27 14:07:09.237 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByName     : ==>  Preparing: select count(*) from user where user_name=? 
2020-05-27 14:07:09.237 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByName     : ==> Parameters: planeswalker(String)
2020-05-27 14:07:09.238 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByName     : <==      Total: 1
2020-05-27 14:07:09.250 ERROR 18375 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 插入失敗,user_name 重複] with root cause

java.lang.RuntimeException: 插入失敗,user_name 重複
	at com.biosan.databasehandler.TestController.transactionalDemo(TestController.java:43) ~[classes/:na]
	at com.biosan.databasehandler.TestController$$FastClassBySpringCGLIB$$dadcb476.invoke(<generated>) ~[classes/:na]
    ......
複製代碼

在第二個校驗方法的時候拋出了錯誤,說明數據庫中存在相同 user_name 的數據,而後我又把 SQL 拿出來單獨去數據庫中執行,發現沒有數據!

不信邪的我又在第二個校驗方法上打了端點,當程序執行到此處時,它的執行結果是:

image2.png

也就是說確實這時候存在這樣的數據!

而此時我又在數據庫當中查詢,居然也查不到這條數據!

這就讓我開始考慮到,可能不是代碼或者框架的緣由,而是其餘的問題了,好比數據庫事務。

2.4. 緣由

咱們知道在 SpringBoot 的接口上標註了 @Transactional註解,就至關於開啓了一個事務。

MySQL 默認的事務隔離級別是讀已提交,即一個事務提交以後,它作的變動纔會被其餘事務看到。而在同一個事務中,若是先插入後查詢,若是查詢條件符合,是能夠查詢到插入的數據的。

當個人程序在執行完 insert 方法後,又去根據 user_name 查詢,就能夠查詢到插入的數據,而此時我直接在數據庫中查詢該 user_name,至關於又開啓了一個事務進行查詢,因爲讀已提交的隔離級別,一個事務提交以後,它作的變動纔會被其餘事務看到,且業務方法未提交,因此在數據庫中查詢不到數據。

這也就是我在程序中和數據庫中用一樣的 SQL 進行查詢,但查詢結果卻不相同的緣由。

2.5. 修復

這個問題從業務上來講本來就是不合理的,我在查詢重複數據時本就應該排除與將要插入數據相同 id 的數據,即 SQL 應該是:

select count(*) from user where user_name='planeswalker' and user_id!='userId'
複製代碼

同時,驗證重複的業務邏輯應該在插入語句以前...

固然這就是後話了。

3. 小結

本文記錄了一個關於 SpringBoot+MyBatis 框架查詢數據庫的小問題,這實際上是數據庫事務與隔離級別的問題,同時這也是一個業務上的問題,應該在插入以前進行驗重。

關於數據庫隔離級別,這裏只是小小提了一下,之後有空再總結吧。

相關文章
相關標籤/搜索