MyBatis学习知识点大全(三)

简介: 教程来源 https://app-abggx9rbr6dd.appmiaoda.com 本文系统讲解MyBatis与Spring集成(XML/Boot配置、事务管理、SqlSessionTemplate)、PageHelper分页插件(基础用法与自定义实现)、代码生成器(MBG配置与手写生成器),以及配置优化、SQL性能调优和N+1等问题解决方案,助力高效开发。

七、Spring 集成

7.1 配置集成
Maven 依赖:

xml
<dependencies>
    <!-- MyBatis Spring 集成 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>3.0.3</version>
    </dependency>

    <!-- Spring 相关 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.30</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.30</version>
    </dependency>

    <!-- 连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.20</version>
    </dependency>
</dependencies>

Spring XML 配置:

xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
       xsi:schemaLocation="...">

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 配置 SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <property name="mapperLocations" value="classpath:mapper/**/*.xml"/>
        <property name="typeAliasesPackage" value="com.example.entity"/>
    </bean>

    <!-- 配置 Mapper 扫描 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.example.mapper"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" 
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 开启注解事务 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

Spring Boot 配置:

yaml
# application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis_demo
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      initial-size: 5
      max-active: 20
      min-idle: 5

mybatis:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.example.entity
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: true
    lazy-loading-enabled: true
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

Spring Boot 配置类:

java
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);

        // 配置 MyBatis 设置
        org.apache.ibatis.session.Configuration configuration = 
            new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(true);
        configuration.setLazyLoadingEnabled(true);
        sessionFactory.setConfiguration(configuration);

        // 设置类型别名包
        sessionFactory.setTypeAliasesPackage("com.example.entity");

        // 设置 mapper 位置
        sessionFactory.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/**/*.xml"));

        return sessionFactory.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

7.2 事务管理

java
@Service
@Transactional
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private LogMapper logMapper;

    // 事务传播行为
    @Transactional(propagation = Propagation.REQUIRED)
    public void createUser(User user) {
        userMapper.insert(user);
        logMapper.insertLog("创建用户: " + user.getUsername());

        // 手动回滚
        if (user.getAge() < 0) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }

    // 只读事务
    @Transactional(readOnly = true)
    public User getUser(Long id) {
        return userMapper.selectById(id);
    }

    // 指定回滚异常
    @Transactional(rollbackFor = Exception.class, noRollbackFor = IllegalArgumentException.class)
    public void updateUser(User user) {
        userMapper.update(user);
        if (user.getAge() > 150) {
            throw new IllegalArgumentException("年龄无效");
        }
    }

    // 事务超时
    @Transactional(timeout = 30)
    public void batchInsert(List<User> users) {
        for (User user : users) {
            userMapper.insert(user);
        }
    }

    // 事务隔离级别
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public User getUserWithLock(Long id) {
        return userMapper.selectForUpdate(id);
    }
}

7.3 使用 SqlSessionTemplate

java
@Repository
public class UserDao {

    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;

    public User selectById(Long id) {
        return sqlSessionTemplate.selectOne("com.example.mapper.UserMapper.selectById", id);
    }

    public List<User> selectAll() {
        return sqlSessionTemplate.selectList("com.example.mapper.UserMapper.selectAll");
    }

    public void insert(User user) {
        sqlSessionTemplate.insert("com.example.mapper.UserMapper.insert", user);
    }

    public void update(User user) {
        sqlSessionTemplate.update("com.example.mapper.UserMapper.update", user);
    }

    public void delete(Long id) {
        sqlSessionTemplate.delete("com.example.mapper.UserMapper.deleteById", id);
    }

    // 批量操作
    public void batchInsert(List<User> users) {
        sqlSessionTemplate.execute(new SqlSessionCallback<Void>() {
            @Override
            public Void doInSqlSession(SqlSession session) {
                UserMapper mapper = session.getMapper(UserMapper.class);
                for (User user : users) {
                    mapper.insert(user);
                }
                return null;
            }
        });
    }
}

八、分页插件

