SpringFramework框架-JDBC

Spring JDBC ( Java Database Connectivity )是 Spring 框架对 JDBC API 的轻量级封装,旨在简化数据库操作代码,减少样板代码,同时保留直接使用 JDBC 的灵活性和性能

配置

修改JDBC配置文件,在resources目录下新建jdbc.properties配置文件,设置相应配置信息

#驱动名
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
#数据库连接基本设置
jdbc.url=jdbc:mysql://localhost:3306/databassName?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
#数据库用户名
jdbc.username=username
#数据库用户密码
jdbc.password=password

可选配置如下

#指定连接池的初始化连接数。取值应在minPoolSize与maxPoolSize之间,Default:3
initialPoolSize=20
#指定连接池中保留的最大连接数,Default:15
maxPoolSize=100
#指定连接池中保留的最小连接数
minPoolSize=10
#最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃,Default:0
maxIdlerime=600
#当连接池中的连接耗尽的时候c3p0一次同时获取的连接数,Default:3
acquireIncrement=5
#JDBC的标准,用以控制数据源内加载的PreparedStatements数量
maxStatements=5
#每60秒检查所有连接池中的空闲连接。Default:0
idleConnectionTestPeriod=60

修改spring配置文件,加载jdbc.properties

<context:property-placeholder location="jdbc.properties"/>

为数据库连接池添加bean标签

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 基本配置 -->
<property name="driverClass" value="${jdbc.driverClassName}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 其他配置 -->
<property name="maxPoolSize" value="${maxPoolSize}"/>
</bean>

配置模板类,JDBC模板类是Spring JDBC的核心组件,它的主要作用是简化JDBC编程,解决传统JDBC代码中的重复和繁琐问题

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>

测试

运行以下代码,可以正常连接数据库

public class TestJdbc {
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = (JdbcTemplate) context.getBean("jdbcTemplate");
String sql = "SELECT * FROM user";
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql);
System.out.println(result);
}
}

优化

实际运行中,在每个连接数据库的场景都配置上下文环境将会非常繁琐,可以使用@Before注解进行提前初始化,将获取模板类对象的操作剥离出来,这里的@Before注解是junit包下的,在每个测试方法执行前运行,用于测试环境的初始化

@Before
public void init(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
JdbcTemplate jdbcTemplate = (JdbcTemplate) context.getBean("jdbcTemplate");
}

由于两个方法被拆分,此时test()方法无法接收jdbcTemplate,此时可以将模板提升到属性字段

private JdbcTemplate jdbcTemplate;

@Before
public void init(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
this.jdbcTemplate = (JdbcTemplate) context.getBean("jdbcTemplate");
}

其实还可以更进一步,将Junit的测试环境运行在Spring测试环境中,使用@RunWith注解完成,其接受一个测试运行器(Runner)类,用于指定 JUnit 如何运行测试。此外,还需要@ContextConfiguration指定配置文件,该注解有location属性接受一个集合,可用于加载多个配置文件

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring.xml"})
public class TestJdbc {
@Resource
private JdbcTemplate jdbcTemplate;

@Test
public void test() {
String sql = "SELECT * FROM user";
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql);
System.out.println(result);
}
}

Spring JDBC进行单表CRUD操作

我们用一个账号管理系统来演示使用JDBC与数据库交互

Account

public class Account {
private Integer UserId;
private Integer accountId;
private String accountName;
private String accountType;
private double balance;
private String remark;
private Date createTime;
private Date updateTime;

public Account() {}
public Account(int accountId, String accountName, String accountType, double balance, String remark) {
this.accountId = accountId;
this.accountName = accountName;
this.accountType = accountType;
this.balance = balance;
this.remark = remark;
}
@Override
public String toString() {
return "Account{" +
"accountId=" + accountId +
", accountName='" + accountName + '\'' +
", accountType='" + accountType + '\'' +
", balance=" + balance +
", remark='" + remark + '\'' +
", createTime=" + createTime +
", updateTime=" + updateTime +
'}';
}

public Date getUpdateTime() {
return updateTime;
}

public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}

public Date getCreateTime() {
return createTime;
}

public void setCreateTime(Date createTime) {
this.createTime = createTime;
}

public String getRemark() {
return remark;
}

public void setRemark(String remark) {
this.remark = remark;
}

public double getBalance() {
return balance;
}

public void setBalance(double balance) {
this.balance = balance;
}

public String getAccountType() {
return accountType;
}

public void setAccountType(String accountType) {
this.accountType = accountType;
}

public String getAccountName() {
return accountName;
}

public void setAccountName(String accountName) {
this.accountName = accountName;
}

