Javaweb---MyBatis增删改查

MyBatis 增删改查 CRUD ------ 从零开始的持久层框架学习

原创 · 适合刚学完 Java 基础、第一次碰框架的同学。本文用 XML注解 两种方式,把一张 user 表的增删改查从头到尾跑通,最后再聊聊底层原理。代码全都能直接抄去用


一. 前言:MyBatis 到底是个啥?

ok,我们先别急着写代码,先搞清楚 MyBatis 在你的项目里到底干了啥

如果你之前手写过 JDBC,一定被这一坨东西折磨过:注册驱动、建连接、写 PreparedStatement、处理 ResultSet、关连接......又臭又长,还全是样板代码。

MyBatis 就是一个"半自动 ORM 框架",它帮你把这些脏活累活全包了:

  • 连接、预编译、结果集解析 ------ MyBatis 帮你做;
  • SQL 语句 ------ 还是要你自己写(这恰恰是它"灵活、好控制"的原因,想怎么写怎么写)。

一句话:MyBatis = JDBC 的"自动挡"版本,但方向盘还在你手里。

对比一下你就懂了:

原生 JDBC MyBatis
写 SQL 要写 要写
建连接/关连接 手动 自动
参数设值 手动 setXxx #{} 自动
结果映射成对象 手动 getString 自动封装成实体类

本文学完,你就能用 MyBatis 把一个 user 表的 增(Create)、查(Retrieve)、改(Update)、删(Delete) ------ 也就是 CRUD ------ 完完整整跑起来 🚀


二. 环境准备:Maven 依赖

MyBatis 本身只有一个核心包,再配一个 MySQL 驱动就够;测试用 JUnit,实体类咱们偷懒用 Lombok。

pom.xml 直接贴上:

xml 复制代码
<dependencies>
    <!-- MyBatis 核心 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.9</version>
    </dependency>

    <!-- MySQL 驱动(版本与本地 MySQL 对应,8.x 用 cj 驱动)-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.31</version>
    </dependency>

    <!-- Lombok:自动生成 getter/setter/构造器 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
    </dependency>

    <!-- JUnit 单元测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

📌 驱动小贴士 :MySQL 8.x 用 com.mysql.cj.jdbc.Driver;5.x 用 com.mysql.jdbc.Driver。版本不匹配会报"驱动类找不到"或"时区"错误,新手十有八九栽在这。


三. 数据库与实体类

3.1 建表 SQL

先整张表,字段就那几个最基础的:

sql 复制代码
CREATE TABLE user (
    id       INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(20),
    password VARCHAR(20),
    age      INT,
    sex      CHAR(1),
    email    VARCHAR(30)
);

3.2 实体类 User(Lombok 版)

java 复制代码
package com.itheima.pojo;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

@Data               // 自动生成 getter/setter/toString/equals/hashCode
@NoArgsConstructor  // 无参构造(MyBatis 反射创建对象需要)
@AllArgsConstructor // 全参构造(测试时快速 new)
public class User {
    private Integer id;
    private String  username;
    private String  password;
    private Integer age;
    private Character sex;
    private String  email;
}

📌 两个坑提前说(后面会反复遇到):

  • 实体类字段名尽量和表列名一致;
  • @NoArgsConstructor 超级关键 ------ MyBatis 查询时是用"无参构造 + setter"把列值填进对象的,没有无参构造会直接炸。

四. 核心配置文件 mybatis-config.xml

这是 MyBatis 的"总开关":声明连哪个库、用哪种事务、去哪找 Mapper。

mybatis-config.xml(放在 resources 目录下):

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!-- 引入外部 jdbc.properties,避免把账号密码写死 -->
    <properties resource="jdbc.properties"/>

    <!-- 别名:包下所有实体类,别名=类名(User 即可代表 com.itheima.pojo.User)-->
    <typeAliases>
        <package name="com.itheima.pojo"/>
    </typeAliases>

    <!-- 环境:连哪个库、用什么事务管理、什么数据源 -->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>    <!-- 用原生 JDBC 事务 -->
            <dataSource type="POOLED">            <!-- 连接池 -->
                <property name="driver"   value="${jdbc.driver}"/>
                <property name="url"      value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 注册 Mapper:写包名可一次引入该包下所有映射文件 -->
    <mappers>
        <package name="com.itheima.mapper"/>
    </mappers>
</configuration>