8.1 PageHelper 使用
Maven 依赖:

xml
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.6</version>
</dependency>

配置 PageHelper:

yaml
# application.yml
pagehelper:
  helper-dialect: mysql
  reasonable: true
  support-methods-arguments: true
  params: count=countSql

使用示例:

java
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    // 基础分页
    public PageInfo<User> getUsersByPage(int pageNum, int pageSize) {
        // 在查询前设置分页参数
        PageHelper.startPage(pageNum, pageSize);

        // 执行查询
        List<User> users = userMapper.selectAll();

        // 包装为 PageInfo
        return new PageInfo<>(users);
    }

    // 带条件的分页
    public PageInfo<User> searchUsers(String username, Integer age, 
                                       int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<User> users = userMapper.selectByCondition(username, age);
        return new PageInfo<>(users);
    }

    // 排序分页
    public PageInfo<User> getUsersOrderByAge(int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize, "age desc, id asc");
        List<User> users = userMapper.selectAll();
        return new PageInfo<>(users);
    }

    // 获取分页信息
    public void testPageInfo() {
        PageInfo<User> pageInfo = getUsersByPage(1, 10);

        System.out.println("当前页: " + pageInfo.getPageNum());
        System.out.println("每页大小: " + pageInfo.getPageSize());
        System.out.println("总记录数: " + pageInfo.getTotal());
        System.out.println("总页数: " + pageInfo.getPages());
        System.out.println("第一页: " + pageInfo.getFirstPage());
        System.out.println("最后一页: " + pageInfo.getLastPage());
        System.out.println("是否有上一页: " + pageInfo.isHasPreviousPage());
        System.out.println("是否有下一页: " + pageInfo.isHasNextPage());

        // 获取数据
        List<User> users = pageInfo.getList();
    }

    // 自定义分页参数
    public PageInfo<User> getUsersWithCustomPage(PageParam pageParam) {
        PageHelper.startPage(pageParam.getPage(), pageParam.getSize());
        List<User> users = userMapper.selectByCondition(
            pageParam.getUsername(), pageParam.getAge());
        return new PageInfo<>(users);
    }

    // 使用 RowBounds 分页
    public List<User> getUsersWithRowBounds(int offset, int limit) {
        RowBounds rowBounds = new RowBounds(offset, limit);
        return userMapper.selectByRowBounds(rowBounds);
    }
}

8.2 自定义分页插件

java
@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
public class CustomPageInterceptor implements Interceptor {

    private String dialect = "mysql";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];

        // 判断是否需要分页
        if (parameter instanceof Page) {
            Page page = (Page) parameter;

            // 获取原始 SQL
            BoundSql boundSql = ms.getBoundSql(parameter);
            String sql = boundSql.getSql();

            // 查询总记录数
            String countSql = getCountSql(sql);
            Long total = getTotal(ms, parameter, countSql);
            page.setTotal(total);

            // 添加分页 SQL
            String pageSql = getPageSql(sql, page);

            // 创建新的 BoundSql
            BoundSql newBoundSql = new BoundSql(
                ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

            // 创建新的 MappedStatement
            MappedStatement newMs = copyMappedStatement(ms, newBoundSql);
            args[0] = newMs;
        }

        return invocation.proceed();
    }

    private String getCountSql(String sql) {
        return "SELECT COUNT(*) FROM (" + sql + ") temp_count";
    }

    private Long getTotal(MappedStatement ms, Object parameter, String countSql) {
        // 执行 count 查询
        // 实现省略
        return 0L;
    }

    private String getPageSql(String sql, Page page) {
        if ("mysql".equals(dialect)) {
            return sql + " LIMIT " + page.getOffset() + "," + page.getLimit();
        } else if ("oracle".equals(dialect)) {
            return "SELECT * FROM (SELECT t.*, ROWNUM rn FROM (" + sql + 
                   ") t WHERE ROWNUM <= " + (page.getOffset() + page.getLimit()) + 
                   ") WHERE rn > " + page.getOffset();
        }
        return sql;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.dialect = properties.getProperty("dialect", "mysql");
    }
}