public void setAccountId(int accountId) {
this.accountId = accountId;
}

public Integer getUserId() {
return UserId;
}

public void setUserId(Integer userId) {
UserId = userId;
}

public Integer getAccountId() {
return accountId;
}

public void setAccountId(Integer accountId) {
this.accountId = accountId;
}

数据添加

添加单条数据,返回受影响的行数

public int addAccount(Account account) {
String sql = "insert into tb_account(account_name,account_type,balance,remark," +
"user_id,create_time,update_time) values (?,?,?,?,now(),now());"
Object[] objs = {account.getAccountName(),account.getAccountType(),
account.getBalance(),account.getRemark(),account.getUserId());
return jdbcTemplate.update(sql,objs);
}

添加单条数据,返回主键

public int addAccountHaskey(Account account) {
String sql = "insert into tb_account(account_name,account_type,balance,remark,user_id,create_time.update_time) values (?,?,?,?,?,now(),now())";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
ps.setString(1,account.getAccountName());
ps.setString(2,account.getAccountType());
ps.setDouble(3,account.getBalance());
ps.setString(4,account.getRemark());
ps.setInt(5,account.getUserId());
return ps;
},keyHolder);
Integer key = keyHolder.getKey().intValue();
return key;
}

update()方法的一个参数PreparedStatementCreator是一个接口,故此处直接传入了一个lambda表达式,而不是该lambda表达式的返回值,lambda表达式本身就是一个自动实现PreparedStatementCreator接口的对象,update()方法内部调用接口中的createPreparedStatement()获得配置好的 PreparedStatement执行 SQL

批量添加数据,返回受影响的行数

public int addAccountBatch(final List<Account> accounts) {
String sql = "insert into tb_account(account_name,account_type,balance,remark," +
"user_id,create_time,update_time) values (?,?,?,?,?,now(),now())";
int[] rows = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {
preparedStatement.setString(1, accounts.get(i).getAccountName());
preparedStatement.setString(2, accounts.get(i).getAccountType());
preparedStatement.setDouble(3, accounts.get(i).getBalance());
preparedStatement.setString(4, accounts.get(i).getRemark());
preparedStatement.setInt(5, accounts.get(i).getUserId());
}
@Override
public int getBatchSize() {
return accounts.size();
}
}).length;

return rows;
}

数据查询

查询指定用户的信息数

public int queryAccountCount(Integer userId) {
String sql = "select count(1) from tb_account where user_id = ?";
int count = jdbcTemplate.queryForObject(sql, Integer.class, userId);
return count;
}

查询指定用户的账户详情

public Account queryAccountById(Integer accountId) {
String sql = "select * from tb_account where account_id = ?";
Account account = jdbcTemplate.queryForObject(sql, new Object[]{accountId},
(ResultSet resultSet,int i) -> {
Account acc = new Account();
acc.setAccountId(resultSet.getInt("account_id"));
acc.setBalance(resultSet.getDouble("Balance"));
acc.setAccountName(resultSet.getString("account_name"));
acc.setAccountType(resultSet.getString("account_type"));
acc.setRemark(resultSet.getString("remark"));
acc.setCreateTime(resultSet.getDate("create_time"));
acc.setUpdateTime(resultSet.getDate("update_time"));
acc.setUserId(resultSet.getInt("user_id"));
return acc;
});
return account;
}

多条件查询用户账户信息搭配模糊搜索

public List<Account> queryAccountByParams(Interger userId, String accountName, String accountType, String createTime) {
String sql = "select * from tb_account where user_id = ?";
List<Object> params = new ArrayList<>();
params.add(userId);

if(StringUtils.isNotBlank(accountName)) {
sql += " and account_name like concat('%',?,'%')";
params.add(accountName);
}
if(StringUtils.isNotBlank(accountType)) {
sql += " and account_type = ?";
params.add(accountType);
}
if(StringUtils.isNotBlank(creatTime)){
sql += " and creat_time < ?";
params.add(creatTime);
}

Object[] objs = params.toArray();
List<Account> accounts = jdbcTemplate.queryForObject(sql, objs,
(ResultSet resultSet,int i) -> {
Account acc = new Account();
acc.setAccountId(resultSet.getInt("account_id"));
acc.setBalance(resultSet.getDouble("Balance"));
acc.setAccountName(resultSet.getString("account_name"));
acc.setAccountType(resultSet.getString("account_type"));
acc.setRemark(resultSet.getString("remark"));
acc.setCreateTime(resultSet.getTimestamp("create_time"));
acc.setUpdateTime(resultSet.getTimestamp("update_time"));
acc.setUserId(resultSet.getInt("user_id"));
return acc;
});
return accounts;
}

