第三天

mapper层的Options注解有什么用?

在 MyBatis 中,@Options 注解是 mapper 层非常常用的注解,主要作用是为 SQL 操作配置额外的执行选项 ,比如主键生成策略、缓存控制、批量操作等,替代传统 XML 映射文件中的 <selectKey>useGeneratedKeys 等配置,让注解式开发更完整。

一、核心作用与常用配置项

先通过通俗的方式解释核心配置项,再结合代码示例说明:

表格

配置项 作用 适用场景
useGeneratedKeys 是否使用数据库自动生成的主键(如 MySQL 的自增 ID) 插入操作(INSERT)
keyProperty 映射生成主键的实体类属性名 配合 useGeneratedKeys 使用
keyColumn 数据库表中自增主键的列名(属性名和列名不一致时需要指定) 同上
flushCache 是否刷新一级 / 二级缓存(默认 INSERT/UPDATE/DELETE 为 true,SELECT 为 false) 要求实时获取最新数据的查询
timeout 设置 SQL 执行超时时间(单位:秒) 防止慢 SQL 占用资源
fetchSize 设置 JDBC 批量获取数据的行数 大批量查询
statementType 指定执行 SQL 的 Statement 类型(STATEMENT/PREPARED/CALLABLE) 调用存储过程(CALLABLE)

二、代码示例(最常用场景)

场景 1:插入数据时获取自增主键(MySQL)

这是 @Options 最核心的使用场景,插入数据后自动把数据库生成的自增 ID 回填到实体类中。

java

运行

java 复制代码
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;
import org.springframework.stereotype.Repository;
import com.example.entity.User;

@Repository
public interface UserMapper {

    // 插入用户,并获取自增主键
    @Insert("INSERT INTO user (username, password, age) VALUES (#{username}, #{password}, #{age})")
    @Options(
        useGeneratedKeys = true,  // 开启自增主键获取
        keyProperty = "id",       // 实体类中主键属性名(User类的id字段)
        keyColumn = "id"          // 数据库表中主键列名(可选,属性名和列名一致时可省略)
    )
    int insertUser(User user);
}

使用效果

java

运行

java 复制代码
// 测试代码
User user = new User();
user.setUsername("张三");
user.setPassword("123456");
user.setAge(20);

userMapper.insertUser(user);
System.out.println(user.getId()); // 输出数据库自动生成的自增ID(如 1)

场景 2:查询时强制刷新缓存

默认情况下,SELECT 操作会使用缓存,若需要实时获取最新数据,可强制刷新缓存:

java

运行

java 复制代码
@Select("SELECT * FROM user WHERE id = #{id}")
@Options(flushCache = Options.FlushCachePolicy.TRUE) // 每次查询都刷新缓存
User selectUserById(Long id);

三、注意事项

  1. useGeneratedKeys 仅适用于支持自增主键的数据库 (如 MySQL、PostgreSQL),Oracle 需使用序列 +@SelectKey 注解。
  2. keyProperty 必须是实体类的属性名 (不是数据库列名),若属性名和列名不一致,需配合 keyColumn
  3. 注解式 @Options 优先级高于 XML 配置,若两者同时配置,以注解为准。

总结

  1. @Options 是 MyBatis 注解式开发的补充,核心用于配置 SQL 执行的额外选项,替代 XML 中的相关配置。
  2. 最常用场景是插入操作时获取数据库自增主键useGeneratedKeys + keyProperty)。
  3. 其他常用配置包括缓存控制(flushCache)、超时时间(timeout)等,可根据业务场景灵活使用

可以设计VO,当前端展示的属性和实体类属性有差异时

注意一下前端传递参数的问题

当遇到前端传递的参数是数组时,可以用@RequestParam接收

在遇到多行增删改查数据库时,尽量不要在java中使用for循环

一、为什么 Java 中多行数据库操作不建议用 for 循环?

在 Java 中通过 for 循环逐行执行增删改查(比如批量插入 1000 条数据,循环调用insert语句),核心问题集中在性能、资源消耗、数据库压力三个维度,具体原因如下:

1. 性能极低:频繁的网络 / 数据库交互

  • 每次循环执行 SQL 时,都会建立一次数据库连接(若未用连接池)或从连接池获取连接→发送 SQL→数据库解析执行→返回结果→释放连接,这个过程有大量网络 IO 开销数据库解析 SQL 的开销
  • 举例:循环插入 1000 条数据,会产生 1000 次网络交互 + 1000 次 SQL 解析;而批量操作仅需 1-2 次交互,性能差距可能达几十倍甚至上百倍。

2. 数据库压力大:锁 / 事务开销叠加

  • 若循环内未统一管理事务(比如每次循环都提交事务),会产生大量小事务,数据库需要频繁刷写日志(redo log/undo log)、释放锁,增加数据库的事务管理成本。
  • 频繁的单行操作还可能导致数据库锁竞争加剧(比如多行更新时逐行加锁),引发死锁或性能瓶颈。

3. 资源浪费:连接池 / 线程资源耗尽

  • 若循环速度快、数据量大,会持续占用连接池中的连接,导致其他业务无法获取连接(连接池耗尽);同时循环阻塞线程,消耗 JVM 线程资源。

4. 代码冗余且易出错

  • 循环内需要重复处理异常、连接管理,代码冗余;若循环中某一条数据执行失败,还需手动处理 "部分成功、部分失败" 的回滚逻辑,容易出现数据不一致

二、解决多行增删改查的最优方案

针对不同数据库操作场景,推荐以下批量处理方案,核心思路是 "减少交互次数、统一事务管理":

场景 1:批量插入 / 更新(最常见)