jdbc.properties(同目录):

properties 复制代码
# 这些 key 会被上面 ${jdbc.xxx} 引用
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis_study?useSSL=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456

📌 三处"名字对应"是新手最容易错的地方,记死了:

  1. namespace 必须等于 Mapper 接口全限定名
  2. 映射标签 id 必须等于接口方法名
  3. SQL 里 #{属性名} 必须等于实体/参数对象的属性名

五. 方式一:XML 映射 CRUD(最经典)

这是 MyBatis 最原汁原味的写法:接口里只声明方法,SQL 全写在外面的 .xml 里。

5.1 Mapper 接口(只声明,不写实现)

java 复制代码
package com.itheima.mapper;

import com.itheima.pojo.User;
import java.util.List;

public interface UserMapper {
    // 查全部
    List<User> findAll();
    // 按 id 查
    User findById(Integer id);
    // 增
    int insert(User user);
    // 改
    int update(User user);
    // 删
    int delete(Integer id);
}

5.2 映射文件 UserMapper.xml(SQL 写这里)

位置:resources/com/itheima/mapper/UserMapper.xml(目录结构和接口包名一致)。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace 必须等于接口全限定名,MyBatis 靠它把接口和 SQL 绑在一起 -->
<mapper namespace="com.itheima.mapper.UserMapper">

    <!-- 查全部:resultType 是返回的单条记录类型(用了别名 User)-->
    <select id="findAll" resultType="User">
        select * from user
    </select>

    <!-- 按 id 查:parameterType 是入参类型,#{id} 从方法参数取值 -->
    <select id="findById" parameterType="int" resultType="User">
        select * from user where id = #{id}
    </select>

    <!-- 增:#{username} 等取 user 对象的属性 -->
    <insert id="insert" parameterType="User">
        insert into user(username, password, age, sex, email)
        values(#{username}, #{password}, #{age}, #{sex}, #{email})
    </insert>

    <!-- 改 -->
    <update id="update" parameterType="User">
        update user set username=#{username}, password=#{password},
        age=#{age}, sex=#{sex}, email=#{email} where id=#{id}
    </update>

    <!-- 删 -->
    <delete id="delete" parameterType="int">
        delete from user where id = #{id}
    </delete>
</mapper>

📌 为什么用 #{id} 而不是拼字符串? #{} 会被 MyBatis 替换成 预编译占位符 ?,再由 JDBC 安全地设参数------既防 SQL 注入,又自动处理类型。这个点在"常见坑"里还会再强调。


六. 方式二:注解 CRUD(更简洁)

不想额外写 XML?把 SQL 直接写在接口方法的注解里,接口和 SQL 合二为一

java 复制代码
package com.itheima.mapper;

import com.itheima.pojo.User;
import org.apache.ibatis.annotations.*;   // 注意导入 annotations 包
import java.util.List;

public interface UserMapper {

    @Select("select * from user")
    List<User> findAll();

    @Select("select * from user where id = #{id}")
    User findById(@Param("id") Integer id);

    @Insert("insert into user(username,password,age,sex,email) " +
            "values(#{username},#{password},#{age},#{sex},#{email})")
    int insert(User user);

    @Update("update user set username=#{username},password=#{password} " +
             "where id=#{id}")
    int update(User user);

    @Delete("delete from user where id = #{id}")
    int delete(@Param("id") Integer id);
}

📌 划重点

  • 注解里写的是"一条 SQL 字符串",参数用 #{属性名} 占位;
  • 多参数建议加 @Param("名字"),注解里才能对应上;
  • 增删改返回 int(受影响行数);查询靠方法返回类型决定返回对象还是列表;
  • XML 和注解二选一即可,不能对同一方法重复定义。

七. 会话工具类:获取 SqlSession

MyBatis 操作数据库前,必须先拿到 SqlSession(Java 与数据库之间的"会话")。把它封装成工具类,避免到处重复写。

java 复制代码
package com.itheima.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.*;
import java.io.IOException;
import java.io.InputStream;

public class MyBatisUtils {
    private static SqlSessionFactory factory;

    static {
        try {
            // 1. 读核心配置文件
            InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. 构建工厂(全局唯一,线程安全)
            factory = new SqlSessionFactoryBuilder().build(is);
        } catch (IOException e) { e.printStackTrace(); }
    }

    // 3. 拿到会话;true = 自动提交事务(省得每次 commit)
    public static SqlSession getSqlSession() {
        return factory.openSession(true);
    }
}

📌 三个核心对象要分清

  • SqlSessionFactoryBuilder:用完即丢,专门建工厂;
  • SqlSessionFactory:全局一个,专门建会话;
  • SqlSession:一次数据库操作一个,用完要关

openSession(true) 自动提交,openSession() 默认手动提交(必须显式 commit())。


八. 测试类:JUnit 跑通 CRUD

java 复制代码
public class UserMapperTest {

    @Test
    public void testFindAll() {
        try (SqlSession session = MyBatisUtils.getSqlSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);  // 拿到代理对象
            mapper.findAll().forEach(System.out::println);
        }   // try-with-resources 自动 close
    }

    @Test
    public void testInsert() {
        try (SqlSession session = MyBatisUtils.getSqlSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            int rows = mapper.insert(
                new User(null, "张三", "123", 23, '男', "z@163.com"));
            // 若 openSession() 默认手动提交,这里必须 session.commit();
            System.out.println(rows > 0 ? "插入成功" : "失败");
        }
    }

    @Test
    public void testUpdate() {
        try (SqlSession session = MyBatisUtils.getSqlSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            mapper.update(new User(1, "李四", "999", 25, '女', "l@163.com"));
        }
    }

    @Test
    public void testDelete() {
        try (SqlSession session = MyBatisUtils.getSqlSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            mapper.delete(1);
        }
    }
}