九、代码生成器

9.1 MyBatis Generator 使用
Maven 插件配置:

xml
<build>
    <plugins>
        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.4.2</version>
            <configuration>
                <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
                <overwrite>true</overwrite>
                <verbose>true</verbose>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>8.0.33</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

generatorConfig.xml:

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
  PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
  "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <context id="MySqlContext" targetRuntime="MyBatis3" defaultModelType="flat">

        <!-- 注释生成器 -->
        <commentGenerator>
            <property name="suppressDate" value="true"/>
            <property name="suppressAllComments" value="false"/>
            <property name="addRemarkComments" value="true"/>
        </commentGenerator>

        <!-- 数据库连接 -->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/mybatis_demo"
                        userId="root"
                        password="123456">
            <property name="useSSL" value="false"/>
        </jdbcConnection>

        <!-- Java类型解析器 -->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
            <property name="useJSR310Types" value="true"/>
        </javaTypeResolver>

        <!-- 实体类配置 -->
        <javaModelGenerator targetPackage="com.example.entity" 
                            targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="true"/>
            <property name="immutable" value="false"/>
        </javaModelGenerator>

        <!-- Mapper XML 配置 -->
        <sqlMapGenerator targetPackage="mapper" 
                         targetProject="src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!-- Mapper 接口配置 -->
        <javaClientGenerator type="XMLMAPPER" 
                             targetPackage="com.example.mapper"
                             targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!-- 表配置 -->
        <table tableName="user" domainObjectName="User"
               enableCountByExample="true"
               enableDeleteByExample="true"
               enableSelectByExample="true"
               enableUpdateByExample="true">
            <generatedKey column="id" sqlStatement="MySql" identity="true"/>
            <columnOverride column="create_time" javaType="java.time.LocalDateTime"/>
            <columnOverride column="update_time" javaType="java.time.LocalDateTime"/>
        </table>

        <table tableName="department" domainObjectName="Department">
            <generatedKey column="id" sqlStatement="MySql" identity="true"/>
        </table>

    </context>
</generatorConfiguration>

9.2 自定义代码生成器

java
public class CustomCodeGenerator {

    public static void main(String[] args) throws Exception {
        // 配置信息
        String jdbcUrl = "jdbc:mysql://localhost:3306/mybatis_demo";
        String username = "root";
        String password = "123456";
        String packageName = "com.example";
        String baseDir = "src/main/java";

        // 创建连接
        Connection conn = DriverManager.getConnection(jdbcUrl, username, password);
        DatabaseMetaData metaData = conn.getMetaData();

        // 获取所有表
        ResultSet tables = metaData.getTables(null, null, "%", new String[]{"TABLE"});

        while (tables.next()) {
            String tableName = tables.getString("TABLE_NAME");
            String className = toCamelCase(tableName, true);

            // 生成实体类
            generateEntity(conn, tableName, className, packageName, baseDir);

            // 生成 Mapper 接口
            generateMapper(className, packageName, baseDir);

            // 生成 XML 映射文件
            generateXml(conn, tableName, className, packageName, baseDir);
        }

        conn.close();
    }

