资讯详情 MyBatis入门实战:从JDBC到增删改查,掌握Java后端数据库操作
📅 2026/10/10 10:47:39
1. 为什么Java后端绕不开MyBatis这个老伙计1.1 翻出三年前的JDBC代码我默默删掉了做Java后端开发数据库操作永远是绕不开的一环。刚工作那阵子我写过不少原生JDBC的代码每次要查一张表流程都是固定的加载驱动、获取Connection、拼SQL、创建PreparedStatement、遍历ResultSet、手动关闭资源。如果查询条件稍微变一下SQL就要重新拼一遍。一个简单的列表查询代码量轻松突破四五十行其中真正跟业务相关的SQL就那么一两句剩下的全是样板代码。更难受的是结果集的映射。表里有几个字段代码里就要写几行rs.getXxx(字段名)字段多了手都抽筋。后来我尝试过把公共部分抽成工具类稍微好了一点但只要涉及关联查询、多表分页、动态条件JDBC的方案依然非常折腾。所以当我第一次接触MyBatis的时候最大的感受就是它把SQL和Java代码的翻译官这件事彻底做好了。我再也不用自己写那一堆Connection、Statement、ResultSet的重复操作只需要把SQL写在XML里定义一个接口方法MyBatis就会自动把参数传入SQL把查询结果映射成对象返回。这个转变对开发效率的提升几乎可以用质的飞越来形容。今天这篇就从零开始把我自己实际项目里最常用到MyBatis的一整套增删改查流程按入门顺序完整过一遍给还没上手或者刚上手的朋友一个可以直接照着跑的参考。1.2 在Hibernate、JPA盛行的今天为什么我还选MyBatis可能有人会问现在Spring Data JPA、Hibernate这些全自动ORM框架也很流行为什么还要学MyBatis这个问题我在实际项目中感受很深。JPA这类框架的理念是你用对象操作我帮你自动生成SQL听起来很美好但一旦遇到复杂的多表关联、动态条件、批量更新自动生成的SQL往往不够灵活要么性能不理想要么逻辑对不上最后还得退回写原生SQL。MyBatis走的是另一条路它把SQL的控制权完全交还给开发者你写什么SQL它就执行什么SQL。这就带来两个实际好处第一SQL是自己写的执行计划、索引命中、查询效率心里有数第二动态SQL能力很强可以在XML里用if、where、foreach等标签拼出不同场景下需要的语句尤其适合业务逻辑变化频繁的系统。另外MyBatis的学习曲线非常平缓。它没有很多复杂的概念理解了Mapper接口和XML映射文件这两件事基本就能上手干活。对于团队协作来说一个新人翻两三天文档就能产出代码这种低成本上手特性在中小型团队和创业项目里吸引力很大。这也是很多Java岗位的面试题里MyBatis总是占着一席之地的原因。2. 从零搭建一个能跑起来的MyBatis项目2.1 依赖与目录结构动手写代码之前先把项目骨架搭好。我用的是Maven管理依赖JDK版本1.8以上数据库以MySQL为例。MyBatis本身只是一个ORM框架没有绑定Spring所以入门阶段完全可以直接用原生MyBatis跑通流程理解底层机制后再接入Spring这样排查问题的时候思路更清晰。在pom.xml里引入MyBatis依赖和MySQL驱动dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.16/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency我习惯的目录结构是这样清晰分区方便后续扩展src/main/java/com/demo/ ├── entity/ # 实体类和数据库表字段对应 ├── mapper/ # Mapper接口定义方法 └── util/ # 工具类比如SqlSessionFactory工厂 src/main/resources/ ├── db.properties # 数据库连接配置 ├── mybatis-config.xml # MyBatis全局配置 └── mapper/ # Mapper XML映射文件这里有个新手容易忽略的点Maven默认只把src/main/resources下的文件打包进classpath如果你把XML文件放在src/main/java下面默认是不被打进去的运行时会报Invalid bound statement。我把Mapper XML统一放在resources/mapper目录下既符合Maven规范也方便管理。2.2 全局配置文件mybatis-config.xml怎么配全局配置文件是MyBatis运行的基础里面主要配置数据库连接、事务方式、日志、类型别名等。下面这份配置是我个人比较常用的精简版可以直接复制使用?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-config.dtd configuration !-- 引入外部数据库配置 -- properties resourcedb.properties/ settings !-- 控制台打印SQL开发期必开 -- setting namelogImpl valueSTDOUT_LOGGING/ !-- 开启驼峰映射user_name - userName -- setting namemapUnderscoreToCamelCase valuetrue/ /settings !-- 实体类别名com.demo.entity.User 可以简写为 User -- typeAliases package namecom.demo.entity/ /typeAliases environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver value${driver}/ property nameurl value${url}/ property nameusername value${username}/ property namepassword value${password}/ /dataSource /environment /environments !-- 加载Mapper XML这里直接扫描包下所有接口 -- mappers package namecom.demo.mapper/ /mappers /configurationdb.properties文件放在同目录下内容如下drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/mybatis_demo?useSSLfalse usernameroot password123456从配置里能看到几个关键信息transactionManager typeJDBC表示事务管理交给MyBatis底层的JDBC事务由我们手动提交或回滚POOLED数据源是MyBatis内置的连接池入门阶段完全够用比Druid这些第三方连接池少一些配置成本typeAliases配置包别名后XML映射文件里写resultTypeUser就行不用写全限定名mappers里的package标签会自动扫描com.demo.mapper包下所有接口并寻找同路径的XML映射文件。2.3 实体类与Mapper接口这里的表结构很简单我用一张用户表user来做示例包含id、user_name、age、email四个字段。对应的实体类User如下package com.demo.entity; public class User { private Integer id; private String userName; private Integer age; private String email; // 无参构造 public User() { } // getter/setter 略 // 为了省篇幅实际代码里一定记得生成 }Mapper接口定义查询方法。MyBatis的Mapper是一个神奇的存在接口里只声明方法签名具体的SQL写在同名的XML文件里运行时MyBatis会生成接口的代理实现类把方法调用翻译成SQL执行。这也是MyBatis对开发者最友好的一点写接口的时候完全不需要写实现类。package com.demo.mapper; import com.demo.entity.User; public interface UserMapper { User selectById(Integer id); }2.4 第一次跑通select查询XML映射文件放在src/main/resources/mapper/UserMapper.xml?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.demo.mapper.UserMapper select idselectById resultTypeUser select id, user_name, age, email from user where id #{id} /select /mapper这里有个核心规则需要特别强调namespace必须写Mapper接口的全限定名id必须写接口方法名这样MyBatis才能把接口方法和XML里的SQL对应起来。没有对应关系启动时就会报Invalid bound statement (not found)。接下来写一个工具类创建SqlSessionFactory全局单例package com.demo.util; 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 SqlSessionUtil { private static final SqlSessionFactory FACTORY; static { try { InputStream inputStream Resources.getResourceAsStream(mybatis-config.xml); FACTORY new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { throw new RuntimeException(e); } } public static SqlSession openSession() { return FACTORY.openSession(); } }然后用一个简单的main方法测试查询import com.demo.entity.User; import com.demo.mapper.UserMapper; import com.demo.util.SqlSessionUtil; import org.apache.ibatis.session.SqlSession; public class Main { public static void main(String[] args) { try (SqlSession session SqlSessionUtil.openSession()) { UserMapper mapper session.getMapper(UserMapper.class); User user mapper.selectById(1); System.out.println(user); } } }第一次跑通这段代码把logImpl配置成STDOUT_LOGGING后控制台会打印出执行的SQL语句、参数和查询结果。看到那行 Preparing: select id, user_name, age, email from user where id ?说明你的第一个MyBatis查询已经成功跑通了。3. 映射文件里的门道resultType、resultMap与字段映射3.1 resultType和resultMap怎么选刚入门的时候很多人会在resultType和resultMap之间犹豫。我的建议是简单查询用resultType涉及字段名不一致、关联查询、多表映射用resultMap。resultType其实就是一个自动映射的方式。MyBatis会把查询结果的每一列按照列名和实体属性名的对应关系填充到对象里。如果数据库字段是user_name实体属性是userName在开启了mapUnderscoreToCamelCase之后MyBatis会自动把下划线风格转换为驼峰你不需要额外写任何映射关系。当查询结果比较复杂时比如一个订单关联查询用户信息返回字段来自多张表此时再用resultType指向某一个实体就不够用了需要借助resultMapresultMap iduserMap typeUser id propertyid columnid/ result propertyuserName columnuser_name/ result propertyage columnage/ result propertyemail columnemail/ /resultMap select idselectByMap resultMapuserMap select id, user_name, age, email from user where age #{age} /selectresultMap还可以处理更高级的场景一对多、多对一。用collection和association标签可以把关联表的数据嵌套映射到对象里。这一块内容比较深入门阶段先用好自动映射就够了等遇到实际需求再加不迟。3.2 下划线字段的三种处理方案数据库命名习惯通常是下划线风格Java命名习惯是驼峰风格这俩之间的转换是新手最常踩的坑。如果查询结果查询出来的列名是user_name而实体属性是userNameMyBatis默认不会自动转换结果就是userName属性为null。处理这个问题有三种方案按推荐排序第一种全局开启驼峰映射。在mybatis-config.xml的settings里配置mapUnderscoreToCamelCase为true这一点我在上文已经提过一劳永逸。第二种在SQL里给列起别名写成user_name as userName局部解决适合不想改全局配置的情况。第三种用resultMap显式映射适合复杂场景。我最推荐第一种。它几乎没有副作用也不影响已有的SQL改一行配置就能解决90%的字段映射问题。3.3 别过度依赖全局别名typeAliases配置了package namecom.demo.entity/之后XML里可以用User代替com.demo.entity.User。这能少打几个字但我不建议在resultType里写缩写因为成员多了之后IDE自动补全反而会因为重名干扰判断。如果要写resultType要么用全限定名要么确保实体类在全局范围内不重名。实际开发里一个项目可能有好几个User相关的类如果命名不当一旦出现了同名的类全局限定别名会失效排查起来很麻烦。我个人的习惯是resultType写全限定名虽然长一点但可读性和可维护性更稳parameterType反而不建议写让MyBatis自己推断参数类型可以减少很多配置错误。4. 增删改查逐个击破SQL与事务齐头并进4.1 查询单条记录与列表查询先补全UserMapper接口把增删改查的方法都定义好package com.demo.mapper; import com.demo.entity.User; import java.util.List; public interface UserMapper { User selectById(Integer id); ListUser selectAll(); User selectByUserName(String userName); int insert(User user); int updateById(User user); int deleteById(Integer id); int batchDelete(ListInteger ids); }对应的XML里查询相关的SQL如下select idselectById resultTypeUser select id, user_name, age, email from user where id #{id} /select select idselectAll resultTypeUser select id, user_name, age, email from user /select select idselectByUserName resultTypeUser select id, user_name, age, email from user where user_name #{name} /selectselectById返回单个对象selectAll返回ListUser。MyBatis会根据方法返回值类型自动选择合适的处理逻辑返回值既可以写成接口的返回类型也可以写成接口方法返回值类型的元素类型。用的时候注意一个细节如果你的查询条件实际上可能查出多条记录但方法返回值是单个对象MyBatis会抛出TooManyResultsException这一点和JPA的行为不太一样要留心。4.2 新增useGeneratedKeys拿到自增主键插入是增删改查里最有门道的一个操作因为新增之后往往需要拿到自增主键用于后续业务处理。比如新增一个用户马上要拿这个用户的id生成订单号少了主键回填就得再查一次麻烦且多一次数据库往返。XML里这样写insert idinsert parameterTypeUser useGeneratedKeystrue keyPropertyid insert into user (user_name, age, email) values (#{userName}, #{age}, #{email}) /insertuseGeneratedKeystrue表示使用JDBC生成的自增主键keyPropertyid表示把生成的主键值写入传入对象的id属性里。测试代码可以验证User user new User(); user.setUserName(张三); user.setAge(25); user.setEmail(zhangsanexample.com); try (SqlSession session SqlSessionUtil.openSession(true)) { UserMapper mapper session.getMapper(UserMapper.class); mapper.insert(user); System.out.println(插入后的主键 user.getId()); }第二个参数传true表示自动提交事务。如果不传默认是手动提交必须调用session.commit()否则数据不会真正写入数据库。这个细节我见过太多同事踩过后面第6章会专门细讲。4.3 更新动态结合if标签更新操作最常见的场景是前端只提交了部分字段后端只想更新有值的字段不希望把其他字段覆盖成null。这种需求用if标签配合set标签来实现update idupdateById update user set if testuserName ! null and userName ! user_name #{userName}, /if if testage ! null age #{age}, /if if testemail ! null and email ! email #{email}, /if /set where id #{id} /updateset标签会自动处理几个动态字段之间的逗号问题并且会在没有任何字段需要更新的时候避免生成一个语法错误的update user set where ...。你可以打开日志看看生成的SQL会发现MyBatis在动态拼接这块替我们做了不少细节处理。4.4 删除单条删除与批量删除单条删除比较简单delete iddeleteById delete from user where id #{id} /delete批量删除则会用到foreach标签delete idbatchDelete delete from user where id in foreach collectionids itemid open( separator, close) #{id} /foreach /delete这段SQL的最终效果是delete from user where id in (1,2,3)。collection表示传入参数的属性名item是集合元素的临时变量名open和close决定前缀后缀separator决定元素之间的分隔符。调用方式ListInteger ids List.of(1, 2, 3); try (SqlSession session SqlSessionUtil.openSession(true)) { UserMapper mapper session.getMapper(UserMapper.class); mapper.batchDelete(ids); }批量删除这个案例背后的逻辑很简单但也很典型MyBatis的动态SQL本质上是在XML标签的帮助下用代码逻辑生成最终SQL理解了这个核心思想foreach、if这些标签就都好学了。5. 参数传递的几种姿势#{}、${}和动态SQL5.1 参数传递到底有哪几种方式很多入门教程讲参数传递时会列出一大堆场景其实归纳起来主要就三种单参数、多参数、对象参数。单参数最简单直接传一个值给SQL里的#{xxx}xxx是什么名字都无所谓都能取到值User user mapper.selectById(1);多参数就必须借助Param注解或者封装成对象/Map。例如接口方法User selectByNameAndAge(Param(name) String name, Param(age) Integer age);XML里这样引用select idselectByNameAndAge resultTypeUser select id, user_name, age, email from user where user_name #{name} and age #{age} /select如果不加Param注解MyBatis会按参数位置提供默认的param1、param2这种名字比如#{param1}。短期看能用但代码可读性极差我不建议这么写。对象参数是最常用的方式尤其是新增和更新直接把实体对象传进去#{}里的名字就是实体属性名insert idinsert insert into user (user_name, age, email) values (#{userName}, #{age}, #{email}) /insert传Map也是同理#{}里的名字就是Map的key。对象参数同时携带多个字段时阅读性最好也是我在实际项目里用得最多的方式。5.2 #{}和${}的本质区别一句话讲透这个问题面试被问的概率很高。简单来说#{}是预编译参数占位符MyBatis会把它替换成?再通过PreparedStatement的setXxx方法设置参数值这个过程对传入值做了安全处理能有效防止SQL注入。${}则是字符串直接拼接MyBatis会把它替换成实际的内容写进SQL里不做任何转义。看一个对比就明白了。假设传入了name 张三 OR 11用#{}最终SQL是where name ?参数值被当作纯字符串处理用${}最终SQL变成where name 张三 OR 11条件被改写危险。那么${}完全不能用吗也不是。它适合那些无法用占位符表达的场景比如动态排序字段order by ${sortColumn}或者动态表名select * from ${tableName}。这类场景下参数值来自系统内部配置而不是用户直接输入风险可控。核心原则是能写#{}的地方不用${}必须用${}的时候参数来源要严格校验白名单。5.3 动态SQL里的where、if、set、foreach怎么配合动态SQL是MyBatis的精髓。前面更新操作用了set和if批量删除用了foreach再看一个组合使用的多条件查询select idselectByCondition resultTypeUser select id, user_name, age, email from user where if testuserName ! null and userName ! and user_name like concat(%, #{userName}, %) /if if testage ! null and age #{age} /if if testemail ! null and email ! and email #{email} /if /where order by id /selectwhere标签的聪明之处在于如果内部所有if都不成立它不会生成where关键字只要有一个条件成立它会自动去掉第一个条件前面的and。这个设计免去了我们手工判断要不要拼where、第一个and要不要去掉的繁琐逻辑。结合ifwheresetforeach这几个标签你基本就能应付90%以上的动态SQL业务场景了。至于choose、trim这些更高级的标签建议入门后遇到具体需求再边查边用不用一开始就全部背下来。6. 入门最常踩的坑与排查思路6.1 Mapper接口和XML绑定失败报Invalid bound statement我见过太多次这个报错代码写得没问题SQL也正确一运行就抛出Invalid bound statement (not found): com.demo.mapper.UserMapper.selectById。这个坑的原因通常有以下几种第一种XML映射文件的namespace和Mapper接口全限定名不一致。检查一下namespace是不是写成了类名而不是全限定名或者接口的包路径和XML里的package配置不对。第二种XML文件没有被打包到classes目录。确认XML是否放在了src/main/resources下或者构建工具是否配置了对*.xml资源的过滤排除规则。第三种接口方法和XML里的id名字对不上。方法名、参数数量可以不同但id必须精确匹配。排查方法很直接打开target/classes目录看XML有没有被复制进去没有的话检查Maven配置在pom.xml里补上资源声明build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory includes include**/*/include /includes /resource /resources /build这是最简单的排查链路只要按顺序走一遍大概率能定位问题。6.2 SqlSession的提交与关闭很多人栽在这里MyBatis的SqlSession默认不是自动提交的。执行完insert、update、delete之后如果不调用session.commit()事务会一直挂起直到session关闭时回滚。这也是很多新人最常见的疑惑我明明执行了insert数据库里怎么没有数据正确做法是写完数据变更操作后记得提交。两个方式一个是显式调用session.commit()一个是在openSession时传入true开启自动提交SqlSession session SqlSessionUtil.openSession(true);另外SqlSession使用完应该关闭。MyBatis的SqlSession不是线程安全的每个线程都应该有自己的实例用完及时关闭释放连接。我习惯用try-with-resources写法保证无论正常还是异常都能关闭try (SqlSession session SqlSessionUtil.openSession(true)) { UserMapper mapper session.getMapper(UserMapper.class); mapper.insert(user); }6.3 一条SQL调试到底日志输出的正确姿势入门阶段排错最有效的工具就是日志。MyBatis的logImpl设置成STDOUT_LOGGING后SQL、参数、返回结果都会打印到控制台这是最直观的调试方式。生产环境里则建议接上SLF4J Logback把SQL日志单独配置一个logger按需开启settings setting namelogImpl valueSLF4J/ /settings日志里能看到的不仅仅是SQL本身还包括 Parameters:这一行它会打印实际传入的每个参数值。我强烈建议入门阶段每次都盯着这一行看它往往能帮你快速定位参数类型错误、空值、格式不对之类的问题。比如说你本来想传一个Integer类型的id结果调用时传了个String日志的Parameters那行通常就会显示类型不匹配。把这个当成日常习惯之后很多排查工作根本不需要Debug断点看一眼日志就够了。做了几年Java开发MyBatis算是陪我走过了绝大多数项目的一个框架。它没有很复杂的花活但把数据库操作这件最容易写出重复代码的事情简化成了接口加XML的清爽结构。如果你正在入门不用急着记口诀按我上面的顺序把环境搭起来把增删改查逐个跑一遍遇到报错就开日志看SQL再对照第6章的常见坑排查一遍基本两天就能上手。后面再深入接触Spring整合、动态SQL进阶、缓存机制就会顺畅很多。