尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SpringBoot整合MyBatis与Thymeleaf实现CRUD实战
1. 从JDBC到MyBatis为什么SpringBoot项目里我最终选了它做Java后端的人几乎都经历过这样一个阶段刚开始写数据库操作时用原生JDBC一个简单的查询要写Connection、PreparedStatement、ResultSet最后还得在finally里手动关资源代码又长又容易出错。后来用上了Spring的JdbcTemplate确实省了不少事但SQL还是散落在Java代码里拼接字符串拼到手软。再后来接触到MyBatis才算真正找到了一个平衡点——SQL归SQLJava归Java两边各司其职。这次要聊的是SpringBoot整合MyBatis做一套完整的CRUD。技术栈是SpringBoot MyBatis SpringMVC Thymeleaf说白了就是一个带页面的Web应用能从浏览器上对数据进行增删改查。关键词里提到了CRUD、SpringBoot、MyBatis、Thymeleaf这几个词基本勾勒出了整个项目的轮廓。适合谁看如果你已经写过Hello World级别的SpringBoot项目知道Controller和Service大概是怎么回事但对“怎么把数据库接进来、怎么让页面动起来”还不太清楚那这篇内容就是给你准备的。我先说结论MyBatis在SpringBoot生态里的定位非常清晰它不像JPA那样帮你把SQL全藏起来也不像原生JDBC那样什么都得自己来。它给你一个Mapper接口你写SQL它帮你做映射。这种“半自动”的方式在实际项目里特别受欢迎因为复杂查询的SQL往往需要精细调优全自动的ORM在这种场景下反而碍手碍脚。提示如果你的项目是简单的单表操作JPA可能更省事但只要涉及到多表关联、动态条件查询、批量操作MyBatis的优势就会立刻显现出来。在动手之前有必要把整体思路理清楚。一个标准的CRUD应用数据流向是这样的浏览器发请求 → SpringMVC的DispatcherServlet接收 → 找到对应的Controller → Controller调用Service → Service调用Mapper → Mapper执行SQL操作数据库 → 结果逐层返回 → Thymeleaf渲染页面 → 浏览器展示。这条链路里MyBatis处在最底层负责和数据库打交道Thymeleaf在最上层负责把数据变成HTML。很多人第一次整合的时候容易懵是因为配置文件太多、注解太多不知道哪个该写在哪里。我当时的做法是先把依赖理清楚再把配置写对然后从一张表开始把查询跑通再逐步加上增删改。这个顺序很重要不要一上来就想把所有功能都写完那样出了问题很难定位。还有一个点值得提前说MyBatis的Mapper接口和XML映射文件之间的对应关系是新手最容易踩坑的地方。接口在哪个包、XML放在哪个目录、namespace怎么写、方法名和id怎么对应这些细节如果有一个对不上启动就会报BindingException。后面我会专门用一节来讲这个问题的排查思路。2. 依赖与目录结构整合前的准备工作2.1 依赖选型与版本搭配的考量SpringBoot整合MyBatis核心依赖其实就两个一个是mybatis-spring-boot-starter另一个是数据库驱动。但实际项目里还需要Web、Thymeleaf、连接池等所以完整的依赖清单大概是这样dependencies !-- Web支持包含SpringMVC -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf模板引擎 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- MyBatis整合SpringBoot的核心starter -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency !-- MySQL驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 连接池SpringBoot默认用HikariCP -- dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId /dependency !-- Lombok减少getter/setter的样板代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里有个版本搭配的问题需要说明。mybatis-spring-boot-starter的版本和SpringBoot的版本是有对应关系的。如果你用的是SpringBoot 3.x那starter也要用3.x版本因为SpringBoot 3把Java基线提到了17而且把javax包换成了jakarta包。如果版本对不上启动时会报各种ClassNotFoundException。我一般建议直接去Maven中央仓库查一下starter的版本说明确认它支持的SpringBoot版本范围。连接池这块SpringBoot 2.x之后默认就是HikariCP性能很好一般不需要换。但如果你有特殊需求比如需要监控SQL执行情况可以换成Druid不过那又是另一套配置了。对于学习阶段来说HikariCP完全够用。2.2 目录结构怎么摆才不容易乱目录结构这件事看起来是小事但实际影响很大。我见过不少初学者把所有文件都堆在一个包下面Controller、Service、Mapper、实体类混在一起找起来非常痛苦。推荐的做法是按职责分层src/main/java/com/example/demo/ ├── controller/ # 控制器层处理HTTP请求 │ └── UserController.java ├── service/ # 业务逻辑层 │ ├── UserService.java │ └── impl/ │ └── UserServiceImpl.java ├── mapper/ # MyBatis的Mapper接口 │ └── UserMapper.java ├── entity/ # 实体类和数据库表对应 │ └── User.java └── DemoApplication.java # 启动类 src/main/resources/ ├── mapper/ # MyBatis的XML映射文件 │ └── UserMapper.xml ├── templates/ # Thymeleaf模板 │ ├── user/ │ │ ├── list.html │ │ ├── add.html │ │ └── edit.html ├── static/ # 静态资源 │ ├── css/ │ └── js/ └── application.yml # 配置文件这个结构里有两个地方需要特别注意。第一Mapper接口和XML文件的包路径最好保持一致。比如接口在com.example.demo.mapper包下那XML放在resources/mapper目录下然后在配置里指定mapper-locations。第二Thymeleaf模板默认放在templates目录下这是SpringBoot的约定不要随意改改了就要在配置里额外声明。注意XML映射文件不要放在src/main/java目录下虽然编译时可以通过配置把XML一起打包但这样做会让Maven的资源配置变得复杂容易出问题。老老实实放在resources下最省心。2.3 application.yml里的关键配置项配置文件是整合的“总开关”写对了后面就顺写错了各种报错。我用YAML格式来写比properties更清晰spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_password hikari: minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 300000 connection-timeout: 30000 thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html encoding: UTF-8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里逐项解释一下。url里的serverTimezone一定要加否则MySQL 8.x会报时区错误。useSSLfalse在本地开发时加上避免SSL握手警告。map-underscore-to-camel-case这个配置非常实用它能让数据库的user_name字段自动映射到Java的userName属性省去了在XML里写resultMap的麻烦。log-impl配置成StdOutImpl后MyBatis会把执行的SQL打印到控制台调试阶段强烈建议打开能帮你快速定位SQL问题。Thymeleaf的cache: false在开发阶段一定要设否则你改了HTML模板刷新页面看不到变化会以为代码写错了。上线时再改成true。3. 实体类与数据库表映射关系的建立3.1 建表语句与字段设计先建一张用户表字段不用太多够演示CRUD就行CREATE TABLE t_user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, user_name varchar(50) NOT NULL COMMENT 用户名, email varchar(100) DEFAULT NULL COMMENT 邮箱, age int DEFAULT NULL COMMENT 年龄, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;字段设计有几个考虑。id用bigint自增这是常规做法。user_name用了下划线命名正好可以演示驼峰映射。create_time和update_time交给数据库自动维护减少Java代码的负担。字符集用utf8mb4支持完整的Unicode包括emoji。3.2 实体类的写法与Lombok的使用对应的实体类package com.example.demo.entity; import lombok.Data; import java.time.LocalDateTime; Data public class User { private Long id; private String userName; private String email; private Integer age; private LocalDateTime createTime; private LocalDateTime updateTime; }Data是Lombok的注解自动生成getter、setter、toString、equals、hashCode。这里有个细节age用Integer而不是int因为数据库里age允许为null如果用int接收null会报错。时间类型用LocalDateTime这是Java 8之后推荐的日期时间API比java.util.Date更好用。提示如果你在IDE里用了Lombok但代码提示找不到getter/setter需要安装Lombok插件并开启注解处理。这不是代码问题是IDE配置问题。3.3 Mapper接口与XML的对应关系Mapper接口package 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 public interface UserMapper { ListUser selectAll(); User selectById(Param(id) Long id); int insert(User user); int update(User user); int deleteById(Param(id) Long id); ListUser selectByCondition(Param(userName) String userName, Param(email) String email); }Mapper注解告诉MyBatis这是一个Mapper接口SpringBoot启动时会自动扫描并生成代理实现类。如果你不想在每个接口上都加Mapper可以在启动类上加MapperScan(com.example.demo.mapper)效果一样。Param注解用于给参数命名当方法有多个参数时XML里就需要用这个名字来引用。如果只有一个参数且是基本类型可以不加但加上更清晰不容易出错。XML映射文件?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.UserMapper select idselectAll resultTypeUser SELECT * FROM t_user ORDER BY id DESC /select select idselectById resultTypeUser SELECT * FROM t_user WHERE id #{id} /select insert idinsert useGeneratedKeystrue keyPropertyid INSERT INTO t_user (user_name, email, age) VALUES (#{userName}, #{email}, #{age}) /insert update idupdate UPDATE t_user SET user_name #{userName}, email #{email}, age #{age} WHERE id #{id} /update delete iddeleteById DELETE FROM t_user WHERE id #{id} /delete select idselectByCondition resultTypeUser SELECT * FROM t_user where if testuserName ! null and userName ! AND user_name LIKE CONCAT(%, #{userName}, %) /if if testemail ! null and email ! AND email LIKE CONCAT(%, #{email}, %) /if /where ORDER BY id DESC /select /mapper这里有几个关键点。namespace必须和Mapper接口的全限定名完全一致一个字母都不能差。id必须和接口里的方法名一致。resultType写的是User这是因为在配置里指定了type-aliases-packageMyBatis会自动把User解析成com.example.demo.entity.User。useGeneratedKeystrue和keyPropertyid配合使用插入后会自动把数据库生成的主键回填到实体对象的id属性上这个在实际开发中非常有用。where标签和if标签是MyBatis动态SQL的基础。where会自动处理第一个条件前面的AND避免出现WHERE AND这种语法错误。if根据参数是否为空来决定是否拼接条件这在做搜索功能时特别实用。4. Service与Controller业务逻辑与请求处理4.1 Service层的职责划分Service层是业务逻辑的载体虽然在这个简单的CRUD里逻辑不多但分层的好处是显而易见的——Controller只负责接收请求和返回响应Service负责业务规则Mapper负责数据访问。这样当业务变复杂时代码不会乱。package com.example.demo.service; import com.example.demo.entity.User; import java.util.List; public interface UserService { ListUser findAll(); User findById(Long id); void save(User user); void update(User user); void deleteById(Long id); ListUser search(String userName, String email); }实现类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 java.util.List; Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override public ListUser findAll() { return userMapper.selectAll(); } Override public User findById(Long id) { return userMapper.selectById(id); } Override public void save(User user) { userMapper.insert(user); } Override public void update(User user) { userMapper.update(user); } Override public void deleteById(Long id) { userMapper.deleteById(id); } Override public ListUser search(String userName, String email) { return userMapper.selectByCondition(userName, email); } }Service注解把这个类注册为Spring的BeanAutowired把Mapper注入进来。这里用的是字段注入虽然Spring官方现在更推荐构造器注入但在学习阶段字段注入更直观。如果你要用构造器注入可以用Lombok的RequiredArgsConstructor配合final字段代码会更简洁。4.2 Controller的路由设计与请求参数处理Controller是SpringMVC的核心负责把HTTP请求映射到具体的方法上package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; Controller RequestMapping(/user) public class UserController { Autowired private UserService userService; GetMapping(/list) public String list(RequestParam(value userName, required false) String userName, RequestParam(value email, required false) String email, Model model) { model.addAttribute(users, userService.search(userName, email)); model.addAttribute(userName, userName); model.addAttribute(email, email); return user/list; } GetMapping(/add) public String addPage() { return user/add; } PostMapping(/add) public String add(User user) { userService.save(user); return redirect:/user/list; } GetMapping(/edit/{id}) public String editPage(PathVariable(id) Long id, Model model) { model.addAttribute(user, userService.findById(id)); return user/edit; } PostMapping(/edit) public String edit(User user) { userService.update(user); return redirect:/user/list; } GetMapping(/delete/{id}) public String delete(PathVariable(id) Long id) { userService.deleteById(id); return redirect:/user/list; } }这里用Controller而不是RestController因为我们要返回的是视图名称让Thymeleaf去渲染页面。RequestMapping(/user)定义了基础路径后面的方法路径都是相对于它的。GetMapping和PostMapping分别对应GET和POST请求。查询和页面跳转用GET数据提交用POST这是RESTful风格的基本约定。PathVariable用于从URL路径中提取参数比如/user/edit/5里的5。RequestParam用于从查询字符串中提取参数比如?userNamezhang。Model对象用于向视图传递数据model.addAttribute(users, ...)之后在Thymeleaf模板里就可以用${users}来访问。注意新增和编辑成功后用redirect:前缀做重定向而不是直接返回视图名。这样做是为了避免刷新页面时重复提交表单。这是Web开发中的一个经典问题叫“表单重复提交”用重定向可以很好地解决。4.3 统一异常处理的简单实践虽然这个项目很小但加一个简单的异常处理会让代码更健壮ControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(Exception.class) public String handleException(Exception e, Model model) { model.addAttribute(message, e.getMessage()); return error; } }ControllerAdvice会拦截所有Controller抛出的异常统一处理。这里简单地返回一个错误页面实际项目中可以根据异常类型做更细粒度的处理。5. Thymeleaf模板让数据在页面上动起来5.1 列表页的表格渲染与搜索框Thymeleaf的语法核心是th:属性它能在HTML标签上执行表达式。列表页list.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title用户列表/title link relstylesheet th:href{/css/style.css} /head body h2用户列表/h2 form th:action{/user/list} methodget input typetext nameuserName th:value${userName} placeholder用户名 input typetext nameemail th:value${email} placeholder邮箱 button typesubmit搜索/button a th:href{/user/add}新增用户/a /form table border1 cellpadding8 cellspacing0 thead tr thID/th th用户名/th th邮箱/th th年龄/th th创建时间/th th操作/th /tr /thead tbody tr th:eachuser : ${users} td th:text${user.id}/td td th:text${user.userName}/td td th:text${user.email}/td td th:text${user.age}/td td th:text${#temporals.format(user.createTime, yyyy-MM-dd HH:mm)}/td td a th:href{/user/edit/{id}(id${user.id})}编辑/a a th:href{/user/delete/{id}(id${user.id})} onclickreturn confirm(确定删除吗)删除/a /td /tr tr th:if${#lists.isEmpty(users)} td colspan6 styletext-align:center;暂无数据/td /tr /tbody /table /body /htmlth:each用于循环遍历集合th:text用于设置文本内容。{...}是URL表达式会自动加上应用的上下文路径。#temporals.format是Thymeleaf对Java 8时间API的支持用于格式化日期。#lists.isEmpty判断集合是否为空为空时显示提示信息。搜索框里的th:value${userName}是为了在搜索后保留用户输入的条件提升体验。这个细节很多人会忽略但用户能明显感觉到差别。5.2 新增与编辑页的表单绑定新增页add.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title新增用户/title /head body h2新增用户/h2 form th:action{/user/add} methodpost p用户名input typetext nameuserName required/p p邮箱input typeemail nameemail/p p年龄input typenumber nameage/p pbutton typesubmit保存/button a th:href{/user/list}返回/a/p /form /body /html编辑页edit.html和新增页类似但需要回填数据form th:action{/user/edit} methodpost input typehidden nameid th:value${user.id} p用户名input typetext nameuserName th:value${user.userName} required/p p邮箱input typeemail nameemail th:value${user.email}/p p年龄input typenumber nameage th:value${user.age}/p pbutton typesubmit保存/button a th:href{/user/list}返回/a/p /form编辑页多了一个隐藏的id字段用于提交时标识要更新哪条记录。th:value把当前值回填到输入框里用户就能看到原来的数据并进行修改。表单提交后SpringMVC会自动把请求参数绑定到User对象上前提是参数名和实体类的属性名一致。这就是为什么nameuserName能对应到User.userName。5.3 Thymeleaf的缓存与热部署问题开发阶段最让人头疼的问题之一就是改了HTML但页面没变化。这通常是Thymeleaf的缓存导致的。在application.yml里设置spring.thymeleaf.cache: false可以解决。但如果你用了IDEA还需要确认一下修改HTML后是否触发了重新编译。IDEA默认不会自动编译静态资源需要按CtrlF9手动编译或者开启自动编译。另一个常见问题是静态资源404。Thymeleaf默认从classpath:/static/目录加载静态资源所以/css/style.css对应的实际路径是src/main/resources/static/css/style.css。如果你把CSS放在了别的地方就会404。提示Thymeleaf模板里引用静态资源一定要用th:href{/css/style.css}或th:src{/js/app.js}不要直接写href/css/style.css。前者会自动处理上下文路径后者在应用部署到子路径下时会失效。6. 踩坑排查那些让我熬夜的报错与解决思路6.1 BindingExceptionMapper绑定失败的排查链路这个报错信息通常是这样的org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.demo.mapper.UserMapper.selectAll意思是MyBatis找不到selectAll这个方法对应的SQL语句。排查思路按以下顺序来第一步检查XML文件的namespace是否和Mapper接口的全限定名完全一致。注意大小写注意包名一个字符都不能差。第二步检查XML里select的id是否和方法名一致。方法名是selectAllid也必须是selectAll。第三步检查mapper-locations配置是否正确。如果XML放在resources/mapper/下配置应该是classpath:mapper/*.xml。如果放在resources/mybatis/mapper/下配置就要相应调整。第四步检查Maven是否把XML文件打包到了target/classes目录下。有时候IDEA不会自动把resources下的XML复制到target需要手动执行mvn clean compile。可以在pom.xml的build里加资源配置resources resource directorysrc/main/resources/directory includes include**/*.xml/include include**/*.yml/include include**/*.html/include /includes /resource /resources第五步如果以上都没问题检查启动类上是否有MapperScan注解且扫描的包路径是否正确。如果用了Mapper注解就不需要MapperScan两者选其一即可。我踩过的坑是XML文件放在了src/main/java目录下本地IDE运行没问题但打包成jar后XML没被包含进去导致线上报错。后来老老实实把XML移到了resources目录下问题解决。6.2 数据库连接失败的几种典型情况连接失败的表现是启动时报Communications link failure或Access denied for user。常见原因报错信息原因解决方案Access denied for user用户名或密码错误检查application.yml里的username和passwordUnknown database数据库不存在确认数据库已创建或URL里的库名拼写正确Communications link failure数据库服务未启动或端口不对确认MySQL服务运行中端口默认3306The server time zone value is unrecognized时区未配置URL里加serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowedMySQL 8的SSL配置URL里加allowPublicKeyRetrievaltrue这些报错信息其实都很直白关键是不要慌逐条读错误信息基本都能定位到问题。6.3 页面404与模板解析错误的区分页面404和模板解析错误是两回事。404是请求的URL没有对应的Controller方法或者Controller返回的视图名找不到对应的HTML文件。模板解析错误是HTML文件找到了但Thymeleaf在解析时遇到了语法问题比如表达式写错了。区分方法很简单看控制台的报错。如果是404通常不会有异常堆栈只是浏览器显示404页面。如果是模板解析错误控制台会打印TemplateProcessingException或SpelEvaluationException并指出具体哪一行出了问题。我遇到过一次th:eachuser : ${users}写成了th:eachuser : ${user}少了一个s结果页面直接报错。这种拼写错误很隐蔽但报错信息会告诉你user属性不存在顺着这个线索就能找到。6.4 表单提交后数据不更新的诡异问题有一次我改了编辑页的代码提交后数据库里的数据没变但页面显示保存成功。排查了半天发现是update语句的WHERE id #{id}里的id没传进去。原因是编辑页的表单里id字段的name写成了userId而实体类的属性是idSpringMVC绑定不上id就是nullSQL执行时WHERE id null匹配不到任何记录但也不报错。这个问题的教训是表单字段的name必须和实体类的属性名严格一致。如果实体类属性叫id表单里就必须是nameid。可以用浏览器的开发者工具查看提交的表单数据确认参数名是否正确。7. 从能跑到好用几个提升开发效率的实践7.1 用MyBatis的日志定位SQL问题前面提到过在配置里加上log-impl: org.apache.ibatis.logging.stdout.StdOutImplMyBatis会把执行的SQL和参数都打印出来。这在调试时非常有用。比如你写了一个动态SQL不确定最终拼接成什么样看日志就一目了然。日志大概长这样 Preparing: SELECT * FROM t_user WHERE user_name LIKE CONCAT(%, ?, %) ORDER BY id DESC Parameters: zhang(String) Total: 2Preparing行是最终执行的SQLParameters行是参数值Total行是返回的记录数。如果SQL不符合预期一眼就能看出来。7.2 分页查询的简单实现思路数据量大了之后列表页需要分页。MyBatis本身不提供分页功能但可以通过LIMIT语句手动实现select idselectPage resultTypeUser SELECT * FROM t_user ORDER BY id DESC LIMIT #{offset}, #{pageSize} /select对应的Mapper方法ListUser selectPage(Param(offset) int offset, Param(pageSize) int pageSize);Service里计算offsetpublic ListUser findPage(int pageNum, int pageSize) { int offset (pageNum - 1) * pageSize; return userMapper.selectPage(offset, pageSize); }当然实际项目中更推荐用PageHelper这样的分页插件它能自动拦截SQL并加上分页语句还能自动查询总数。但手动实现一遍有助于理解分页的原理。7.3 事务管理什么时候需要加TransactionalCRUD操作里单条INSERT、UPDATE、DELETE语句本身是原子性的不需要额外的事务。但如果你有一个业务操作需要执行多条SQL比如“删除用户的同时删除他的所有订单”那就需要事务来保证要么都成功要么都失败。在Service方法上加Transactional注解即可Transactional public void deleteUserAndOrders(Long userId) { userMapper.deleteById(userId); orderMapper.deleteByUserId(userId); }Transactional默认只对RuntimeException回滚如果抛的是检查型异常需要指定rollbackFor Exception.class。这个细节很多人会忽略导致事务没生效。注意Transactional注解在同一个类内部方法调用时不会生效因为Spring的事务是基于代理的。如果methodA调用了同一个类里的methodB而methodB上有Transactional事务不会起作用。解决办法是把methodB放到另一个Service里或者通过AopContext.currentProxy()获取代理对象来调用。7.4 实体类字段与数据库字段不一致时的处理虽然map-underscore-to-camel-case能解决大部分命名不一致的问题但有些情况下字段名差异太大比如数据库叫full_nameJava里叫realName。这时候有两种方案方案一在SQL里用别名SELECT full_name AS realName FROM t_user方案二用resultMap显式映射resultMap iduserResultMap typeUser id propertyid columnid/ result propertyrealName columnfull_name/ /resultMap select idselectAll resultMapuserResultMap SELECT * FROM t_user /select方案一简单直接适合少量字段不一致的情况。方案二更规范适合字段差异较多或需要处理复杂映射的场景。我一般优先用方案一因为改SQL比改XML更直观。8. 写在最后的一些个人体会这套SpringBoot MyBatis SpringMVC Thymeleaf的组合是我带过的新人最容易上手的全栈入门方案。它不像前后端分离那样需要同时理解两套技术栈也不像纯后端接口那样看不到直观的效果。从数据库到页面一条链路走通对理解Web应用的整体运作非常有帮助。我在实际教学中发现很多人卡住的地方不是某个技术点太难而是不知道各个部分怎么串起来。Mapper里的SQL怎么被Controller调用到Thymeleaf里的${users}是从哪里来的这些问题在单独学每个技术时不会遇到只有整合起来才会暴露。所以我的建议是不要等到把MyBatis完全学透了再动手整合而是先跑通一个最简单的查询然后再逐步加功能。遇到问题就查、就问解决问题的过程本身就是最好的学习。另外代码写完之后花点时间做代码审查。看看有没有SQL注入的风险用#{}而不是${}、有没有遗漏的参数校验、有没有不必要的重复代码。这些习惯在项目变大之后会救你的命。如果你已经跟着走完了这一遍不妨试试把项目改造成前后端分离的版本用RestController返回JSON前端用Vue或React来渲染。对比一下两种方式的差异你会对SpringMVC的理解更深一层。
RELATED

