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月前
|
存储 人工智能 关系型数据库
OpenClaw怎么可能没痛点?用RDS插件来释放OpenClaw全部潜力
OpenClaw插件是深度介入Agent生命周期的扩展机制,提供24个钩子,支持自动注入知识、持久化记忆等被动式干预。相比Skill/Tool,插件可主动在关键节点(如对话开始/结束)执行逻辑,适用于RAG增强、云化记忆等高级场景。
895 56
OpenClaw怎么可能没痛点?用RDS插件来释放OpenClaw全部潜力
|
1月前
|
人工智能 安全 前端开发
阿里开源 Team 版 OpenClaw,5分钟完成本地安装
HiClaw 是 OpenClaw 的升级版,通过引入 Manager Agent 架构和分布式设计,解决了 OpenClaw 在安全性、多任务协作、移动端体验、记忆管理等方面的核心痛点。
1902 60
阿里开源 Team 版 OpenClaw,5分钟完成本地安装
|
10天前
|
人工智能 安全 Linux
OpenClaw 对接阿里云百炼 API:本地 AI 助手快速部署指南
本文详解如何将轻量开源AI助手OpenClaw与阿里云百炼平台快速对接:通过配置兼容OpenAI接口、设置API密钥及模型参数,仅需数步即可本地调用通义千问系列大模型(如qwen-plus、qwen3-max),实现安全、可控、低门槛的智能助手部署。
282 14
|
28天前
|
人工智能 自然语言处理 Linux
保姆级图文教程!手机操控OpenClaw后台全自动运行Blender建模:全流程部署、命令执行与笔记输出实战指南
在AI原生工具链快速普及的今天,专业软件的使用方式正在发生结构性变化——从手动操作图形界面,转向AI通过命令行直接驱动软件内核。OpenClaw(Clawdbot)结合香港大学开源项目CLI-Anything,可实现对Blender的无界面(后台)自动化建模,无需打开图形窗口、无需手动点击,只需通过自然语言下达指令,AI即可自动生成Python脚本、调用Blender后台执行、输出3D模型(obj/blend),并同步整理操作笔记。
1061 2
|
6天前
|
人工智能 弹性计算 关系型数据库
阿里云企业新用户定义及专属优惠政策解读
阿里云面向企业新用户推出多重专属优惠:需完成企业实名认证、无历史付费记录、同一主体仅享一次。涵盖38元/年起的轻量服务器、199元/年u1实例、5亿算力补贴、最高4万元AI抵扣金及10万元出海资源包,助力中小企业降本增效、快速上云。
87 12
|
6天前
|
人工智能 弹性计算 自然语言处理
阿里云学生算力包:大学生上云练手、做毕设、玩 AI 的全能方案
阿里云推出“学生算力包”,19元起享灵活按小时抵扣的云资源,支持一键部署AI简历、个人网站等实战项目;深度联动清华、浙大等数十所高校,提供课程、实训营与赛事支持,助力学生低成本入门AI开发与云实践。
104 9
|
23天前
|
人工智能 IDE 前端开发
|
9天前
|
人工智能 API Docker
claude_code_mineru_skill
为Claude Code等Code Agent新增PDF解析能力!一行命令安装MinerU Skill,无需Docker、API Key或服务器,自动智能解析PDF/截图,支持复杂表格、公式与双栏论文,转为高质量Markdown供AI理解,大幅提升文档驱动开发效率。
213 10

热门文章

最新文章

下一篇
开通oss服务