JPA下的事务
当我们访问数据库时,如果使用 JPA,需要引入依赖 spring-boot-starter-jpa,才会产生 JpaTransactionManager。这里做两个实例,一是没有事务的情况,二是添加上事务的情况。
普通的数据库访问
在介绍数据源时,我们已经学习过 JPA 的使用,这里的示例直接在 JPA 的代码上进行修改即可。为了有一个直观的印象,先看图7.1。

在图7.1中,可以看到,使用的包是 dataSource。首先,要看看 application.properties 的配置,代码如下所示。
#系统的配置数据源
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3308/test?serverTimezo ne=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
#JPA
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
在上面的代码中,第一部分是 MySQL 的数据源配置,使用 Spring Boot 默认的连接池,第二部分是 JPA 的配置。因为是演示程序,每次都新建一次表格,而且最开始没有新建,所以使用 create 的属性值。
然后,创建实体类 NewUser.java,代码如下所示。
package com.springBoot.dataSource.pojo;
import javax.persistence.*;
@Entity
@Table(name = "new_user")
public class NewUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 2,name="username",nullable = true)
private String username;
@Column(length = 2)
private String password;
public NewUser(){}
public NewUser(Long id, String username, String password){
this.id=id;
this.username=username;
this.password=password;
}
public NewUser(String username, String password){
this.username=username;
this.password=password;
}
//set and get
}
在上面的代码中,我们添加 @Entity,说明这个类是一个实体类,并且将对应表名设为 new_user。这里要说的是 @Column 中的属性 length 定义为 2,表示生成的字段长度是 2,至于 2 的作用,在7.3.2节会体现出来。然后,新建一个数据访问接口 UserRepository,代码如下所示。
package com.springBoot.dataSource.dao;
public interface UserRepository extends JpaRepository<NewUser,Long> {
public List<NewUser> findNewUserById(Long id);
@Query("select art from com.springBoot.dataSource.pojo.NewUser art where username=:username")
public List<NewUser> queryByUsername(@Param("username") String username);
public NewUser save(NewUser newUser);
}
上面的代码中,在原有的基础上添加了新的方法,就是加粗的 save 方法,用于保存对象。最后,还是使用前面写的数据源测试类程序,直接测试保存,代码如下所示。
package com.springBoot.dataSource;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.springBoot.dataSource. dao")
@EntityScan("com.springBoot.dataSource.pojo")
public class NewUserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
PlatformTransactionManager platformTransactionManager;
@Test
public void save(){
userRepository.save(new NewUser("a","1"));
userRepository.save(new NewUser("b","2"));
userRepository.save(new NewUser("c","3"));
userRepository.save(new NewUser("d","4"));
userRepository.save(new NewUser("e","55555"));
System.out.println("----");
}
}
上面的测试类程序,我们运行的是 save 方法,可以看到要在数据库中插入五条记录。执行后观察数据库,最终的效果如图7.2所示。

在图7.2中,发现只插入了前四条记录,第五条记录没有成功。让我们看看控制台上的执行日志如下。
Hibernate: insert into new_user (password, username) values (?, ?)
Hibernate: insert into new_user (password, username) values (?, ?)
Hibernate: insert into new_user (password, username) values (?, ?)
Hibernate: insert into new_user (password, username) values (?, ?)
Hibernate: insert into new_user (password, username) values (?, ?)
2018-12-10 22:17:33.393 WARN 14928 --- [ main] o.h.engine. jdbc.spi.SqlExceptionHelper : SQL Error: 1406, SQLState: 22001
2018-12-10 22:17:33.393 ERROR 14928 --- [ main] o.h.engine. jdbc.spi.SqlExceptionHelper : Data truncation: Data too long for column 'password' at row 1
从日志上可以看到,在执行第五条记录时,出现了报错,因为 password 的列太长,所以会插入失败。
事务
在7.3.1节的示例中,成功插入四条记录,失败一条。按逻辑来说,不符合条件的记录失败很正常,但是有些场景本身就有问题。
所以我们需要使用事务来解决。那么 JPA 支持事务吗?如何使用事务?下面开始讲解我们的实例,相关程序还是在7.3.1节的基础上进行修改。
首先,我们需要注意不要修改 NewUser 实体类,重点是注意列的长度,这是造成程序出错的地方。下面是模拟程序出错,进行事务回滚。
@Column(length = 2,name="username",nullable = true)
private String username;
@Column(length = 2)
private String password;
然后,在测试类的方法上添加事务注解 @Transactional,代码如下所示。
package com.springBoot.dataSource;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.springBoot.dataSource.dao")
@EntityScan("com.springBoot.dataSource.pojo")
public class NewUserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
PlatformTransactionManager platformTransactionManager;
@Test
@Transactional
public void save(){
System.out.println(">>>>>>>>>>" + platformTransactionManager. getClass().getName());
userRepository.save(new NewUser("a","1"));
userRepository.save(new NewUser("b","2"));
userRepository.save(new NewUser("c","3"));
userRepository.save(new NewUser("d","4"));
userRepository.save(new NewUser("e","55555"));
System.out.println("----");
}
}
在上面的代码中,我们注入了 PlatformTransactionManager。顺便说明一下,在开始事务时,Spring Boot 会自动生成 JpaTransactionManager,具体值看输出效果。
执行程序,观察执行效果,下面是执行的日志。
>>>>>>>>>>org.springframework.orm.jpa.JpaTransactionManager
Hibernate: insert into new_user (password, username) values (?, ?)
Hibernate: insert into new_user (password, username) values (?, ?)
Hibernate: insert into new_user (password, username) values (?, ?)
Hibernate: insert into new_user (password, username) values (?, ?)
Hibernate: insert into new_user (password, username) values (?, ?)
2018-12-10 22:31:26.287 WARN 2624 --- [ main] o.h.engine. jdbc.spi.SqlExceptionHelper: SQL Error: 1406, SQLState: 22001
2018-12-10 22:31:26.287 ERROR 2624 --- [ main] o.h.engine. jdbc.spi.SqlExceptionHelper: Data truncation: Data too long for column 'password' at row 1
2018-12-10 22:31:26.300 INFO 2624 --- [ main] o.s.t.c.transaction. TransactionContext: Rolled back transaction for test:[DefaultTestContext@4b2c5e02 testClass = NewUserRepositoryTest, testInstance = com.springBoot.dataSource.NewUserRepositoryTest@2697c156, testMethod = save@NewUserRepositoryTest,
在控制台上,可以看到第一行加粗的部分,说明自动生成的事务管理器是我们所预想的。在后面的加粗日志上,可以看到 rooled back transaction for test,这里是进行了回滚。为了确认,接下来看看数据库的执行效果,如图7.3所示。

实际写事务时,肯定不是写在 test 类中,这时我们需要把 save 方法放到 service 中,这时注解也就移动到 service。