    private static void generateEntity(Connection conn, String tableName, 
                                        String className, String packageName, 
                                        String baseDir) throws Exception {
        // 获取列信息
        ResultSet columns = conn.getMetaData().getColumns(null, null, tableName, null);

        StringBuilder sb = new StringBuilder();
        sb.append("package ").append(packageName).append(".entity;\n\n");
        sb.append("import java.io.Serializable;\n");
        sb.append("import java.time.LocalDateTime;\n\n");
        sb.append("public class ").append(className).append(" implements Serializable {\n\n");
        sb.append("    private static final long serialVersionUID = 1L;\n\n");

        // 生成字段
        List<ColumnInfo> columnInfos = new ArrayList<>();
        while (columns.next()) {
            String columnName = columns.getString("COLUMN_NAME");
            String columnType = columns.getString("TYPE_NAME");
            String propertyName = toCamelCase(columnName, false);
            String javaType = toJavaType(columnType);

            columnInfos.add(new ColumnInfo(columnName, propertyName, javaType));
            sb.append("    private ").append(javaType).append(" ").append(propertyName).append(";\n");
        }

        // 生成 getter/setter
        for (ColumnInfo col : columnInfos) {
            sb.append("\n    public ").append(col.javaType).append(" get")
              .append(toCamelCase(col.propertyName, true)).append("() {\n");
            sb.append("        return ").append(col.propertyName).append(";\n");
            sb.append("    }\n\n");

            sb.append("    public void set").append(toCamelCase(col.propertyName, true))
              .append("(").append(col.javaType).append(" ").append(col.propertyName).append(") {\n");
            sb.append("        this.").append(col.propertyName).append(" = ").append(col.propertyName).append(";\n");
            sb.append("    }\n");
        }

        sb.append("}");

        // 写入文件
        String filePath = baseDir + "/" + packageName.replace(".", "/") + 
                          "/entity/" + className + ".java";
        Files.write(Paths.get(filePath), sb.toString().getBytes());
    }

    private static void generateMapper(String className, String packageName, 
                                        String baseDir) throws Exception {
        StringBuilder sb = new StringBuilder();
        sb.append("package ").append(packageName).append(".mapper;\n\n");
        sb.append("import ").append(packageName).append(".entity.").append(className).append(";\n");
        sb.append("import org.apache.ibatis.annotations.Mapper;\n");
        sb.append("import org.apache.ibatis.annotations.Param;\n");
        sb.append("import java.util.List;\n\n");

        sb.append("@Mapper\n");
        sb.append("public interface ").append(className).append("Mapper {\n\n");
        sb.append("    ").append(className).append(" selectById(@Param(\"id\") Long id);\n\n");
        sb.append("    List<").append(className).append("> selectAll();\n\n");
        sb.append("    int insert(").append(className).append(" ").append(toCamelCase(className, false)).append(");\n\n");
        sb.append("    int update(").append(className).append(" ").append(toCamelCase(className, false)).append(");\n\n");
        sb.append("    int deleteById(@Param(\"id\") Long id);\n");
        sb.append("}");

        String filePath = baseDir + "/" + packageName.replace(".", "/") + 
                          "/mapper/" + className + "Mapper.java";
        Files.write(Paths.get(filePath), sb.toString().getBytes());
    }

    private static String toCamelCase(String name, boolean capitalize) {
        StringBuilder result = new StringBuilder();
        boolean upper = capitalize;
        for (char c : name.toLowerCase().toCharArray()) {
            if (c == '_') {
                upper = true;
            } else if (upper) {
                result.append(Character.toUpperCase(c));
                upper = false;
            } else {
                result.append(c);
            }
        }
        return result.toString();
    }

    private static String toJavaType(String dbType) {
        switch (dbType.toUpperCase()) {
            case "INT":
            case "INTEGER":
            case "SMALLINT":
            case "TINYINT":
                return "Integer";
            case "BIGINT":
                return "Long";
            case "VARCHAR":
            case "CHAR":
            case "TEXT":
                return "String";
            case "DATE":
            case "TIMESTAMP":
            case "DATETIME":
                return "LocalDateTime";
            case "DECIMAL":
            case "NUMERIC":
                return "java.math.BigDecimal";
            default:
                return "Object";
        }
    }

    static class ColumnInfo {
        String columnName;
        String propertyName;
        String javaType;

        ColumnInfo(String columnName, String propertyName, String javaType) {
            this.columnName = columnName;
            this.propertyName = propertyName;
            this.javaType = javaType;
        }
    }
}

十、最佳实践与优化

10.1 配置优化

