Spring笔记(4)-JdbcTemplate与声明式事务

JdbcTemplate

环境准备

在Spring框架中,对JDBC进行了一定程度的封装,使用JdbcTemplate来实现对数据库的操作。我们在这里进行简单的介绍。

在Spring的基础依赖上,我们需要引入下面的相关依赖,其中除了基础的context,还需要导入持久层相关依赖以及测试相关依赖。

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
<!--Spring Framework-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>

<!--Spring 持久层相关依赖-->
<!--Spring在与持久化层技术进行整合是,需要使用orm、jdbc、tx三个jar包-->
<!--通过导入orm,通过Maven的依赖传递将其他两个包进行导入-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.1</version>
</dependency>

<!--Spring 测试相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.1</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>

<!--数据源依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>

之后,我们创建jdbc.properties文件,在其中完成连接数据库的基本信息:

1
2
3
4
jdbc.username=root
jdbc.password=123123
jdbc.url=jdbc:mysql://localhost:3306/ssm_data
jdbc.driver=com.mysql.cj.jdbc.Driver

然后创建Spring的配置文件,在其中我们首先需要导入外部属性文件,在其中配置数据源之后,配置JdbcTemplate。JdbcTemplate是Spring提供的交由IOC管理的类,我们只需要在其中配置数据源即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--导入外部属性文件-->
<context:property-placeholder location="jdbc.properties"/>

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

<!--配置JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--装配数据源-->
<property name="dataSource" ref="druidDataSource"/>
</bean>

我们可以使用@Test测试接口来进行方法测试,不过这里我们使用Spring整合junit的方式进行相关测试。我们可以创建一个测试类,利用相关注解标注之后,就可以直接获取IOC容器中的Bean对象,而不需要像前面一样先获取ApplicationContext,在getBean获取对象。我们创建测试类如下:

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:jdbcContext.xml")
public class JdbcTemplateTest {

@Autowired
private JdbcTemplate jdbcTemplate;

@Test
public void test() {
System.out.println(jdbcTemplate);
}
}

其中我们使用到的接口如下:

  • @RunWith:在其中给定SpringJunit4ClassRuner.class,指定当前测试类在Spring的测试环境中执行,测试可以通过注入的方式直接获取IOC容器中的Bean
  • @ContextConfiguration:设置Spring测试环境的对应配置环境
  • @Autowired:通过自动装配来获取相关Bean对象

这样,我们类中的测试方法可以直接使用成员对象jdbcTemplate,并且这个对象就是我们在配置文件中配置的对应对象。

Spring整合junit主要是通过注解完成的,不过随着版本不同,整合使用的注解也有所不同。

基本操作

下面我们测试JdbcTemplate的相关操作。

增删改操作通过update方法,在sql字符串中,我们可以通过?来作为占位符。

1
2
3
4
5
6
7
8
@Test
// 测试增加功能
public void testUpdate() {
// schema(id, username, password, age, gender, email)
String sql = "insert into users values(null, ?, ?, ?, ?, ?)";
int result = jdbcTemplate.update(sql, "jdbcMan", "123456", 32, "女", "hello@haha.com");
System.out.println(result);
}

我们可以查询一条数据为实体类对象,也可以查询多条数据,或者查询值,只需要指定对应的转换类型即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
// 查询一条数据为一个实体类对象
public void testSelectOneLine(){
String sql = "select * from users where id = ?";
User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 3);
System.out.println(user);
}

@Test
// 查询多条数据为一个List集合
public void testSelectList(){
String sql = "select * from users";
List<User> resList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
resList.forEach(System.out::println);
}

@Test
// 查询一个具体的值
public void testSelectCount(){
String sql = "select count(*) from users";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println(count);
}

声明式事务

背景介绍

数据库中的事务操作,对应到Java代码中,可以看作一个try-catch-finally的执行逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Connection conn = ...;

try{
// 开启事务,关闭事务的自动提交
conn.setAutoCommit(false);

// 事务的核心操作...

// 提交事务
conn.commit();
}catch(Exception e){
// 事务回滚
conn.rollBack();
}finally{
//...
}

在实际场景中,一般不会出现SQL执行错误的逻辑,更多的情况是因为业务逻辑不符合而需要回滚。如果不做任何处理,这种SQL代码能够执行,但是不符合逻辑。因此我们需要在数据库中设置一些限制,如数据类型等,或者在代码中检查业务逻辑,如果不符合则手动抛出运行时异常。

我们想要在代码中应用事务,就可以采用上面的逻辑。但是事务的基本框架都是一样的,如果每次都让程序员来写这一套代码,则会导致很大程度的代码冗余,代码的复用性不高。

我们可以通过AOP的思想,将事务的提交框架抽取出来,进行相关的封装,而让程序员能更多地关注核心操作即事务的核心逻辑。完成封装之后,我们能够提高开发效率并消除冗余代码。

要完成代码的抽取,我们可以通过代理模式来实现,或者利用Spring中的AOP相关注解配置或者XML来实现。这种实现为编程式实现。而实际上,由于事务操作的常用性,Spring已经给我们实现了相关的功能,我们只需要使用相关的注解或者XML,就能够达到效果,这种实现称之为声明式实现。声明式实现指的是通过配置让框架来帮我们实现功能。

基于注解的声明式事务

环境准备

