MyBatis核心功能一

本文最后更新于:3 年前

引言

MyBatis作为一个强大的持久层框架,提供了面向接口编程的方法,通过动态代理技术,自动地生成接口实现,本文将对这一特点进行详细介绍。

面向接口编程

简介

面向接口编程是一种常用的设计模式,在MyBatis中,可以定义一个Java接口,其中的每个方法对应于SQL映射文件中的一个SQL语句。MyBatis利用动态代理技术在运行时自动生成这些接口的实现类。开发者只需调用接口方法,即可执行对应的SQL操作。这种方法将接口定义与具体的SQL操作分离,使得代码更加整洁、易于管理和扩展。

原理

  1. 接口定义:首先定义一个接口,声明所需的数据库操作,如插入、查询、更新和删除。
  2. 映射文件:为接口中的每个方法提供相应的SQL语句映射。这些映射定义在Mapper文件中,或通过注解直接在接口方法上指定。
  3. SqlSessionFactory:通过配置信息,MyBatis构建一个SqlSessionFactory实例,这是创建SQL会话的工厂。
  4. SqlSession:从SqlSessionFactory获取SqlSession,它是执行SQL命令的实际对象。
  5. 动态代理:当调用接口方法时,MyBatis会通过动态代理拦截这些调用,并根据方法名称匹配Mapper文件或注解中定义的 SQL 语句,执行数据库操作。

优点

  • 解耦合:接口和SQL映射的分离使得业务逻辑和数据库操作代码解耦,提高了代码的可维护性。
  • 简洁性:开发者只需通过调用接口方法来执行数据库操作,无需关心底层的SQL详情,代码更简洁。
  • 易于维护:SQL语句集中管理,在Mapper文件或接口注解中修改SQL语句即可,无需修改业务代码。
  • 灵活性:易于实现和切换不同的数据库操作逻辑,只需更改SQL映射即可,不影响业务逻辑层。
  • 类型安全:由于使用Java接口,所有的操作都是类型安全的,减少了运行时错误。