方案 1:JDBC 原生批量操作(addBatch() + executeBatch()

JDBC 提供了批量执行 SQL 的 API,将多条 SQL 打包成一批发送给数据库,仅需一次网络交互,是最基础且高效的方案。

java

运行

复制代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JdbcBatchExample {
    private static final String URL = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";

    public static void batchInsert() {
        Connection conn = null;
        PreparedStatement pstmt = null;
        try {
            // 1. 获取连接(实际项目中用连接池,如Druid/HikariCP)
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            // 关闭自动提交,统一事务
            conn.setAutoCommit(false);

            // 2. 预编译SQL(仅解析一次,复用)
            String sql = "INSERT INTO user (name, age) VALUES (?, ?)";
            pstmt = conn.prepareStatement(sql);

            // 3. 批量添加参数
            for (int i = 0; i < 1000; i++) {
                pstmt.setString(1, "用户" + i);
                pstmt.setInt(2, 20 + i);
                // 添加到批处理队列(仅内存操作,未发送到数据库)
                pstmt.addBatch();

                // 可选:每100条执行一次(避免批处理队列过大)
                if (i % 100 == 0) {
                    pstmt.executeBatch(); // 执行批处理
                    pstmt.clearBatch();   // 清空队列
                }
            }

            // 4. 执行剩余的批处理
            pstmt.executeBatch();
            // 统一提交事务
            conn.commit();

            System.out.println("批量插入成功");
        } catch (SQLException e) {
            // 异常时回滚事务
            if (conn != null) {
                try {
                    conn.rollback();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
            e.printStackTrace();
        } finally {
            // 5. 关闭资源
            try {
                if (pstmt != null) pstmt.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        batchInsert();
    }
}

关键说明

  • addBatch():将 SQL 参数添加到本地批处理队列,不立即执行;
  • executeBatch():将队列中的所有 SQL 一次性发送到数据库执行;
  • 分批次执行(如每 100 条):避免内存中积压过多参数,防止 OOM。
方案 2:数据库原生批量语法(如 MySQL 的INSERT ... VALUES (),(),()

部分数据库支持多行合并的 SQL 语法,只需一条 SQL 即可插入多行,性能接近 JDBC 批量操作,适合中小批量数据:

java

运行

复制代码
// 示例:MySQL批量插入语法
String sql = "INSERT INTO user (name, age) VALUES (?, ?), (?, ?), (?, ?)";
// 批量设置参数:用户0(20)、用户1(21)、用户2(22)
pstmt.setString(1, "用户0");
pstmt.setInt(2, 20);
pstmt.setString(3, "用户1");
pstmt.setInt(4, 21);
pstmt.setString(5, "用户2");
pstmt.setInt(6, 22);
pstmt.executeUpdate();

注意 :需控制单条 SQL 长度(MySQL 默认max_allowed_packet限制),避免 SQL 过长导致执行失败。

方案 3:ORM 框架批量操作(MyBatis/MyBatis-Plus)

实际项目中常用 ORM 框架,其封装了更易用的批量操作 API:

MyBatis 批量插入示例(Mapper.xml):

xml

复制代码
<!-- UserMapper.xml -->
<insert id="batchInsert">
    INSERT INTO user (name, age)
    VALUES
    <foreach collection="list" item="item" separator=",">
        (#{item.name}, #{item.age})
    </foreach>
</insert>
MyBatis-Plus 批量插入示例:

java

运行

复制代码
// 直接调用内置方法,底层封装了JDBC批量操作
List<User> userList = new ArrayList<>();
// 填充1000条用户数据
for (int i = 0; i < 1000; i++) {
    userList.add(new User("用户" + i, 20 + i));
}
// 批量插入
userMapper.insertBatchSomeColumn(userList);

场景 2:批量删除 / 更新

方案 1:IN 条件 + 批量参数(适合少量 ID)

若批量删除 / 更新的条件是 "ID 列表",可通过IN (?, ?, ?)语法一次性执行:

java

运行

复制代码
// MyBatis批量删除示例(Mapper.xml)
<delete id="batchDelete">
    DELETE FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>

注意:IN 后的参数数量不宜过多(MySQL 建议不超过 1000 个),否则会导致 SQL 解析变慢,可拆分为多个 IN 批次。

方案 2:分批执行 + 事务(适合大量数据)

若需更新 / 删除 10 万 + 条数据,可拆分为多个批次(如每 1000 条一批),在同一个事务中执行,避免单条 SQL 锁表时间过长:

java

运行

复制代码
// 伪代码:批量更新10万条数据,每1000条一批
@Transactional
public void batchUpdate(List<Long> ids) {
    int batchSize = 1000;
    for (int i = 0; i < ids.size(); i += batchSize) {
        int end = Math.min(i + batchSize, ids.size());
        List<Long> batchIds = ids.subList(i, end);
        userMapper.batchUpdate(batchIds); // 执行单批次更新
    }
}

场景 3:批量查询

方案 1:IN 条件查询(替代循环单查)

java

运行

复制代码
// MyBatis批量查询示例
<select id="batchSelect" resultType="User">
    SELECT * FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>
方案 2:分页查询(适合超大量数据)

若需查询百万级数据,避免一次性加载到内存,使用分页查询分批获取:

java

运行

复制代码
// 伪代码:分页查询所有用户,每页1000条
int pageNum = 1;
int pageSize = 1000;
while (true) {
    List<User> userList = userMapper.selectPage(new Page<>(pageNum, pageSize));
    if (userList.isEmpty()) {
        break;
    }
    // 处理当前页数据
    processData(userList);
    pageNum++;
}

三、进阶优化:提升批量操作性能的补充建议

  1. 关闭自动提交 :所有批量操作统一在一个事务中提交(conn.setAutoCommit(false)),减少事务刷盘开销;

  2. 使用连接池:如 HikariCP、Druid,避免频繁创建 / 销毁数据库连接;

  3. 调整数据库参数

    • MySQL:增大rewriteBatchedStatements=true(JDBC URL 中添加),优化批量插入性能;
    • 增大max_allowed_packet,支持更长的批量 SQL;
  4. 避免大事务:批量操作拆分到多个事务(如每 1 万条一个事务),防止事务过大导致锁表或回滚耗时。

遇到改变多表操作时,在方法上加上@Transactional保证数据库数据安全