⚠️ 增删改别忘了提交事务! 这是各路教程都重点提醒的坑。若 openSession() 没传 true,增删改执行后必须调 session.commit() ,否则数据不会真正写入库(程序还不报错,但库里没变化)。本例用 openSession(true) 规避了这点。


九. 原理:接口没实现类,为什么能调用?

很多人第一次用 MyBatis 都会懵:UserMapper 我压根没写实现类,为啥 mapper.findById(1) 能直接跑?

复制代码
你调用 mapper.findById(1)
   → 实际是动态代理对象
      → 代理读 SQL → 执行 JDBC → 映射结果

session.getMapper(UserMapper.class) 返回的不是你写的实现类,而是 MyBatis 用 JDK 动态代理 在运行时"现场生成"的代理对象。流程是这样的:

  • 接口只是契约(定义能做什么),没有方法体;
  • 代理对象"截胡"了你的方法调用,进到统一的 invoke() 逻辑里;
  • 该逻辑:读取注解/XML 里对应的 SQL → 用 JDBC 预编译执行 → 把 ResultSet 按列名映射成 User 对象 → 返回给你。

📌 这就是"只写接口 + SQL 就能干活"的真相:脏活(连接、预编译、防注入、结果映射)全被动态代理兜底了。


十. XML vs 注解:到底怎么选?

维度 XML 映射 注解
SQL 位置 独立 .xml 文件 写在接口方法上
简单 CRUD 可用,但多一个文件 更简洁,推荐
动态 SQL(if/foreach) 原生支持,清爽 <script> 包裹,难看
改 SQL 是否重编译 不用(改完重启即可)
对应关系约束 namespace=接口全名,id=方法名 方法名即 SQL 标识

📌 实战惯例 :简单单表用注解快速搞定;涉及多表关联、条件拼接、批量操作时,改用 XML。两者可混用(同一接口的不同方法分别用注解和 XML),但同一方法只能有一种


十一. 常见坑(踩过才记得住)

  1. 事务没提交 :增删改后数据没进库 → 用 openSession(true) 或手动 commit()
  2. namespace 写错:必须 = Mapper 接口全限定名,否则"绑定语句找不到"。
  3. id 与接口方法名不一致:映射标签 id 必须等于方法名。
  4. resultType 漏写:查询必须指定返回类型(或 resultMap)。
  5. 参数名不匹配 :多参数用 @Param,XML 里 #{名字} 与之一致;单对象用属性名。
  6. SQL 末尾加分号:部分数据库会报语法错,建议不写。
  7. 字段名 vs 属性名 :不一致用 resultMap 或开驼峰映射。
  8. ${} 拼接值 :有 SQL 注入风险,值参数一律用 #{}${} 仅用于动态表名/排序列名。

十二. 进阶与 SpringBoot 整合

12.1 多参数与 Map

不想建实体时,可用 Map@Param 传参:

java 复制代码
// 接口
void addUser2(Map<String, Object> map);

// XML:key 直接当占位名
<insert id="addUser2" parameterType="map">
    insert into user(id, username, password) values(#{userId}, #{userName}, #{userPwd})
</insert>

12.2 MyBatisX 插件

IDEA 装 MyBatisX 后,接口方法与 XML 标签之间可点箭头互跳,还能按接口方法名自动生成 XML 骨架,效率直接拉满 🚀

12.3 SpringBoot 整合(你后续会学)

在 SpringBoot 里,不再手写 SqlSessionFactory,而是:

  • mybatis-spring-boot-starter 依赖;
  • 在启动类或配置类上用 @MapperScan("com.itheima.mapper") 扫描 Mapper 接口;
  • 每个 Mapper 接口加 @Mapper(或靠扫描),框架自动注入代理对象;
  • 配置写在 application.ymlmybatis: 下,数据源交给 Spring 管理。

📌 本质没变:SpringBoot 只是把"构建 SqlSessionFactory、注册 Mapper、注入代理对象"这些步骤自动化了。底层 CRUD 原理(动态代理 + JDBC)和你本章手写的完全一致。


十三. 面试题速记

  • #{}${} 的区别? #{} 是预编译占位符(?),安全防注入、自动加类型引号;${} 是字符串直接拼接,有注入风险,仅用于动态表名/排序字段。
  • ② 为什么 Mapper 接口能直接注入调用? MyBatis 用 JDK 动态代理在运行时为接口生成代理对象,代理里封装了"读 SQL → 执行 JDBC → 映射结果"的全部逻辑,并由框架注册可用。
  • ③ 增删改后查不到数据? 多半是事务没提交。手动提交需 commit(),或 openSession(true) 自动提交。
  • ④ XML 里 namespace 和 id 有什么用? namespace 把 XML 绑定到某个 Mapper 接口;id 对应接口里的方法名,两者合起来唯一定位一条 SQL。
  • ⑤ 注解和 XML 能混用吗? 可以混用在不同方法上,但同一方法只能有一种 SQL 定义,否则启动报错。

十四. 结语

ok,到这儿 MyBatis 的增删改查 CRUD 就完整跑通了 🎉 咱们来捋一下记忆主线:

  1. 准备 :Maven 依赖(mybatis + mysql驱动 + junit/lombok)→ 建表 → 实体类(@Data + 无参构造)。
  2. 配置mybatis-config.xml 管环境/别名/注册 Mapper;SQL 写在 XML 映射或接口注解里。
  3. CRUD :查用 @Select/<select>resultType;增删改用 @Insert/@Update/@Delete 或对应标签,返回受影响行数。
  4. 执行getMapper 拿代理 → 调用方法;增删改务必提交事务。
  5. 原理 :动态代理让"只写接口+SQL"成为可能;#{} 预编译防注入。

如果对你有帮助的话,不妨点一个免费的赞和收藏 👍 你们的支持就是我最大的动力!有问题欢迎评论区交流,咱们下篇继续"从零开始"系列~

标签#mybatis #java #spring boot #数据库 #后端

相关推荐
Lost of 程序猿2 小时前
ASP.NET Core 认证授权实战:从 JWT 到 Policy,设计一套企业级 RBAC 权限系统
后端·asp.net
Lost of 程序猿2 小时前
ASP.NET Core 后台任务全景:从 BackgroundService 到 Channel 队列,再到分布式调度
后端·asp.net·.netcore
倾颜2 小时前
NestJS 核心概念梳理:从 Module、Controller 到 Guard、Interceptor
后端·node.js·nestjs
xier_ran2 小时前
【infra之路】GPU 存储层次总结:L1 / L2 / HBM
java·网络·数据库
东风破_2 小时前
JWT 1:从一个登录请求开始,理解 React 项目里的 API 层与 Mock
前端·后端
东风破_3 小时前
JWT 3:为什么 Token 要放进 Authorization?Axios 拦截器到底解决了什么?
前端·后端
东风破_3 小时前
JWT 5:路由守卫是什么?把整个 JWT 登录鉴权流程串起来
前端·后端
东风破_3 小时前
JWT 2:HTTP 是无状态的,为什么登录成功后还要给 Token?
前端·后端
东风破_3 小时前
JWT 4:Zustand 到底解决了什么?为什么登录状态要放进 Store?
前端·后端