数据库基本上是现代应用程序的标准存储,绝大多数程序都把自己的业务数据存储在关系数据库中,可见,访问数据库几乎是所有应用程序必备能力。我们在前面已经介绍了Java程序访问数据库的标准接口JDBC,它的实现方式非常简洁,即:Java标准库定义接口,各数据库厂商以“驱动”的形式实现接口。 应用程序要使用哪个数据库,就把该数据库厂商的驱动以jar包形式引入进来,同时自身仅使用JDBC接口,编译期并不需要特定厂商的驱动。
使用JDBC虽然简单,但代码比较繁琐。Spring为了简化数据库访问,主要做了以下几点工作:
提供了简化的访问JDBC的模板类,不必手动释放资源
提供了一个统一的DAO类以实现Data Access Object模式
把SQLException
封装为DataAccessException
,这个异常是一个RuntimeException
,并且让我们能区分SQL异常的原因,例如,DuplicateKeyException
表示违反了一个唯一约束
能方便地集成Hibernate、JPA和MyBatis这些数据库访问框架
本章我们将详细讲解在Spring中访问数据库的最佳实践。
使用JDBC Java程序使用JDBC接口访问关系数据库的时候,需要以下几步:
创建全局DataSource
实例,表示数据库连接池;
在需要读写数据库的方法内部,按如下步骤访问数据库:
从全局DataSource
实例获取Connection
实例;
通过Connection
实例创建PreparedStatement
实例;
执行SQL语句,如果是查询,则通过ResultSet
读取结果集,如果是修改,则获得int
结果。
正确编写JDBC代码的关键是使用try ... finally
释放资源,涉及到事务的代码需要正确提交或回滚事务。
在Spring使用JDBC,首先我们通过IoC容器创建并管理一个DataSource
实例,然后,Spring提供了一个JdbcTemplate
,可以方便地让我们操作JDBC,因此,通常情况下,我们会实例化一个JdbcTemplate
。顾名思义,这个类主要使用了Template模式。
编写示例代码或者测试代码时,我们强烈推荐使用HSQLDB 这个数据库,它是一个用Java编写的关系数据库,可以以内存模式或者文件模式运行,本身只有一个jar包,非常适合演示代码或者测试代码。
我们以实际工程为例,先创建Maven工程spring-data-jdbc
,然后引入以下依赖:
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 <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.2.0.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 5.2.0.RELEASE</version > </dependency > <dependency > <groupId > javax.annotation</groupId > <artifactId > javax.annotation-api</artifactId > <version > 1.3.2</version > </dependency > <dependency > <groupId > com.zaxxer</groupId > <artifactId > HikariCP</artifactId > <version > 3.4.2</version > </dependency > <dependency > <groupId > org.hsqldb</groupId > <artifactId > hsqldb</artifactId > <version > 2.5.0</version > </dependency > </dependencies >
在AppConfig中,我们需要创建以下几个必须的Bean:
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 @Configuration @ComponentScan @PropertySource("jdbc.properties") public class AppConfig { @Value("${jdbc.url}") String jdbcUrl; @Value("${jdbc.username}") String jdbcUsername; @Value("${jdbc.password}") String jdbcPassword; @Bean 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 JdbcTemplate createJdbcTemplate (@Autowired DataSource dataSource) { return new JdbcTemplate(dataSource); } }
在上述配置中:
通过@PropertySource("jdbc.properties")
读取数据库配置文件
通过@Value("${jdbc.url}")
注入配置文件的相关配置
创建一个DataSource实例,它的实际类型是HikariDataSource
,创建时需要用到注入的配置
创建一个JdbcTemplate实例,它需要注入DataSource
,这是通过方法参数完成注入的
最后,针对HSQLDB写一个配置文件jdbc.properties
:
1 2 3 4 5 6 # 数据库文件名为testdb: jdbc.url=jdbc:hsqldb:file:testdb # Hsqldb默认的用户名是sa,口令是空字符串: jdbc.username=sa jdbc.password=
可以通过HSQLDB自带的工具来初始化数据库表,这里我们写一个Bean,在Spring容器启动时自动创建一个users
表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component public class DatabaseInitializer { @Autowired JdbcTemplate jdbcTemplate; @PostConstruct public void init () { 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))" ); } }
现在,所有准备工作都已完毕。我们只需要在需要访问数据库的Bean中,注入JdbcTemplate
即可:
1 2 3 4 5 6 @Component public class UserService { @Autowired JdbcTemplate jdbcTemplate; ... }
JdbcTemplate的用法 Spring提供的JdbcTemplate
采用Template模式,提供了一系列以回调为特点的工具方法,目的是避免繁琐的try...catch
语句。
我们以具体的示例来说明JdbcTemplate的用法。
首先我们看T execute(ConnectionCallback<T> action)
方法,它提供了Jdbc的Connection
供我们使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public User getUserById (long id) { return jdbcTemplate.execute((Connection conn) -> { 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." ); } } }); }
也就是说,上述回调方法允许获取Connection,然后做任何基于Connection的操作。
我们再看T execute(String sql, PreparedStatementCallback<T> action)
的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public User getUserByName (String name) { return jdbcTemplate.execute("SELECT * FROM users WHERE name = ?" , (PreparedStatement ps) -> { 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." ); } }); }
最后,我们看T queryForObject(String sql, Object[] args, RowMapper<T> rowMapper)
方法:
1 2 3 4 5 6 7 8 9 10 11 12 public User getUserByEmail (String email) { return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email = ?" , new Object[] { email }, (ResultSet rs, int rowNum) -> { return new User( rs.getLong("id" ), rs.getString("email" ), rs.getString("password" ), rs.getString("name" )); }); }
在queryForObject()
方法中,传入SQL以及SQL参数后,JdbcTemplate
会自动创建PreparedStatement
,自动执行查询并返回ResultSet
,我们提供的RowMapper
需要做的事情就是把ResultSet
的当前行映射成一个JavaBean并返回。整个过程中,使用Connection
、PreparedStatement
和ResultSet
都不需要我们手动管理。
RowMapper
不一定返回JavaBean,实际上它可以返回任何Java对象。例如,使用SELECT COUNT(*)
查询时,可以返回Long
:
1 2 3 4 5 6 public long getUsers () { return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users" , null , (ResultSet rs, int rowNum) -> { return rs.getLong(1 ); }); }
如果我们期望返回多行记录,而不是一行,可以用query()
方法:
1 2 3 4 5 6 public List<User> getUsers (int pageIndex) { int limit = 100 ; int offset = limit * (pageIndex - 1 ); return jdbcTemplate.query("SELECT * FROM users LIMIT ? OFFSET ?" , new Object[] { limit, offset }, new BeanPropertyRowMapper<>(User.class)); }
上述query()
方法传入的参数仍然是SQL、SQL参数以及RowMapper
实例。这里我们直接使用Spring提供的BeanPropertyRowMapper
。如果数据库表的结构恰好和JavaBean的属性名称一致,那么BeanPropertyRowMapper
就可以直接把一行记录按列名转换为JavaBean。
如果我们执行的不是查询,而是插入、更新和删除操作,那么需要使用update()
方法:
1 2 3 4 5 6 public void updateUser (User user) { if (1 != jdbcTemplate.update("UPDATE user SET name = ? WHERE id=?" , user.getName(), user.getId())) { throw new RuntimeException("User not found by id" ); } }
只有一种INSERT
操作比较特殊,那就是如果某一列是自增列(例如自增主键),通常,我们需要获取插入后的自增值。JdbcTemplate
提供了一个KeyHolder
来简化这一操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public User register (String email, String password, String name) { KeyHolder holder = new GeneratedKeyHolder(); if (1 != jdbcTemplate.update( (conn) -> { var ps = conn.prepareStatement("INSERT INTO users(email,password,name) VALUES(?,?,?)" , Statement.RETURN_GENERATED_KEYS); ps.setObject(1 , email); ps.setObject(2 , password); ps.setObject(3 , name); return ps; }, holder) ) { throw new RuntimeException("Insert failed." ); } return new User(holder.getKey().longValue(), email, password, name); }
JdbcTemplate
还有许多重载方法,这里我们不一一介绍。需要强调的是,JdbcTemplate
只是对JDBC操作的一个简单封装,它的目的是尽量减少手动编写try(resource) {...}
的代码。对于查询,主要通过RowMapper
实现了JDBC结果集到Java对象的转换。
我们总结一下JdbcTemplate
的用法,那就是:
针对简单查询,优选query()
和queryForObject()
,因为只需提供SQL语句、参数和RowMapper
;
针对更新操作,优选update()
,因为只需提供SQL语句和参数;
任何复杂的操作,最终也可以通过execute(ConnectionCallback)
实现,因为拿到Connection
就可以做任何JDBC操作。
实际上我们使用最多的仍然是各种查询。如果在设计表结构的时候,能够和JavaBean的属性一一对应,那么直接使用BeanPropertyRowMapper
就很方便。如果表结构和JavaBean不一致怎么办?那就需要稍微改写一下查询,使结果集的结构和JavaBean保持一致。
例如,表的列名是office_address
,而JavaBean属性是workAddress
,就需要指定别名,改写查询如下:
1 SELECT id, email, office_address AS workAddress, name FROM users WHERE email = ?
小结 Spring提供了JdbcTemplate
来简化JDBC操作;
使用JdbcTemplate
时,根据需要优先选择高级方法;
任何JDBC操作都可以使用保底的execute(ConnectionCallback)
方法。
使用声明式事务 使用Spring操作JDBC虽然方便,但是我们在前面讨论JDBC的时候,讲到过JDBC事务,如果要在Spring中操作事务,没必要手写JDBC事务,可以使用Spring提供的高级接口来操作事务。
Spring提供了一个PlatformTransactionManager
来表示事务管理器,所有的事务都由它负责管理。而事务由TransactionStatus
表示。如果手写事务代码,使用try...catch
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 TransactionStatus tx = null ;try { tx = txManager.getTransaction(new DefaultTransactionDefinition()); jdbcTemplate.update("..." ); jdbcTemplate.update("..." ); txManager.commit(tx); } catch (RuntimeException e) { txManager.rollback(tx); throw e; }
Spring为啥要抽象出PlatformTransactionManager
和TransactionStatus
?原因是JavaEE除了提供JDBC事务外,它还支持分布式事务JTA(Java Transaction API)。分布式事务是指多个数据源(比如多个数据库,多个消息系统)要在分布式环境下实现事务的时候,应该怎么实现。分布式事务实现起来非常复杂,简单地说就是通过一个分布式事务管理器实现两阶段提交,但本身数据库事务就不快,基于数据库事务实现的分布式事务就慢得难以忍受,所以使用率不高。
Spring为了同时支持JDBC和JTA两种事务模型,就抽象出PlatformTransactionManager
。因为我们的代码只需要JDBC事务,因此,在AppConfig
中,需要再定义一个PlatformTransactionManager
对应的Bean,它的实际类型是DataSourceTransactionManager
:
1 2 3 4 5 6 7 8 9 10 @Configuration @ComponentScan @PropertySource("jdbc.properties") public class AppConfig { ... @Bean PlatformTransactionManager createTxManager (@Autowired DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
使用编程的方式使用Spring事务仍然比较繁琐,更好的方式是通过声明式事务来实现。使用声明式事务非常简单,除了在AppConfig
中追加一个上述定义的PlatformTransactionManager
外,再加一个@EnableTransactionManagement
就可以启用声明式事务:
1 2 3 4 5 6 7 @Configuration @ComponentScan @EnableTransactionManagement @PropertySource("jdbc.properties") public class AppConfig { ... }
然后,对需要事务支持的方法,加一个@Transactional
注解:
1 2 3 4 5 6 7 8 @Component public class UserService { @Transactional public User register (String email, String password, String name) { ... } }
或者更简单一点,直接在Bean的class
处加上,表示所有public
方法都具有事务支持:
1 2 3 4 5 @Component @Transactional public class UserService { ... }
Spring对一个声明式事务的方法,如何开启事务支持?原理仍然是AOP代理,即通过自动创建Bean的Proxy实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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; } } ... }
注意:声明了@EnableTransactionManagement
后,不必额外添加@EnableAspectJAutoProxy
。
回滚事务 默认情况下,如果发生了RuntimeException
,Spring的声明式事务将自动回滚。在一个事务方法中,如果程序判断需要回滚事务,只需抛出RuntimeException
,例如:
1 2 3 4 5 6 7 8 9 @Transactional public buyProducts (long productId, int num) { ... if (store < num) { throw new IllegalArgumentException("No enough products" ); } ... }
如果要针对Checked Exception回滚事务,需要在@Transactional
注解中写出来:
1 2 3 4 @Transactional(rollbackFor = {RuntimeException.class, IOException.class}) public buyProducts (long productId, int num) throws IOException { ... }
上述代码表示在抛出RuntimeException
或IOException
时,事务将回滚。
为了简化代码,我们强烈建议业务异常体系从RuntimeException
派生,这样就不必声明任何特殊异常即可让Spring的声明式事务正常工作:
1 2 3 4 5 6 7 8 9 10 11 public class BusinessException extends RuntimeException { ... }public class LoginException extends BusinessException { ... }public class PaymentException extends BusinessException { ... }
事务边界 在使用事务的时候,明确事务边界非常重要。对于声明式事务,例如,下面的register()
方法:
1 2 3 4 5 6 7 @Component public class UserService { @Transactional public User register (String email, String password, String name) { ... } }
它的事务边界就是register()
方法开始和结束。
类似的,一个负责给用户增加积分的addBonus()
方法:
1 2 3 4 5 6 7 @Component public class BonusService { @Transactional public void addBonus (long userId, int bonus) { ... } }
它的事务边界就是addBonus()
方法开始和结束。
在现实世界中,问题总是要复杂一点点。用户注册后,能自动获得100积分,因此,实际代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Component public class UserService { @Autowired BonusService bonusService; @Transactional public User register (String email, String password, String name) { User user = jdbcTemplate.insert("..." ); bonusService.addBonus(user.id, 100 ); } }
现在问题来了:调用方(比如RegisterController
)调用UserService.register()
这个事务方法,它在内部又调用了BonusService.addBonus()
这个事务方法,一共有几个事务?如果addBonus()
抛出了异常需要回滚事务,register()
方法的事务是否也要回滚?
问题的复杂度是不是一下子提高了10倍?
事务传播 要解决上面的问题,我们首先要定义事务的传播模型。
假设用户注册的入口是RegisterController
,它本身没有事务,仅仅是调用UserService.register()
这个事务方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Controller public class RegisterController { @Autowired UserService userService; @PostMapping("/register") public ModelAndView doRegister (HttpServletRequest req) { String email = req.getParameter("email" ); String password = req.getParameter("password" ); String name = req.getParameter("name" ); User user = userService.register(email, password, name); return ... } }
因此,UserService.register()
这个事务方法的起始和结束,就是事务的范围。
我们需要关心的问题是,在UserService.register()
这个事务方法内,调用BonusService.addBonus()
,我们期待的事务行为是什么:
1 2 3 4 5 6 7 @Transactional public User register (String email, String password, String name) { User user = jdbcTemplate.insert("..." ); bonusService.addBonus(user.id, 100 ); }
对于大多数业务来说,我们期待BonusService.addBonus()
的调用,和UserService.register()
应当融合在一起,它的行为应该如下:
UserService.register()
已经开启了一个事务,那么在内部调用BonusService.addBonus()
时,BonusService.addBonus()
方法就没必要再开启一个新事务,直接加入到BonusService.register()
的事务里就好了。
其实就相当于:
UserService.register()
先执行了一条INSERT语句:INSERT INTO users ...
BonusService.addBonus()
再执行一条INSERT语句:INSERT INTO bonus ...
因此,Spring的声明式事务为事务传播定义了几个级别,默认传播级别就是REQUIRED,它的意思是,如果当前没有事务,就创建一个新事务,如果当前有事务,就加入到当前事务中执行。
我们观察UserService.register()
方法,它在RegisterController
中执行,因为RegisterController
没有事务,因此,UserService.register()
方法会自动创建一个新事务。
在UserService.register()
方法内部,调用BonusService.addBonus()
方法时,因为BonusService.addBonus()
检测到当前已经有事务了,因此,它会加入到当前事务中执行。
因此,整个业务流程的事务边界就清晰了:它只有一个事务,并且范围就是UserService.register()
方法。
有的童鞋会问:把BonusService.addBonus()
方法的@Transactional
去掉,变成一个普通方法,那不就规避了复杂的传播模型吗?
去掉BonusService.addBonus()
方法的@Transactional
,会引来另一个问题,即其他地方如果调用BonusService.addBonus()
方法,那就没法保证事务了。例如,规定用户登录时积分+5:
1 2 3 4 5 6 7 8 9 10 11 @Controller public class LoginController { @Autowired BonusService bonusService; @PostMapping("/login") public ModelAndView doLogin (HttpServletRequest req) { User user = ... bonusService.addBonus(user.id, 5 ); } }
可见,BonusService.addBonus()
方法必须要有@Transactional
,否则,登录后积分就无法添加了。
默认的事务传播级别是REQUIRED
,它满足绝大部分的需求。还有一些其他的传播级别:
SUPPORTS
:表示如果有事务,就加入到当前事务,如果没有,那也不开启事务执行。这种传播级别可用于查询方法,因为SELECT语句既可以在事务内执行,也可以不需要事务;
MANDATORY
:表示必须要存在当前事务并加入执行,否则将抛出异常。这种传播级别可用于核心更新逻辑,比如用户余额变更,它总是被其他事务方法调用,不能直接由非事务方法调用;
REQUIRES_NEW
:表示不管当前有没有事务,都必须开启一个新的事务执行。如果当前已经有事务,那么当前事务会挂起,等新事务完成后,再恢复执行;
NOT_SUPPORTED
:表示不支持事务,如果当前有事务,那么当前事务会挂起,等这个方法执行完成后,再恢复执行;
NEVER
:和NOT_SUPPORTED
相比,它不但不支持事务,而且在监测到当前有事务时,会抛出异常拒绝执行;
NESTED
:表示如果当前有事务,则开启一个嵌套级别事务,如果当前没有事务,则开启一个新事务。
上面这么多种事务的传播级别,其实默认的REQUIRED
已经满足绝大部分需求,SUPPORTS
和REQUIRES_NEW
在少数情况下会用到,其他基本不会用到,因为把事务搞得越复杂,不仅逻辑跟着复杂,而且速度也会越慢。
定义事务的传播级别也是写在@Transactional
注解里的:
1 2 3 4 @Transactional(propagation = Propagation.REQUIRES_NEW) public Product createProduct () { ... }
现在只剩最后一个问题了:Spring是如何传播事务的?
我们在JDBC中使用事务 的时候,是这么个写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Connection conn = openConnection();try { conn.setAutoCommit(false ); insert(); update(); delete(); conn.commit(); } catch (SQLException e) { conn.rollback(); } finally { conn.setAutoCommit(true ); conn.close(); }
Spring使用声明式事务,最终也是通过执行JDBC事务来实现功能的,那么,一个事务方法,如何获知当前是否存在事务?
答案是使用ThreadLocal 。Spring总是把JDBC相关的Connection
和TransactionStatus
实例绑定到ThreadLocal
。如果一个事务方法从ThreadLocal
未取到事务,那么它会打开一个新的JDBC连接,同时开启一个新的事务,否则,它就直接使用从ThreadLocal
获取的JDBC连接以及TransactionStatus
。
因此,事务能正确传播的前提是,方法调用是在一个线程内才行。如果像下面这样写:
1 2 3 4 5 6 7 8 9 @Transactional public User register (String email, String password, String name) { User user = jdbcTemplate.insert("..." ); new Thread(() -> { bonusService.addBonus(user.id, 100 ); }).start(); }
在另一个线程中调用BonusService.addBonus()
,它根本获取不到当前事务,因此,UserService.register()
和BonusService.addBonus()
两个方法,将分别开启两个完全独立的事务。
换句话说,事务只能在当前线程传播,无法跨线程传播。
那如果我们想实现跨线程传播事务呢?原理很简单,就是要想办法把当前线程绑定到ThreadLocal
的Connection
和TransactionStatus
实例传递给新线程,但实现起来非常复杂,根据异常回滚更加复杂,不推荐自己去实现。
Spring提供的声明式事物极大地方便了在数据库中使用事务,正确使用声明式事务的关键在于确定好事务边界,理解事物传播级别。
使用DAO 在传统的多层应用程序中,通常是Web层调用业务层,业务层调用数据访问层。业务层负责处理各种业务逻辑,而数据访问层只负责对数据进行增删改查。因此,实现数据访问层就是用JdbcTemplate
实现对数据库的操作。
编写数据访问层的时候,可以使用DAO模式。DAO即Data Access Object的缩写,它没有什么神秘之处,实现起来基本如下:
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 public class UserDao { @Autowired JdbcTemplate jdbcTemplate; User getById (long id) { ... } List<User> getUsers (int page) { ... } User createUser (User user) { ... } User updateUser (User user) { ... } void deleteUser (User user) { ... } }
Spring提供了一个JdbcDaoSupport
类,用于简化DAO的实现。这个JdbcDaoSupport
没什么复杂的,核心代码就是持有一个JdbcTemplate
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public 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; } ... }
它的意图是子类直接从JdbcDaoSupport
继承后,可以随时调用getJdbcTemplate()
获得JdbcTemplate
的实例。那么问题来了:因为JdbcDaoSupport
的jdbcTemplate
字段没有标记@Autowired
,所以,子类想要注入JdbcTemplate
,还得自己想个办法:
1 2 3 4 5 6 7 8 9 10 11 @Component @Transactional public class UserDao extends JdbcDaoSupport { @Autowired JdbcTemplate jdbcTemplate; @PostConstruct public void init () { super .setJdbcTemplate(jdbcTemplate); } }
有的童鞋可能看出来了:既然UserDao
都已经注入了JdbcTemplate
,那再把它放到父类里,通过getJdbcTemplate()
访问岂不是多此一举?
如果使用传统的XML配置,并不需要编写@Autowired JdbcTemplate jdbcTemplate
,但是考虑到现在基本上是使用注解的方式,我们可以编写一个AbstractDao
,专门负责注入JdbcTemplate
:
1 2 3 4 5 6 7 8 9 public abstract class AbstractDao extends JdbcDaoSupport { @Autowired private JdbcTemplate jdbcTemplate; @PostConstruct public void init () { super .setJdbcTemplate(jdbcTemplate); } }
这样,子类的代码就非常干净,可以直接调用getJdbcTemplate()
:
1 2 3 4 5 6 7 8 9 10 11 12 @Component @Transactional public class UserDao extends AbstractDao { public User getById (long id) { return getJdbcTemplate().queryForObject( "SELECT * FROM users WHERE id = ?" , new BeanPropertyRowMapper<>(User.class), id ); } ... }
倘若肯再多写一点样板代码,就可以把AbstractDao
改成泛型,并实现getById()
,getAll()
,deleteById()
这样的通用方法:
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 public abstract class AbstractDao <T > extends JdbcDaoSupport { private String table; private Class<T> entityClass; private RowMapper<T> rowMapper; public AbstractDao () { this .entityClass = getParameterizedType(); this .table = this .entityClass.getSimpleName().toLowerCase() + "s" ; this .rowMapper = new BeanPropertyRowMapper<>(entityClass); } public T getById (long id) { return getJdbcTemplate().queryForObject("SELECT * FROM " + table + " WHERE id = ?" , this .rowMapper, id); } public List<T> getAll (int pageIndex) { int limit = 100 ; int offset = limit * (pageIndex - 1 ); return getJdbcTemplate().query("SELECT * FROM " + table + " LIMIT ? OFFSET ?" , new Object[] { limit, offset }, this .rowMapper); } public void deleteById (long id) { getJdbcTemplate().update("DELETE FROM " + table + " WHERE id = ?" , id); } ... }
这样,每个子类就自动获得了这些通用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component @Transactional public class UserDao extends AbstractDao <User > { }@Component @Transactional public class BookDao extends AbstractDao <Book > { }
可见,DAO模式就是一个简单的数据访问模式,是否使用DAO,根据实际情况决定,因为很多时候,直接在Service层操作数据库也是完全没有问题的。
集成Hibernate 使用JdbcTemplate
的时候,我们用的最多的方法就是List<T> query(String sql, Object[] args, RowMapper rowMapper)
,这个RowMapper
的作用是把ResultSet的一行记录映射为Java Bean。这种把关系数据库的表记录转换为Java对象的过程就是ORM:Object Relational Mapping 。ORM既可以把行记录转换成行对象,也可以把Java对象转换为行记录。
使用JdbcTemplate配合RowMapper可以看作最原始的ORM,如果要实现更自动化的ORM,可以选择更成熟的ORM框架,例如Hibernate。
我们来看看如何在Spring中集成Hibernate。
Hibernate作为ORM框架,它可以替代JdbcTemplate
,但Hibernate仍然需要JDBC驱动,所以,我们需要引入JDBC驱动、连接池,以及Hibernate本身。在Maven中,我们加入以下依赖项:
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 <dependency > <groupId > org.hsqldb</groupId > <artifactId > hsqldb</artifactId > <version > 2.5.0</version > </dependency > <dependency > <groupId > com.zaxxer</groupId > <artifactId > HikariCP</artifactId > <version > 3.4.2</version > </dependency > <dependency > <groupId > org.hibernate</groupId > <artifactId > hibernate-core</artifactId > <version > 5.4.2.Final</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.2.0.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-orm</artifactId > <version > 5.2.0.RELEASE</version > </dependency >
在AppConfig中,我们仍然需要创建DataSource、引入JDBC配置文件,以及启用声明式事务:
1 2 3 4 5 6 7 8 9 10 @Configuration @ComponentScan @EnableTransactionManagement @PropertySource("jdbc.properties") public class AppConfig { @Bean DataSource createDataSource () { ... } }
为了启用Hibernate,我们需要创建一个LocalSessionFactoryBean
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class AppConfig { @Bean LocalSessionFactoryBean createSessionFactory (@Autowired DataSource dataSource) { var props = new Properties(); props.setProperty("hibernate.hbm2ddl.auto" , "update" ); props.setProperty("hibernate.dialect" , "org.hibernate.dialect.HSQLDialect" ); props.setProperty("hibernate.show_sql" , "true" ); var sessionFactoryBean = new LocalSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setPackagesToScan("com.itranswarp.learnjava.entity" ); sessionFactoryBean.setHibernateProperties(props); return sessionFactoryBean; } }
注意我们在定制Bean 中讲到过FactoryBean
,LocalSessionFactoryBean
是一个FactoryBean
,它会再自动创建一个SessionFactory
,在Hibernate中,Session
是封装了一个JDBC Connection
的实例,而SessionFactory
是封装了JDBC DataSource
的实例,即SessionFactory
持有连接池,每次需要操作数据库的时候,SessionFactory
创建一个新的Session
,相当于从连接池获取到一个新的Connection
。SessionFactory
就是Hibernate提供的最核心的一个对象,但LocalSessionFactoryBean
是Spring提供的为了让我们方便创建SessionFactory
的类。
注意到上面创建LocalSessionFactoryBean
的代码,首先用Properties
持有Hibernate初始化SessionFactory
时用到的所有设置,常用的设置请参考Hibernate文档 ,这里我们只定义了3个设置:
hibernate.hbm2ddl.auto=update
:表示自动创建数据库的表结构,注意不要在生产环境中启用;
hibernate.dialect=org.hibernate.dialect.HSQLDialect
:指示Hibernate使用的数据库是HSQLDB。Hibernate使用一种HQL的查询语句,它和SQL类似,但真正在“翻译”成SQL时,会根据设定的数据库“方言”来生成针对数据库优化的SQL;
hibernate.show_sql=true
:让Hibernate打印执行的SQL,这对于调试非常有用,我们可以方便地看到Hibernate生成的SQL语句是否符合我们的预期。
除了设置DataSource
和Properties
之外,注意到setPackagesToScan()
我们传入了一个package
名称,它指示Hibernate扫描这个包下面的所有Java类,自动找出能映射为数据库表记录的JavaBean。后面我们会仔细讨论如何编写符合Hibernate要求的JavaBean。
紧接着,我们还需要创建HibernateTemplate
以及HibernateTransactionManager
:
1 2 3 4 5 6 7 8 9 10 11 public class AppConfig { @Bean HibernateTemplate createHibernateTemplate (@Autowired SessionFactory sessionFactory) { return new HibernateTemplate(sessionFactory); } @Bean PlatformTransactionManager createTxManager (@Autowired SessionFactory sessionFactory) { return new HibernateTransactionManager(sessionFactory); } }
这两个Bean的创建都十分简单。HibernateTransactionManager
是配合Hibernate使用声明式事务所必须的,而HibernateTemplate
则是Spring为了便于我们使用Hibernate提供的工具类,不是非用不可,但推荐使用以简化代码。
到此为止,所有的配置都定义完毕,我们来看看如何将数据库表结构映射为Java对象。
考察如下的数据库表:
1 2 3 4 5 6 7 8 9 CREATE TABLE user id BIGINT NOT NULL AUTO_INCREMENT, email VARCHAR (100 ) NOT NULL, password VARCHAR (100 ) NOT NULL, name VARCHAR (100 ) NOT NULL, createdAt BIGINT NOT NULL, PRIMARY KEY (`id`) , UNIQUE KEY `email` (`email`) ) ;
其中,id
是自增主键,email
、password
、name
是VARCHAR
类型,email
带唯一索引以确保唯一性,createdAt
存储整型类型的时间戳。用JavaBean表示如下:
1 2 3 4 5 6 7 8 9 10 public class User { private Long id; private String email; private String password; private String name; private Long createdAt; ... }
这种映射关系十分易懂,但我们需要添加一些注解来告诉Hibernate如何把User
类映射到表记录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false, updatable = false) public Long getId () { ... } @Column(nullable = false, unique = true, length = 100) public String getEmail () { ... } @Column(nullable = false, length = 100) public String getPassword () { ... } @Column(nullable = false, length = 100) public String getName () { ... } @Column(nullable = false, updatable = false) public Long getCreatedAt () { ... } }
如果一个JavaBean被用于映射,我们就标记一个@Entity
。默认情况下,映射的表名是user
,如果实际的表名不同,例如实际表名是users
,可以追加一个@Table(name="users")
表示:
1 2 3 4 5 @Entity @Table(name="users) public class User { ... }
每个属性到数据库列的映射用@Column()
标识,nullable
指示列是否允许为NULL
,updatable
指示该列是否允许被用在UPDATE
语句,length
指示String
类型的列的长度(如果没有指定,默认是255
)。
对于主键,还需要用@Id
标识,自增主键再追加一个@GeneratedValue
,以便Hibernate能读取到自增主键的值。
细心的童鞋可能还注意到,主键id
定义的类型不是long
,而是Long
。这是因为Hibernate如果检测到主键为null
,就不会在INSERT
语句中指定主键的值,而是返回由数据库生成的自增值,否则,Hibernate认为我们的程序指定了主键的值,会在INSERT
语句中直接列出。long
型字段总是具有默认值0
,因此,每次插入的主键值总是0,导致除第一次外后续插入都将失败。
createdAt
虽然是整型,但我们并没有使用long
,而是Long
,这是因为使用基本类型会导致某种查询会添加意外的条件,后面我们会详细讨论,这里只需牢记,作为映射使用的JavaBean,所有属性都使用包装类型而不是基本类型。
使用Hibernate时,不要使用基本类型的属性,总是使用包装类型,如Long或Integer。
类似的,我们再定义一个Book
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false, updatable = false) public Long getId () { ... } @Column(nullable = false, length = 100) public String getTitle () { ... } @Column(nullable = false, updatable = false) public Long getCreatedAt () { ... } }
如果仔细观察User
和Book
,会发现它们定义的id
、createdAt
属性是一样的,这在数据库表结构的设计中很常见:对于每个表,通常我们会统一使用一种主键生成机制,并添加createdAt
表示创建时间,updatedAt
表示修改时间等通用字段。
不必在User
和Book
中重复定义这些通用字段,我们可以把它们提到一个抽象类中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @MappedSuperclass public abstract class AbstractEntity { private Long id; private Long createdAt; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false, updatable = false) public Long getId () { ... } @Column(nullable = false, updatable = false) public Long getCreatedAt () { ... } @Transient public ZonedDateTime getCreatedDateTime () { return Instant.ofEpochMilli(this .createdAt).atZone(ZoneId.systemDefault()); } @PrePersist public void preInsert () { setCreatedAt(System.currentTimeMillis()); } }
对于AbstractEntity
来说,我们要标注一个@MappedSuperclass
表示它用于继承。此外,注意到我们定义了一个@Transient
方法,它返回一个“虚拟”的属性。因为getCreatedDateTime()
是计算得出的属性,而不是从数据库表读出的值,因此必须要标注@Transient
,否则Hibernate会尝试从数据库读取名为createdDateTime
这个不存在的字段从而出错。
再注意到@PrePersist
标识的方法,它表示在我们将一个JavaBean持久化到数据库之前(即执行INSERT语句),Hibernate会先执行该方法,这样我们就可以自动设置好createdAt
属性。
有了AbstractEntity
,我们就可以大幅简化User
和Book
:
1 2 3 4 5 6 7 8 9 10 11 12 @Entity public class User extends AbstractEntity { @Column(nullable = false, unique = true, length = 100) public String getEmail () { ... } @Column(nullable = false, length = 100) public String getPassword () { ... } @Column(nullable = false, length = 100) public String getName () { ... } }
注意到使用的所有注解均来自javax.persistence
,它是JPA规范的一部分。这里我们只介绍使用注解的方式配置Hibernate映射关系,不再介绍传统的比较繁琐的XML配置。通过Spring集成Hibernate时,也不再需要hibernate.cfg.xml
配置文件,用一句话总结:
使用Spring集成Hibernate,配合JPA注解,无需任何额外的XML配置。
类似User
、Book
这样的用于ORM的Java Bean,我们通常称之为Entity Bean。
最后,我们来看看如果对user
表进行增删改查。因为使用了Hibernate,因此,我们要做的,实际上是对User
这个JavaBean进行“增删改查”。我们编写一个UserService
,注入HibernateTemplate
以便简化代码:
1 2 3 4 5 6 @Component @Transactional public class UserService { @Autowired HibernateTemplate hibernateTemplate; }
Insert操作 要持久化一个User
实例,我们只需调用save()
方法。以register()
方法为例,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public User register (String email, String password, String name) { User user = new User(); user.setEmail(email); user.setPassword(password); user.setName(name); hibernateTemplate.save(user); System.out.println(user.getId()); return user; }
Delete操作 删除一个User
相当于从表中删除对应的记录。注意Hibernate总是用id
来删除记录,因此,要正确设置User
的id
属性才能正常删除记录:
1 2 3 4 5 6 7 8 public boolean deleteUser (Long id) { User user = hibernateTemplate.get(User.class, id); if (user != null ) { hibernateTemplate.delete(user); return true ; } return false ; }
通过主键删除记录时,一个常见的用法是先根据主键加载该记录,再删除。load()
和get()
都可以根据主键加载记录,它们的区别在于,当记录不存在时,get()
返回null
,而load()
抛出异常。
Update操作 更新记录相当于先更新User
的指定属性,然后调用update()
方法:
1 2 3 4 5 public void updateUser (Long id, String name) { User user = hibernateTemplate.load(User.class, id); user.setName(name); hibernateTemplate.update(user); }
前面我们在定义User
时,对有的属性标注了@Column(updatable=false)
。Hibernate在更新记录时,它只会把@Column(updatable=true)
的属性加入到UPDATE
语句中,这样可以提供一层额外的安全性,即如果不小心修改了User
的email
、createdAt
等属性,执行update()
时并不会更新对应的数据库列。但也必须牢记:这个功能是Hibernate提供的,如果绕过Hibernate直接通过JDBC执行UPDATE
语句仍然可以更新数据库的任意列的值。
最后,我们编写的大部分方法都是各种各样的查询。根据id
查询我们可以直接调用load()
或get()
,如果要使用条件查询,有3种方法。
假设我们想执行以下查询:
1 SELECT * FROM user WHERE email = ? AND password = ?
我们来看看可以使用什么查询。
使用Example查询 第一种方法是使用findByExample()
,给出一个User
实例,Hibernate把该实例所有非null
的属性拼成WHERE
条件:
1 2 3 4 5 6 7 public User login (String email, String password) { User example = new User(); example.setEmail(email); example.setPassword(password); List<User> list = hibernateTemplate.findByExample(example); return list.isEmpty() ? null : list.get(0 ); }
因为example
实例只有email
和password
两个属性为非null
,所以最终生成的WHERE
语句就是WHERE email = ? AND password = ?
。
如果我们把User
的createdAt
的类型从Long
改为long
,findByExample()
的查询将出问题,原因在于example
实例的long
类型字段有了默认值0,导致Hibernate最终生成的WHERE
语句意外变成了WHERE email = ? AND password = ? AND createdAt = 0
。显然,额外的查询条件将导致错误的查询结果。
使用findByExample()时,注意基本类型字段总是会加入到WHERE条件!
使用Criteria查询 第二种查询方法是使用Criteria查询,可以实现如下:
1 2 3 4 5 6 7 public User login (String email, String password) { DetachedCriteria criteria = DetachedCriteria.forClass(User.class); criteria.add(Restrictions.eq("email" , email)) .add(Restrictions.eq("password" , password)); List<User> list = (List<User>) hibernateTemplate.findByCriteria(criteria); return list.isEmpty() ? null : list.get(0 ); }
DetachedCriteria
使用链式语句来添加多个AND
条件。和findByExample()
相比,findByCriteria()
可以组装出更灵活的WHERE
条件,例如:
1 SELECT * FROM user WHERE (email = ? OR name = ?) AND password = ?
上述查询没法用findByExample()
实现,但用Criteria查询可以实现如下:
1 2 3 4 5 6 7 8 9 10 DetachedCriteria criteria = DetachedCriteria.forClass(User.class); criteria.add( Restrictions.and( Restrictions.or( Restrictions.eq("email" , email), Restrictions.eq("name" , email) ), Restrictions.eq("password" , password) ) );
只要组织好Restrictions
的嵌套关系,Criteria查询可以实现任意复杂的查询。
使用HQL查询 最后一种常用的查询是直接编写Hibernate内置的HQL查询:
1 List<User> list = (List<User>) hibernateTemplate.find("FROM User WHERE email=? AND password=?" , email, password);
和SQL相比,HQL使用类名和属性名,由Hibernate自动转换为实际的表名和列名。详细的HQL语法可以参考Hibernate文档 。
除了可以直接传入HQL字符串外,Hibernate还可以使用一种NamedQuery
,它给查询起个名字,然后保存在注解中。使用NamedQuery
时,我们要先在User
类标注:
1 2 3 4 5 6 7 8 9 10 11 12 @NamedQueries( @NamedQuery( // 查询名称: name = "login", // 查询语句: query = "SELECT u FROM User u WHERE u.email=?0 AND u.password=?1" ) ) @Entity public class User extends AbstractEntity { ... }
注意到引入的NamedQuery是javax.persistence.NamedQuery
,它和直接传入HQL有点不同的是,占位符使用?0
、?1
,并且索引是从0
开始的(真乱)。
使用NamedQuery
只需要引入查询名和参数:
1 2 3 4 public User login (String email, String password) { List<User> list = (List<User>) hibernateTemplate.findByNamedQuery("login" , email, password); return list.isEmpty() ? null : list.get(0 ); }
直接写HQL和使用NamedQuery
各有优劣。前者可以在代码中直观地看到查询语句,后者可以在User
类统一管理所有相关查询。
使用Hibernate原生接口 如果要使用Hibernate原生接口,但不知道怎么写,可以参考HibernateTemplate
的源码。使用Hibernate的原生接口实际上总是从SessionFactory
出发,它通常用全局变量存储,在HibernateTemplate
中以成员变量被注入。有了SessionFactory
,使用Hibernate用法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void operation () { Session session = null ; boolean isNew = false ; try { session = this .sessionFactory.getCurrentSession(); } catch (HibernateException e) { session = this .sessionFactory.openSession(); isNew = true ; } try { User user = session.load(User.class, 123L ); } finally { if (isNew) { session.close(); } } }
集成JPA 在讨论JPA(Java Persistence API)之前,我们要注意到JavaEE早在1999年就发布了,并有Servlet、JMS等诸多标准。和其他平台不同,Java世界早期热衷于标准先行,大家先坐下来把接口定了,然后,各自回家去实现接口。这样,用户就可以在不同厂家提供的产品进行选择,还可以随意切换,因为用户编写代码时只需要引用接口,不需要引用具体的底层实现,这一点很类似JDBC。
JPA就是JavaEE的一个ORM标准,它的实现其实和Hibernate没啥本质区别,但是如果用户使用JPA,引用的就是javax.persistance
这个包,而不是org.hibernate
这样的第三方包。
我们使用JPA时也完全可以选择Hibernate作为底层实现,但也可以选择其他产品,比如EclipseLink。Spring内置了JPA的集成,并支持选择Hibernate或EclipseLink作为实现。这里我们仍然以主流的Hibernate作为JPA实现的例子,演示JPA的基本用法。
和使用Hibernate一样,我们只需要引入如下依赖:
org.springframework:spring-context:5.2.0.RELEASE
org.springframework:spring-orm:5.2.0.RELEASE
javax.annotation:javax.annotation-api:1.3.2
org.hibernate:hibernate-core:5.4.2.Final
com.zaxxer:HikariCP:3.4.2
org.hsqldb:hsqldb:2.5.0
然后,在AppConfig
中启用声明式事务管理,创建DataSource
:
1 2 3 4 5 6 7 8 @Configuration @ComponentScan @EnableTransactionManagement @PropertySource("jdbc.properties") public class AppConfig { @Bean DataSource createDataSource () { ... } }
使用Hibernate时,我们需要创建一个LocalSessionFactoryBean
,并让它再自动创建一个SessionFactory
。使用JPA也是类似的,我们需要创建一个LocalContainerEntityManagerFactoryBean
,并让它再自动创建一个EntityManagerFactory
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Bean LocalContainerEntityManagerFactoryBean createEntityManagerFactory (@Autowired DataSource dataSource) { var entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); entityManagerFactoryBean.setDataSource(dataSource); entityManagerFactoryBean.setPackagesToScan("com.itranswarp.learnjava.entity" ); JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter); var props = new Properties(); props.setProperty("hibernate.hbm2ddl.auto" , "update" ); props.setProperty("hibernate.dialect" , "org.hibernate.dialect.HSQLDialect" ); props.setProperty("hibernate.show_sql" , "true" ); entityManagerFactoryBean.setJpaProperties(props); return entityManagerFactoryBean; }
观察上述代码,除了需要注入DataSource
和设定自动扫描的package
外,还需要指定JPA的提供商,这里使用Spring提供的一个HibernateJpaVendorAdapter
,最后,针对Hibernate自己需要的配置,以Properties
的形式注入。
最后,我们还需要实例化一个JpaTransactionManager
,以实现声明式事务:
1 2 3 4 @Bean PlatformTransactionManager createTxManager (@Autowired EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); }
这样,我们就完成了JPA的全部初始化工作。有些童鞋可能从网上搜索得知JPA需要persistence.xml
配置文件,以及复杂的orm.xml
文件。这里我们负责地告诉大家,使用Spring+Hibernate作为JPA实现,无需任何配置文件。
所有Entity Bean的配置和上一节完全相同,全部采用Annotation标注。我们现在只需关心具体的业务类如何通过JPA接口操作数据库。
还是以UserService
为例,除了标注@Component
和@Transactional
外,我们需要注入一个EntityManager
,但是不要使用Autowired
,而是@PersistenceContext
:
1 2 3 4 5 6 @Component @Transactional public class UserService { @PersistenceContext EntityManager em; }
我们回顾一下JDBC、Hibernate和JPA提供的接口,实际上,它们的关系如下:
JDBC
Hibernate
JPA
DataSource
SessionFactory
EntityManagerFactory
Connection
Session
EntityManager
SessionFactory
和EntityManagerFactory
相当于DataSource
,Session
和EntityManager
相当于Connection
。每次需要访问数据库的时候,需要获取新的Session
和EntityManager
,用完后再关闭。
但是,注意到UserService
注入的不是EntityManagerFactory
,而是EntityManager
,并且标注了@PersistenceContext
。难道使用JPA可以允许多线程操作同一个EntityManager
?
实际上这里注入的并不是真正的EntityManager
,而是一个EntityManager
的代理类,相当于:
1 2 3 public class EntityManagerProxy implements EntityManager { private EntityManagerFactory emf; }
Spring遇到标注了@PersistenceContext
的EntityManager
会自动注入代理,该代理会在必要的时候自动打开EntityManager
。换句话说,多线程引用的EntityManager
虽然是同一个代理类,但该代理类内部针对不同线程会创建不同的EntityManager
实例。
简单总结一下,标注了@PersistenceContext
的EntityManager
可以被多线程安全地共享。
因此,在UserService
的每个业务方法里,直接使用EntityManager
就很方便。以主键查询为例:
1 2 3 4 5 6 7 public User getUserById (long id) { User user = this .em.find(User.class, id); if (user == null ) { throw new RuntimeException("User not found by id: " + id); } return user; }
JPA同样支持Criteria查询,比如我们需要的查询如下:
1 SELECT * FROM user WHERE email = ?
使用Criteria查询的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public User fetchUserByEmail (String email) { var cb = em.getCriteriaBuilder(); CriteriaQuery<User> q = cb.createQuery(User.class); Root<User> r = q.from(User.class); q.where(cb.equal(r.get("email" ), cb.parameter(String.class, "e" ))); TypedQuery<User> query = em.createQuery(q); query.setParameter("e" , email); List<User> list = query.getResultList(); return list.isEmpty() ? null : list.get(0 ); }
一个简单的查询用Criteria写出来就像上面那样复杂,太恐怖了,如果条件多加几个,这种写法谁读得懂?
所以,正常人还是建议写JPQL查询,它的语法和HQL基本差不多:
1 2 3 4 5 6 7 8 9 10 public User getUserByEmail (String email) { TypedQuery<User> query = em.createQuery("SELECT u FROM User u WHERE u.email = :e" , User.class); query.setParameter("e" , email); List<User> list = query.getResultList(); if (list.isEmpty()) { throw new RuntimeException("User not found by email." ); } return list.get(0 ); }
同样的,JPA也支持NamedQuery,即先给查询起个名字,再按名字创建查询:
1 2 3 4 5 6 7 public User login (String email, String password) { TypedQuery<User> query = em.createNamedQuery("login" , User.class); query.setParameter("e" , email); query.setParameter("p" , password); List<User> list = query.getResultList(); return list.isEmpty() ? null : list.get(0 ); }
NamedQuery通过注解标注在User
类上,它的定义和上一节的User
类一样:
1 2 3 4 5 6 7 8 9 10 @NamedQueries( @NamedQuery( name = "login", query = "SELECT u FROM User u WHERE u.email=:e AND u.password=:p" ) ) @Entity public class User { ... }
对数据库进行增删改的操作,可以分别使用persist()
、remove()
和merge()
方法,参数均为Entity Bean本身,使用非常简单,这里不再多述。
集成Mybatis 使用Hibernate或JPA操作数据库时,这类ORM干的主要工作就是把ResultSet的每一行变为Java Bean,或者把Java Bean自动转换到INSERT或UPDATE语句的参数中,从而实现ORM。而ORM框架之所以知道如何把行数据映射到Java Bean,是因为我们在Java Bean的属性上给了足够的注解作为元数据,ORM框架获取Java Bean的注解后,就知道如何进行双向映射。
那么,ORM是如何跟踪Java Bean的修改,以便在update()
操作中更新必要的属性?
答案是使用Proxy模式,从ORM框架读取到的是User实例实际上并不是User类,而是代理类,代理类继承自User类,但针对每个setter
方法做了覆写。
1 2 3 4 5 6 7 8 public class UserProxy extends User { boolean _isNameChanged; public void setName (String name) { super .setName(name); _isNameChanged = true ; } }
这样,代理类就能跟踪到每个属性的变化。针对一对多或多对一关系时,代理类可以直接通过getter
方法查询数据库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class UserProxy extends User { Session _session; boolean _isNameChanged; public void setName (String name) { super .setName(name); _isNameChanged = true ; } public Address getAddress () { Query q = _session.createQuery("from Address where userId = :userId" ); q.setParameter("userId" , this .getId()); List<Address> list = query.list(); return list.isEmpty() ? null : list(0 ); } }
为了实现这样的查询,UserProxy必须保存Hibernate的当前Session。但是,当事务提交后,Session自动关闭,此时再获取getAddress()
将无法访问数据库,或者获取的不是事务一致的数据。因此,ORM框架总是引入了Attached/Detached状态,表示当前此Java Bean到底是在Session的范围内,还是脱离了Session变成了一个“游离”对象。很多初学者无法正确理解状态变化和事务边界,就会造成大量的PersistentObjectException
异常。这种隐式状态使得普通Java Bean的生命周期变得复杂。
此外,Hibernate和JPA为了实现兼容多种数据库,它使用HQL或JPQL查询,经过一道转换,变成特定数据库的SQL,理论上这样可以做到无缝切换数据库,但这一层自动转换除了少许的性能开销外,给SQL级别的优化带来了麻烦。
最后,ORM框架通常提供了缓存,并且还分为一级缓存和二级缓存。一级缓存是指在一个Session范围内的缓存,常见的情景是根据主键查询时,两次查询可以返回同一实例:
1 2 User user1 = session.load(User.class, 123 ); User user2 = session.load(User.class, 123 );
二级缓存是指跨Session的缓存,一般默认关闭,需要手动配置。二级缓存极大的增加了数据的不一致性,原因在于SQL非常灵活,常常会导致意外的更新。例如:
1 2 3 4 5 User user1 = session1.load(User.class, 123 ); ... User user2 = session2.load(User.class, 123 );
当二级缓存生效的时候,两个线程读取的User实例是一样的,但是,数据库对应的行记录完全可能被修改,例如:
1 2 UPDATE users SET bonus = bonus + 100 WHERE createdAt <= ?
ORM无法判断id=123
的用户是否受该UPDATE
语句影响。考虑到数据库通常会支持多个应用程序,此UPDATE语句可能由其他进程执行,ORM框架就更不知道了。
我们把这种ORM框架称之为全自动ORM框架,对比Spring提供的JdbcTemplate,主要有以下几点差别:
查询后需要手动提供Mapper实例以便把ResultSet的每一行变为Java对象
增删改操作所需的参数列表,需要手动传入,即把User实例变为[user.id, user.name, user.email]这样的列表,比较麻烦
但是JdbcTemplate的优势在于它的确定性:即每次读操作一定是数据库操作而不是缓存,所执行的SQL是完全确定的,缺点是代码比较繁琐,构造INSERT INTO users VALUES (?,?,?)
更是复杂。
所以,介于全自动ORM如Hibernate和手写全部如JdbcTemplate之间,还有一种半自动ORM,它只负责把ResultSet自动映射到Java Bean,或者自动填充Java Bean参数,但仍需要自己写出SQL。MyBatis就是这样一种半自动化框架。
我们来看看如何在Spring中集成MyBatis。
首先,我们要引入MyBatis本身,其次,由于Spring并没有内置对MyBatis的集成,所以,我们需要再引入MyBatis官方自己开发的一个与Spring集成的库:
org.mybatis:mybatis:3.5.4
org.mybatis:mybatis-spring:2.0.4
和前面一样,先创建DataSource:
1 2 3 4 5 6 7 8 @Configuration @ComponentScan @EnableTransactionManagement @PropertySource("jdbc.properties") public class AppConfig { @Bean DataSource createDataSource () { ... } }
再回顾一下Hibernate和JPA的SessionFactory
与EntityManagerFactory
,MyBatis与之对应的是SqlSessionFactory
和SqlSession
:
JDBC
Hibernate
JPA
MyBatis
DataSource
SessionFactory
EntityManagerFactory
SqlSessionFactory
Connection
Session
EntityManager
SqlSession
可见,ORM的设计套路都是类似的,使用MyBatis的核心是创建SqlSessionFactory
,这里我们需要创建的是SqlSessionFactoryBean
:
1 2 3 4 5 6 @Bean SqlSessionFactoryBean createSqlSessionFactoryBean (@Autowired DataSource dataSource) { var sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean; }
MyBatis可以直接使用Spring管理的声明式事务,因此,创建事务管理器和使用JDBC是一样的:
1 2 3 4 @Bean PlatformTransactionManager createTxManager (@Autowired DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
和Hibernate不同的是,MyBatis使用Mapper来实现映射,而且Mapper必须是接口。我们以User类为例,在User类和users表之间映射的UserMapper编写如下:
1 2 3 4 public interface UserMapper { @Select("SELECT * FROM users WHERE id = #{id}") User getById (@Param("id") long id) ; }
注意:这里的Mapper不是JdbcTemplate的RowMapper的概念,它是定义访问users表的接口方法。比如我们定义了一个User getById(long)
的主键查询方法,不仅要定义接口方法本身,还要明确写出查询的SQL,这里用注解@Select
标记。SQL语句的任何参数,都与方法参数按名称对应。例如,方法参数id的名字通过注解@Param()
标记为id
,则SQL语句里将来替换的占位符就是#{id}
。
如果有多个参数,那么每个参数命名后直接在SQL中写出对应的占位符即可:
1 2 @Select("SELECT * FROM users LIMIT #{offset}, #{maxResults}") List<User> getAll (@Param("offset") int offset, @Param("maxResults") int maxResults) ;
注意:MyBatis执行查询后,将根据方法的返回类型自动把ResultSet的每一行转换为User实例,转换规则是按列名和属性名对应。如果列名和属性名不同,最简单的方式是编写SELECT语句的别名。
1 2 SELECT id, name, email, created_time AS createdAt FROM users
执行INSERT语句就稍微麻烦点,因为我们希望传入User实例,因此,定义的方法接口与@Insert
注解如下:
1 2 @Insert("INSERT INTO users (email, password, name, createdAt) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.createdAt})") void insert (@Param("user") User user) ;
上述方法传入的参数名称是user
,参数类型是User类,在SQL中引用的时候,以#{obj.property}
的方式写占位符。和Hibernate这样的全自动化ORM相比,MyBatis必须写出完整的INSERT语句。
如果users
表的id是自增主键,那么,我们在SQL中不传入id,但希望获取插入后的主键,需要再加一个@Options
注解:
1 2 3 @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") @Insert("INSERT INTO users (email, password, name, createdAt) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.createdAt})") void insert (@Param("user") User user) ;
keyProperty
和keyColumn
分别指出JavaBean的属性和数据库的主键列名。
执行UPDATE和DELETE语句相对比较简单,我们定义方法如下:
1 2 3 4 5 @Update("UPDATE users SET name = #{user.name}, createdAt = #{user.createdAt} WHERE id = #{user.id}") void update (@Param("user") User user) ;@Delete("DELETE FROM users WHERE id = #{id}") void deleteById (@Param("id") long id) ;
有了UserMapper
接口,还需要对应的实现类才能真正执行这些数据库操作的方法。虽然可以自己写实现类,但我们除了编写UserMapper
接口外,还有BookMapper
、BonusMapper
……一个一个写太麻烦,因此,MyBatis提供了一个MapperFactoryBean
来自动创建所有Mapper的实现类。可以用一个简单的注解来启用它:
1 2 3 4 5 @MapperScan("com.itranswarp.learnjava.mapper") ...其他注解...public class AppConfig { ... }
有了@MapperScan
,就可以让MyBatis自动扫描指定包的所有Mapper并创建实现类。在真正的业务逻辑中,我们可以直接注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component @Transactional public class UserService { @Autowired UserMapper userMapper; public User getUserById (long id) { User user = userMapper.getById(id); if (user == null ) { throw new RuntimeException("User not found by id." ); } return user; } }
可见,业务逻辑主要就是通过XxxMapper
定义的数据库方法来访问数据库。
XML配置 上述Spring集成MyBatis的方式,我们只需要用到注解,并没有使用任何任何XML配置文件。MyBatis也允许使用XML配置映射关系和SQL语句,例如,更新User
时根据属性值构造动态SQL:
1 2 3 4 5 6 7 8 9 <update id ="updateUser" > UPDATE users SET <set > <if test ="user.name != null" > name = #{user.name} </if > <if test ="user.hobby != null" > hobby = #{user.hobby} </if > <if test ="user.summary != null" > summary = #{user.summary} </if > </set > WHERE id = #{user.id}</update >
编写XML配置的优点是可以组装出动态SQL,并且把所有SQL操作集中在一起,缺点是配置起来太繁琐,调用方法时如果想查看SQL还需要定位到XML配置中。这里我们不介绍XML的配置方式,需要了解的童鞋请自行阅读官方文档 。
使用MyBatis最大的问题是所有SQL都需要全部手写,优点是执行的SQL就是我们自己写的SQL,对SQL进行优化非常简单,也可以编写任意复杂的SQL,或者使用数据库的特定语法,但切换数据库可能就不太容易。好消息是大部分项目并没有切换数据库的需求,完全可以针对某个数据库编写尽可能优化的SQL。
设计ORM ORM框架就是自动映射数据库表结构到Java Bean的工具,设计并实现一个简单高效的ORM框架并不难。