params一开始用数组后面又转回数组的原因是传入的参数未定,在经过后面的if判断后才能确定传入的参数个数

数据更新

更新账户信息,返回更新的行数

public int updateAccountById(Account account) {
String sql = "update tb_account set account_name = ?, account_type = ?, " +
" balance = ? ,remark = ?,user_id = ? ,update_time = now() " +
" where account_id = ? ";
Object[] objs = {account.getAccountName(), account.getAccountType(),
account.getBalance(), account.getRemark(), account.getUserId(),
account.getAccountId()};
return jdbcTemplate.update(sql, objs);
}

批量更新账户信息,返回受影响的行数

public int updateAccountBatch(List<Account> accounts) {
String sql = "update tb_account set account_name = ?, account_type = ?, " +
" balance = ? ,remark = ?,user_id = ? ,update_time = now() " +
" where account_id = ? ";
int[] rows = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setString(1, accounts.get(i).getAccountName());
ps.setString(2, accounts.get(i).getAccountType());
ps.setDouble(3, accounts.get(i).getBalance());
ps.setString(4, accounts.get(i).getRemark());
ps.setInt(5, accounts.get(i).getUserId());
ps.setInt(6, accounts.get(i).getAccountId());
}

@Override
public int getBatchSize() {
return accounts.size();
}
}).length;
return rows;
}

数据删除

删除账户信息,返回更新的行数

public Integer deleteAccountById(Integer accountId) {
String sql = "delete from tb_account where account_id = ?";
Object[] objs = {accountId};
return jdbcTemplate.update(sql, objs);
}

批量删除账户信息,返回更新的行数

public int deleteAccountBatch(Integer[] ids) {
String sql = "delete from tb_account where account_id = ?";
int[] rows = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setInt(1, ids[i]);
}

@Override
public int getBatchSize() {
return ids.length;
}
}).length;
return rows;
}

SpringFramework框架-事务控制

Spring事务管理让多个数据库操作要么全部成功要么全部失败,通过@Transactional注解自动处理事务开始、提交和回滚,确保数据一致性,避免手动管理事务的繁琐代码

事务的特性

原子性(Atomicity)

要么全部成功,要么全部失败

一致性(Consistency)

事务在执行前后,数据库中数据要保持一致性状态。(如转账的过程 账户操作后数据必须保持一致)

隔离性(Isolation)

事务与事务之间的执行应当是相互隔离互不影响的。(多个角色对同一记录进行操作必须保证没有任何干扰),当然没有影响是不可能的,为了让影响级别降到最低,通过隔离级别加以限制:

隔离级别 英文名称 脏读 不可重复读 幻读 描述
读未提交 READ_UNCOMMITTED 隔离级别最低,会引发脏读、不可重复读和幻读问题
读已提交 READ_COMMITTED 只能读取已提交的数据,避免了脏读,但仍有不可重复读和幻读问题
可重复读 REPEATABLE_READ 确保同一事务中多次读取同一数据结果一致,避免了脏读和不可重复读,但仍存在幻读问题
串行化 SERIALIZABLE 最严格的隔离级别,事务串行执行,完全避免脏读、不可重复读和幻读问题,但性能最低

持久性(Durability)

事务提交完毕后,数据库中的数据变更是永久的

事务接口

Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,该接口提供入下方法

public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;

void commit(TransactionStatus status) throws TransactionException;

void rollback(TransactionStatus status) throws TransactionException;
}

JDBC事务管理

当应用程序直接使用 JDBC 进行数据持久化时,Spring 通过 DataSourceTransactionManager 来管理事务边界。配置方式如下

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>

DataSourceTransactionManager 通过从 DataSource 获取 java.sql.Connection 对象来管理事务,事务提交时调用 Connection.commit() 方法,事务回滚时调用 Connection.rollback() 方法

Hibernate 事务管理

当应用程序使用 Hibernate 作为持久化框架时,Spring 通过 HibernateTransactionManager 来管理事务。对于 Hibernate 3,配置如下

<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>

HibernateTransactionManager 需要注入 Hibernate 的 SessionFactory,该管理器将事务管理职责委托给 Hibernate 的 org.hibernate.Transaction 对象,Transaction 对象从 Hibernate Session 中获取,事务成功完成时调用 Transaction.commit() 方法,事务失败时调用 Transaction.rollback() 方法

JPA(Java持久化API)事务管理

当应用程序使用 JPA 作为持久化标准时,Spring 通过 JpaTransactionManager 来管理事务。配置方式如下

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>

