在 IDEA 中配置 MyBatis 配置文件模版
新建 MyBatis 核心配置文件模版
<?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> <properties resource=""/> <typeAliases> <package name=""/> </typeAliases> <environments default="dev"> <environment id="dev"> <transactionManager type="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> <mappers> <package name=""/> </mappers> </configuration>
测试使用新建的MyBatis核心配置文件模版
在 IDEA 中配置 SqlMapper 配置文件模版
新建 SqlMapper 配置文件模版
<?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=""> </mapper>
测试使用新建的SqlMapper配置文件模版
插入数据时获取自动生成的主键
业务场景说明
- 存在业务场景,当我们向一张表中插入了一条数据,由于主键一般是自增的,在插入数据时,一般不指定主键,但是此时还有另外一张表,与新插入数据的表有关联关系,该表的外键需要使用新插入的数据的主键值,建立数据之间的关联关系,此时我们就需要获取自动生成的主键值
- 业务背景:
- 一个用户有多个角色:插入一个用户数据的同时需要给该用户分配角色:需要将生成的用户的id插入到角色表的user_id字段上
- 实现方式有两种:
- 第一种方式:可以先插入用户数据,再写一条查询语句获取新插入用户数据的id,然后再在角色表插入user_id字段。【比较麻烦】
- 第二种方式:mybatis提供了一种方式更加便捷。
接口声明
/** * 插入汽车信息,同时使用生成的主键值 * * @param car 汽车信息 * @return 影响数据库的条数 */ int insertCarUseGeneratedKey(Car car);
SQL
<!-- useGeneratedKeys="true" 开启使用自动生成的主键 keyProperty="id" 指定要将自动生成的主键值保存到用于 传递数据给SQL语句的对象的哪个属性上 --> <insert id="insertCarUseGeneratedKey" useGeneratedKeys="true" keyProperty="id"> insert into t_car values (null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType}); </insert>
测试
@Test public void testInsertCarUseGeneratedKey() { SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = new Car(null, "9199", "凯美瑞", 32.0, "2020-11-12", "电车"); int count = mapper.insertCarUseGeneratedKey(car); System.out.println("影响数据库的条数:" + count); System.out.println(car); SqlSessionUtil.close(); }
Mybatis 参数处理
- 数据库表:t_student
USE dbtest; DROP TABLE IF EXISTS t_student; CREATE TABLE t_student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), age INT, height DOUBLE, birth DATE, sex CHAR(1) ); INSERT INTO t_student(name, age, height, birth, sex) VALUES ('张三', 20, 1.77, '2001-01-02', '男'); INSERT INTO t_student(name, age, height, birth, sex) VALUES ('李四', 20, 1.67, '2001-11-22', '女');
- 学生类:Student
package cw.study.mybatis.pojo; import java.util.Date; /** * ClassName: Student * Package: cw.study.mybatis.pojo * Description: * * @Author tcw * @Create 2023-05-28 11:49 * @Version 1.0 */ public class Student { private Long id; private String name; private Integer age; private Double height; private Character sex; private Date birth; // constructor // setter and getter // toString }
单个简单类型参数
- 简单类型包括:
- byte short int long float double char
- Byte Short Integer Long Float Double Character
- String
- java.util.Date
- java.sql.Date
- select 标签中有个 parameterType属性:告诉mybatis框架,我这个方法的参数类型是什么类型。
- 明确指明类型,MyBatis 可以不用进行类型推断,可以提高效率
<!-- select 标签中有个 parameterType属性 告诉mybatis框架,我这个方法的参数类型是什么类型。 mybatis框架自身带有类型自动推断机制,所以大部分情况下parameterType属性都是可以省略不写的。 --> <select id="selectById" resultType="Student" parameterType="java.lang.Long"> select * from t_student where id = #{id} </select>
- mybatis框架自身带有类型自动推断机制,所以大部分情况下parameterType属性都是可以省略不写的。
SQL语句编译后:select * from t_student where id = ? JDBC代码是一定要给 ? 传值的,会调用 ps.setXxx(第几个问号, 传什么值); ps.setLong(1, 1L); ps.setString(1, "zhangsan"); ps.setDate(1, new Date()); ps.setInt(1, 100); ...
- mybatis底层到底调用setXxx的哪个方法,取决于parameterType属性的值。
- 注意:mybatis框架实际上内置了很多别名,可以参考开发手册:https://mybatis.net.cn/configuration.html#typeAliases
<!-- mybatis框架实际上内置了很多别名,可以参考开发手册 --> <select id="selectByName" resultType="Student" parameterType="string"> select * from t_student where name = #{name} </select>
- 在#{}中,可以明确告诉mybatis框架,该参数会用什么类型的数据进行填充,对应数据库中的数据类是什么
- 明确指明类型,MyBatis 可以不用进行类型推断,可以提高效率
<select id="selectByName" resultType="Student" parameterType="string"> select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR} </select>
- 根据id查询、name查询、birth查询、sex查询
package cw.study.mybatis.mapper; import cw.study.mybatis.pojo.Student; import java.util.Date; import java.util.List; /** * ClassName: StudentMapper * Package: cw.study.mybatis.mapper * Description: * * @Author tcw * @Create 2023-05-28 11:55 * @Version 1.0 */ public interface StudentMapper { // 根据id查询 List<Student> selectById(Long id); // name查询 List<Student> selectByName(String name); // birth查询 List<Student> selectByBirth(Date birth); // sex查询 List<Student> selectBySex(Character sex); }
<?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="cw.study.mybatis.mapper.StudentMapper"> <!-- select 标签中有个 parameterType属性 告诉mybatis框架,我这个方法的参数类型是什么类型。 mybatis框架自身带有类型自动推断机制,所以大部分情况下parameterType属性都是可以省略不写的。 --> <select id="selectById" resultType="Student" parameterType="java.lang.Long"> select * from t_student where id = #{id} </select> <!-- mybatis框架实际上内置了很多别名,可以参考开发手册 --> <select id="selectByName" resultType="Student" parameterType="string"> select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR} </select> <select id="selectByBirth" resultType="Student" parameterType="date"> select * from t_student where birth = #{birth} </select> <select id="selectBySex" resultType="Student"> select * from t_student where sex = #{sex} </select> </mapper>
/** * ClassName: StudentMapperTest * Package: cw.study.mybatis.test * Description: * * @Author tcw * @Create 2023-05-28 11:59 * @Version 1.0 */ public class StudentMapperTest { @Test public void testSelectById() { SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List<Student> students = mapper.selectById(1L); students.forEach(System.out::println); SqlSessionUtil.close(); } @Test public void testSelectByName() { SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List<Student> students = mapper.selectByName("李四"); students.forEach(System.out::println); SqlSessionUtil.close(); } @Test public void testSelectByBirth() throws ParseException { SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); List<Student> students = mapper.selectByBirth(simpleDateFormat.parse("2001-11-22")); students.forEach(System.out::println); SqlSessionUtil.close(); } @Test public void testSelectBySex() { SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List<Student> students = mapper.selectBySex('男'); students.forEach(System.out::println); SqlSessionUtil.close(); } }
Map 集合
- 使用Map集合进行参数的传递,将每个条件以key和value的形式存放到集合中,在使用集合中的数据的时候需要通过
#{map集合的key}
来取值。
/** * 通过集合传递要插入数据库中的学生信息 * * @param map 学生信息 * @return 影响数据库的条数 */ int insertStudentByMap(Map<String, Object> map);
<!-- parameterType="map" 可以省略, 可以自动推出传入的参数为map集合 在使用集合中的数据的时候需要通过#{map集合的key}来取值。 --> <insert id="insertStudentByMap" parameterType="map"> insert into t_student(id, name, age, sex, height, birth) values (null, #{name}, #{age}, #{sex}, #{height}, #{birth}); </insert>
@Test public void testInsertStudentByMap() { // 封装数据 HashMap<String, Object> map = new HashMap<>(); // name, age, sex, height, birth map.put("name", "张三"); map.put("age", 20); map.put("sex", '男'); map.put("height", 1.80); map.put("birth", new Date()); SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); int count = mapper.insertStudentByMap(map); System.out.println(count); sqlSession.commit(); SqlSessionUtil.close(); }
实体类 POJO
- 使用实体类pojo进行参数的传递,将数据封装到实体类中,在使用实体类中的数据的时候需要通过
#{get方法去掉get首字母小写}
来取值。
/** * 通过实体类传递要插入数据库的学生信息 * * @param student 学生信息 * @return 影响数据库记录的条数 */ int insertStudentByPojo(Student student);
<!-- parameterType="student" 可以省略,可以自动推出类型为Student类 可以使用 student 是因为在MyBatis核心配置文件中配置了别名 在使用集合中的数据的时候需要通过#{get方法去掉get首字母小写}来取值。 --> <insert id="insertStudentByPojo" parameterType="student"> insert into t_student(id, `name`, age, sex, height, birth) values (null, #{name}, #{age}, #{sex}, #{height}, #{birth}); </insert>
@Test public void testInsertStudentByPojo() { // 封装数据 Student student = new Student(null, "李四", 23, 1.78, '男', new Date()); SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); int count = mapper.insertStudentByPojo(student); System.out.println(count); sqlSession.commit(); SqlSessionUtil.close(); }
多参数
- 如果是操作数据库的方法的形参列表是多个参数的话,mybatis框架会自动创建一个Map集合。并且Map集合是以这种方式存储参数的:
map.put("arg0", name); map.put("arg1", sex); map.put("param1", name); map.put("param2", sex);
- 注意:低版本的mybatis中,使用的是:#{0}和#{1},以及#{2}…,使用mybatis3.4.2之前的版本时:要用#{0}和#{1}这种形式。
- 高版本的mybatis中,使用的是:
// arg 从 0 开始 #{arg0} #{arg1} #{arg2} // param 从 1 开始 #{param1} #{param2} #{param3}
- 对于操作数据库的方法的形参列表是多个参数的情况,在SQL语句中取数据不能直接使用形参的参数名,会报错
/** * 根据学生的姓名和性别进行学生信息的查询 * * @param name 学生的姓名 * @param sex 学生的性别 * @return 学生信息组成的集合 */ List<Student> selectByNameAndSex(String name, Character sex);
<!-- parameterType="" 不用写了 --> <select id="selectByNameAndSex" resultType="student"> select * from t_student where `name` = #{arg0} and sex = #{param2}; </select>
@Test public void testSelectByNameAndSex() { SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List<Student> students = mapper.selectByNameAndSex("张三", '男'); students.forEach(System.out::println); SqlSessionUtil.close(); }
@Param(命名参数)
- 对于操作数据库的方法中形参列表的参数为多个的情况,如果使用arg0 arg1 param1 param2这种方式的话,代码的可读性太差。
- 在MyBatis中,为我们提供了@Param注解,使我们可以不用arg0 arg1 param1 param2,@Param注解使我们可以自定义存放参数的map集合的key,可以增强代码的可读性。
@Param 的使用
/** * 根据学生的姓名和性别进行学生信息的查询 * * @param name 学生的姓名 * @param sex 学生的性别 * @return 学生信息组成的集合 */ List<Student> selectByNameAndSexAnnotation( // 使用@Param注解指定参数在map集合中的key // @Param注解中的value属性用于指定参数在map集合中的key @Param("name") String name, @Param("sex") Character sex );
- 使用Param注解后,mybatis框架底层的实现原理:
map.put("name", name); map.put("sex", sex);
<select id="selectByNameAndSexAnnotation" resultType="student"> select * from t_student where `name` = #{name} and sex = #{sex}; </select>
@Test public void testSelectByNameAndSexAnnotation() { SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List<Student> students = mapper.selectByNameAndSexAnnotation("张三", '男'); students.forEach(System.out::println); SqlSessionUtil.close(); }
注意点
- 使用了@Param注解之后,arg0和arg1失效了
<select id="selectByNameAndSexAnnotation" resultType="student"> select * from t_student where `name` = #{arg0} and sex = #{arg1}; </select>
- 使用了@Param注解之后,param1和param2还可以用
<select id="selectByNameAndSexAnnotation" resultType="student"> select * from t_student where `name` = #{param1} and sex = #{param2}; </select>
@Param源码分析
源码跟踪
// mapper 实际上指向了代理对象 StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); // selectByNameAndSexAnnotation 是代理对象 mapper 的代理方法 // 执行到这时,会调用代理对象mapper的代理方法 List<Student> students = mapper.selectByNameAndSexAnnotation("张三", '男');
- 调用代理对象mapper的代理方法,就是执行 MapperProxy 的 invoke 方法
// Object proxy:代理对象 // Method method:目标方法 // Object[] args:目标方法的参数 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession); } catch (Throwable var5) { throw ExceptionUtil.unwrapThrowable(var5); } }
- 接下来执行 this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession)
// MapperProxy 内部类 PlainMethodInvoker 的方法 public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { return this.mapperMethod.execute(sqlSession, args); }
- 执行 MapperMethod 的 execute 方法进行 SQL 语句类和代理目标方法返回值类型的匹配
// MapperMethod 的方法 public Object execute(SqlSession sqlSession, Object[] args) { Object result; Object param; // this.command.getType() 获取 SQL 语句的类型(insert|update|select|delete) switch (this.command.getType()) { case INSERT: param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.insert(this.command.getName(), param)); break; case UPDATE: param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.update(this.command.getName(), param)); break; case DELETE: param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.delete(this.command.getName(), param)); break; case SELECT: // this.method.returnsVoid() 我们定义的Mapper接口中的方法无返回值 if (this.method.returnsVoid() && this.method.hasResultHandler()) { this.executeWithResultHandler(sqlSession, args); result = null; // 我们定义的Mapper接口中的方法返回多条记录 } else if (this.method.returnsMany()) { result = this.executeForMany(sqlSession, args); } else if (this.method.returnsMap()) { result = this.executeForMap(sqlSession, args); } else if (this.method.returnsCursor()) { result = this.executeForCursor(sqlSession, args); } else { param = this.method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(this.command.getName(), param); if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + this.command.getName()); } if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) { throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ")."); } else { return result; } }
- SQL 语句的类型以及返回结果的类型匹配完成后,进行Mapper接口代理方法的执行
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { // 将Mapper接口参数组成的数组转换为SQL语句中占位符填充的参数 // 转换成 param1 = value1 param2 = value2 ... 的形式 // 或者 arg1 = value1 arg2 = value2 ... 的形式 // 或者 'name' = value1 'sex' = value2 ... 的形式 Object param = this.method.convertArgsToSqlCommandParam(args); List result; if (this.method.hasRowBounds()) { RowBounds rowBounds = this.method.extractRowBounds(args); result = sqlSession.selectList(this.command.getName(), param, rowBounds); } else { result = sqlSession.selectList(this.command.getName(), param); } if (!this.method.getReturnType().isAssignableFrom(result.getClass())) { return this.method.getReturnType().isArray() ? this.convertToArray(result) : this.convertToDeclaredCollection(sqlSession.getConfiguration(), result); } else { return result; } }
public Object convertArgsToSqlCommandParam(Object[] args) { return this.paramNameResolver.getNamedParams(args); }
- 调用 ParamNameResolver 类的 getNamedParams(args) 方法进行转换
public Object getNamedParams(Object[] args) { // private final SortedMap<Integer, String> names; // 是一个Map集合,key为第几个参数,value为参数对应的名字 int paramCount = this.names.size(); if (args != null && paramCount != 0) { // 判断方法参数上是否有Param注解 if (!this.hasParamAnnotation && paramCount == 1) { Object value = args[(Integer)this.names.firstKey()]; return wrapToMapIfCollection(value, this.useActualParamName ? (String)this.names.get(0) : null); } else { // 有Param注解走该分支 // 有Param注解会新建一个Map集合 param集合 Map<String, Object> param = new MapperMethod.ParamMap(); int i = 0; // 遍历存储参数名字的Map集合 for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) { Map.Entry<Integer, String> entry = (Map.Entry)var5.next(); // 向param集合中添加数据,key为接口方法参数的名字 entry.getValue() // value为接口方法参数的参数值 args[(Integer)entry.getKey()] param.put(entry.getValue(), args[(Integer)entry.getKey()]); // genericParamName 通用参数名 String genericParamName = "param" + (i + 1); // param1 param2 ... // 集合是否包含通用参数名 param1 param2 ... if (!this.names.containsValue(genericParamName)) { // 这个就是可以使用 param1 param2 获取参数值 // 向集合中添加 param1=value1 param2=value2 param.put(genericParamName, args[(Integer)entry.getKey()]); } } return param; } } else { return null; } }
图示分析
MyBatis查询语句专题
返回 POJO(一条记录)
public interface CarMapper { /** * 根据id查询汽车信息 * * @param id 汽车信息的id * @return 汽车信息 */ Car selectById(Long id); }
<!-- 如果SQL语句中不使用别名, 则读取数据后字段名与对象的属性名不一致的话 不一致的属性的值会为null --> <select id="selectById" resultType="car"> select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car where id = #{id}; </select>
@org.junit.Test public void testSelectById() { SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = mapper.selectById(5L); System.out.println(car); SqlSessionUtil.close(); }
- 如果返回的结果集可能有多条记录,则不能使用一个pojo对象进行接收,否则会报TooManyResultsException,返回的结果集可能有多条数据记录,则需要使用集合进行接收