当前位置: 首页 > news >正文

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)。

    • 最终回退为 arg0arg1, ... 或 param1param2, ...。


②参数包装: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") 定义的键。

      • 通用名称:如 param1param2(兼容旧版本)。


⑶.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>

⑸.关键设计思想

  1. 兼容性:支持 param1 等传统占位符。

  2. 灵活性:允许通过 @Param 自定义名称,提升可读性。

  3. 性能优化:单参数直接传递,避免不必要的 Map 包装。


⑹.总结

@Param 的源码实现通过 ParamNameResolver 解析参数名称,并在执行时通过 ParamMap 统一处理多参数场景。这一机制使得 MyBatis 能够灵活适配不同参数命名需求,同时保持与旧版本的兼容性。

相关文章:

  • 【Firewalld】Linux中firewall-cmd的基本使用
  • Runnable组件容灾回退机制 with_fallback 深度解析降低程序错误率
  • 单链表的实现 | 附学生信息管理系统的实现
  • 3D打印技术助力高精密零件制造与维修工具革新
  • C# Winform 入门(13)之通过WebServer查询天气预报
  • 网络钓鱼攻击的威胁和执法部门的作用(第一部分)
  • 架构师面试(二十六):系统拆分
  • 【Csharp】获取实时的鼠标光标位置,测试用——做窗口软件绘图需要确定光标位置
  • GenerationMixin概述
  • Python Cookbook-5.5 根据内嵌的数字将字符串排序
  • 清明假期间
  • 数据分析-Excel-学习笔记
  • AI大模型:(二)2.1 从零训练自己的大模型概述
  • 【LeetCode 热题100】55:跳跃游戏(详细解析)(Go语言版)
  • 用python来操作mysql(复习一,主要是mysql连接和授权)
  • 【清明折柳】写在扬马三周目后
  • 【NLP 54、大模型训练相关知识】
  • Sentinel核心源码分析(上)
  • InfoSec Prep: OSCP靶场渗透
  • 定义和初始化 vector 对象(三十八)
  • 虎门h5网站建设/百度权重4网站值多少钱
  • 用ssh做的网站/郑州网站推广公司咨询
  • 手机网站建设系统/sem竞价账户托管
  • 如何做微信电子书下载网站/做网站多少钱
  • 动画制作app/seo优化排名教程
  • 溧水区住房和城乡建设厅网站/seo服务公司