以下内容转载和参考自 使用JDBC - 廖雪峰的官方网站
1、Spring JDBC
Spring中包含支持JDBC的数据访问模块,其提供了简化的JDBC操作,不必手动释放资源,通过使用JdbcTemplate。如下所示,我们通过IoC创建并管理一个DataSource实例(DataSource使用HikariCP),一个JdbcTemplate实例,,然后就可以通过JdbcTemplate来执行SQL操作:
@Configuration
@ComponentScan
@PropertySource("jdbc.properties") //读取数据库配置文件jdbc.properties
public class AppConfig {
@Value("${jdbc.url}") //获取配置文件中jdbc.url属性值到jdbcUrl
String jdbcUrl;
@Value("${jdbc.username}") //获取配置文件中jdbc.username属性值到jdbcUsername
String jdbcUsername;
@Value("${jdbc.password}") //获取配置文件中jdbc.password属性值到jdbcPassword
String jdbcPassword;
@Bean //提供给IoC创建DataSource对象的方法
DataSource createDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcUrl);
config.setUsername(jdbcUsername);
config.setPassword(jdbcPassword);
config.addDataSourceProperty("autoCommit", "true");
config.addDataSourceProperty("connectionTimeout", "5");
config.addDataSourceProperty("idleTimeout", "60");
return new HikariDataSource(config);
}
@Bean //提供给IoC创建JdbcTemplate对象的方法
JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
配置文件jdbc.properties:
# 数据库文件名为testdb:
jdbc.url=jdbc:hsqldb:file:testdb
# Hsqldb默认的用户名是sa,口令是空字符串:
jdbc.username=sa
jdbc.password=
因为我们要使用@PostConstruct注解,以及使用的JDBC连接池是HikariCP,所以除了Spring和JDBC依赖外,还需要Annotation和HikariCP相关依赖。另外我们需要添加使用的MySQL或者Oracle等数据库的驱动依赖,我们这里使用的是HSQLDB这个数据库:
<dependencies>
<!--Spring Context-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!--Spring JDBC-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!--@PostConstruct注解-->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<!--JDBC连接池-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.2</version>
</dependency>
<!--JDBC驱动-->
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.5.0</version>
</dependency>
</dependencies>
在需要访问数据库的类中,注入JdbcTemplate,然后可以通过JdbcTemplate的queryForObject()来执行SQL查询、update()来执行插入、更新和删除操作:
@Component
public class DatabaseOper {
@Autowired
JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
//创建数据库表
//int update(String sql) 返回值为生效的行数
jdbcTemplate.update("CREATE TABLE IF NOT EXISTS users (" //
+ "id BIGINT IDENTITY NOT NULL PRIMARY KEY, " //
+ "email VARCHAR(100) NOT NULL, " //
+ "password VARCHAR(100) NOT NULL, " //
+ "name VARCHAR(100) NOT NULL, " //
+ "UNIQUE (email))");
}
public void updateUser(User user) {
//int update(String sql, Object... args/*SQL语句参数*/) 返回值为更新的行数
if (1 != jdbcTemplate.update("UPDATE users SET name = ? WHERE id=?", user.getName(), user.getId())) {
throw new RuntimeException("User not found by id");
}
}
public User getUserByEmail(String email) {
User user = null;
//T queryForObject(String sql /*SQL语句*/, Object[] args/*SQL语句的参数,不需要可以为null*/, RowMapper<T> rowMapper/*包含结果的Lambda表达式*/)
user = jdbcTemplate.queryForObject("SELECT * FROM users WHERE email = ?", new Object[] { email },
(ResultSet rs/*结果集*/, int rowNum/*行号?行数?*/) -> {
//Lambda表达式可以返回一个对象
return new User(
rs.getLong("id"), // id
rs.getString("email"), // email
rs.getString("password"), // password
rs.getString("name")); // name
});
return user;
}
}
如果SQL查询结果的结构恰好和JavaBean的属性名称一致,那么使用BeanPropertyRowMapper就可以直接把一行记录按列名转换为JavaBean。如果数据库表中列名与JavaBean不一致的话,也可以在查询的时候指定列的别名与JavaBean中一致,如"SELECT id, email, office_address AS workAddress, name FROM users WHERE email = ?"。
public User getUserByEmail(String email) {
User user = null;
user = jdbcTemplate.queryForObject("SELECT * FROM users WHERE email = ?", new Object[] { email },
new BeanPropertyRowMapper<>(User.class));
return user;
}
如果SQL查询返回的是多行记录,可以使用query()方法:
public List<User> getUsers(int pageIndex) {
int limit = 100;
int offset = limit * (pageIndex - 1);
//<T> List<T> query(String sql, Object[] args, RowMapper<T> rowMapper)
return jdbcTemplate.query("SELECT * FROM users LIMIT ? OFFSET ?", new Object[] { limit, offset },
new BeanPropertyRowMapper<>(User.class));
}
可以看到,使用Spring JDBC可以直接使用JdbcTemplate来进行SQL操作,不用再使用Connection、Statement / PreparedStatementJdbcTemplate对象,如果我们需要自己手动操作Jdbc的Connection或PreparedStatement来执行SQL语句的话,可以使用jdbcTemplate的execute()方法:
public User getUserById(long id) {
//<T> T execute(ConnectionCallback<T> action)
return jdbcTemplate.execute((Connection conn) -> {
//回调结束后Connection自动释放,在回调中自己创建的PreparedStatement、ResultSet必须用try(...)手动释放
try (var ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
ps.setObject(1, id);
try (var rs = ps.executeQuery()) {
if (rs.next()) {
return new User(
rs.getLong("id"),
rs.getString("email"),
rs.getString("password"),
rs.getString("name"));
}
throw new RuntimeException("user not found by id.");
}
}
});
}
public User getUserByName(String name) {
//<T> T execute(String sql, PreparedStatementCallback<T> action)
return jdbcTemplate.execute("SELECT * FROM users WHERE name = ?", (PreparedStatement ps) -> {
// PreparedStatement会在回调后自动释放,在回调中自己创建的ResultSet必须用try(...)手动释放
ps.setObject(1, name);
try (var rs = ps.executeQuery()) {
if (rs.next()) {
return new User(
rs.getLong("id"),
rs.getString("email"),
rs.getString("password"),
rs.getString("name"));
}
throw new RuntimeException("user not found by id.");
}
});
}
如果某一列是自增列(例如自增主键),我们需要获取其插入后的自增值的话,那么JdbcTemplate提供了一个KeyHolder来进行这一操作:
public User register(String email, String password, String name) {
KeyHolder holder = new GeneratedKeyHolder(); // 创建一个KeyHolder
//int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)
int iRet = jdbcTemplate.update((conn) -> {
var ps = conn.prepareStatement("INSERT INTO users(email,password,name) VALUES(?,?,?)",
Statement.RETURN_GENERATED_KEYS); // 创建PreparedStatement时,必须指定RETURN_GENERATED_KEYS
ps.setObject(1, email);
ps.setObject(2, password);
ps.setObject(3, name);
return ps;
}, holder);
if(iRet != 1){
throw new RuntimeException("Insert failed.");
}
// 从KeyHolder中获取返回的自增值
return new User(holder.getKey().longValue(), email, password, name);
}
2、使用事务
Spring提供了一个PlatformTransactionManager来表示事务管理器,而事务由TransactionStatus表示,Spring除了提供标准JDBC事务外,还支持分布式事务JTA(Java Transaction API),如果仅使用JDBC事务的话,事务管理器的实际类型是DataSourceTransactionManager:
@Configuration
@ComponentScan
@PropertySource("jdbc.properties")
public class AppConfig {
......
@Bean
PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
@Component
public class Test {
@Autowired
private PlatformTransactionManager txManager;
@Autowired
JdbcTemplate jdbcTemplate;
public void func(){
TransactionStatus tx = null;
try {
tx = txManager.getTransaction(new DefaultTransactionDefinition()); // 开启事务
jdbcTemplate.update("...");
jdbcTemplate.update("...");
txManager.commit(tx); // 提交事务
} catch (RuntimeException e) {
txManager.rollback(tx); // 回滚事务
throw e;
}
}
}
也可以使用声明式事务来简化事务代码,对需要成为事务的方法或者所有public方法都需要成为事务方法的类,使用@EnableTransactionManagement和@Transactional注解即可。
@Configuration
@ComponentScan
@EnableTransactionManagement // 启用声明式事务
@PropertySource("jdbc.properties")
public class AppConfig {
......
@Bean
PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
Component
public class UserService {
@Transactional // 此public方法自动具有事务支持
public User register(String email, String password, String name) {
...
}
}
@Component
@Transactional // 此类的所有public方法自动具有事务支持
public class UserService {
...
}
声明式事务的原理仍然是AOP代理,即通过自动创建Bean的Proxy实现:
public class UserService$$EnhancerBySpringCGLIB extends UserService {
UserService target;
PlatformTransactionManager txManager;
......
public User register(String email, String password, String name) {
TransactionStatus tx = null;
try {
tx = txManager.getTransaction(new DefaultTransactionDefinition());
target.register(email, password, name);
txManager.commit(tx);
} catch (RuntimeException e) {
txManager.rollback(tx);
throw e;
}
}
......
}
在事务方法中如果想要手动回滚事务的话,抛出RuntimeException或其子类对象即可,如果要针对Checked Exception回滚事务,需要在@Transactional注解中指定:
@Transactional(rollbackFor = {RuntimeException.class, IOException.class}) //在抛出RuntimeException或IOException时,事务将回滚
public buyProducts(long productId, int num) {
...
if (store < num) {
// 库存不够,购买失败,回滚事务:
throw new IllegalArgumentException("No enough products");
}
...
}
如果一个方法A是事务方法,在A中又调用了一个事务方法B,那么默认情况下B事务方法会融合到A事务方法中,即相当于B方法中没有了事务。我们也可以改变事务的传播模型,通过@Transactional注解,默认的事务传播级别是REQUIRED,
还有一些其他的传播级别:
REQUIRED
:如果当前没有事务,就创建一个新事务,如果当前有事务,就加入到当前事务中执行。
SUPPORTS
:表示如果有事务,就加入到当前事务,如果没有,那也不开启事务执行。这种传播级别可用于查询方法,因为SELECT语句既可以在事务内执行,也可以不需要事务;
MANDATORY
:表示必须要存在当前事务并加入执行,否则将抛出异常。这种传播级别可用于核心更新逻辑,比如用户余额变更,它总是被其他事务方法调用,不能直接由非事务方法调用;
REQUIRES_NEW
:表示不管当前有没有事务,都必须开启一个新的事务执行。如果当前已经有事务,那么当前事务会挂起,等新事务完成后,再恢复执行;
NOT_SUPPORTED
:表示不支持事务,如果当前有事务,那么当前事务会挂起,等这个方法执行完成后,再恢复执行;
NEVER
:和NOT_SUPPORTED
相比,它不但不支持事务,而且在监测到当前有事务时,会抛出异常拒绝执行;
NESTED
:表示如果当前有事务,则开启一个嵌套级别事务,如果当前没有事务,则开启一个新事务。
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Product A() {
...
B()
...
}
如果在A事务方法中使用另一个线程来执行B方法,那么将分别开启A和B两个完全独立的事务,即事务的默认传播模型或者我们设置的传播模型会失效。因此,事务能正确传播的前提是,方法调用是在一个线程内才行,换句话说,事务只能在当前线程传播,无法跨线程传播:
@Transactional
public void A() {
...
new Thread(() -> {
B();
}).start();
...
}
3、DAO
编写数据访问层的时候,可以使用DAO模式,DAO即Data Access Object的缩写,如下针对数据库中的用户表Users编写了一个DAO模式的类:
public class User{ //对应Users表中一条记录
public int id;
public string name;
public int age;
...
}
public class UserDao { //Users表操作类
@Autowired
JdbcTemplate jdbcTemplate;
User getUserById(long id) {
...
}
List<User> getUsers(int page) {
...
}
User createUser(User user) {
...
}
User updateUser(User user) {
...
}
void deleteUser(User user) {
...
}
}
Spring提供了一个JdbcDaoSupport类,用于简化DAO的实现:
ublic abstract class JdbcDaoSupport extends DaoSupport {
private JdbcTemplate jdbcTemplate;
public final void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
initTemplateConfig();
}
public final JdbcTemplate getJdbcTemplate() {
return this.jdbcTemplate;
}
...
}
public class UserDao extends JdbcDaoSupport {
@Autowired
private JdbcTemplate jdbcTemplate;
@PostConstruct
public UserDao() {
super.setJdbcTemplate(jdbcTemplate);
}
public User getUserById(long id) {
return getJdbcTemplate().queryForObject(
"SELECT * FROM users WHERE id = ?",
new BeanPropertyRowMapper<>(User.class),
id);
}
......
}
我们可以编写一个样板类,有需要的类从这个样板类继承即拥有了样板类中的SQL操作方法:
public abstract class AbstractDao<T> extends JdbcDaoSupport { //泛型T应该传入记录类的类型
private String table; //表名
private RowMapper<T> rowMapper; //保存查询结果
public AbstractDao() {
Class<T> entityClass = getParameterizedType(); // 获取当前泛型T的类型
this.table = entityClass.getSimpleName().toLowerCase() + "s"; //数据库中的表名是"记录类名称 + s"
this.rowMapper = new BeanPropertyRowMapper<>(entityClass);
}
public T getById(long id) {
return getJdbcTemplate().queryForObject("SELECT * FROM " + table + " WHERE id = ?", this.rowMapper, id);
}
public void deleteById(long id) {
getJdbcTemplate().update("DELETE FROM " + table + " WHERE id = ?", id);
}
...
private Class getParameterizedType(){
Type type = getClass().getGenericSuperclass();
Type[] types = ((ParameterizedType)type).getActualTypeArguments();
Class<T> clazz = (Class<T>) types[0];
return clazz;
}
}
public class User{
public int id;
public string name;
public int age;
...
}
@Component
@Transactional
public class UserDao extends AbstractDao<User> {
// 自动有了下列方法:
// User getById(long)
// void deleteById(long)
//...
}
public class Book{
public int id;
public string name;
public int pageNum;
...
}
@Component
@Transactional
public class BookDao extends AbstractDao<Book> {
// 自动有了下列方法:
// Book getById(long)
// void deleteById(long)
//...
}
4、总结
通过Spring JDBC,只需要创建一个JdbcTemplate就可以通过它来执行SQL操作。
使用Spring JDBC进行SQL查询的话,可以将用来保存查询结果的JavaBean中的属性名(成员名)设置成与数据库表中列名一致,这样可以直接将查询结果保存到JavaBean对象中。
Spring JDBC支持事务操作。
DAO模式即根据一定规则编写访问数据库表的类。