第一部分:流程复用思路
1.1 核心思想
⭐老师强调:首个流程走通后,后续流程无需重复写前期逻辑,因为基础已铺垫好,只需编写并调用核心业务段即可。
流程复用示例:
| 流程 | 需要写的代码 |
|---|---|
| 第一个流程(findAll) | 完整写加载配置、创建工厂、会话、映射、查询、处理结果 |
| 后续流程(findById、insert等) | 只写接口方法 + 映射文件 + 测试调用 |
⭐老师强调:区分"写逻辑"和"调业务"——第一个流程要完整写查找等逻辑,后续只管写核心段、调核心业务,不必重铺前文。
第二部分:增删改查完整实现
2.1 接口定义(StudentDao.java)
package com.qcby.dao; import com.qcby.entity.Person; import com.qcby.entity.Student; import org.apache.ibatis.annotations.Param; import java.util.List; public interface StudentDao { // 查找全部学生信息 List<Student> findAll(); // 根据id查找学生信息 Student findById(int id); // 插入 int insert(Student student); // 删除 int delete(int id); // 修改 int update(Student student); // 模糊查找 List<Student> findByName(String name); // 分页查找 List<Student> findStudent(@Param("name123") String name, @Param("start") int start, @Param("limit") int limit); // 包装对象参数 List<Student> findAllPerson(Person person); }2.2 映射文件(StudentDao.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"> <mapper namespace="com.qcby.dao.StudentDao"> <!-- 查找全部(使用resultMap映射) --> <select id="findAll" resultMap="demo"> select * from student </select> <resultMap id="demo" type="com.qcby.entity.Student"> <result column="id" property="id" jdbcType="INTEGER" javaType="java.lang.Integer"/> <result column="sname" property="name" jdbcType="VARCHAR" javaType="java.lang.String"/> <result column="sage" property="age"/> <result column="sex" property="sex"/> </resultMap> <!-- 根据id查找 --> <select id="findById" parameterType="java.lang.Integer" resultType="com.qcby.entity.Student"> select * from student where id = #{id} </select> <!-- 插入(带selectKey获取自增id) --> <insert id="insert" parameterType="com.qcby.entity.Student"> insert into student(name,age,sex) values(#{name},#{age},#{sex}) <selectKey keyProperty="id" keyColumn="id" resultType="java.lang.Integer" order="AFTER"> select last_insert_id() </selectKey> </insert> <!-- 删除 --> <delete id="delete" parameterType="com.qcby.entity.Student"> delete from student where id = #{id} </delete> <!-- 修改 --> <update id="update" parameterType="com.qcby.entity.Student"> update student set name = #{name},age=#{age},sex=#{sex} where id = #{id} </update> <!-- 模糊查找(推荐用concat) --> <select id="findByName" parameterType="java.lang.String" resultType="com.qcby.entity.Student"> select * from student where name like concat("%",#{name},"%") </select> <!-- 分页查找(多参数用@Param注入) --> <select id="findStudent" resultType="com.qcby.entity.Student"> select * from student where name like concat("%",#{name123},"%") limit #{start},#{limit} </select> <!-- 包装类对象参数 --> <select id="findAllPerson" parameterType="com.qcby.entity.Person" resultType="com.qcby.entity.Student"> select * from student where age = #{student.age} </select> </mapper>第三部分:#{}与${}的区别
3.1 核心对比
| 对比 | #{} | ${} |
|---|---|---|
| 本质 | 占位符赋值,预编译 | 字符串拼接 |
| 防SQL注入 | ✅ 能 | ❌ 不能 |
| 简单类型变量名 | 任意写 | 低版本必须value,高版本任意写 |
| 字符串字段 | 自动加'' | 不会自动加'' |
| 实体类对象 | 必须是引用类型的属性 | 必须是引用类型的属性 |
⭐老师强调:
#{}本质同JDBC预编译占位符,把用户输入与指令分离,能挡SQL注入;${}是纯拼接,先拼成完整SQL再编译执行,你传什么就拼什么去查。
3.2 示例说明
<!-- #{}:预编译占位,安全 --> <select id="findById" parameterType="java.lang.Integer" resultType="com.qcby.entity.Student"> select * from student where id = #{id} </select> <!-- ${}:字符串拼接,有注入风险 --> <select id="findByName" parameterType="java.lang.String" resultType="com.qcby.entity.Student"> select * from student where name like "%${name}%" </select> <!-- ✅ 推荐:concat + #{},既安全又能模糊查询 --> <select id="findByName" parameterType="java.lang.String" resultType="com.qcby.entity.Student"> select * from student where name like concat("%",#{name},"%") </select>⭐老师强调:
#{}写在SQL引号内无法将参数塞入占位符,必须写在引号外。涉及引号时框架会自动加引号。
3.3 字符串字段的引号处理
<!-- ${}:不会自动加引号,字符串字段必须手动加 --> insert into student(name,age,sex) values('${name}',${age},'${sex}') <!-- #{}:会自动加引号,无需手动处理 --> insert into student(name,age,sex) values(#{name},#{age},#{sex})第四部分:参数类型详解
4.1 四种入参类型
⭐老师强调:
parameterType入参类型分四类。
| 类型 | 说明 | 示例 |
|---|---|---|
| 简单数据类型 | 8个基本类型+包装类+String | java.lang.Integer、java.lang.String |
| 实体类对象类型 | 整个对象作为参数 | com.qcby.entity.Student |
| 多个数据 | 多个参数用@Param注入 | @Param("name") String name, @Param("start") int start |
| 包装对象类型 | 实体类嵌套实体类 | com.qcby.entity.Person(内含Student) |
4.2 简单类型
<select id="findById" parameterType="java.lang.Integer" resultType="com.qcby.entity.Student"> select * from student where id = #{id} </select>⭐老师强调:简单类型参数括号里变量名可任意写,都能识别。推荐写全称如
java.lang.Integer更规范。String本质是引用类型,但因太常用,传参时和int无区别。
4.3 实体类对象类型
<insert id="insert" parameterType="com.qcby.entity.Student"> insert into student(name,age,sex) values(#{name},#{age},#{sex}) </insert>⭐老师强调:括号里的名字必须与实体类字段名严格匹配,随便写demo这类不存在的字段就识别不到。传入的是对象,就得按对象结构取属性。
4.4 多个参数(@Param注入)
// 接口 List<Student> findStudent(@Param("name123") String name, @Param("start") int start, @Param("limit") int limit);<!-- 映射文件 --> <select id="findStudent" resultType="com.qcby.entity.Student"> select * from student where name like concat("%",#{name123},"%") limit #{start},#{limit} </select>⭐老师强调:多参数时
#{}无法直接绑定,必须用@Param注解注入——注解里写什么名,#{}里就用什么名。多个参数写法直接写在方法括号中即可。
4.5 包装对象类型(实体类嵌套)
Person实体类:
package com.qcby.entity; public class Person { private Student student; public Student getStudent() { return student; } public void setStudent(Student student) { this.student = student; } @Override public String toString() { return "Person{student=" + student + '}'; } }映射文件:
<select id="findAllPerson" parameterType="com.qcby.entity.Person" resultType="com.qcby.entity.Student"> select * from student where age = #{student.age} </select>测试代码:
@Test public void findAllPerson() { Person person = new Person(); Student student = new Student(); student.setAge(25); student.setName("小宇"); student.setSex("男"); student.setId(28); person.setStudent(student); List<Student> students = mapper.findAllPerson(person); for (Student s : students) { System.out.println(s); } }⭐老师强调:当入参是包含嵌套类型的实体类(如Person里嵌了Student),若想用Student里的字段做条件,直接用点形式访问即可,如
#{student.age}。难点在于理解嵌套对象字段的访问路径——不像平铺字段直接取,得明确层级。
第五部分:resultMap结果集映射
5.1 为什么需要resultMap
⭐老师强调:当数据库字段和实体属性名称不匹配(如库里是
sname/sage,代码里是name/age)时,不能用resultType,必须改用resultMap做结果集映射。
5.2 使用方式
<resultMap id="demo" type="com.qcby.entity.Student"> <!-- column: 数据库中的字段名称 --> <!-- property: 实体类中的名字 --> <!-- jdbcType: 数据库中的字段类型 --> <!-- javaType: 实体类中数据的类型 --> <result column="id" property="id" jdbcType="INTEGER" javaType="java.lang.Integer"/> <result column="sname" property="name" jdbcType="VARCHAR" javaType="java.lang.String"/> <result column="sage" property="age"/> <result column="sex" property="sex"/> </resultMap> <select id="findAll" resultMap="demo"> select * from student </select>5.3 属性说明
| 属性 | 说明 | 是否必须 |
|---|---|---|
column | 数据库字段名 | ✅ 必须 |
property | 实体类属性名 | ✅ 必须 |
jdbcType | 数据库字段类型(大写) | ❌ 可省略 |
javaType | 实体类数据类型 | ❌ 可省略 |
⭐老师强调:
jdbcType和javaType不写也能自动识别,写了反而容易出错,所以尽量别写。column和property是映射核心不能省。
第六部分:插入后获取自增id
6.1 selectKey的使用
<insert id="insert" parameterType="com.qcby.entity.Student"> insert into student(name,age,sex) values(#{name},#{age},#{sex}) <selectKey keyProperty="id" keyColumn="id" resultType="java.lang.Integer" order="AFTER"> select last_insert_id() </selectKey> </insert>6.2 属性说明
| 属性 | 说明 |
|---|---|
keyProperty | 把查出的主键放到哪个属性(如Student的id) |
keyColumn | 数据库主键名(如id) |
resultType | 主键类型 |
order | AFTER=插入后查;BEFORE=插入前查 |
⭐老师强调:
- order的区分:设了自动递增用
AFTER(插入后自动把新id返到对象里);没设自动递增用BEFORE(添加前先查id)- 为什么不查max(id):单线程下查最大id可行,但并发场景下你插入完、查最大id前,可能别的线程也插了数据。必须用返回刚插入id的机制才可靠
6.3 测试代码
@Test public void insert() { Student student = new Student(); student.setName("小红"); student.setAge(18); student.setSex("女"); int code = mapper.insert(student); // 事务提交 session.commit(); System.out.println(code); // 受影响行数 System.out.println(student.getId()); // 刚插入的id System.out.println(student); }⭐老师强调:执行后返回两样:一是受影响行数;二是最新添加记录的id(直接存入Student对象,不单独返回)。
第七部分:事务提交
7.1 何时需要事务提交
| 操作 | 是否需要commit |
|---|---|
| 增删改 | ✅ 必须 |
| 查询 | ❌ 不需要 |
⭐老师强调:只有增删改这类改动数据库的操作才需要事务提交,查找不用。不加事务提交,数据无法真正写入库里。
7.2 测试示例
@Test public void delete() { int code = mapper.delete(26); session.commit(); // 必须提交 System.out.println(code); } @Test public void update() { Student student = new Student(); student.setName("小宇"); student.setAge(20); student.setSex("男"); student.setId(28); int code = mapper.update(student); session.commit(); // 必须提交 System.out.println(code); }⭐老师强调:修改操作时name、age、sex三个参数必须全传,少一个就会出错;后续可加动态SQL来避免这个问题。
第八部分:日志配置
8.1 settings配置
<configuration> <settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <environments default="mysql"> ... </environments> <mappers> ... </mappers> </configuration>⭐老师强调:MyBatis配置文件有严格的标签顺序约束——
settings必须最前,接着是environments,最后是mappers。乱序会报错。
8.2 日志效果
配置后可以看到SQL执行全过程:
==> Preparing: select * from student where id = ? ==> Parameters: 10(Integer) <== Columns: id, sname, sage, sex⭐老师强调:日志体现预编译防注入机制——先有
?占位符,再传入值塞进占位处。
第九部分:测试类完整代码
package com.qcby; import com.qcby.dao.StudentDao; import com.qcby.entity.Person; import com.qcby.entity.Student; 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 org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.IOException; import java.io.InputStream; import java.util.List; public class StudentTest { private InputStream inputStream = null; private SqlSession session = null; private StudentDao mapper = null; @Before // 前置通知 public void init() throws IOException { // 加载主配置文件 inputStream = Resources.getResourceAsStream("SqlMapConfig.xml"); // 创建SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 创建SqlSession对象 session = sqlSessionFactory.openSession(); // 通过sqlsession创建接口的代理对象 mapper = session.getMapper(StudentDao.class); } @After public void destroy() throws IOException { session.close(); inputStream.close(); } @Test public void run() { List<Student> students = mapper.findAll(); for (Student student : students) { System.out.println(student); } } @Test public void findById() { Student student = mapper.findById(10); System.out.println(student); } @Test public void insert() { Student student = new Student(); student.setName("小红"); student.setAge(18); student.setSex("女"); int code = mapper.insert(student); session.commit(); // 事务提交 System.out.println(code); System.out.println(student.getId()); System.out.println(student); } @Test public void delete() { int code = mapper.delete(26); session.commit(); // 事务提交 System.out.println(code); } @Test public void update() { Student student = new Student(); student.setName("小宇"); student.setAge(20); student.setSex("男"); student.setId(28); int code = mapper.update(student); session.commit(); // 事务提交 System.out.println(code); } @Test public void findByName() { List<Student> students = mapper.findByName("李"); for (Student student : students) { System.out.println(student); } } @Test public void findStudent() { List<Student> students = mapper.findStudent("李", 1, 3); for (Student student : students) { System.out.println(student); } } @Test public void findAllPerson() { Person person = new Person(); Student student = new Student(); student.setAge(25); student.setName("小宇"); student.setSex("男"); student.setId(28); person.setStudent(student); List<Student> students = mapper.findAllPerson(person); for (Student student1 : students) { System.out.println(student1); } } }附录一:核心对比速查表
#{}vs${}
| 对比 | #{} | ${} |
|---|---|---|
| 本质 | 占位符预编译 | 字符串拼接 |
| 防注入 | ✅ | ❌ |
| 简单类型命名 | 任意写 | 低版本必须value |
| 字符串引号 | 自动加 | 不自动加 |
| 实体类属性 | 必须是引用类型属性 | 必须是引用类型属性 |
入参类型速查
| 类型 | parameterType写法 | 示例 |
|---|---|---|
| 简单类型 | java.lang.Integer/java.lang.String | findById |
| 实体类 | com.qcby.entity.Student | insert |
| 多参数 | 无需parameterType,用@Param | findStudent |
| 包装对象 | com.qcby.entity.Person | findAllPerson |
出参类型速查
| 操作 | resultType/resultMap | 说明 |
|---|---|---|
| 查单条 | resultType | 实体类 |
| 查多条 | resultType | 实体类(框架自动组装List) |
| 字段不匹配 | resultMap | 手动指定column→property |
| 增删改 | 无需声明 | 固定返回int |