MyBatis小技巧与MyBatis参数处理
一、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;
@Override
public 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 mapper
PUBLIC "-//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">
select
id,
car_num as carNum,
brand,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from
t_car
where
car_type = ${carType}
/* car_type = '${carType}'*/
</select>
</mapper>
jdbc.properties
logback.xml
mybatis-config.xml
test
@Test
public 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 mapper
PUBLIC "-//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">
select
id,
car_num as carNum,
brand,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from
t_car
order by
produce_time ${ascOrDesc}
</select>
</mapper>
测试程序
@Test
public 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_20220901 t_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 mapper
PUBLIC "-//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">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
${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">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
where
brand 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 mapper
PUBLIC "-//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">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
where
brand like concat('%',#{brand},'%')
</select>
</mapper>
①使用${}
②使用#{}
第三种方案:比较鸡肋了。可以不算。 concat('%','${brand}','%')
第四种方案: "%"#{brand}"%"
<?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="org.example1.mapper.CarMapper">
<select id="selectLikeByBrand" resultType="org.example1.pojo.Car">
select
id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from
t_car
where
brand 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.xml LogMapper接口-> 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 mapper
PUBLIC "-//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
@Test
public 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 mapper
PUBLIC "-//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
@Test
public void testSelectBySex(){
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
// char --> Character
Character sex = Character.valueOf('男');
List<Student> students = mapper.selectBySex(sex);
students.forEach(student -> System.out.println(student));
sqlSession.close();
}
// java.util.Date java.sql.Date,他们都是简单类型。
@Test
public 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();
}
@Test
public 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();
}
@Test
public 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>
@Test
public 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>
@Test
public 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]去参数
@Test
public 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>
@Test
public 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 获取参数 Map
Object 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 能够灵活适配不同参数命名需求,同时保持与旧版本的兼容性。