JpaTransactionManager 需要装配一个 JPA 实体管理工厂(javax.persistence.EntityManagerFactory 的实现),该管理器与 JPA EntityManager 协作来构建和管理事务,通过 EntityManagerFactory 获取 EntityManager,并在其基础上进行事务控制

JTA(Java事务API)事务管理

当应用程序涉及分布式事务或多个事务资源(如多个不同数据源)时,Spring 通过 JtaTransactionManager 来管理事务。配置方式如下

<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManagerName" value="java:/TransactionManager" />
</bean>

JtaTransactionManager 将事务管理职责委托给 Java 事务 API(JTA),具体委托给 javax.transaction.UserTransactionjavax.transaction.TransactionManager 对象,事务成功时调用 UserTransaction.commit() 方法提交,事务失败时调用 UserTransaction.rollback() 方法回滚

XML配置文件实现事务管理

添加命名空间

事务命名空间

xmlns:tx="http://www.springframework.org/schema/tx"

http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd

AOP命名空间

xmlns:aop="http://www.springframework.org/schema/aop"

http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd

设置AOP代理

<aop:aspect-autoproxy />

配置事务管理器

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource"></property>
</bean>

配置通知

<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- 对以add update delete query开头的所有方法进行事务处理 -->
<tx:attributes>
<!-- 定义什么方法需要使用事务 name代表的是方法名(或方法匹配) -->
<!-- 匹配以 add 开头的所有方法均加入事务 -->
<tx:method name="add*" propagation="REQUIRED" />
<!-- 匹配以 update 开头的所有方法均加入事务 -->
<tx:method name="update*" propagation="REQUIRED" />
<!-- 匹配以 delete 开头的所有方法均加入事务 -->
<tx:method name="delete*" propagation="REQUIRED" />
<!-- 匹配以 query 开头的所有方法均加入事务 -->
<tx:method name="query*" read-only="true" />
</tx:attributes>
</tx:advice>

属性含义与可选值

属性名 是否必须 默认值 可选值 说明
name 必须 方法名/通配符 与事务关联的方法名,支持通配符:
* - 匹配所有方法
get* - 匹配以”get”开头的方法
handle* - 匹配以”handle”开头的方法
on*Event - 匹配如”onClickEvent”等方法
propagation 可选 REQUIRED REQUIRED - 支持当前事务,如果不存在则创建新事务
SUPPORTS - 支持当前事务,如果不存在则以非事务方式执行
MANDATORY - 支持当前事务,如果不存在则抛出异常
REQUIRES_NEW - 创建新事务,如果存在当前事务则挂起
NOT_SUPPORTED - 以非事务方式执行,如果存在当前事务则挂起
NEVER - 以非事务方式执行,如果存在事务则抛出异常
NESTED - 如果当前存在事务,则在嵌套事务内执行
事务传播行为,控制方法如何参与事务
isolation 可选 DEFAULT DEFAULT - 使用数据库默认隔离级别
READ_UNCOMMITTED - 读未提交
READ_COMMITTED - 读已提交
REPEATABLE_READ - 可重复读
SERIALIZABLE - 串行化
事务隔离级别,控制事务并发访问的隔离程度
timeout 可选 -1 正整数(秒) 事务超时时间:
-1 - 永不超时
30 - 30秒后超时回滚
60 - 60秒后超时回滚
read-only 可选 false true - 只读事务
false - 读写事务
事务是否只读,只读事务可进行查询优化
rollback-for 可选 异常类全限定名(逗号分隔) 指定哪些异常触发回滚:
java.lang.Exception - 所有Exception
com.example.BusinessException - 特定业务异常,默认回滚RuntimeExceptionError
no-rollback-for 可选 异常类全限定名(逗号分隔) 指定哪些异常不触发回滚:
java.sql.SQLException - SQL异常不触发回滚
com.example.NoRollbackException - 特定不回滚异常

配置AOP

<aop:config>
<!-- 设置切入点:定义需要被拦截的方法 -->
<aop:pointcut expression="execution(* com.xxxx.service..*.*(..))" id="cut" />
<!-- 设置通知:将事务通知应用到切入点 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="cut"/>
</aop:config>

初现端倪了,这仔细一看还是AOP动态代理那一块

注解实现事务管理

配置事务管理器

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>

配置注解支持

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

添加事务注解

在需要配置事务管理的方法上加入事务注解

@Transactional(propagation=Propagation.REQUIRED)
public void saveUser(String userName, String userPwd) {

}

@Transactional注解具有XML配置中列举的所有属性,可以根据需求自己配置