一、MyBatis小技巧
1 #{}和${}
#{}:先编译sql语句,再给占位符传值,底层是PreparedStatement实现。可以防止sql注入,比较常用。
${}:先进行sql语句拼接,然后再编译sql语句,底层是Statement实现。存在sql注入现象。只有在需要进行sql语句关键字拼接的情况下才会用到。
需求:根据car_type查询汽车
模块名:mybatis-005-antic
⑴.使用#{}
依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example1</groupId><artifactId>mybatis-006antic</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--mybatis依赖--><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.10</version></dependency><!--mysql驱动依赖--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version></dependency><!--junit依赖--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency><!--logback依赖--><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.11</version></dependency></dependencies></project>
接口CarMapper
package org.example1.mapper;import org.example1.pojo.Car;import java.util.List;public interface CarMapper {List<Car> selectByCarType(String carType);//根据汽车类型获取汽车信息}
Car类
package org.example1.pojo;/*** 封装汽车相关信息的pojo类。普通的java类。*/
public class Car {// 数据库表当中的字段应该和pojo类的属性一一对应。// 建议使用包装类,这样可以防止null的问题。private Long id;private String carNum;private String brand;private Double guidePrice;private String produceTime;private String carType;@Overridepublic String toString() {return "Car{" +"id=" + id +", carNum='" + carNum + '\'' +", brand='" + brand + '\'' +", guidePrice=" + guidePrice +", produceTime='" + produceTime + '\'' +", carType='" + carType + '\'' +'}';}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getCarNum() {return carNum;}/*public String getXyz() {return carNum;}*/public void setCarNum(String carNum) {this.carNum = carNum;}public String getBrand() {return brand;}public void setBrand(String brand) {this.brand = brand;}public Double getGuidePrice() {return guidePrice;}public void setGuidePrice(Double guidePrice) {this.guidePrice = guidePrice;}public String getProduceTime() {return produceTime;}public void setProduceTime(String produceTime) {this.produceTime = produceTime;}public String getCarType() {return carType;}public void setCarType(String carType) {this.carType = carType;}public Car(Long id, String carNum, String brand, Double guidePrice, String produceTime, String carType) {this.id = id;this.carNum = carNum;this.brand = brand;this.guidePrice = guidePrice;this.produceTime = produceTime;this.carType = carType;}public Car() {}
}
SqlSessionUtil类
package org.example1.utils;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;/*** MyBatis工具类*/
public class SqlSessionUtil {private SqlSessionUtil(){}private static SqlSessionFactory sqlSessionFactory;static {try {sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));} catch (IOException e) {throw new RuntimeException(e);}}// 全局的,服务器级别的,一个服务器当中定义一个即可。// 为什么把SqlSession对象放到ThreadLocal当中呢?为了保证一个线程对应一个SqlSession。private static ThreadLocal<SqlSession> local = new ThreadLocal<>();/*** 获取会话对象。* @return 会话对象*/public static SqlSession openSession(){SqlSession sqlSession = local.get();if (sqlSession == null) {sqlSession = sqlSessionFactory.openSession();// 将sqlSession对象绑定到当前线程上。local.set(sqlSession);}return sqlSession;}/*** 关闭SqlSession对象(从当前线程中移除SqlSession对象。)* @param sqlSession*/public static void close(SqlSession sqlSession){if (sqlSession != null) {sqlSession.close();// 注意移除SqlSession对象和当前线程的绑定关系。// 因为Tomcat服务器支持线程池。也就是说:用过的线程对象t1,可能下一次还会使用这个t1线程。local.remove();}}}
CarMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example1.mapper.CarMapper"><select id="selectByCarType" resultType="org.example1.pojo.Car">selectid,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carTypefromt_carwherecar_type = ${carType}/* car_type = '${carType}'*/</select></mapper>
jdbc.properties
logback.xml
mybatis-config.xml
test
@Testpublic void testSelectByCarType(){SqlSession sqlSession = SqlSessionUtil.openSession();// mapper实际上就是daoImpl对象.// 底层不但为CarMapper接口生成了字节码,并且还new实现类对象了。CarMapper mapper = sqlSession.getMapper(CarMapper.class);List<Car> cars = mapper.selectByCarType("新能源");// 遍历cars.forEach(car -> System.out.println(car));sqlSession.close();}
执行结果:
通过执行可以清楚的看到,sql语句中是带有 ? 的,这个 ? 就是大家在JDBC中所学的占位符,专门用来接收值的。 把“燃油车”以String类型的值,传递给 ? 这就是 #{},它会先进行sql语句的预编译,然后再给占位符传值
⑵.使用${}
同样的需求,我们使用${}来完成 CarMapper.xml文件修改如下:
再次运行测试程序:
出现异常了,这是为什么呢?看看生成的sql语句:
很显然,${} 是先进行sql语句的拼接,然后再编译,出现语法错误是正常的,因为 燃油车 是一个字符串,在sql语句中应该添加单引号 修改:
再执行测试程序:
通过以上测试,可以看出,对于以上这种需求来说,还是建议使用 #{} 的方式。 原则:能用 #{} 就不用 ${}
⑶.什么情况下必须使用${}
当需要进行sql语句关键字拼接的时候。必须使用${} 需求:通过向sql语句中注入asc或desc关键字,来完成数据的升序或降序排列。
-
先使用#{}尝试:
CarMapper接口:
package org.example1.mapper;import org.example1.pojo.Car;import java.util.List;public interface CarMapper {/*** 查询所有的汽车信息。然后通过asc升序,desc降序。* @param ascOrDesc* @return*/List<Car> selectAllByAscOrDesc(String ascOrDesc);List<Car> selectByCarType(String carType);//根据汽车类型获取汽车信息}
CarMapper.xml文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example1.mapper.CarMapper"><select id="selectAllByAscOrDesc" resultType="org.example1.pojo.car">selectid,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carTypefromt_carorder byproduce_time ${ascOrDesc}</select></mapper>
测试程序
@Testpublic void testSelectAllByAscOrDesc(){SqlSession sqlSession = SqlSessionUtil.openSession();CarMapper mapper = sqlSession.getMapper(CarMapper.class);List<Car> cars = mapper.selectAllByAscOrDesc("desc");cars.forEach(car -> System.out.println(car));sqlSession.close();}
报错的原因是sql语句不合法,因为采用这种方式传值,最终sql语句会是这样:
select id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType from t_car order by carNum 'desc'
desc是一个关键字,不能带单引号的,所以在进行sql语句关键字拼接的时候,必须使用${}
-
使用${} 改造
再次执行测试程序:
#{}和${}的区别:#{}: 底层使用PreparedStatement。特点:先进行SQL语句的编译,然后给SQL语句的占位符问号?传值。可以避免SQL注入的风险。${}:底层使用Statement。特点:先进行SQL语句的拼接,然后再对SQL语句进行编译。存在SQL注入的风险。优先使用#{},这是原则。避免SQL注入的风险。
如果需要SQL语句的关键字放到SQL语句中,只能使用${},因为#{}是以值的形式放到SQL语句当中的。
⑷.向SQL语句当中拼接表名,就需要使用${}
现实业务当中,可能会存在分表存储数据的情况。因为一张表存的话,数据量太大。查询效率比较低。 可以将这些数据有规律的分表存储,这样在查询的时候效率就比较高。因为扫描的数据量变少了。 日志表:专门存储日志信息的。如果t_log只有一张表,这张表中每一天都会产生很多log,慢慢的,这个表中数据会很多。 怎么解决问题?可以每天生成一个新表。每张表以当天日期作为名称,例如:t_log_20220901t_log_20220902.... 你想知道某一天的日志信息怎么办?假设今天是20220901,那么直接查:t_log_20220901的表即可。
业务背景:
实际开发中,有的表数据量非常庞大,可能会采用分表方式进行存储,比如每天生成一张表,表的名字与日期挂钩,例如:2022年8月1日生成的表:t_user20220108。2000年1月1日生成的表:t_user20000101。此时前端在进行查询的时候会提交一个具体的日期,比如前端提交的日期为:2000年1月1日,那么后端就会根据这个日期动态拼接表名为:t_user20000101。有了这个表名之后,将表名拼接到sql语句当中,返回查询结果。
那么大家思考一下,拼接表名到sql语句当中应该使用#{} 还是 ${} 呢?
使用#{}会是这样:select * from 't_car'
使用${}会是这样:select * from t_car
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example1.mapper.CarMapper"><select id="selectAllByTableName" resultType="org.example1.pojo.Car">selectid,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carTypefrom${tableName}</select></mapper>
/*** 根据表名查询所有的Car* @param tableName* @return*/
List<Car> selectAllByTableName(String tableName);
@Test
public void testSelectAllByTableName(){CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);List<Car> cars = mapper.selectAllByTableName("t_car");cars.forEach(car -> System.out.println(car));
}
执行结果:
⑸.批量删除
批量删除:一次删除多条记录。批量删除的SQL语句有两种写法:第一种or:delete from t_car where id=1 or id=2 or id=3;第二种int:delete from t_car where id in(1,2,3);应该采用${}的方式:delete from t_car where id in(${ids});
业务背景:一次删除多条记录。
对应的sql语句:
-
delete from t_user where id = 1 or id = 2 or id = 3;
-
delete from t_user where id in(1, 2, 3);
假设现在使用in的方式处理,前端传过来的字符串:1, 2, 3
如果使用mybatis处理,应该使用#{} 还是 ${}
使用#{} :delete from t_user where id in('1,2,3') 执行错误:1292 - Truncated incorrect DOUBLE value: '1,2,3'
使用${} :delete from t_user where id in(1, 2, 3)
package org.example1.mapper;import org.example1.pojo.Car;import java.util.List;public interface CarMapper {int deleteBatch(String ids);}
<delete id="deleteBatch">delete from t_car where id in(${ids})
</delete>
@Test
public void testDeleteBatch(){CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);int count = mapper.deleteBatch("1,2,3");System.out.println("删除了几条记录:" + count);SqlSessionUtil.openSession().commit();
}
执行结果:
⑹.模糊查询
模糊查询:like需求:根据汽车品牌进行模糊查询select * from t_car where brand like '%奔驰%';select * from t_car where brand like '%比亚迪%';第一种方案:'%${brand}%'第二种方案:concat函数,这个是mysql数据库当中的一个函数,专门进行字符串拼接concat('%',#{brand},'%')第三种方案:比较鸡肋了。可以不算。concat('%','${brand}','%')第四种方案:"%"#{brand}"%"
需求:查询奔驰系列的汽车。【只要品牌brand中含有奔驰两个字的都查询出来。】
第一种方案: '%${brand}%'
①使用${}
package org.example1.mapper;import org.example1.pojo.Car;import java.util.List;public interface CarMapper {List<Car> selectLikeByBrand(String likeBrank);}
<select id="selectLikeByBrand" resultType="Car">selectid,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carTypefromt_carwherebrand like '%${brand}%'
</select>
@Test
public void testSelectLikeByBrand(){CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);List<Car> cars = mapper.selectLikeByBrand("奔驰");cars.forEach(car -> System.out.println(car));
}
执行结果:
②使用#{}
第二种方案:concat函数
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example1.mapper.CarMapper"><select id="selectLikeByBrand" resultType="org.example1.pojo.Car">selectid,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carTypefromt_carwherebrand like concat('%',#{brand},'%')</select></mapper>
①使用${}
②使用#{}
第三种方案:比较鸡肋了。可以不算。 concat('%','${brand}','%')
第四种方案: "%"#{brand}"%"
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example1.mapper.CarMapper"><select id="selectLikeByBrand" resultType="org.example1.pojo.Car">selectid,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carTypefromt_carwherebrand like "%"#{brand}"%"</select></mapper>
2.mybatis-config.xml文件中的typeAliases标签
我们来观察一下CarMapper.xml中的配置信息:
resultType属性用来指定查询结果集的封装类型,这个名字太长,可以起别名吗?
可以。 在mybatis-config.xml文件中使用typeAliases标签来起别名,包括两种方式:
⑴.第一种方式:typeAlias
<typeAliases><typeAlias type="org.example1.pojo.Car" alias="Car"/></typeAliases>
-
首先要注意typeAliases标签的放置位置,如果不清楚的话,可以看看错误提示信息。
-
typeAliases标签中的typeAlias可以写多个。
-
typeAlias:
-
type属性:指定给哪个类起别名
-
alias属性:别名。
-
alias属性不是必须的,如果缺省的话,type属性指定的类型名的简类名作为别名。
-
alias是大小写不敏感的。也就是说假设alias="Car",再用的时候,可以CAR,也可以car,也可以Car,都行。
-
-
alias属性是可以省略的。有默认的别名。<!--省略alias之后,别名就是类的简名,比如:org.example1.pojo.Car的别名就是Car/car/cAR/cAr,不缺分大小写。 -->
⑵.第二种方式:package
如果一个包下的类太多,每个类都要起别名,会导致typeAlias标签配置较多,所以mybatis用提供package的配置方式,只需要指定包名,该包下的所有类都自动起别名,别名就是简类名。并且别名不区分大小写。
package也可以配置多个的。
3. mybatis-config.xml文件中的mappers标签。
SQL映射文件的配置方式包括四种:
-
resource:从类路径中加载
-
url:从指定的全限定资源路径中加载
-
class:使用映射器接口实现类的完全限定类名
-
package:将包内的映射器接口实现全部注册为映射器
⑴.resource
这种方式是从类路径中加载配置文件,所以这种方式要求SQL映射文件必须放在resources目录下或其子目录下。
<mappers><mapper resource="org/mybatis/builder/AuthorMapper.xml"/><mapper resource="org/mybatis/builder/BlogMapper.xml"/><mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
⑵.url
这种方式显然使用了绝对路径的方式,这种配置对SQL映射文件存放的位置没有要求,随意。
<mappers><mapper url="file:///var/mappers/AuthorMapper.xml"/><mapper url="file:///var/mappers/BlogMapper.xml"/><mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
⑶.class
如果使用这种方式必须满足以下条件:
-
SQL映射文件和mapper接口放在同一个目录下。
-
SQL映射文件的名字也必须和mapper接口名一致。
<mapper class="全限定接口名,带有包名"/>class: 这个位置提供的是mapper接口的全限定接口名,必须带有包名的。思考:mapper标签的作用是指定SqlMapper.xml文件的路径,指定接口名有什么用呢?<mapper class="org.example1.mapper.CarMapper"/>如果你class指定是:org.example1.mapper.CarMapper 那么mybatis框架会自动去org/example1/mapper目录下查找CarMapper.xml文件。注意:也就是说:如果你采用这种方式,那么你必须保证CarMapper.xml文件和CarMapper接口必须在同一个目录下。并且名字一致。CarMapper接口-> CarMapper.xmlLogMapper接口-> LogMapper.xml
⑷.package
如果class较多,可以使用这种package的方式,但前提条件和上一种方式一样。
4.idea配置文件模板
mybatis-config.xml和SqlMapper.xml文件可以在IDEA中提前创建好模板,以后通过模板创建配置文件。
5.插入数据时获取自动生成的主键
前提是:主键是自动生成的。 业务背景:一个用户有多个角色。
插入一条新的记录之后,自动生成了主键,而这个主键需要在其他表中使用时。 插入一个用户数据的同时需要给该用户分配角色:需要将生成的用户的id插入到角色表的user_id字段上。
第一种方式:可以先插入用户数据,再写一条查询语句获取id,然后再插入user_id字段。【比较麻烦】
第二种方式:mybatis提供了一种方式更加便捷。
CarMapper接口
package org.example1.mapper;import org.example1.pojo.Car;import java.util.List;public interface CarMapper {/*** 插入Car信息,并且使用生成的主键值。* @param car* @return*/int insertCarUseGeneratedKeys(Car car);}
CarMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example1.mapper.CarMapper"><!--useGeneratedKeys="true" 使用自动生成的主键值。keyProperty="id" 指定主键值赋值给对象的哪个属性。这个就表示将主键值赋值给Car对象的id属性。
--><insert id="insertCarUseGeneratedKeys" useGeneratedKeys="true" keyProperty="id">insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})</insert></mapper>
Test
@Testpublic void testInsertCarUseGeneratedKeys(){SqlSession sqlSession = SqlSessionUtil.openSession();CarMapper mapper = sqlSession.getMapper(CarMapper.class);Car car = new Car(null,"9991", "凯美瑞", 30.0, "2020-11-11", "燃油车");mapper.insertCarUseGeneratedKeys(car);System.out.println(car);sqlSession.commit();sqlSession.close();}
二、MyBatis参数处理
模块名:mybatis-007-param 表:t_student
1.单个简单类型参数
简单类型包括:
-
byte short int long float double char
-
Byte Short Integer Long Float Double Character
-
String
-
java.util.Date
-
java.sql.Date
需求:根据name查、根据id查、根据birth查、根据sex查
StudentMapper接口
package org.example1.mapper;import org.example1.pojo.Student;import java.util.Date;
import java.util.List;public interface StudentMapper {/*** 当接口中的方法的参数只有一个(单个参数),并且参数的数据类型都是简单类型。* 根据id查询、name查询、birth查询、sex查询*/List<Student> selectById(Long id);List<Student> selectByName(String name);List<Student> selectByBirth(Date birth);List<Student> selectBySex(Character sex);}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="org.example1.mapper.StudentMapper"><!-- parameterType属性的作用:告诉mybatis框架,我这个方法的参数类型是什么类型。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框架实际上内置了很多别名。可以参考开发手册。--><select id="selectById" resultType="Student" >select * from t_student where id = #{id}</select><select id="selectByName" resultType="student">select * from t_student where name = #{name}</select><select id="selectByBirth" resultType="student">select * from t_student where birth = #{birth}</select><select id="selectBySex" resultType="student">select * from t_student where sex = #{sex}</select></mapper>
Test
@Testpublic void testSelectBySex(){SqlSession sqlSession = SqlSessionUtil.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);// char --> CharacterCharacter sex = Character.valueOf('男');List<Student> students = mapper.selectBySex(sex);students.forEach(student -> System.out.println(student));sqlSession.close();}// java.util.Date java.sql.Date,他们都是简单类型。@Testpublic void testSelectByBirth() throws Exception{SqlSession sqlSession = SqlSessionUtil.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");Date birth = sdf.parse("2017-04-06");List<Student> students = mapper.selectByBirth(birth);students.forEach(student -> System.out.println(student));sqlSession.close();}@Testpublic void testSelectByName(){SqlSession sqlSession = SqlSessionUtil.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);List<Student> students = mapper.selectByName("李四");students.forEach(student -> System.out.println(student));sqlSession.close();}@Testpublic void testSelectById(){SqlSession sqlSession = SqlSessionUtil.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);List<Student> students = mapper.selectById(1L);students.forEach(student -> System.out.println(student));sqlSession.close();}
⑴id
parameterType属性的作用:告诉mybatis框架,我这个方法的参数类型是什么类型。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框架实际上内置了很多别名。可以参考开发手册。
⑵name
通过测试得知,简单类型对于mybatis来说都是可以自动类型识别的:
-
也就是说对于mybatis来说,它是可以自动推断出ps.setXxxx()方法的。ps.setString()还是ps.setInt()。它可以自动推断。
其实SQL映射文件中的配置比较完整的写法是:
<select id="selectByName" resultType="student">select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR}</select>
其中sql语句中的javaType,jdbcType,以及select标签中的parameterType属性,都是用来帮助mybatis进行类型确定的。不过这些配置多数是可以省略的。因为mybatis它有强大的自动类型推断机制。
-
javaType:可以省略
-
jdbcType:可以省略
-
parameterType:可以省略
如果参数只有一个的话,#{} 里面的内容就随便写了。对于 ${} 来说,注意加单引号。
⑶date
⑷sex
2.Map参数
需求:根据name和age查询
StudentMapper接口
package org.example1.mapper;import org.example1.pojo.Student;import java.util.Date;
import java.util.List;
import java.util.Map;public interface StudentMapper {/*** 保存学生信息,通过Map参数。以下是单个参数。但是参数的类型不是简单类型。是Map集合。* @param map* @return*/int insertStudentByMap(Map<String, Object> map);}
<!--<insert id="insertStudentByMap" parameterType="map">--><insert id="insertStudentByMap">insert into t_student(id,name,age,sex,birth,height) values(null,#{姓名},#{年龄},#{性别},#{生日},#{身高})</insert>
@Testpublic void testInsertStudentByMap(){SqlSession sqlSession = SqlSessionUtil.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);Map<String,Object> map = new HashMap<>();map.put("姓名", "赵六");map.put("年龄", 20);map.put("身高", 1.81);map.put("性别", '男');map.put("生日", new Date());mapper.insertStudentByMap(map);sqlSession.commit();sqlSession.close();}
测试运行正常。
这种方式是手动封装Map集合,将每个条件以key和value的形式存放到集合中。然后在使用的时候通过#{map集合的key}来取值。
3.实体类参数
需求:插入一条Student数据
StudentMapper接口
package org.example1.mapper;import org.example1.pojo.Student;import java.util.Date;
import java.util.List;
import java.util.Map;public interface StudentMapper {/*** 保存学生信息,通过POJO参数。Student是单个参数。但是不是简单类型。* @param student* @return*/int insertStudentByPOJO(Student student);}
<!--<insert id="insertStudentByPOJO" parameterType="student">--><insert id="insertStudentByPOJO">insert into t_student(id,name,age,sex,birth,height) values(null,#{name},#{age},#{sex},#{birth},#{height})</insert>
@Testpublic void testInsertStudentByPOJO(){SqlSession sqlSession = SqlSessionUtil.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);// POJO对象Student student = new Student();student.setName("张飞");student.setAge(50);student.setSex('女');student.setBirth(new Date());student.setHeight(10.0);mapper.insertStudentByPOJO(student);sqlSession.commit();sqlSession.close();}
运行正常,数据库中成功添加一条数据。
这里需要注意的是:#{} 里面写的是属性名字。这个属性名其本质上是:set/get方法名去掉set/get之后的名字。
4.多参数
需求:通过name和sex查询
* 这是多参数。 * 根据name和sex查询Student信息。 * 如果是多个参数的话,mybatis框架底层是怎么做的呢? * mybatis框架会自动创建一个Map集合。并且Map集合是以这种方式存储参数的: * map.put("arg0", name); * map.put("arg1", sex); * map.put("param1", name); * map.put("param2", sex);
package org.example1.mapper;import org.example1.pojo.Student;import java.util.Date;
import java.util.List;
import java.util.Map;public interface StudentMapper {/*** 这是多参数。* 根据name和sex查询Student信息。* 如果是多个参数的话,mybatis框架底层是怎么做的呢?* mybatis框架会自动创建一个Map集合。并且Map集合是以这种方式存储参数的:* map.put("arg0", name);* map.put("arg1", sex);* map.put("param1", name);* map.put("param2", sex);** @param name* @param sex* @return*/List<Student> selectByNameAndSex(String name, Character sex);}
执行结果:
异常信息描述了:name参数找不到,可用的参数包括[arg1, arg0, param1, param2] 修改StudentMapper.xml配置文件:尝试使用[arg1, arg0, param1, param2]去参数
@Testpublic void testSelectByNameAndSex(){SqlSession sqlSession = SqlSessionUtil.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);List<Student> students = mapper.selectByNameAndSex("张三", '男');students.forEach(student -> System.out.println(student));sqlSession.close();}
通过测试可以看到:
-
arg0 是第一个参数
-
param1是第一个参数
-
arg1 是第二个参数
-
param2是第二个参数
实现原理:实际上在mybatis底层会创建一个map集合,以arg0/param1为key,以方法上的参数为value,例如以下代码:
Map<String,Object> map = new HashMap<>();
map.put("arg0", name);
map.put("arg1", sex);
map.put("param1", name);
map.put("param2", sex);// 所以可以这样取值:#{arg0} #{arg1} #{param1} #{param2}
// 其本质就是#{map集合的key}
注意:使用mybatis3.4.2之前的版本时:要用#{0}和#{1}这种形式。
5.@Param注解(命名参数)
可以不用arg0 arg1 param1 param2吗?这个map集合的key我们自定义可以吗?
当然可以。使用@Param注解即可。这样可以增强可读性。 需求:根据name和age查询
package org.example1.mapper;import org.apache.ibatis.annotations.Param;
import org.example1.pojo.Student;import java.util.Date;
import java.util.List;
import java.util.Map;public interface StudentMapper {/*** Param注解。** mybatis框架底层的实现原理:* map.put("name", name);* map.put("sex", sex);** @param name* @param sex* @return*/List<Student> selectByNameAndSex2(@Param("name") String name, @Param("sex") Character sex);}
<select id="selectByNameAndSex2" resultType="Student"><!--使用了@Param注解之后,arg0和arg1失效了--><!--select * from t_student where name = #{arg0} and sex = #{arg1}--><!--使用了@Param注解之后,param1和param2还可以用--><!--select * from t_student where name = #{param1} and sex = #{param2}-->select * from t_student where name = #{name} and sex = #{sex}</select>
@Testpublic void testSelectByNameAndSex(){SqlSession sqlSession = SqlSessionUtil.openSession();StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);List<Student> students = mapper.selectByNameAndSex("张三", '男');students.forEach(student -> System.out.println(student));sqlSession.close();}
6.@Param源码分析
MyBatis 的 @Param
注解在源码中主要用于解决 Mapper 接口方法多参数命名问题,其核心逻辑集中在 参数解析 和 参数绑定 过程中。以下是源码分析的关键点:
⑴.@Param
的作用
-
用途:为方法参数指定名称,使得在 XML 映射文件中可以通过名称引用参数。
-
场景:当方法参数超过 1 个,或参数需要明确名称时使用。
⑵.源码核心入口:ParamNameResolver
MyBatis 通过 ParamNameResolver
类解析方法参数名称,处理 @Param
注解。以下是关键逻辑:
①构造方法解析参数名
public class ParamNameResolver {// 存储参数索引与名称的映射private final SortedMap<Integer, String> names;public ParamNameResolver(Configuration config, Method method) {final Class<?>[] paramTypes = method.getParameterTypes();final Annotation[][] paramAnnotations = method.getParameterAnnotations();final SortedMap<Integer, String> map = new TreeMap<>();for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) {String name = null;// 1. 检查 @Param 注解for (Annotation annotation : paramAnnotations[paramIndex]) {if (annotation instanceof Param) {name = ((Param) annotation).value();break;}}// 2. 无 @Param 时尝试通过反射获取参数名if (name == null) {if (config.isUseActualParamName()) {name = getActualParamName(method, paramIndex);}// 3. 默认回退为 arg0, arg1...if (name == null) {name = String.valueOf(map.size());}}map.put(paramIndex, name);}names = Collections.unmodifiableSortedMap(map);}
}
-
关键点:
-
优先使用
@Param("name")
定义的名称。 -
未使用
@Param
时,若配置useActualParamName=true
(默认),尝试通过反射获取参数名(需编译时启用-parameters
)。 -
最终回退为
arg0
,arg1
, ... 或param1
,param2
, ...。
-
②参数包装:getNamedParams
在 SQL 执行时,通过 getNamedParams
方法将参数包装为 Map 或单一对象:
public Object getNamedParams(Object[] args) {final int paramCount = names.size();if (args == null || paramCount == 0) {return null;} else if (!hasParamAnnotation && paramCount == 1) {// 无 @Param 且仅一个参数:直接返回该参数对象return args[names.firstKey()];} else {final Map<String, Object> param = new ParamMap<>();for (Map.Entry<Integer, String> entry : names.entrySet()) {// 组合名称:@Param 值 + "param" + 索引param.put(entry.getValue(), args[entry.getKey()]);param.put("param" + (entry.getKey() + 1), args[entry.getKey()]);}return param;}
}
-
逻辑说明:
-
无
@Param
且单参数:直接返回参数对象(如User
),XML 中可直接引用其属性。 -
有
@Param
或多参数:包装为ParamMap
,包含两种键:-
自定义名称:通过
@Param("name")
定义的键。 -
通用名称:如
param1
,param2
(兼容旧版本)。
-
-
⑶.SQL 参数绑定
在 DefaultParameterHandler
中,通过 ParameterHandler
处理参数映射:
public class DefaultParameterHandler implements ParameterHandler {public void setParameters(PreparedStatement ps) {// 从 ParamNameResolver 获取参数 MapObject parameterObject = boundSql.getParameterObject();// 遍历参数映射,替换 SQL 中的 #{name}for (ParameterMapping paramMapping : parameterMappings) {String property = paramMapping.getProperty();Object value;if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} else {// 从 ParamMap 中按名称获取值MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(property);}// 设置 PreparedStatement 参数typeHandler.setParameter(ps, i + 1, value, paramMapping.getJdbcType());}}
}
⑷.示例场景
①. 使用 @Param
的 Mapper 方法
User selectUser(@Param("name") String name, @Param("age") int age);
-
生成的 ParamMap:
{"name": "Alice","param1": "Alice","age": 25,"param2": 25 }
② XML 中的引用
<select id="selectUser">SELECT * FROM user WHERE name = #{name} AND age = #{age}
</select>
⑸.关键设计思想
-
兼容性:支持
param1
等传统占位符。 -
灵活性:允许通过
@Param
自定义名称,提升可读性。 -
性能优化:单参数直接传递,避免不必要的 Map 包装。
⑹.总结
@Param
的源码实现通过 ParamNameResolver
解析参数名称,并在执行时通过 ParamMap
统一处理多参数场景。这一机制使得 MyBatis 能够灵活适配不同参数命名需求,同时保持与旧版本的兼容性。