示例

  1. tt_vehicle为示例表,定义DAO接口VehicleMapper.class,这个接口包含了的各种操作如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public interface VehicleMapper {

    void addVehicle(VehiclePO vehiclePO);

    void deleteVehicle(long id);

    VehiclePO getVehicleById(long id);

    List<VehiclePO> getAllVehicles();

    void updateVehicle(VehiclePO vehiclePO);

    }
  2. 定义对应的Mapper文件VehicleMapper.xml,接口与Mapper文件之间通过namespace进行关联(Mapper文件的namespace=接口的全限定类名),SQL语句标签的id=接口方法名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="space.yangtao.mapper.VehicleMapper">
    <insert id="addVehicle" parameterType="space.yangtao.domain.po.VehiclePO">
    insert into tt_vehicle (product_name, vin, production_date, guide_price)
    values (#{productName}, #{vin}, #{productionDate}, #{guidePrice})
    </insert>

    <delete id="deleteVehicle" parameterType="long">
    delete from tt_vehicle where id = #{id}
    </delete>

    <select id="getVehicleById" resultType="space.yangtao.domain.po.VehiclePO">
    select id as id,
    product_name as productName,
    vin as vin,
    production_date as productionDate,
    guide_price as guidePrice
    from tt_vehicle
    where id = #{id}
    </select>

    <select id="getAllVehicles" resultType="space.yangtao.domain.po.VehiclePO">
    select id as id,
    product_name as productName,
    vin as vin,
    production_date as productionDate,
    guide_price as guidePrice
    from tt_vehicle
    </select>

    <update id="updateVehicle" parameterType="space.yangtao.domain.po.VehiclePO">
    update tt_vehicle
    set product_name = #{productName},
    vin = #{vin},
    production_date = #{productionDate},
    guide_price = #{guidePrice}
    where id = #{id}
    </update>
    </mapper>
  3. 使用时,不再通过SqlSession对象调用Mapper文件中的方法,而是通过SqlSession获取到对应的接口的代理对象,通过调用代理对象的方法,实现对Mapper文件中对应SQL的调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
    public class VehicleMapperTest {

    private static final Logger log = LoggerFactory.getLogger(VehicleMapperTest.class);

    @Test
    @Order(1)
    public void testAddVehicle() {
    log.info("--- testAddVehicle start ---");
    VehiclePO audiA6L = new VehiclePO()
    .setProductName("Audi A6L")
    .setVin("LVIN1")
    .setProductionDate(LocalDate.now())
    .setGuidePrice(new BigDecimal("450000"));
    VehiclePO bwm530Li = new VehiclePO()
    .setProductName("BMW 530Li")
    .setVin("LVIN2")
    .setProductionDate(LocalDate.now())
    .setGuidePrice(new BigDecimal("600000"));
    VehiclePO benzE300L = new VehiclePO()
    .setProductName("Benz E300L")
    .setVin("LVIN3")
    .setProductionDate(LocalDate.now())
    .setGuidePrice(new BigDecimal("500000"));
    SqlSession session = SqlSessionUtil.getSession();
    session.getMapper(VehicleMapper.class).addVehicle(audiA6L);
    session.getMapper(VehicleMapper.class).addVehicle(bwm530Li);
    session.getMapper(VehicleMapper.class).addVehicle(benzE300L);
    session.commit();
    log.info("--- testAddVehicle end ---");
    }

    @Test
    @Order(2)
    public void testGetVehicleById() {
    log.info("--- testGetVehicleById start ---");
    SqlSession session = SqlSessionUtil.getSession();
    VehiclePO vehiclePO = session.getMapper(VehicleMapper.class).getVehicleById(1L);
    System.out.println(vehiclePO);
    log.info("--- testGetVehicleById end ---");
    }

    @Test
    @Order(3)
    public void testUpdateVehicle() {
    log.info("--- testUpdateVehicle start ---");
    VehiclePO vehiclePO = new VehiclePO()
    .setId(1L)
    .setProductName("Audi A6L")
    .setVin("LVIN1")
    .setProductionDate(LocalDate.now())
    .setGuidePrice(new BigDecimal("450000"));
    SqlSession session = SqlSessionUtil.getSession();
    session.getMapper(VehicleMapper.class).updateVehicle(vehiclePO);
    session.commit();
    log.info("--- testUpdateVehicle end ---");
    }

    @Test
    @Order(4)
    public void testDeleteVehicle() {
    log.info("--- testDeleteVehicle start ---");
    SqlSession session = SqlSessionUtil.getSession();
    session.getMapper(VehicleMapper.class).deleteVehicle(1L);
    session.commit();
    log.info("--- testDeleteVehicle end ---");
    }

    @Test
    @Order(5)
    public void testGetAllVehicles() {
    log.info("--- testGetAllVehicles start ---");
    SqlSession session = SqlSessionUtil.getSession();
    session.getMapper(VehicleMapper.class).getAllVehicles().forEach(System.out::println);
    log.info("--- testGetAllVehicles end ---");
    }
    }
  4. 运行程序,控制台输出日志如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    21:01:57.345 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
    21:01:58.177 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 34073107.
    21:01:58.177 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.179 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - ==> Preparing: insert into tt_vehicle (product_name, vin, production_date, guide_price) values (?, ?, ?, ?)
    21:01:58.197 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - ==> Parameters: Audi A6L(String), LVIN1(String), 2024-07-18(LocalDate), 450000(BigDecimal)
    21:01:58.204 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - <== Updates: 1
    21:01:58.213 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - ==> Preparing: insert into tt_vehicle (product_name, vin, production_date, guide_price) values (?, ?, ?, ?)
    21:01:58.214 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - ==> Parameters: BMW 530Li(String), LVIN2(String), 2024-07-18(LocalDate), 600000(BigDecimal)
    21:01:58.215 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - <== Updates: 1
    21:01:58.215 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - ==> Preparing: insert into tt_vehicle (product_name, vin, production_date, guide_price) values (?, ?, ?, ?)
    21:01:58.215 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - ==> Parameters: Benz E300L(String), LVIN3(String), 2024-07-18(LocalDate), 500000(BigDecimal)
    21:01:58.216 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - <== Updates: 1
    21:01:58.216 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.218 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.218 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.219 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 34073107 to pool.
    21:01:58.219 [main] INFO space.yangtao.client.VehicleMapperTest - --- testAddVehicle end ---
    21:01:58.223 [main] INFO space.yangtao.client.VehicleMapperTest - --- testGetVehicleById start ---
    21:01:58.224 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
    21:01:58.224 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Checked out connection 34073107 from pool.
    21:01:58.224 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.225 [main] DEBUG space.yangtao.mapper.VehicleMapper.getVehicleById - ==> Preparing: select id as id, product_name as productName, vin as vin, production_date as productionDate, guide_price as guidePrice from tt_vehicle where id = ?
    21:01:58.226 [main] DEBUG space.yangtao.mapper.VehicleMapper.getVehicleById - ==> Parameters: 1(Long)
    21:01:58.235 [main] TRACE space.yangtao.mapper.VehicleMapper.getVehicleById - <== Columns: id, productName, vin, productionDate, guidePrice
    21:01:58.236 [main] TRACE space.yangtao.mapper.VehicleMapper.getVehicleById - <== Row: 1, Audi A6L, LVIN1, 2024-07-18, 450000.00
    21:01:58.238 [main] DEBUG space.yangtao.mapper.VehicleMapper.getVehicleById - <== Total: 1
    VehiclePO(id=1, productName=Audi A6L, vin=LVIN1, productionDate=2024-07-18, guidePrice=450000.00)
    21:01:58.240 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.240 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.240 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 34073107 to pool.
    21:01:58.240 [main] INFO space.yangtao.client.VehicleMapperTest - --- testGetVehicleById end ---
    21:01:58.241 [main] INFO space.yangtao.client.VehicleMapperTest - --- testUpdateVehicle start ---
    21:01:58.242 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
    21:01:58.242 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Checked out connection 34073107 from pool.
    21:01:58.242 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.242 [main] DEBUG space.yangtao.mapper.VehicleMapper.updateVehicle - ==> Preparing: update tt_vehicle set product_name = ?, vin = ?, production_date = ?, guide_price = ? where id = ?
    21:01:58.242 [main] DEBUG space.yangtao.mapper.VehicleMapper.updateVehicle - ==> Parameters: Audi A6L(String), LVIN1(String), 2024-07-18(LocalDate), 450000(BigDecimal), 1(Long)
    21:01:58.243 [main] DEBUG space.yangtao.mapper.VehicleMapper.updateVehicle - <== Updates: 1
    21:01:58.243 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.244 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.244 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.244 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 34073107 to pool.
    21:01:58.244 [main] INFO space.yangtao.client.VehicleMapperTest - --- testUpdateVehicle end ---
    21:01:58.245 [main] INFO space.yangtao.client.VehicleMapperTest - --- testDeleteVehicle start ---
    21:01:58.245 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
    21:01:58.245 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Checked out connection 34073107 from pool.
    21:01:58.245 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.246 [main] DEBUG space.yangtao.mapper.VehicleMapper.deleteVehicle - ==> Preparing: delete from tt_vehicle where id = ?
    21:01:58.246 [main] DEBUG space.yangtao.mapper.VehicleMapper.deleteVehicle - ==> Parameters: 1(Long)
    21:01:58.247 [main] DEBUG space.yangtao.mapper.VehicleMapper.deleteVehicle - <== Updates: 1
    21:01:58.247 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.249 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.250 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.250 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 34073107 to pool.
    21:01:58.250 [main] INFO space.yangtao.client.VehicleMapperTest - --- testDeleteVehicle end ---
    21:01:58.251 [main] INFO space.yangtao.client.VehicleMapperTest - --- testGetAllVehicles start ---
    21:01:58.251 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
    21:01:58.251 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Checked out connection 34073107 from pool.
    21:01:58.252 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.252 [main] DEBUG space.yangtao.mapper.VehicleMapper.getAllVehicles - ==> Preparing: select id as id, product_name as productName, vin as vin, production_date as productionDate, guide_price as guidePrice from tt_vehicle
    21:01:58.253 [main] DEBUG space.yangtao.mapper.VehicleMapper.getAllVehicles - ==> Parameters:
    21:01:58.253 [main] TRACE space.yangtao.mapper.VehicleMapper.getAllVehicles - <== Columns: id, productName, vin, productionDate, guidePrice
    21:01:58.253 [main] TRACE space.yangtao.mapper.VehicleMapper.getAllVehicles - <== Row: 2, BMW 530Li, LVIN2, 2024-07-18, 600000.00
    21:01:58.253 [main] TRACE space.yangtao.mapper.VehicleMapper.getAllVehicles - <== Row: 3, Benz E300L, LVIN3, 2024-07-18, 500000.00
    21:01:58.253 [main] DEBUG space.yangtao.mapper.VehicleMapper.getAllVehicles - <== Total: 2
    VehiclePO(id=2, productName=BMW 530Li, vin=LVIN2, productionDate=2024-07-18, guidePrice=600000.00)
    VehiclePO(id=3, productName=Benz E300L, vin=LVIN3, productionDate=2024-07-18, guidePrice=500000.00)
    21:01:58.255 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.255 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
    21:01:58.255 [main] DEBUG o.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 34073107 to pool.
    21:01:58.255 [main] INFO space.yangtao.client.VehicleMapperTest - --- testGetAllVehicles end ---

    由日志可以看出,SqlSession通过接口的动态代理对象已经实现了对Mapper文件中的SQL执行,并且在编码时分层清晰且可维护性高。

占位符

场景一

VehicleMapper.xml中定义两个测试方法,都是以VIN为条件,分别用#{}${}传递参数

1
2
3
4
5
6
<select id="testD" resultType="space.yangtao.domain.po.VehiclePO">
select id from tt_vehicle where vin = #{vin}
</select>
<select id="testE" resultType="space.yangtao.domain.po.VehiclePO">
select id from tt_vehicle where vin = ${vin}
</select>

VehicleMapper.class中定义对应的接口方法

1
2
3
List<VehiclePO> testD(String vin);

List<VehiclePO> testE(String vin);

运行测试程序

1
2
3
4
SqlSession session = SqlSessionUtil.getSession();
VehicleMapper vehicleMapper = session.getMapper(VehicleMapper.class);
vehicleMapper.testD("LVIN1");
vehicleMapper.testE("LVIN1");

控制台输出日志如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
21:03:22.737 [main] DEBUG space.yangtao.mapper.VehicleMapper.testD - ==> Preparing: select id from tt_vehicle where vin = ?
21:03:22.760 [main] DEBUG space.yangtao.mapper.VehicleMapper.testD - ==> Parameters: LVIN1(String)
21:03:22.775 [main] DEBUG space.yangtao.mapper.VehicleMapper.testD - <== Total: 0
21:03:22.790 [main] DEBUG space.yangtao.mapper.VehicleMapper.testE - ==> Preparing: select id from tt_vehicle where vin = LVIN1
21:03:22.790 [main] DEBUG space.yangtao.mapper.VehicleMapper.testE - ==> Parameters:

org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column 'LVIN1' in 'where clause'
### The error may exist in space/yangtao/mapper/VehicleMapper.xml
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: select id from tt_vehicle where vin = LVIN1
### Cause: java.sql.SQLSyntaxErrorException: Unknown column 'LVIN1' in 'where clause'

由日志可知,方法执行testD方法时报错了,从日志的SQL条件看,两条语句中,使用#{}语句将#{}替换为了?,并通过预处理语句设置参数值,而${}语句不使用预处理语句,而是直接将${}替换为了参数值,SQL语句中对于字符串参数没有加引号'',因此报错。

场景二

VehicleMapper.xml中定义两个测试方法,查询参数为排序方式

1
2
3
4
5
6
<select id="testF" resultType="space.yangtao.domain.po.VehiclePO">
select id from tt_vehicle order by production_date ${order}
</select>
<select id="testG" resultType="space.yangtao.domain.po.VehiclePO">
select id from tt_vehicle order by production_date #{order}
</select>

VehicleMapper.class中定义对应的接口方法

1
2
3
List<VehiclePO> testF(String vin);

List<VehiclePO> testG(String vin);

运行测试程序

1
2
3
4
SqlSession session = SqlSessionUtil.getSession();
VehicleMapper vehicleMapper = session.getMapper(VehicleMapper.class);
vehicleMapper.testF("asc");
vehicleMapper.testG("asc");

控制台输出日志如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
21:06:25.044 [main] DEBUG space.yangtao.mapper.VehicleMapper.testF - ==> Preparing: select id from tt_vehicle order by production_date asc
21:06:25.063 [main] DEBUG space.yangtao.mapper.VehicleMapper.testF - ==> Parameters:
21:06:25.080 [main] TRACE space.yangtao.mapper.VehicleMapper.testF - <== Columns: id
21:06:25.081 [main] TRACE space.yangtao.mapper.VehicleMapper.testF - <== Row: 2
21:06:25.082 [main] TRACE space.yangtao.mapper.VehicleMapper.testF - <== Row: 3
21:06:25.082 [main] DEBUG space.yangtao.mapper.VehicleMapper.testF - <== Total: 2
21:06:25.083 [main] DEBUG space.yangtao.mapper.VehicleMapper.testG - ==> Preparing: select id from tt_vehicle order by production_date ?
21:06:25.083 [main] DEBUG space.yangtao.mapper.VehicleMapper.testG - ==> Parameters: asc(String)

org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''asc'' at line 1
### The error may exist in space/yangtao/mapper/VehicleMapper.xml
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: select id from tt_vehicle order by production_date ?
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''asc'' at line 1

由日志可知,方法在执行testG方法时报错了,从日志的SQL看,使用${}传参的语句正确的实现了预想的功能,而#{}传参的语句使用了预处理,将MySQL关键字asc当成了参数,导致执行报错。

总结

  1. 预处理与直接替换

    • #{}:使用预处理,安全地绑定变量。

    • ${}:直接进行字符串替换,存在SQL注入的风险。

  1. 应用场景

    • #{}:对于大多数需要动态数据绑定的场景。

    • ${}:可用于那些动态构建SQL语句的场景,如动态表名或动态列名或排序方式,但必须非常小心处理变量值,确保安全性。

  1. SQL 注入防御

    • #{}:因为使用了预处理和参数绑定,能有效防止SQL注入。

    • ${}:由于缺乏预处理,用户输入如果不被适当清理和限制,可能会引起SQL注入问题。

取别名

简介

在 MyBatis 中,<typeAliases>标签用于定义Java类型的别名,以便在配置文件和映射文件中简化对Java类的引用。通过使用别名,可以避免在XML配置中反复写出长的类名,从而使配置文件更加简洁易读。

作用

  • 简化类名:为长的完全限定类名(FQCN)提供一个简短的名称,使得配置文件中的引用更加简洁。
  • 提高可读性:通过使用别名,配置文件的可读性得到提高,配置更加直观。
  • 配置灵活性:允许开发者自定义别名,根据项目的需要,映射任意的类名。

使用方式

mybatis-config.xml中,在<typeAliases>标签下取别名,别名可以在Mapper文件中使用

1
2
3
4
5
6
7
8
9
10
11
12
<typeAliases>

<!-- 单个自定义别名 -->
<typeAlias type="space.yangtao.domain.po.VehiclePO" alias="VehiclePO"/>

<!-- 默认的别名机制,类的简名ownerPO,不区分大小写 -->
<typeAlias type="space.yangtao.domain.po.OwnerPO"/>

<!-- 包下所有类批量起别名,规则为默认别名机制 -->
<package name="space.yangtao.domain"/>

</typeAliases>

注意

  • 别名默认不区分大小写,但可以在设置中,通过caseSensitiveAliases开启别名大小写敏感。
  • 如果不同的类使用相同的简单名称,自动扫描可能会引发冲突,此时需要为冲突的类指定具体的别名。
  • 使用别名时,确保别名的唯一性和一致性,避免在项目中引起混淆。
  • namespace不能取别名。

获取主键

在许多开发场景中,插入一条数据库后,通常会希望的到新插入行自动生成的主键值,MyBatis中通过设置Mapper文件中对应的insert标签即可实现,如

1
2
3
4
5
6
7
<insert id="addVehicle" 
parameterType="space.yangtao.domain.po.VehiclePO"
useGeneratedKeys="true"
keyProperty="id">
insert into tt_vehicle (product_name, vin, production_date, guide_price)
values (#{productName}, #{vin}, #{productionDate}, #{guidePrice})
</insert>
  • useGeneratedKeys:是否使用自动生成的主键
  • keyProperty:生成的主键值赋值给对象的哪个属性

运行测试程序

1
2
3
4
5
6
7
8
9
SqlSession session = SqlSessionUtil.getSession();
VehicleMapper vehicleMapper = session.getMapper(VehicleMapper.class);
VehiclePO vehiclePO = new VehiclePO()
.setProductName("Audi A6L")
.setVin("LVIN1")
.setProductionDate(LocalDate.now())
.setGuidePrice(new BigDecimal("450000"));
vehicleMapper.addVehicle(vehiclePO);
log.info("Generated key: {}", vehiclePO.getId());

控制台输出日志如下

1
2
3
4
5
6
...
21:13:16.484 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - ==> Preparing: insert into tt_vehicle (product_name, vin, production_date, guide_price) values (?, ?, ?, ?)
21:13:16.504 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - ==> Parameters: Audi A6L(String), LVIN1(String), 2024-07-18(LocalDate), 450000(BigDecimal)
21:13:16.507 [main] DEBUG space.yangtao.mapper.VehicleMapper.addVehicle - <== Updates: 1
21:13:16.515 [main] INFO space.yangtao.client.VehicleMapperTest - Generated key: 4
...

由日志可知新插入数据的主键ID为4,经核查也与数据库中对应的主键ID一致。

拓展:本方式适用于支持自动生成主键的数据库(如MySQL的AUTO_INCREMENT)。对于不支持自动生成主键的数据库,需要使用selectKey标签来获取主键。

参数处理

创建测试表tt_student,同时插入3条测试数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `tt_student`
(
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(50) NOT NULL COMMENT '姓名',
`height` double NOT NULL COMMENT '身高(cm)',
`gender` char NOT NULL COMMENT '性别',
`birthday` date NOT NULL COMMENT '出生日期',
`create_time` datetime NOT NULL default current_timestamp COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='学生表';

INSERT INTO mybatis_db.tt_student (id, name, height, gender, birthday, create_time) VALUES (1, 'zhangsan', 176.1, '男', '1990-01-01', '2022-07-01 22:10:22');
INSERT INTO mybatis_db.tt_student (id, name, height, gender, birthday, create_time) VALUES (2, 'zhangsan1', 180, '男', '1990-01-01', '2022-07-01 21:10:22');
INSERT INTO mybatis_db.tt_student (id, name, height, gender, birthday, create_time) VALUES (3, 'zhangsan2', 180, '男', '1990-01-01', '2022-07-01 21:10:22');

简单类型自动转换

  • 基本数据类型:byte、short、int、long、float、double、char
  • 基本数据类型包装类:Byte、Short、Integer、Long、Float、Double、Character
  • 字符串类型:String
  • 日期:java.util.Date、java.sql.Date、java.time.LocalDate、java.time.LocalDateTime

在Mapper文件StudentMapper.xml中定义表tt_student对应的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<select id="getById" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where id = #{id}
</select>
<select id="getByName" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where name = #{name}
</select>
<select id="getByHeight" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where height = #{height}
</select>
<select id="getByGender" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where gender = #{gender}
</select>
<select id="getByBirthday" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where birthday = #{birthday}
</select>
<select id="getByCreateTime" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where create_Time = #{createTime}
</select>

编写对应的接口方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
StudentPO getById(Long id);

List<StudentPO> getByName(String name);

List<StudentPO> getByHeight(double height);

List<StudentPO> getByGender(char gender);

List<StudentPO> getByBirthday(Date birthday);

List<StudentPO> getByCreateTime(Date createTime);

List<StudentPO> getByBirthday2(LocalDate birthday);

List<StudentPO> getByCreateTime2(LocalDateTime createTime2);

运行测试程序

1
2
3
4
5
6
7
8
9
10
11
SqlSession session = SqlSessionUtil.getSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
log.info("Long查询 students -> {}", studentMapper.getById(1L));
log.info("String查询 students -> {}", studentMapper.getByName("zhangsan"));
log.info("double查询 students -> {}", studentMapper.getByHeight(176.1));
log.info("char查询 students -> {}", studentMapper.getByGender('男'));
log.info("Date查询 students -> {}", studentMapper.getByBirthday(new SimpleDateFormat("yyyy-MM-dd").parse("1990-01-01")));
log.info("Date查询 students -> {}", studentMapper.getByCreateTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2022-07-01 22:10:22")));
log.info("LocalDate查询 students -> {}", studentMapper.getByBirthday2(LocalDate.of(1990, 1, 1)));
log.info("LocalDateTime查询 students -> {}", studentMapper.getByCreateTime2(LocalDate.of(2022, 7, 1).atTime(21, 10, 22)));
session.close();

控制台输出日志如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
...
21:23:33.807 [main] DEBUG space.yangtao.mapper.StudentMapper.getById - ==> Preparing: select * from tt_student where id = ?
21:23:33.825 [main] DEBUG space.yangtao.mapper.StudentMapper.getById - ==> Parameters: 1(Long)
21:23:33.837 [main] TRACE space.yangtao.mapper.StudentMapper.getById - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:23:33.838 [main] TRACE space.yangtao.mapper.StudentMapper.getById - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:23:33.839 [main] DEBUG space.yangtao.mapper.StudentMapper.getById - <== Total: 1
21:23:33.840 [main] INFO space.yangtao.client.StudentMapperTest - Long查询 students -> StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)
21:23:33.841 [main] DEBUG space.yangtao.mapper.StudentMapper - Cache Hit Ratio [space.yangtao.mapper.StudentMapper]: 0.0
21:23:33.841 [main] DEBUG space.yangtao.mapper.StudentMapper.getByName - ==> Preparing: select * from tt_student where name = ?
21:23:33.841 [main] DEBUG space.yangtao.mapper.StudentMapper.getByName - ==> Parameters: zhangsan(String)
21:23:33.842 [main] TRACE space.yangtao.mapper.StudentMapper.getByName - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:23:33.842 [main] TRACE space.yangtao.mapper.StudentMapper.getByName - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:23:33.842 [main] DEBUG space.yangtao.mapper.StudentMapper.getByName - <== Total: 1
21:23:33.843 [main] INFO space.yangtao.client.StudentMapperTest - String查询 students -> [StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
21:23:33.843 [main] DEBUG space.yangtao.mapper.StudentMapper - Cache Hit Ratio [space.yangtao.mapper.StudentMapper]: 0.0
21:23:33.843 [main] DEBUG space.yangtao.mapper.StudentMapper.getByHeight - ==> Preparing: select * from tt_student where height = ?
21:23:33.843 [main] DEBUG space.yangtao.mapper.StudentMapper.getByHeight - ==> Parameters: 176.1(Double)
21:23:33.844 [main] TRACE space.yangtao.mapper.StudentMapper.getByHeight - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:23:33.844 [main] TRACE space.yangtao.mapper.StudentMapper.getByHeight - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:23:33.846 [main] DEBUG space.yangtao.mapper.StudentMapper.getByHeight - <== Total: 1
21:23:33.846 [main] INFO space.yangtao.client.StudentMapperTest - double查询 students -> [StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
21:23:33.846 [main] DEBUG space.yangtao.mapper.StudentMapper - Cache Hit Ratio [space.yangtao.mapper.StudentMapper]: 0.0
21:23:33.846 [main] DEBUG space.yangtao.mapper.StudentMapper.getByGender - ==> Preparing: select * from tt_student where gender = ?
21:23:33.846 [main] DEBUG space.yangtao.mapper.StudentMapper.getByGender - ==> Parameters: 男(String)
21:23:33.847 [main] TRACE space.yangtao.mapper.StudentMapper.getByGender - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:23:33.847 [main] TRACE space.yangtao.mapper.StudentMapper.getByGender - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:23:33.847 [main] TRACE space.yangtao.mapper.StudentMapper.getByGender - <== Row: 2, 2, zhangsan1, 180.0, 男, 1990-01-01, 2022-07-01 21:10:22
21:23:33.848 [main] TRACE space.yangtao.mapper.StudentMapper.getByGender - <== Row: 3, 2, zhangsan2, 180.0, 男, 1990-01-01, 2022-07-01 21:10:22
21:23:33.848 [main] DEBUG space.yangtao.mapper.StudentMapper.getByGender - <== Total: 3
21:23:33.848 [main] INFO space.yangtao.client.StudentMapperTest - char查询 students -> [StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null), StudentPO(id=2, name=zhangsan1, height=180.0, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null), StudentPO(id=3, name=zhangsan2, height=180.0, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
21:23:33.848 [main] DEBUG space.yangtao.mapper.StudentMapper - Cache Hit Ratio [space.yangtao.mapper.StudentMapper]: 0.0
21:23:33.849 [main] DEBUG space.yangtao.mapper.StudentMapper.getByBirthday - ==> Preparing: select * from tt_student where birthday = ?
21:23:33.855 [main] DEBUG space.yangtao.mapper.StudentMapper.getByBirthday - ==> Parameters: 1990-01-01 00:00:00.0(Timestamp)
21:23:33.856 [main] TRACE space.yangtao.mapper.StudentMapper.getByBirthday - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:23:33.856 [main] TRACE space.yangtao.mapper.StudentMapper.getByBirthday - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:23:33.857 [main] TRACE space.yangtao.mapper.StudentMapper.getByBirthday - <== Row: 2, 2, zhangsan1, 180.0, 男, 1990-01-01, 2022-07-01 21:10:22
21:23:33.857 [main] TRACE space.yangtao.mapper.StudentMapper.getByBirthday - <== Row: 3, 2, zhangsan2, 180.0, 男, 1990-01-01, 2022-07-01 21:10:22
21:23:33.858 [main] DEBUG space.yangtao.mapper.StudentMapper.getByBirthday - <== Total: 3
21:23:33.858 [main] INFO space.yangtao.client.StudentMapperTest - Date查询 students -> [StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null), StudentPO(id=2, name=zhangsan1, height=180.0, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null), StudentPO(id=3, name=zhangsan2, height=180.0, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
21:23:33.858 [main] DEBUG space.yangtao.mapper.StudentMapper - Cache Hit Ratio [space.yangtao.mapper.StudentMapper]: 0.0
21:23:33.858 [main] DEBUG space.yangtao.mapper.StudentMapper.getByCreateTime - ==> Preparing: select * from tt_student where create_Time = ?
21:23:33.858 [main] DEBUG space.yangtao.mapper.StudentMapper.getByCreateTime - ==> Parameters: 2022-07-01 22:10:22.0(Timestamp)
21:23:33.859 [main] TRACE space.yangtao.mapper.StudentMapper.getByCreateTime - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:23:33.860 [main] TRACE space.yangtao.mapper.StudentMapper.getByCreateTime - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:23:33.861 [main] DEBUG space.yangtao.mapper.StudentMapper.getByCreateTime - <== Total: 1
21:23:33.861 [main] INFO space.yangtao.client.StudentMapperTest - Date查询 students -> [StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
21:23:33.861 [main] DEBUG space.yangtao.mapper.StudentMapper - Cache Hit Ratio [space.yangtao.mapper.StudentMapper]: 0.0
21:23:33.861 [main] DEBUG space.yangtao.mapper.StudentMapper.getByBirthday2 - ==> Preparing: select * from tt_student where birthday = ?
21:23:33.862 [main] DEBUG space.yangtao.mapper.StudentMapper.getByBirthday2 - ==> Parameters: 1990-01-01(LocalDate)
21:23:33.863 [main] TRACE space.yangtao.mapper.StudentMapper.getByBirthday2 - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:23:33.863 [main] TRACE space.yangtao.mapper.StudentMapper.getByBirthday2 - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:23:33.863 [main] TRACE space.yangtao.mapper.StudentMapper.getByBirthday2 - <== Row: 2, 2, zhangsan1, 180.0, 男, 1990-01-01, 2022-07-01 21:10:22
21:23:33.863 [main] TRACE space.yangtao.mapper.StudentMapper.getByBirthday2 - <== Row: 3, 2, zhangsan2, 180.0, 男, 1990-01-01, 2022-07-01 21:10:22
21:23:33.863 [main] DEBUG space.yangtao.mapper.StudentMapper.getByBirthday2 - <== Total: 3
21:23:33.863 [main] INFO space.yangtao.client.StudentMapperTest - LocalDate查询 students -> [StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null), StudentPO(id=2, name=zhangsan1, height=180.0, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null), StudentPO(id=3, name=zhangsan2, height=180.0, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
21:23:33.864 [main] DEBUG space.yangtao.mapper.StudentMapper - Cache Hit Ratio [space.yangtao.mapper.StudentMapper]: 0.0
21:23:33.864 [main] DEBUG s.yangtao.mapper.StudentMapper.getByCreateTime2 - ==> Preparing: select * from tt_student where create_Time = ?
21:23:33.865 [main] DEBUG s.yangtao.mapper.StudentMapper.getByCreateTime2 - ==> Parameters: 2022-07-01T21:10:22(LocalDateTime)
21:23:33.866 [main] TRACE s.yangtao.mapper.StudentMapper.getByCreateTime2 - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:23:33.866 [main] TRACE s.yangtao.mapper.StudentMapper.getByCreateTime2 - <== Row: 2, 2, zhangsan1, 180.0, 男, 1990-01-01, 2022-07-01 21:10:22
21:23:33.866 [main] TRACE s.yangtao.mapper.StudentMapper.getByCreateTime2 - <== Row: 3, 2, zhangsan2, 180.0, 男, 1990-01-01, 2022-07-01 21:10:22
21:23:33.866 [main] DEBUG s.yangtao.mapper.StudentMapper.getByCreateTime2 - <== Total: 2
21:23:33.866 [main] INFO space.yangtao.client.StudentMapperTest - LocalDateTime查询 students -> [StudentPO(id=2, name=zhangsan1, height=180.0, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null), StudentPO(id=3, name=zhangsan2, height=180.0, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
...

根据日志可知,MyBatis自身有类型推断机制,对于简单数据类型和常用的时间日期类型可自动转换映射。

除了类型推断,还可以在Mapper中通过parameterType指定参数类型,如

  1. 通过全限定类名指定类型

    1
    2
    3
    <select id="getById" resultType="space.yangtao.domain.po.StudentPO" parameterType="java.lang.Long">
    select * from tt_student where id = #{id}
    </select>
  2. 通过MyBatis的默认别名转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- java.lang.String类型的默认别名为string -->
    <select id="getByName" resultType="space.yangtao.domain.po.StudentPO" parameterType="string">
    select * from tt_student where name = #{name}
    </select>

    <!-- Java基本数据类型double的默认别名为_double -->
    <select id="getByHeight" resultType="space.yangtao.domain.po.StudentPO" parameterType="_double">
    select * from tt_student where height = #{height}
    </select>
  3. #{}中通过属性javaType指定Java的参数类型,通过属性jdbcType指定MySQL表字段的参数类型

    1
    2
    3
    <select id="getByGender" resultType="space.yangtao.domain.po.StudentPO">
    select * from tt_student where gender = #{gender, javaType=Character, jdbcType=C23 AR}
    </select>123456789 -

对象或Map类型参数

在MyBatis Mapper中还支持将所有的参数都封装到一个实体对象或Map数据结构中,其中对象属性名或Map的key必须与#{param}中的参数名一致。

在Mapper中定义查询语句

1
2
3
4
5
6
<select id="getByObj" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where id = #{id} and name = #{name}
</select>
<select id="getByMap" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where id = #{id} and name = #{name}
</select>

定义对应的接口方法

1
2
3
List<StudentPO> getByObj(StudentPO studentPO);

List<StudentPO> getByMap(Map<String, Object> map);

运行测试程序

1
2
3
4
5
6
7
8
9
SqlSession session = SqlSessionUtil.getSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
StudentPO zhangsan = new StudentPO().setId(1L).setName("zhangsan");
log.info("Obj查询 students -> {}", studentMapper.getByObj(zhangsan));
HashMap<String, Object> params = new HashMap<String, Object>() {{
put("id", 1L);
put("name", "zhangsan");
}};
log.info("Map查询 students -> {}", studentMapper.getByMap(params));

控制台输出日志如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
21:42:00.498 [main] DEBUG space.yangtao.mapper.StudentMapper.getByObj - ==> Preparing: select * from tt_student where id = ? and name = ?
21:42:00.515 [main] DEBUG space.yangtao.mapper.StudentMapper.getByObj - ==> Parameters: 1(Long), zhangsan(String)
21:42:00.527 [main] TRACE space.yangtao.mapper.StudentMapper.getByObj - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:42:00.528 [main] TRACE space.yangtao.mapper.StudentMapper.getByObj - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:42:00.530 [main] DEBUG space.yangtao.mapper.StudentMapper.getByObj - <== Total: 1
21:42:00.531 [main] INFO space.yangtao.client.StudentMapperTest - Obj查询 students -> [StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
21:42:00.533 [main] DEBUG space.yangtao.mapper.StudentMapper - Cache Hit Ratio [space.yangtao.mapper.StudentMapper]: 0.0
21:42:00.533 [main] DEBUG space.yangtao.mapper.StudentMapper.getByMap - ==> Preparing: select * from tt_student where id = ? and name = ?
21:42:00.533 [main] DEBUG space.yangtao.mapper.StudentMapper.getByMap - ==> Parameters: 1(Long), zhangsan(String)
21:42:00.534 [main] TRACE space.yangtao.mapper.StudentMapper.getByMap - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:42:00.534 [main] TRACE space.yangtao.mapper.StudentMapper.getByMap - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:42:00.535 [main] DEBUG space.yangtao.mapper.StudentMapper.getByMap - <== Total: 1
21:42:00.535 [main] INFO space.yangtao.client.StudentMapperTest - Map查询 students -> [StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
...

由日志可知,Mapper已正确接收接口传递的参数。

多参数

在mapper中定义查询方法如下,其中参数占位符为#{id}#{name}

1
2
3
<select id="getByMultipleParams" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where id = #{id} and name = #{name}
</select>

定义对应的接口方法

1
List<StudentPO> getByMultipleParams(Long id, String name);

运行测试程序

1
2
3
SqlSession session = SqlSessionUtil.getSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
log.info("多个参数查询 students -> {}", studentMapper.getByMultipleParams(1L, "zhangsan"));

控制台输出日志如下

1
2
3
org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database. Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
### Cause: org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]

程序运行出现异常,由日志可知异常原因是没有找到名为id的参数,支持的参数有arg1arg0param1param2,因此修改Mapper文件如下

1
2
3
<select id="getByMultipleParams" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where id = #{arg0} and name = #{arg1}
</select>

再次运行,控制台输出日志如下

1
2
3
4
5
6
7
8
...
21:46:20.044 [main] DEBUG s.yangtao.mapper.StudentMapper.getByMultipleParams - ==> Preparing: select * from tt_student where id = ? and name = ?
21:46:20.062 [main] DEBUG s.yangtao.mapper.StudentMapper.getByMultipleParams - ==> Parameters: 1(Long), zhangsan(String)
21:46:20.074 [main] TRACE s.yangtao.mapper.StudentMapper.getByMultipleParams - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:46:20.075 [main] TRACE s.yangtao.mapper.StudentMapper.getByMultipleParams - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:46:20.076 [main] DEBUG s.yangtao.mapper.StudentMapper.getByMultipleParams - <== Total: 1
21:46:20.077 [main] INFO space.yangtao.client.StudentMapperTest - 多个参数查询 students -> [StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
...

由日志可知,方法执行成功,查找到了需要的数据,此外arg0arg1还可以分别替换为param1param2,如

1
2
3
<select id="getByMultipleParams" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where id = #{param1} and name = #{param2}
</select>

之所以要通过指定的参数,是因为MyBatis底层会自动将多个简单参数封装到一个Map型数据结构中,逻辑类似于

1
2
3
4
5
6
Map<String, Object> params = new HashMap<String, Object>() {{
put("arg0", 第一个参数值);
put("param1", 第一个参数值);
put("arg1", 第二个参数值);
put("param2", 第二个参数值);
}};

@Param命名参数

上面提到的多参数,MyBatis默认创建key为arg0... param1...的Map型数据结构,MyBatis提供了@Param注解,用于指定Mapper接口参数的名称,方便在SQl语句中引用这些参数。

定义SQL如下,其中参数占位符为#{id}#{name}

1
2
3
<select id="getByParamAnno" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where id = #{id} and name = #{name}
</select>

定义对应的接口方法

1
List<StudentPO> getByParamAnno(@Param("id") Long id, @Param("name") String name);

运行测试程序

1
2
3
SqlSession session = SqlSessionUtil.getSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
log.info("Param注解查询 students -> {}", studentMapper.getByParamAnno(1L, "zhangsan"));

控制台输出日志如下

1
2
3
4
5
6
7
8
...
21:50:05.710 [main] DEBUG space.yangtao.mapper.StudentMapper.getByParamAnno - ==> Preparing: select * from tt_student where id = ? and name = ?
21:50:05.728 [main] DEBUG space.yangtao.mapper.StudentMapper.getByParamAnno - ==> Parameters: 1(Long), zhangsan(String)
21:50:05.743 [main] TRACE space.yangtao.mapper.StudentMapper.getByParamAnno - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:50:05.744 [main] TRACE space.yangtao.mapper.StudentMapper.getByParamAnno - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:50:05.745 [main] DEBUG space.yangtao.mapper.StudentMapper.getByParamAnno - <== Total: 1
21:50:05.747 [main] INFO space.yangtao.client.StudentMapperTest - Param注解查询 students -> [StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
...

由日志可知,@Param已经将MyBatis默认的参数命名替换成了idname

除此之外,@Param还可以应用于实体对象以及Map类型参数,甚至还可以使接口同时支持多个这种复杂参数。

在Mapper中指定了id对应参数为#{map1.id},name对应参数为#{map2.name}

1
2
3
<select id="getByMapAndObj" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where id = #{map.id} and name = #{obj.name}
</select>

对应的接口方法中也指定参数的@Param

1
List<StudentPO> getByMapAndObj(@Param("map") Map<String, Object> map1, @Param("obj") StudentPO studentPO);

测试测试程序

1
2
3
4
5
6
7
SqlSession session = SqlSessionUtil.getSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
HashMap<String, Object> map = new HashMap<String, Object>() {{
put("id", 1L);
}};
StudentPO studentPO = new StudentPO().setName("zhangsan");
log.info("多个复杂参数查询 students -> {}", studentMapper.getByMapAndObj(map, studentPO));

控制台输出日志如下

1
2
3
4
5
6
7
8
...
21:52:37.851 [main] DEBUG space.yangtao.mapper.StudentMapper.getByMapAndObj - ==> Preparing: select * from tt_student where id = ? and name = ?
21:52:37.871 [main] DEBUG space.yangtao.mapper.StudentMapper.getByMapAndObj - ==> Parameters: 1(Long), zhangsan(String)
21:52:37.885 [main] TRACE space.yangtao.mapper.StudentMapper.getByMapAndObj - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:52:37.886 [main] TRACE space.yangtao.mapper.StudentMapper.getByMapAndObj - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:52:37.888 [main] DEBUG space.yangtao.mapper.StudentMapper.getByMapAndObj - <== Total: 1
21:52:37.889 [main] INFO space.yangtao.client.StudentMapperTest - 多个复杂参数查询 students -> [StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
...

由日志可知,@Param注解支持为多个参数进行不同的命名。

在实际开发中,对于复杂参数,应明确使用@Param参数注解,特别是在团队协作和代码审查时,有助于避免参数混淆。

返回值处理

单个对象

单个对象可以通过实体对象或Map接收Mapper SQL的返回值。

在mapper中定义查询方法

1
2
3
4
5
6
<select id="getById" resultType="space.yangtao.domain.po.StudentPO" parameterType="java.lang.Long">
select * from tt_student where id = #{id}
</select>
<select id="getById2" resultType="map" parameterType="java.lang.Long">
select * from tt_student where id = #{id}
</select>

定义对应的接口方法

1
2
3
StudentPO getById(Long id);

Map<String, Object> getById2(Long id);

运行测试程序

1
2
3
4
SqlSession session = SqlSessionUtil.getSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
log.info("单个实体对象接收返回值:{}", studentMapper.getById(1L));
log.info("单个Map对象接收返回值:{}", studentMapper.getById2(1L));

控制台输出日志如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
21:55:37.437 [main] DEBUG space.yangtao.mapper.StudentMapper.getById - ==> Preparing: select * from tt_student where id = ?
21:55:37.454 [main] DEBUG space.yangtao.mapper.StudentMapper.getById - ==> Parameters: 1(Long)
21:55:37.469 [main] TRACE space.yangtao.mapper.StudentMapper.getById - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:55:37.471 [main] TRACE space.yangtao.mapper.StudentMapper.getById - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:55:37.472 [main] DEBUG space.yangtao.mapper.StudentMapper.getById - <== Total: 1
21:55:37.474 [main] INFO space.yangtao.client.StudentMapperTest - 单个实体对象接收返回值:StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)
21:55:37.474 [main] DEBUG space.yangtao.mapper.StudentMapper - Cache Hit Ratio [space.yangtao.mapper.StudentMapper]: 0.0
21:55:37.474 [main] DEBUG space.yangtao.mapper.StudentMapper.getById2 - ==> Preparing: select * from tt_student where id = ?
21:55:37.474 [main] DEBUG space.yangtao.mapper.StudentMapper.getById2 - ==> Parameters: 1(Long)
21:55:37.475 [main] TRACE space.yangtao.mapper.StudentMapper.getById2 - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:55:37.476 [main] TRACE space.yangtao.mapper.StudentMapper.getById2 - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:55:37.478 [main] DEBUG space.yangtao.mapper.StudentMapper.getById2 - <== Total: 1
21:55:37.478 [main] INFO space.yangtao.client.StudentMapperTest - 单个Map对象接收返回值:{birthday=1990-01-01, gender=男, create_time=2022-07-01T22:10:22, class_id=1, name=zhangsan, id=1, height=176.1}
...

由日志可知,实体类和Map结构都能接收单行数据。

多个对象

如果SQL查询可能存在多行,则在接口中需要通过集合来接收,如List数据结构

在mapper定义查询方法

1
2
3
4
5
6
<select id="getByIds" resultType="space.yangtao.domain.po.StudentPO">
select * from tt_student where id in (${id})
</select>
<select id="getByIds2" resultType="map">
select * from tt_student where id in (${id})
</select>

定义对应的接口方法

1
2
3
List<StudentPO> getByIds(String ids);

List<Map<String, Object>> getByIds2(String ids);

运行测试程序

1
2
3
4
5
SqlSession session = SqlSessionUtil.getSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
String ids = "1, 2";
log.info("多个实体对象接收返回值:{}", studentMapper.getByIds(ids));
log.info("多个Map对象接收返回值:{}", studentMapper.getByIds2(ids));

控制台输出日志如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
21:58:05.240 [main] DEBUG space.yangtao.mapper.StudentMapper.getByIds - ==> Preparing: select * from tt_student where id in (1, 2)
21:58:05.256 [main] DEBUG space.yangtao.mapper.StudentMapper.getByIds - ==> Parameters:
21:58:05.269 [main] TRACE space.yangtao.mapper.StudentMapper.getByIds - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:58:05.270 [main] TRACE space.yangtao.mapper.StudentMapper.getByIds - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:58:05.272 [main] TRACE space.yangtao.mapper.StudentMapper.getByIds - <== Row: 2, 2, zhangsan1, 180.0, 男, 1990-01-01, 2022-07-01 21:10:22
21:58:05.272 [main] DEBUG space.yangtao.mapper.StudentMapper.getByIds - <== Total: 2
21:58:05.273 [main] INFO space.yangtao.client.StudentMapperTest - 多个实体对象接收返回值:[StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null), StudentPO(id=2, name=zhangsan1, height=180.0, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=null)]
21:58:05.274 [main] DEBUG space.yangtao.mapper.StudentMapper - Cache Hit Ratio [space.yangtao.mapper.StudentMapper]: 0.0
21:58:05.274 [main] DEBUG space.yangtao.mapper.StudentMapper.getByIds2 - ==> Preparing: select * from tt_student where id in (1, 2)
21:58:05.274 [main] DEBUG space.yangtao.mapper.StudentMapper.getByIds2 - ==> Parameters:
21:58:05.275 [main] TRACE space.yangtao.mapper.StudentMapper.getByIds2 - <== Columns: id, class_id, name, height, gender, birthday, create_time
21:58:05.276 [main] TRACE space.yangtao.mapper.StudentMapper.getByIds2 - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
21:58:05.277 [main] TRACE space.yangtao.mapper.StudentMapper.getByIds2 - <== Row: 2, 2, zhangsan1, 180.0, 男, 1990-01-01, 2022-07-01 21:10:22
21:58:05.277 [main] DEBUG space.yangtao.mapper.StudentMapper.getByIds2 - <== Total: 2
21:58:05.277 [main] INFO space.yangtao.client.StudentMapperTest - 多个Map对象接收返回值:[{birthday=1990-01-01, gender=男, create_time=2022-07-01T22:10:22, class_id=1, name=zhangsan, id=1, height=176.1}, {birthday=1990-01-01, gender=男, create_time=2022-07-01T21:10:22, class_id=2, name=zhangsan1, id=2, height=180.0}]
...

由日志可知,对于多行数据,Java的接口方法可以使用集合类数据结构接收,如List,List的泛型可为实体类或Map。

Map<Object, Map<String, Object>>

在日常中可能会遇到需要将SQL查询的结果封装成Map<Object, Map<String, Object>>这种结构,其中Map的key为指定列的值,value为行。

例如将列id作为查询结果的key,行封装为Map作为value

mapper中定义方法

1
2
3
<select id="getIdMap" resultType="map">
select * from tt_student where id in (${id})
</select>

定义对应的接口方法,此时需要使用@MapKey注解定义返回结果的key值取的列名称(此处为id

1
2
@MapKey("id")
Map<Integer, Map<String, Object>> getIdMap(String ids);

运行测试程序

1
2
3
4
SqlSession session = SqlSessionUtil.getSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
String ids = "1, 2";
log.info("getIdMap:{}", studentMapper.getIdMap(ids));

控制台输出日志如下

1
2
3
4
5
6
7
8
9
...
22:02:38.523 [main] DEBUG space.yangtao.mapper.StudentMapper.getIdMap - ==> Preparing: select * from tt_student where id in (1, 2)
22:02:38.544 [main] DEBUG space.yangtao.mapper.StudentMapper.getIdMap - ==> Parameters:
22:02:38.565 [main] TRACE space.yangtao.mapper.StudentMapper.getIdMap - <== Columns: id, class_id, name, height, gender, birthday, create_time
22:02:38.566 [main] TRACE space.yangtao.mapper.StudentMapper.getIdMap - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
22:02:38.569 [main] TRACE space.yangtao.mapper.StudentMapper.getIdMap - <== Row: 2, 2, zhangsan1, 180.0, 男, 1990-01-01, 2022-07-01 21:10:22
22:02:38.569 [main] DEBUG space.yangtao.mapper.StudentMapper.getIdMap - <== Total: 2
22:02:38.571 [main] INFO space.yangtao.client.StudentMapperTest - getIdMap:{1={birthday=1990-01-01, gender=男, create_time=2022-07-01T22:10:22, class_id=1, name=zhangsan, id=1, height=176.1}, 2={birthday=1990-01-01, gender=男, create_time=2022-07-01T21:10:22, class_id=2, name=zhangsan1, id=2, height=180.0}}
...

由日志可知,@MapKey注解可以将查询的结果封装成Map<Object, Map<String, Object>>结构对象。

resultMap

前面提到过,如果数据库中的列名与实体对象中的字段名对应不上,可以在Mapper文件中为SQL字段取别名。除此之外,还可以通过resultMap进行结果映射

在Mapper中定义resultMap以及对应的查询方法,并在标签resultMap中将数据库字段create_time映射到了实体类的createTime属性

1
2
3
4
5
6
7
8
9
10
11
12
13
<resultMap id="studentRm" type="space.yangtao.domain.po.StudentPO">
<!-- 主键id配置,可以提高效率 -->
<id column="id" property="id"/>
<!-- result配置,column为数据库字段名,property为实体类属性名,两者如果一致则可以省略 -->
<result column="name" property="name"/>
<result column="height" property="height"/>
<result column="gender" property="gender"/>
<result column="birthday" property="birthday"/>
<result column="create_time" property="createTime"/>
</resultMap>
<select id="getByResultMap" resultMap="studentRm">
select * from tt_student where id = #{id}
</select>

定义对应的接口方法

1
StudentPO getByResultMap(Long id);

运行测试程序

1
2
3
SqlSession session = SqlSessionUtil.getSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
log.info("resultMap查询:{}", studentMapper.getByResultMap(1L));

控制台输出日志如下

1
2
3
4
5
6
7
8
...
22:19:35.024 [main] DEBUG space.yangtao.mapper.StudentMapper.getByResultMap - ==> Preparing: select * from tt_student where id = ?
22:19:35.046 [main] DEBUG space.yangtao.mapper.StudentMapper.getByResultMap - ==> Parameters: 1(Long)
22:19:35.059 [main] TRACE space.yangtao.mapper.StudentMapper.getByResultMap - <== Columns: id, class_id, name, height, gender, birthday, create_time
22:19:35.062 [main] TRACE space.yangtao.mapper.StudentMapper.getByResultMap - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
22:19:35.063 [main] DEBUG space.yangtao.mapper.StudentMapper.getByResultMap - <== Total: 1
22:19:35.064 [main] INFO space.yangtao.client.StudentMapperTest - resultMap查询:StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=Fri Jul 01 22:10:22 CST 2022)
...

由日志可知,方法的返回结果createTime字段正确取值为SQL中create_time字段的值,resultMap已成功为两个字段进行映射。

驼峰命名自动映射

如果MyBatis开启了驼峰命名自动自动映射、且实体类属性符合驼峰命名规范、SQL字段符合单词小写、单词之间采用下划线分割,那么Java实体类中的属性能与SQL的字段进行映射。

全局配置文件mybatis-config.xmlsetting标签中开启命名自动映射

1
2
3
4
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<.../>
</settings>

Mapper中对应的SQL为

1
2
3
<select id="getById" resultType="space.yangtao.domain.po.StudentPO" parameterType="java.lang.Long">
select * from tt_student where id = #{id}
</select>

对应的接口方法为

1
StudentPO getById(Long id);

此时实体类属性createTime与SQL字段create_time符合驼峰命名自动映射条件

运行测试程序

1
2
3
SqlSession session = SqlSessionUtil.getSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
log.info("自动映射查询:{}", studentMapper.getById(1L));

控制台输出日志如下

1
2
3
4
5
6
7
8
...
22:27:43.693 [main] DEBUG space.yangtao.mapper.StudentMapper.getById - ==> Preparing: select * from tt_student where id = ?
22:27:43.710 [main] DEBUG space.yangtao.mapper.StudentMapper.getById - ==> Parameters: 1(Long)
22:27:43.722 [main] TRACE space.yangtao.mapper.StudentMapper.getById - <== Columns: id, class_id, name, height, gender, birthday, create_time
22:27:43.723 [main] TRACE space.yangtao.mapper.StudentMapper.getById - <== Row: 1, 1, zhangsan, 176.1, 男, 1990-01-01, 2022-07-01 22:10:22
22:27:43.725 [main] DEBUG space.yangtao.mapper.StudentMapper.getById - <== Total: 1
22:27:43.726 [main] INFO space.yangtao.client.StudentMapperTest - 自动映射查询:StudentPO(id=1, name=zhangsan, height=176.1, gender=男, birthday=Mon Jan 01 00:00:00 CST 1990, createTime=Fri Jul 01 22:10:22 CST 2022)
...

由日志可知,驼峰命名自动映射已为实体属性createTime与数据库字段create_time进行了映射。

总结

MyBatis面向接口编程不仅提高了开发效率,也提升了代码的可维护性和可扩展性。通过将SQL操作封装在接口中,MyBatis使得SQL语句的管理变得更加集中和系统化,极大地降低了数据库操作的复杂性。本文详细说明了如何有效地利用MyBatis提供的这一功能,是理解和应用MyBatis核心功能的重要资源。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!