相关推荐

2026.9.29:使用 Milvus Operator 在 Kubernetes 中运行 Milvus

2026.9.29:使用 Milvus Operator 在 Kubernetes 中运行 Milvus

使用 Milvus Operator 在 Kubernetes 中运行 Milvus 使用 Milvus Operator 在 Kubernetes 中运行 Milvus 官方文档教程 如何安装k8s集群? 2026.9.22:3台服务器安装kubernetes集群最新版 概述 Milvus Operator 是一套解决方案,可帮助您在目标 Kubernetes(K8s)集群中部署…

📅 2026/10/17 2:33:57
南京大学计算机学院2026年预推免硕士考核回忆

南京大学计算机学院2026年预推免硕士考核回忆

总览线上考核:2026.8.29 15:00-16:00,线上答题线下考核:2026.9.4-2026.9.6 共3天流程:先经过一轮初筛确定进入线上测试的人员名单,在8.29完成线上测试。当晚根据线上测试结果出入围线下的名单(以邮件形式通…

📅 2026/10/17 2:33:57
友思特应用 | AI 叉车 + IDS 相机:物流装载全程可视化的实战方案

友思特应用 | AI 叉车 + IDS 相机:物流装载全程可视化的实战方案

项目概述行业:物流相机:友思特合作伙伴iDS uEye XC 自动对焦相机应用任务:在作业持续进行期间,对整套装载流程实现自动化、不间断留档记录难点:完整精准记录所有货物转运过程,不中断物料流转,同…

📅 2026/10/17 2:33:57
MORE NEWS

更多资讯

📰

AI痕迹太重怎么办?一套可落地的降AI率改稿实操指南

做内容时间久了,应该都有过这种体验:AI写初稿确实快,但发布完总觉得缺点什么——评论区没动静,甚至原本活跃的账号突然被“冷落”。这段时间“AI痕迹”成了做分享内容的朋友聊天时的高频词,很多平台和合作方也开始关注…

📰

RAG调优实战:从F1 0.42到0.89的30天方法论

做RAG调优这件事,我踩过的坑不比任何人少。去年我接手一个内部知识库项目,检索增强生成系统的F1值一开始只有0.42,辛辛苦苦调了一个月,把最终成绩干到0.89。那段时间我每天做的事,就是把“玄学”变成“旋钮”——凡是能…

📰

Android FTP服务器Demo:手机秒变局域网文件中转站

简介:一份演示在安卓设备上搭建FTP服务器的工程样例,适合需要实现局域网文件共享、远程上传下载的移动开发人员参考。项目基于Apache FtpServer组件完成,以简洁的代码结构展示了ftp服务从初始化、启动到停止的完整调用链,方便快速…

📰

基于深度学习的人脸识别系统实战:从模型选型到毕设落地

1. 项目全貌与选题价值分析1.1 这个毕设到底在做什么人脸识别系统的毕业设计,近几年一直是计算机视觉方向的热门选题。很多人一听“人脸识别”就觉得高大上,以为要自己从零写卷积神经网络、自己造轮子训练模型。实际上,一个合格的毕设项目&am…

📰

图像生成VR体验:低成本三维重建与全景漫游实战

做VR内容的人都有一个共识:建模和场景搭建特别烧钱。我这两年试过不少方案,最后发现一条性价比极高的路子——用图像直接生成VR体验。说到底,图像本身就是一种带了色彩、纹理和几何线索的信息载体,把它拆开、重组,再加…

📰

YOLOv11结合可变形注意力DAT的目标检测改进实践指南

毕设选题表一交,很多人就开始焦虑了。目标检测方向的题年年都有,但每年都有学生栽在同一个地方:要么只是改了改网络层数、换了换激活函数,答辩老师一问"为什么这么改",只能支支吾吾说"试出来效果好&quo…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