十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Mybatis CRUD实战:从配置到动态SQL与批量操作详解

Mybatis CRUD实战:从配置到动态SQL与批量操作详解 1. 从零开始为什么Mybatis依然是Java开发者的必修课如果你刚接触Java后端开发或者正准备从JDBC、JDBCTemplate这类“原始”的数据访问方式升级那么Mybatis绝对是你绕不开的一关。尽管现在Spring Data JPA和Mybatis-Plus等框架热度很高但Mybatis本身作为一款半自动化的ORM框架其核心价值在于它精准地平衡了灵活性与开发效率。它不像JPA那样试图用一套标准“封装”所有数据库操作也不像纯手写SQL那样繁琐。Mybatis让你能完全掌控SQL同时又帮你处理了结果集映射、参数绑定这些重复劳动。很多面试官喜欢问Mybatis的原理不是因为它过时恰恰是因为它的设计思想——通过接口与XML/注解的映射将Java方法与SQL语句优雅地绑定——是理解很多现代框架底层逻辑的绝佳入口。最近在社区里关于Mybatis的讨论依然火热从“mybatis配置打印”调试SQL到“mybatis缓存”机制优化再到“mybatis批量更新batch”处理性能问题以及经典的“mybatis updatebyid更新值为null不更新问题”每一个都是实战中高频出现的痛点。理解Mybatis的CRUD增删改查不仅仅是学会几个标签更是掌握一套与数据库交互的“方言”和最佳实践。今天我就以一个完整的实例为线索带你从环境搭建、配置解读到每一个CRUD操作的细节实现与避坑彻底吃透Mybatis的基础核心。2. 项目初始化与环境搭建不止于Spring Boot Starter很多人一上来就用spring-boot-starter-mybatis这没问题但如果你不清楚它背后帮你做了什么一旦出问题就会很懵。我们从头捋一遍。2.1 依赖引入看清每一个Jar包的作用创建一个标准的Maven项目核心依赖不仅仅是Mybatis和数据库驱动。dependencies !-- Spring Boot Web Starter (非必须但常见于Web项目) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Mybatis Spring Boot Starter核心集成依赖 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version !-- 请使用最新稳定版 -- /dependency !-- 数据库驱动以MySQL 8为例 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Lombok简化实体类代码可选但推荐 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency !-- 单元测试 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies注意mybatis-spring-boot-starter这个包至关重要它自动配置了SqlSessionFactory和SqlSessionTemplate并开启了Mybatis的Mapper接口扫描。如果你用的是非Spring Boot项目则需要手动配置这些Bean过程要复杂得多。2.2 核心配置解析application.yml里的门道在src/main/resources/application.yml中基础的配置如下spring: datasource: url: jdbc:mysql://localhost:3306/mybatis_demo?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver mybatis: # 1. 指定Mapper XML文件的位置。这是最关键的配置之一。 mapper-locations: classpath:mapper/*.xml # 2. 指定实体类的包路径用于简化XML中的resultType书写。 type-aliases-package: com.example.demo.entity # 3. 开启下划线转驼峰命名映射。数据库字段user_name会自动映射到实体属性userName。 configuration: map-underscore-to-camel-case: true # 4. 打印SQL语句到控制台调试神器。对应热搜“mybatis配置打印” log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里有几个实战经验mapper-locations通常我们会把XML文件放在resources/mapper/目录下保持和Java接口的包结构一致是良好习惯。type-aliases-package配置后在XML里写resultTypeUser即可不用写全类名com.example.demo.entity.User。log-impl开发阶段强烈建议开启。除了StdOutImpl还可以集成SLF4Jorg.apache.ibatis.logging.slf4j.Slf4jImpl让SQL日志融入项目的统一日志系统。看到热搜里很多人搜这个就是因为这是排查SQL问题的第一把钥匙。2.3 实体与表结构设计定义清晰的领域模型我们以一个简单的User用户表为例。先看数据库表结构CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, name varchar(100) NOT NULL COMMENT 姓名, age int(11) DEFAULT NULL COMMENT 年龄, email varchar(100) DEFAULT NULL COMMENT 邮箱, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;对应的Java实体类User.javapackage com.example.demo.entity; import lombok.Data; import java.time.LocalDateTime; Data public class User { private Long id; private String name; private Integer age; private String email; private LocalDateTime createTime; // 注意这里驼峰命名对应表字段create_time }使用了Lombok的Data注解自动生成了getter、setter、toString等方法。这里的关键点是createTime属性由于我们在配置中开启了map-underscore-to-camel-caseMybatis会自动将数据库的create_time映射到createTime无需额外注解。3. Mapper层剖析接口与XML的共生关系Mybatis的核心映射就在这一层。它由两部分组成一个Java接口Mapper Interface和一个XML映射文件Mapper XML。两者通过命名空间namespace绑定。3.1 创建Mapper接口定义数据契约首先创建接口UserMapper.javapackage com.example.demo.mapper; import com.example.demo.entity.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; Mapper // Spring Boot中此注解标记该接口为Mybatis的Mapper会被自动扫描并注入 public interface UserMapper { // 1. 插入用户并返回自增主键 int insertUser(User user); // 2. 根据ID更新用户 int updateUserById(User user); // 3. 根据ID删除用户 int deleteUserById(Param(id) Long id); // 4. 根据ID查询单个用户 User selectUserById(Param(id) Long id); // 5. 查询所有用户 ListUser selectAllUsers(); // 6. 根据姓名模糊查询演示动态SQL和参数传递 ListUser selectUsersByName(Param(name) String name); }接口中的每一个方法都对应着后续XML文件里的一个SQL语句标签。Param注解用于给参数命名在XML中可以通过#{name}来引用当方法有多个参数时这个注解是必须的。3.2 编写Mapper XMLSQL的舞台在resources/mapper/目录下创建UserMapper.xml。这是重头戏我们一步步来。?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd !-- namespace必须对应Mapper接口的全限定名 -- mapper namespacecom.example.demo.mapper.UserMapper !-- 定义可重用的SQL片段 -- sql idBase_Column_List id, name, age, email, create_time /sql !-- 1. 插入 (Create) -- insert idinsertUser parameterTypeUser useGeneratedKeystrue keyPropertyid INSERT INTO user (name, age, email) VALUES (#{name}, #{age}, #{email}) /insert !-- 2. 根据ID更新 (Update) -- update idupdateUserById parameterTypeUser UPDATE user set if testname ! null and name ! name #{name}, /if if testage ! null age #{age}, /if if testemail ! null email #{email}, /if /set WHERE id #{id} /update !-- 3. 根据ID删除 (Delete) -- delete iddeleteUserById DELETE FROM user WHERE id #{id} /delete !-- 4. 根据ID查询 (Retrieve - Single) -- select idselectUserById resultTypeUser SELECT include refidBase_Column_List/ FROM user WHERE id #{id} /select !-- 5. 查询所有 (Retrieve - List) -- select idselectAllUsers resultTypeUser SELECT include refidBase_Column_List/ FROM user /select !-- 6. 根据姓名模糊查询 (动态SQL示例) -- select idselectUsersByName resultTypeUser SELECT include refidBase_Column_List/ FROM user WHERE 11 if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if ORDER BY id DESC /select /mapper现在我们来拆解这里面每一个关键点insert标签详解idinsertUser必须与接口中的方法名insertUser完全一致。parameterTypeUser传入参数的类型。因为我们配置了type-aliases-package所以可以直接写User。useGeneratedKeystrue keyPropertyid这是处理数据库自增主键回填的关键配置。执行插入后数据库生成的主键值会自动设置到传入的User对象的id属性中。这样方法执行完user.getId()就能拿到ID了。update标签与动态SQL这里使用了set和if标签实现动态更新这直接关联到热搜问题“mybatis updatebyid更新值为null不更新问题”。问题场景如果你直接写UPDATE user SET name#{name}, age#{age}, email#{email} WHERE id#{id}当age为null时SQL会变成SET name?, agenull, ...这会将数据库中的age字段更新为NULL这可能不是你想要的行为你或许只想更新非空的字段。解决方案使用setif标签。set标签会智能地处理SET子句末尾的逗号。if标签判断属性是否不为空只有不为空的字段才会被加入到SET子句中。这样就实现了“选择性更新”避免用null覆盖已有值。select标签与结果映射resultTypeUser告诉Mybatis将查询结果集的每一行数据自动映射成一个User对象。这依赖于字段名或通过map-underscore-to-camel-case转换后的属性名与对象属性名的一致。include refidBase_Column_List/引用上面定义的SQL片段避免重复书写字段列表提高可维护性。在selectUsersByName中使用了WHERE 11这个小技巧是为了方便后面拼接if条件而不用处理AND开头的问题。当然Mybatis也提供了where标签来更优雅地处理这个问题。4. Service层与单元测试让CRUD跑起来光有Mapper还不够我们需要在业务层调用它并通过测试验证。4.1 编写Service层创建一个UserService接口及其实现package com.example.demo.service; import com.example.demo.entity.User; import java.util.List; public interface UserService { Long addUser(User user); boolean updateUser(User user); boolean deleteUser(Long id); User getUserById(Long id); ListUser getAllUsers(); ListUser getUsersByName(String name); }package com.example.demo.service.impl; import com.example.demo.entity.User; import com.example.demo.mapper.UserMapper; import com.example.demo.service.UserService; 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 UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override Transactional // 声明事务 public Long addUser(User user) { int rows userMapper.insertUser(user); if (rows 0) { // 插入成功后user对象的id已被回填 return user.getId(); } return null; } Override Transactional public boolean updateUser(User user) { if (user.getId() null) { throw new IllegalArgumentException(用户ID不能为空); } return userMapper.updateUserById(user) 0; } Override Transactional public boolean deleteUser(Long id) { return userMapper.deleteUserById(id) 0; } Override public User getUserById(Long id) { return userMapper.selectUserById(id); } Override public ListUser getAllUsers() { return userMapper.selectAllUsers(); } Override public ListUser getUsersByName(String name) { return userMapper.selectUsersByName(name); } }Service层的作用是封装业务逻辑并管理事务通过Transactional注解。注意在addUser方法中我们直接返回了user.getId()这得益于插入配置中的useGeneratedKeys。4.2 编写集成测试在src/test/java下创建测试类UserMapperTest.java使用Spring Boot的测试框架。package com.example.demo; import com.example.demo.entity.User; import com.example.demo.mapper.UserMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; import static org.junit.jupiter.api.Assertions.*; SpringBootTest // 启动完整的Spring应用上下文进行测试 Transactional // 每个测试方法执行后回滚事务不污染数据库 Rollback(true) // 确保回滚 class UserMapperTest { Autowired private UserMapper userMapper; Test void testInsertAndSelect() { User user new User(); user.setName(测试用户); user.setAge(25); user.setEmail(testexample.com); // createTime 由数据库自动生成无需设置 // 测试插入 int insertRows userMapper.insertUser(user); assertEquals(1, insertRows); assertNotNull(user.getId()); // 验证主键回填 // 测试根据ID查询 User fetchedUser userMapper.selectUserById(user.getId()); assertNotNull(fetchedUser); assertEquals(测试用户, fetchedUser.getName()); assertEquals(25, fetchedUser.getAge()); // 数据库生成的createTime应该不为null assertNotNull(fetchedUser.getCreateTime()); } Test void testUpdateUserById() { // 先插入一条数据 User user new User(); user.setName(老名字); user.setAge(30); userMapper.insertUser(user); Long userId user.getId(); // 准备更新数据只更新nameage设为null User updateUser new User(); updateUser.setId(userId); updateUser.setName(新名字); updateUser.setAge(null); // 故意设置为null int updateRows userMapper.updateUserById(updateUser); assertEquals(1, updateRows); // 重新查询验证更新结果 User updatedUser userMapper.selectUserById(userId); assertEquals(新名字, updatedUser.getName()); // 关键验证age字段应该保持原来的30而不是被更新为null assertEquals(30, updatedUser.getAge()); // 这里会成功因为我们的XML用了动态SQL } Test void testDeleteUserById() { User user new User(); user.setName(待删除用户); userMapper.insertUser(user); int deleteRows userMapper.deleteUserById(user.getId()); assertEquals(1, deleteRows); User deletedUser userMapper.selectUserById(user.getId()); assertNull(deletedUser); } Test void testSelectAllUsers() { // 可以先清空或确保环境这里简单测试 ListUser users userMapper.selectAllUsers(); // 至少不应该报错 assertNotNull(users); } Test void testSelectUsersByName() { // 插入一些测试数据 User user1 new User(); user1.setName(张三); userMapper.insertUser(user1); User user2 new User(); user2.setName(张四); userMapper.insertUser(user2); User user3 new User(); user3.setName(李五); userMapper.insertUser(user3); ListUser zhangUsers userMapper.selectUsersByName(张); assertEquals(2, zhangUsers.size()); // 应该找到张三和张四 assertTrue(zhangUsers.stream().allMatch(u - u.getName().contains(张))); } }运行这些测试如果全部通过恭喜你一个完整的Mybatis CRUD流程已经跑通了控制台应该会打印出Mybatis执行的每一条SQL语句因为我们配置了log-impl这对于调试和理解执行过程非常有帮助。5. 进阶话题与高频问题排查掌握了基础CRUD后我们来看看那些热搜词背后代表的高频问题和进阶用法。5.1 分页查询的实现与误区热搜里有“mybatis 分页 怎么设置能全查出来,是page设置成-1吗”和“mybatis 分页查询”。Mybatis本身不提供物理分页功能它需要配合第三方分页插件如PageHelper或数据库方言如MySQL的LIMIT来实现。不推荐的做法有些人在使用某些插件时发现设置pageNum-1或pageSize0可能会绕过插件拦截导致查询全部数据。这是一种对插件行为的 hack强烈不推荐因为行为不确定且破坏了分页的语义。推荐的做法使用PageHelper国内最流行!-- 添加依赖 -- dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version最新版本/version /dependency在Service或Mapper方法调用前使用一行代码即可// pageNum: 页码pageSize: 每页条数 PageHelper.startPage(pageNum, pageSize); ListUser userList userMapper.selectAllUsers(); // 这个查询会被自动改写加上LIMIT // 用PageInfo包装结果获取分页信息 PageInfoUser pageInfo new PageInfo(userList);PageHelper会在执行SQL前通过拦截器动态拼接LIMIT语句。手写LIMIT简单直接 在Mapper接口和XML中手动处理适合简单场景。// Mapper接口 ListUser selectUsersByPage(Param(offset) int offset, Param(limit) int limit);!-- Mapper XML -- select idselectUsersByPage resultTypeUser SELECT include refidBase_Column_List/ FROM user ORDER BY id DESC LIMIT #{offset}, #{limit} /select计算offset (pageNum - 1) * pageSize。这种方式最透明但需要自己计算总数和页码。5.2 批量操作Batch性能优化“mybatis批量更新batch”是性能优化常见问题。逐条执行UPDATE或INSERT效率极低。Mybatis提供了foreach标签来执行批量操作。批量插入示例insert idbatchInsertUsers parameterTypejava.util.List INSERT INTO user (name, age, email) VALUES foreach collectionlist itemuser separator, (#{user.name}, #{user.age}, #{user.email}) /foreach /insert接口方法int batchInsertUsers(Param(list) ListUser userList);批量更新示例根据ID更新不同值 这种情况较复杂MySQL不支持UPDATE ... SET (name,age) VALUES (case1), (case2)...的标准语法。通常有两种方式在Java中循环每条执行一次更新性能差。使用foreach拼接成多条SQL语句通过;分隔并需要允许JDBC执行多条语句allowMultiQueriestrue在连接字符串中但有SQL注入风险需谨慎。使用CASE WHEN语句推荐update idbatchUpdateUsers UPDATE user SET name CASE id foreach collectionlist itemuser WHEN #{user.id} THEN #{user.name} /foreach END, age CASE id foreach collectionlist itemuser WHEN #{user.id} THEN #{user.age} /foreach END WHERE id IN foreach collectionlist itemuser open( separator, close) #{user.id} /foreach /update这种方法能通过一条SQL更新多条记录性能最好但SQL语句会随着数据量增大而变长需注意数据库对SQL长度的限制。5.3 缓存机制浅析“mybatis缓存”是一个重要话题。Mybatis提供了一级缓存和二级缓存。一级缓存本地缓存/SqlSession级别默认开启。在同一个SqlSession通常对应一次数据库会话/事务中执行相同的查询第二次会直接从缓存返回结果不会再次访问数据库。注意一旦执行了增删改操作或者调用了sqlSession.clearCache()或者关闭了SqlSession缓存就会清空。在Spring集成中由于通常将SqlSession生命周期与事务绑定一级缓存的作用范围可能比你想象的小。二级缓存全局缓存/namespace级别需要手动在XML中配置cache/标签开启。多个SqlSession可以共享二级缓存。使用需谨慎因为数据可能被其他会话修改导致缓存数据脏读。通常用于读远多于写、且对实时性要求不高的场景。开启后实体类需要实现Serializable接口。5.4 常见问题排查清单结合热搜词这里整理一个快速排查指南SQL语句不执行或报错检查mapper-locations配置路径是否正确XML文件是否被正确编译到target/classes下。检查接口上的Mapper注解或启动类上的MapperScan。开启log-impl查看控制台确认SQL是否被打印语法是否正确。查询结果映射失败属性为null检查数据库字段名与实体属性名是否匹配确认map-underscore-to-camel-case是否开启。检查XML中resultType或resultMap是否正确。使用SELECT *临时测试看是否是字段列表不匹配。参数绑定失败检查接口方法参数是否使用了Param注解多参数时必须。检查XML中#{paramName}里的paramName是否与Param值或实体属性名一致。更新时字段被意外设置为null回顾我们上面详细讲的动态SQLsetif用法确保你的更新语句使用了动态判断。分页查询返回所有数据如果使用PageHelper检查PageHelper.startPage()是否在查询方法之前调用且中间不能有别的查询语句。确认分页插件版本与Mybatis、Spring Boot版本兼容。6. 从Mybatis到Mybatis-Plus平滑演进的选择热搜里“mybatis plus”和“mybatis plus basemapper.update”的出现频率很高这反映了开发者对更高开发效率的追求。Mybatis-Plus简称MP是在Mybatis基础上只做增强不做改变的框架它提供了强大的CRUD封装、条件构造器、代码生成器等。如果你觉得手写每个Mapper XML还是有些繁琐特别是对于单表的简单CRUD那么MP的BaseMapper会让你眼前一亮。它内置了几乎所有单表方法// 继承BaseMapper后自动拥有这些方法无需写XML public interface UserMapper extends BaseMapperUser { // 你可以继续在这里定义复杂的、需要自定义SQL的方法 } // Service层调用示例 userMapper.insert(user); // 插入 userMapper.updateById(user); // 根据ID更新注意默认是全字段更新需配合TableField策略控制null值 userMapper.deleteById(1L); // 根据ID删除 userMapper.selectById(1L); // 根据ID查询 userMapper.selectList(new QueryWrapperUser().like(name, 张)); // 条件查询对于“mybatis plus basemapper.update”提到的更新问题MP的updateById默认也是全字段更新。MP提供了TableField(updateStrategy FieldStrategy.IGNORED)等策略来控制字段的更新行为例如忽略null值这比在XML中写动态SQL更声明式。如何选择坚持使用原生Mybatis项目SQL极度复杂、高度定制化或团队对SQL掌控要求极高不希望有任何“魔法”。引入Mybatis-Plus项目以单表操作为主追求快速开发希望减少重复的CRUD代码。MP的条件构造器也能很好地处理复杂查询同时保留了原生Mybatis的所有能力你仍然可以写XML处理超级复杂的SQL。无论选择哪个深入理解本章所讲的原生Mybatis核心机制都是你用好MP或在复杂场景下游刃有余的基石。因为MP的很多便捷功能其底层最终还是转换成了Mybatis的SqlSession操作和XML/注解映射。理解了根本才能更好地驾驭工具。
返回列表