我们需要模拟一个购书的场景。这里我们准备相关的数据库表bookscustomers,分别表示书店书目以及顾客:

book_id book_name book_price book_inventory
1 A 50 3
2 B 100 2
customer_id customer_name customer_balance
1 admin 60

这里的库存和余额均设置为int unsigned类型,表示该值不能为负,符合我们的业务逻辑。

然后利用三层架构的思想创建不同的类。

Contorller:

1
2
3
4
5
6
7
8
9
10
@Controller
public class BookController {

@Autowired
private BookService bookService;

public void buyBook(Integer customerId, Integer bookId){
bookService.buyBook(customerId, bookId);
}
}

Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class BookService {
@Autowired
private BookDao bookDao;

public void buyBook(Integer customerId, Integer bookId) {
// 查询图书价格
Integer price = bookDao.getPriceByBookId(bookId);

// 减少图书库存
bookDao.declineInventory(bookId, 1);

// 扣减用户余额
bookDao.declineBalance(customerId, price);
}
}

Dao:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Repository
public class BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;

public Integer getPriceByBookId(Integer bookId) {
String sql = "select book_price from books where book_id = ?";
Integer price = jdbcTemplate.queryForObject(sql, Integer.class, bookId);
return price;
}

public void declineInventory(Integer bookId, int declineNum) {
String sql = "update books set book_inventory = book_inventory - ? where book_id = ?";
jdbcTemplate.update(sql, declineNum, bookId);
}

public void declineBalance(Integer customerId, int declineNum) {
String sql = "update customers set customer_balance = customer_balance - ? where customer_id = ?";
jdbcTemplate.update(sql, declineNum, customerId);
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:jdbcContext.xml")
public class BookTransTest {
@Autowired
private BookController bookController;

@Test
public void testBuyBook() {
bookController.buyBook(1, 1);
}
}

上面的购买事务过程分为三个步骤,查询图书价格,减少图书库存以及扣减用户余额。但是此时并没有任何事务处理的保证,此时的每个SQL都是作为单独的事务进行执行的。如果要使用Spring中提供的声明式事务,我们可以使用@Transactional注解。

注解使用

要使用相关注解,我们首先需要在Spring的配置文件中添加相关配置:

1
2
3
4
5
6
<!--事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"/>
</bean>

<tx:annotation-driven transaction-manager="transactionManager"/>

在上面的配置中,我们首先配置了事务管理器的Bean,对应类DataSourceTransactionManager,之后使用标签tx:annotation-driven开启事务的注解驱动,其中指定对应的事务管理器为我们之前配置的Bean。

之后,我们找到需要处理事务的Service层buyBook方法,在方法上增加@Transactional注解,这样就可以将整个方法处理成事务进行执行了。如果此时我们让客户1购买书2,会发现余额不够而无法购买,由于事务回滚,数据库中不会发生改变。

事务属性

@Transactional注解中可以设置相关属性,下面进行介绍。

  1. readOnley:只读属性。可以设置为true或者为false。对于一个查询操作来说,如果将其设置为只读,就能够明确告诉数据库这个操作不涉及写操作,数据库就能够针对查询操作来进行优化
  2. timeout:设置超时时间。如果超时则进行回滚
  3. 回滚策略:可以设置因为什么而回滚或者不因为什么而回滚
    • rollbackFor:给定一个对应类型的对象
    • rollbackForClassName:设置一个字符串类型的全类名
    • noRollbackFor:给定一个对应类型的对象
    • noRollbackForClassName:设置一个字符串类型的全类名
  4. isolation:事务隔离级别
    • Isolation.DEFAULT:使用数据库默认的隔离级别
    • Isolation.READ_UNCOMMITTED:读未提交
    • Isolation.READ_COMMITED:读以提交
    • Isolation.REPEATABLE_READ:可重复读
    • Isolation.SERIALIZABLE:串行化
  5. propagation:事务传播属性,描述的是事务A中需要执行事务B,事务B是开启一个新的事务还是利用事务A的事务。该属性配置在事务B上
    • Propagation.REQUIRED:如果当前线程上已经有开启的事务可用,则就在该事务中运行
    • Propagation.REQUIRES_NEW:无论当前线程上是否有已经开启的事务,都要开启新的事务

基于XML的声明式事务

在Spring中,同样可以通过XML来配置声明式事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="druidDataSource"/>
</bean>

<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--配置具体的事务方法-->
<tx:method name="get*" read-only="true"/>
<tx:method name="delete*" read-only="false" propagation="REQUIRES_NEW"/>
<tx:method name="*" read-only="false"/>
</tx:attributes>
</tx:advice>

<aop:config>
<!--配置事务通知和切入点表达式-->
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.syh.spring.trans.BookService.* (..))"/>
</aop:config>

首先我们还是需要配置事务管理器Bean对象,之后利用tx:advice标签来配置事务通知,其中需要使用到之前配置的事务管理器Bean对象。在事务通知中,我们需要针对不同的方法,指定不同的事务属性,即注解中的值。然后利用aop:config标签配置事务通知所应用的位置,即指定对应的事务通知以及对应的切入点表达式。


Spring笔记(4)-JdbcTemplate与声明式事务
http://example.com/2022/10/15/Spring笔记-4-JdbcTemplate与声明式事务/
作者
EverNorif
发布于
2022年10月15日
许可协议