本文最后更新于:2022-10-15T22:46:46+08:00
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
| <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>
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>5.3.1</version> </dependency>
<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>
<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() { 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
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,就能够达到效果,这种实现称之为声明式实现。声明式实现指的是通过配置让框架来帮我们实现功能。
基于注解的声明式事务
环境准备
我们需要模拟一个购书的场景。这里我们准备相关的数据库表books
和customers
,分别表示书店书目以及顾客:
这里的库存和余额均设置为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
注解中可以设置相关属性,下面进行介绍。
readOnley
:只读属性。可以设置为true或者为false。对于一个查询操作来说,如果将其设置为只读,就能够明确告诉数据库这个操作不涉及写操作,数据库就能够针对查询操作来进行优化
timeout
:设置超时时间。如果超时则进行回滚
- 回滚策略:可以设置因为什么而回滚或者不因为什么而回滚
rollbackFor
:给定一个对应类型的对象
rollbackForClassName
:设置一个字符串类型的全类名
noRollbackFor
:给定一个对应类型的对象
noRollbackForClassName
:设置一个字符串类型的全类名
isolation
:事务隔离级别
Isolation.DEFAULT
:使用数据库默认的隔离级别
Isolation.READ_UNCOMMITTED
:读未提交
Isolation.READ_COMMITED
:读以提交
Isolation.REPEATABLE_READ
:可重复读
Isolation.SERIALIZABLE
:串行化
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
标签配置事务通知所应用的位置,即指定对应的事务通知以及对应的切入点表达式。