xml
<configuration>
    <settings>
        <!-- 开启驼峰命名自动映射 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>

        <!-- 开启延迟加载 -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>

        <!-- 开启二级缓存 -->
        <setting name="cacheEnabled" value="true"/>

        <!-- 设置默认执行器类型 -->
        <setting name="defaultExecutorType" value="REUSE"/>

        <!-- 设置超时时间 -->
        <setting name="defaultStatementTimeout" value="30"/>

        <!-- 设置获取数据库自增主键值 -->
        <setting name="useGeneratedKeys" value="true"/>

        <!-- 开启日志 -->
        <setting name="logImpl" value="SLF4J"/>

        <!-- 开启自动映射未知列 -->
        <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>

        <!-- 开启局部缓存 -->
        <setting name="localCacheScope" value="SESSION"/>

        <!-- 开启延迟加载触发方法 -->
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
    </settings>
</configuration>

10.2 SQL 性能优化

xml
<!-- 使用 ResultMap 代替 resultType -->
<resultMap id="optimizedMap" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
</resultMap>

<!-- 只查询需要的字段 -->
<select id="selectBasicInfo" resultMap="optimizedMap">
    SELECT id, username FROM user WHERE id = #{id}
</select>

<!-- 使用 bind 避免 SQL 注入 -->
<select id="selectByUsername" resultType="User">
    <bind name="pattern" value="'%' + username + '%'"/>
    SELECT * FROM user WHERE username LIKE #{pattern}
</select>

<!-- 批量操作使用 foreach -->
<insert id="batchInsertOptimized">
    INSERT INTO user (username, age) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.username}, #{user.age})
    </foreach>
</insert>

<!-- 使用动态 SQL 避免无效查询 -->
<select id="selectDynamic" resultType="User">
    SELECT * FROM user
    <where>
        <if test="username != null and username != ''">
            username = #{username}
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
    <if test="orderBy != null">
        ORDER BY ${orderBy}
    </if>
</select>

10.3 常见问题与解决方案

java
// 1. N+1 问题解决
@Select("SELECT * FROM user")
@Results({
    @Result(property = "orders", column = "id",
            many = @Many(select = "com.example.mapper.OrderMapper.selectByUserId",
                         fetchType = FetchType.LAZY))
})
List<User> selectAllWithOrders();

// 使用嵌套结果代替嵌套查询
<resultMap id="userWithOrdersResult" type="User">
    <id property="id" column="id"/>
    <collection property="orders" ofType="Order" resultMap="orderResultMap"/>
</resultMap>

// 2. 批量操作性能优化
public void batchInsertOptimized(List<User> users) {
    try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        for (User user : users) {
            mapper.insert(user);
        }
        session.commit();
    }
}

// 3. 防止 SQL 注入
// 使用 #{} 而不是 ${}
// 使用 bind 标签
// 使用参数校验

// 4. 事务管理
@Transactional(rollbackFor = Exception.class)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
    User from = userMapper.selectForUpdate(fromId);
    User to = userMapper.selectForUpdate(toId);

    if (from.getBalance().compareTo(amount) < 0) {
        throw new BusinessException("余额不足");
    }

    from.setBalance(from.getBalance().subtract(amount));
    to.setBalance(to.getBalance().add(amount));

    userMapper.update(from);
    userMapper.update(to);
}

MyBatis 的世界简洁而强大,愿本文成为你持久层开发之路上的重要指南。持续学习,深入实践,你一定能成为 MyBatis 专家!
来源:
https://app-abggx9rbr6dd.appmiaoda.com

