用一个转账问题串联起java数据库访问层技术

  • Post author:
  • Post category:java




jdbc工具类如何封装?



v1

/**
 * v1.0
 * 工具类包含一个连接池对象,并且对外提供获取连接和回收连接的方法
 * <p>
 * 小建议:
 * 工具类的方法,推荐携程静态,外部调用会更加方便
 * <p>
 * 实现:
 * 属性 连接池对象 [实例化一次]
 * 单例
 * static {
 * 全局调用一次
 * }
 * 方法
 * 对外提供连接的方法
 * 回收外部传入连接方法
 */
public class JDBCToolsVersion1 {

    private static DataSource dataSource;

    static {
        // 初始化连接池对象
        Properties properties = new Properties();
        InputStream ips = JdbcUtils.class.getClassLoader().getResourceAsStream("druid.properties");
        try {
            properties.load(ips);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        try {
            dataSource = DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    public static void free(Connection connection) throws SQLException {
        connection.setAutoCommit(true);
        connection.close();
    }

}

此时在做事务操作需要调用dao层时,需要手动传入 connection 保证两次操作的连接时同一个连接

@Test
    public void testCommitV1() throws SQLException {
        Connection connection = JDBCToolsVersion1.getConnection();
        int flag = 0;

        //利用try代码块,调用dao
        try {

            //开启事务(关闭事务自动提交)
            connection.setAutoCommit(false);

            BankDao bankDao = new BankDao();
            //调用加钱 和 减钱
            bankDao.addMoney1("ergouzi", 500, connection);
            System.out.println("--------------");
            // 手动模拟报错
            int i = 1 / 0;
            bankDao.subMoney1("lvdandan", 500, connection);
            flag = 1;
            //不报错,提交事务
            connection.commit();
        } catch (Exception e) {
            //报错回滚事务
            connection.rollback();
            throw e;
        } finally {
            JDBCToolsVersion1.free(connection);
        }

        if (flag == 1) {
            System.out.println("转账成功!");
        } else {
            System.out.println("转账失败!");
        }
    }

注意一个小细节,在手动 close connection 的时候,配置连接自动提交为 true,避免后面拿到的连接的默认开始了事务



v2

image.png

public class JDBCToolsVersion2 {
    private static DataSource ds;
    private static ThreadLocal<Connection> tl = new ThreadLocal<>();

    static{//静态代码块,JDBCToolsVersion1类初始化执行
        try {
            Properties pro = new Properties();
            pro.load(ClassLoader.getSystemResourceAsStream("druid.properties"));
            ds = DruidDataSourceFactory.createDataSource(pro);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection() throws SQLException {
        Connection connection = tl.get();
        if(connection  == null){//当前线程还没有拿过连接,就给它从数据库连接池拿一个
            connection = ds.getConnection();
            tl.set(connection);
        }
        return connection;
    }

    public static void free() throws SQLException {
        Connection connection = tl.get();
        if(connection != null){
            tl.remove();
            connection.setAutoCommit(true);//避免还给数据库连接池的连接不是自动提交模式(建议)
            connection.close();
        }
    }
}

此时调用时,无需dao层传入 connection,使用了 threadlocal 已保证这个线程中拿到的connection为同一个

@Test
    public void testCommitV2() throws SQLException {
        Connection connection = JDBCToolsVersion2.getConnection();
        int flag = 0;

        //利用try代码块,调用dao
        try {

            //开启事务(关闭事务自动提交)
            connection.setAutoCommit(false);

            BankDao bankDao = new BankDao();
            //调用加钱 和 减钱
            bankDao.addMoney("ergouzi", 500);
            System.out.println("--------------");
            // 手动模拟报错
            int i = 1 / 0;
            bankDao.subMoney("lvdandan", 500);
            flag = 1;
            //不报错,提交事务
            connection.commit();
        } catch (Exception e) {
            //报错回滚事务
            connection.rollback();
            throw e;
        } finally {
            JDBCToolsVersion2.free();
        }

        if (flag == 1) {
            System.out.println("转账成功!");
        } else {
            System.out.println("转账失败!");
        }
    }
    /**
     * 加钱方法
     * @param account
     * @param money
     * @return 影响行数
     */
    public int addMoney(String account, int money) throws SQLException {

        Connection connection = JDBCToolsVersion2.getConnection();


        String sql = "update t_bank set money = money + ? where account = ?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        //占位符赋值
        preparedStatement.setInt(1, money);
        preparedStatement.setString(2, account);

        //发送SQL语句
        int rows = preparedStatement.executeUpdate();

        //输出结果
        System.out.println("加钱执行完毕!");

        //关闭资源close
        preparedStatement.close();

        return rows;
    }

    /**
     * 减钱方法
     * @param account
     * @param money
     * @return 影响行数
     */
    public int subMoney(String account, int money) throws SQLException {

        Connection connection = JDBCToolsVersion2.getConnection();


        String sql = "update t_bank set money = money - ? where account = ?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        //占位符赋值
        preparedStatement.setObject(1, money);
        preparedStatement.setString(2, account);

        //发送SQL语句
        int rows = preparedStatement.executeUpdate();

        //输出结果
        System.out.println("减钱执行完毕!");

        //关闭资源close
        preparedStatement.close();

        return rows;
    }



如何在工具类的基础上封装一个BaseDao?

public abstract class BaseDao {
    /*
    通用的增、删、改的方法
    String sql:sql
    Object... args:给sql中的?设置的值列表,可以是0~n
     */
    protected int update(String sql, Object... args) throws SQLException {
//        创建PreparedStatement对象,对sql预编译
        Connection connection = JDBCToolsVersion2.getConnection();
        PreparedStatement ps = connection.prepareStatement(sql);
        //设置?的值
        // 可变参数当作数据使用
        // 占位符赋值
        if (args != null && args.length > 0) {
            for (int i = 0; i < args.length; i++) {
                ps.setObject(i + 1, args[i]);//?的编号从1开始,不是从0开始,数组的下标是从0开始
            }
        }

        //执行sql
        int len = ps.executeUpdate();
        ps.close();
        // 这里检查下是否开启事务,开启不关闭连接,业务方法关闭!
        // 没有开启事务的话,直接回收关闭即可!
        if (connection.getAutoCommit()) {
            // 回收
            JDBCToolsVersion2.free();
        }
        return len;
    }
}
  1. 关注下可变参数 Object… args 的用法,可遍历,但是从 1 开始
  2. 是否关闭连接呢?如果没有开启事务,连接可关闭,如果开启了,业务层去处理即可
public class BankDao extends BaseDao {

    public int addMoney2(String account, Integer money) throws SQLException {
        return update("update t_bank set money = money + ? where account = ?", money, account);
    }

    public int subMoney2(String account, Integer money) throws SQLException {
        return update("update t_bank set money = money - ? where account = ?", money, account);
    }
}
@Test
    public void testCommitV3() throws SQLException {
        Connection connection = JDBCToolsVersion2.getConnection();
        int flag = 0;

        //利用try代码块,调用dao
        try {

            //开启事务(关闭事务自动提交)
            connection.setAutoCommit(false);

            BankDao bankDao = new BankDao();
            //调用加钱 和 减钱
            bankDao.addMoney2("ergouzi", 500);
            System.out.println("--------------");
            // 手动模拟报错
            int i = 1 / 0;
            bankDao.subMoney2("lvdandan", 500);
            flag = 1;
            //不报错,提交事务
            connection.commit();
        } catch (Exception e) {
            //报错回滚事务
            connection.rollback();
            throw e;
        } finally {
            JDBCToolsVersion2.free();
        }

        if (flag == 1) {
            System.out.println("转账成功!");
        } else {
            System.out.println("转账失败!");
        }
    }



如何基于 Spring 的声明式事务处理?

基于注解的事务管理器配置,数据库使用 JdbcTemplate 操作

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.2</version>
        </dependency>

        <!--spring aop依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>6.0.2</version>
        </dependency>

        <!--spring aspects依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>6.0.2</version>
        </dependency>

        <!-- junit测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

        <!--spring jdbc  Spring 持久化层支持jar包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.2</version>
        </dependency>

        <!--spring对junit的支持相关依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>6.0.2</version>
        </dependency>

        <!--junit5测试-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.9.2</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.16</version>
        </dependency>

jdk17

@Configuration
@ComponentScan("com.example.jdbc")
@Import(
        {BankService.class,
                BankDao.class}
)
@EnableTransactionManagement
public class SpringConfig {

    @Bean
    public DataSource getDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("oracle.jdbc.OracleDriver");
        dataSource.setUrl("jdbc:oracle:thin:@localhost:1521:orcl");
        dataSource.setUsername("wateruser");
        dataSource.setPassword("sys");
        return dataSource;
    }

    @Bean
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    @Bean
    public DataSourceTransactionManager getTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

注意这里的 @EnableTransactionManagement 和 注入的 DataSourceTransactionManager

测试代码

@SpringJUnitConfig(classes = {SpringConfig.class})
public class JDBCTemplateTest {

    @Autowired
    private BankService bankService;

    @Test
    //测试增删改功能
    public void testUpdate() {
        bankService.transferAnno();
    }

}
@Service
public class BankService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void transferAnno() {
        String sql1 = "update t_bank set money = money + ? where account = ?";
        jdbcTemplate.update(sql1, 1000, "ergouzi");

        int i  = 1/0;

        String sql2 = "update t_bank set money = money -? where account =?";
        jdbcTemplate.update(sql2, 1000, "lvdandan");
    }
}

TODO: 剩余事务的嵌套问题的理解

mybatis 版本的和这个基本一致,因为 我们在做整合的时候也是在 mybatis 中使用 spring 的事务管理器

中与 事务相关的部分



多数据源场景下的事务问题

image.png

项目结构

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.7.9</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>


<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.6.1</version>
        </dependency>

        <dependency>
            <groupId>cn.easyproject</groupId>
            <artifactId>orai18n</artifactId>
            <version>12.1.0.2.0</version>
        </dependency>

        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc8</artifactId>
            <version>21.9.0.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

创建表空间,用户,密码

  1. waterboss waterboss sys
  2. waterboss1 waterboss1 sys
-- 创建表空间
create tablespace waterboss
datafile 'd:\waterboss.dbf'
size 100m
autoextend on -- 用于设置自动增长,如果存储量超过初始大小,则开始自动扩容
next 10m;

-- 创建用户
create user wateruser
identified by sys
default tablespace waterboss1;

-- 用户授权
grant dba to wateruser;

-- 创建表空间
create tablespace waterboss1
datafile 'd:\waterboss1.dbf'
size 100m
autoextend on -- 用于设置自动增长,如果存储量超过初始大小,则开始自动扩容
next 10m;

-- 创建用户
create user wateruser1
identified by sys
default tablespace waterboss1;

-- 用户授权
grant dba to wateruser1;

建表语句

-- ----------------------------
-- Table structure for T_BANK
-- ----------------------------
DROP TABLE "T_BANK";
CREATE TABLE "WATERUSER1"."T_BANK" (
  "ID" NUMBER(20,0) VISIBLE NOT NULL,
  "ACCOUNT" VARCHAR2(20 BYTE) VISIBLE NOT NULL,
  "MONEY" NUMBER(20,0) VISIBLE NOT NULL
)
LOGGING
NOCOMPRESS
PCTFREE 10
INITRANS 1
STORAGE (
  INITIAL 65536 
  NEXT 1048576 
  MINEXTENTS 1
  MAXEXTENTS 2147483645
  BUFFER_POOL DEFAULT
)
PARALLEL 1
NOCACHE
DISABLE ROW MOVEMENT
;

-- ----------------------------
-- Records of T_BANK
-- ----------------------------
INSERT INTO "T_BANK" VALUES ('1', 'ergouzi', '2000');
INSERT INTO "T_BANK" VALUES ('2', 'lvdandan', '1000');

-- ----------------------------
-- Primary Key structure for table T_BANK
-- ----------------------------
ALTER TABLE "T_BANK" ADD CONSTRAINT "SYS_C007513" PRIMARY KEY ("ID");

-- ----------------------------
-- Checks structure for table T_BANK
-- ----------------------------
ALTER TABLE "T_BANK" ADD CONSTRAINT "SYS_C007511" CHECK ("ACCOUNT" IS NOT NULL) NOT DEFERRABLE INITIALLY IMMEDIATE NORELY VALIDATE;
ALTER TABLE "T_BANK" ADD CONSTRAINT "SYS_C007512" CHECK ("MONEY" IS NOT NULL) NOT DEFERRABLE INITIALLY IMMEDIATE NORELY VALIDATE;

配置文件

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    dynamic:
      # 设置默认的数据源或者数据源组,默认值即为 master
      primary: master
      # 严格模式 匹配不到数据源则报错
      strict: true
      datasource:
        # 主库数据源
        master:
          driverClassName: oracle.jdbc.OracleDriver
          url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
          username: wateruser
          password: sys
        slave:
          lazy: true
          driverClassName: oracle.jdbc.OracleDriver
          url: jdbc:oracle:thin:@127.0.0.1:1521:orcl
          username: wateruser1
          password: sys
      hikari:
        # 最大连接池数量
        maxPoolSize: 20
        # 最小空闲线程数量
        minIdle: 10
        # 配置获取连接等待超时的时间
        connectionTimeout: 10000
        # 校验超时时间
        validationTimeout: 5000
        # 空闲连接存活最大时间,默认10分钟
        idleTimeout: 60000
        # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟
        maxLifetime: 900000
        # 连接测试query(配置检测连接是否有效)
        connectionTestQuery: SELECT 1 from dual


#mybatis
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  #实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: com.example.datasource.entity
  global-config:
    #数据库相关配置
    db-config:
      #主键类型
      id-type: ASSIGN_ID
    banner: false
  #原生配置
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
    call-setters-on-nulls: true
    jdbc-type-for-null: 'null'
  configuration-properties:
    prefix:
    blobType: BLOB
    boolValue: TRUE
@TableName("t_bank")
@Data
@AllArgsConstructor
@Accessors(chain = true)
public class Bank {

    @TableId(type = IdType.AUTO)
    private Integer id;

    private String account;

    private Integer money;
    
}
@Mapper
public interface BankDao extends BaseMapper<Bank> {

    public default void addMoney(Integer id, Integer money){
        Bank bank = this.selectById(id).setMoney(this.selectById(id).getMoney() + money);
        this.updateById(bank);
    }

    @DS("slave")
    public default void subMoney(Integer id, Integer money){
        Bank bank = this.selectById(id).setMoney(this.selectById(id).getMoney() - money);
        this.updateById(bank);
    }
}
@Service
public class BankService {

    @Resource
    BankDao bankDao;

    @Transactional
    public void transfer() {
        bankDao.addMoney(1, 500);
        int i = 1 / 0;
        bankDao.subMoney(1, 500);
    }
}

测试类

@SpringBootTest
public class TestTransaction {

    @Resource
    BankService bankService;

    @Test
    public void testTransaction() {
        bankService.transfer();
    }
}

可以验证到,多数据源情况下同样会回滚



版权声明:本文为qq_39007838原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。