相关文章
|
1月前
|
存储 人工智能 安全
|
4月前
|
数据采集 安全 网络安全
高质量代理IP:守护隐私、抵御攻击的关键助力
在数字化快速发展的今天,网络安全已成为个人、企业乃至各类组织不可忽视的核心议题,而高质量代理IP在其中扮演着不可或缺的关键角色。它并非简单的“IP替换工具”,而是守护网络隐私、抵御恶意攻击、防范数据泄露的重要屏障。相较于普通代理IP,高质量代理IP凭借稳定性、安全性和可靠性的优势,能有效规避网络风险,为各类在线活动保驾护航,是构建安全网络策略的重要组成部分。
|
4月前
|
人工智能 弹性计算 Ubuntu
OpenClaw+Ollama v0.18.1 联网搜索全攻略:阿里云+本地三系统部署+千问/Coding Plan配置及问题排查
2026年,本地AI智能体已进入实用化阶段,OpenClaw(原Clawdbot/Moltbot)凭借强大的插件生态与多渠道接入能力,成为主流开源AI助手框架。Ollama v0.18.1版本带来工具调用优化、云模型无缝直连、结构化输出等核心升级,搭配@ollama/openclaw-web-search官方联网插件,可让OpenClaw突破本地模型知识截止日期限制,实现实时联网检索、信息整理、数据追踪等能力。本文基于最新版本,完整覆盖Windows11/MacOS/Linux本地部署、阿里云ECS云端部署、Ollama v0.18.1对接、联网搜索插件配置、阿里云千问API与免费Coding
3097 19
|
4月前
|
JSON 前端开发 数据可视化
让API开口说话,Swagger如何把枯燥代码变成“活文档”
好的API文档不是写出来的,是“生成”出来的。Swagger就是你的API的专属化妆师+摄影师+经纪人,让你的接口从“代码宅男”变身“流量明星”!
204 6
|
3月前
|
SQL 机器学习/深度学习 自然语言处理
运营日报自动化:智能问数如何实现“开口即得”?
截至2026年4月初,智能问数技术在运营日报自动化场景中已形成多元实现路径。部分方案依赖预置宽表与指标层,通过自然语言匹配固定查询模板,适合结构稳定、问题明确的“开卷考试”式场景;另一些则基于动态Text2SQL或语义本体建模,试图应对更开放的跨域提问,但对数据治理和语义一致性要求较高。不同路线在前期建设成本、后期扩展性及准确率上各有权衡:前者上线快、维护简单,后者泛化能力强但需持续投入知识治理。实践中,企业往往根据自身数据成熟度与业务复杂度选择适配方案,并非单一技术可通解所有“开口即得”需求。
|
3月前
|
弹性计算 安全 搜索推荐
企业用户注册阿里云账号流程:所需资料、注册及实名步骤及注意事项
企业用户注册阿里云账号需先完成企业实名认证,享受更高购买限额、专属优惠及技术支持。认证需准备营业执照、法人身份证等资料,通过支付宝、银行卡或扫脸等方式完成。注册流程包括访问官网、选择注册方式、完成注册、进行企业实名认证及绑定支付方式。注意事项包括确保信息一致、资料真实有效、关注时效性,并妥善保管账号信息。完成认证后,企业用户可领取专享代金券,享受性价比更高的云服务配置。
|
4月前
|
Java 应用服务中间件 Linux
CentOS 7 使用 apache-tomcat-7.0.27.tar.gz 详细步骤(解压、配置、启动、测试)
本指南详解Tomcat 7.0.27在Linux下的完整部署流程:先安装适配的JDK 8环境,再下载解压Tomcat包;通过startup.sh启动服务,浏览器访问8080端口验证;支持WAR包自动部署至webapps目录,并提供shutdown.sh关闭、防火墙配置及常见问题(如JAVA_HOME未定义、启动慢)解决方案,适合老项目维护与学习。
|
3月前
|
Prometheus Kubernetes 并行计算
智能驾驶感知环境容器镜像预检记录
本文介绍智能驾驶感知环境部署前的镜像预检实践:针对CUDA、ROS2、PyTorch、Prometheus、K8s等多源异构镜像,通过Docker Compose预拉取与验证,隔离环境问题与算法问题;并延伸至K8s节点预拉镜像,规避ImagePullBackOff故障,提升部署可靠性与复用性。(239字)
|
3月前
|
自然语言处理 数据可视化 数据挖掘
大模型应用:从数据分布到智能解读:KDE 与大模型的深度融合实践.96
本文详解KDE与大模型融合的数据分析新范式:KDE精准刻画数据分布规律(峰值、偏态、异常值等),大模型将其转化为通俗易懂、可落地的业务洞察,实现“数据→分布→决策”闭环。兼顾技术严谨性与业务可解释性。
325 3