Spring为了简化数据库访问,主要做了以下几点工作:

  • 提供了简化的访问JDBC的模板类,不必手动释放资源;
  • 提供了一个统一的DAO类以实现Data Access Object模式;
  • 把SQLException封装为DataAccessException,这个异常是一个RuntimeException,并且让我们能区分SQL异常的原因,例如,DuplicateKeyException表示违反了一个唯一约束;
  • 能方便地集成Hibernate、JPA和MyBatis这些数据库访问框架。

    1.使用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. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework</groupId>
  4. <artifactId>spring-context</artifactId>
  5. <version>5.2.0.RELEASE</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework</groupId>
  9. <artifactId>spring-jdbc</artifactId>
  10. <version>5.2.0.RELEASE</version>
  11. </dependency>
  12. <dependency>
  13. <groupId>javax.annotation</groupId>
  14. <artifactId>javax.annotation-api</artifactId>
  15. <version>1.3.2</version>
  16. </dependency>
  17. <dependency>
  18. <groupId>com.zaxxer</groupId>
  19. <artifactId>HikariCP</artifactId>
  20. <version>3.4.2</version>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.hsqldb</groupId>
  24. <artifactId>hsqldb</artifactId>
  25. <version>2.5.0</version>
  26. </dependency>
  27. </dependencies>

在AppConfig中,我们需要创建以下几个必须的Bean:

  1. @Configuration
  2. @ComponentScan
  3. @PropertySource("jdbc.properties")
  4. public class AppConfig {
  5. @Value("${jdbc.url}")
  6. String jdbcUrl;
  7. @Value("${jdbc.username}")
  8. String jdbcUsername;
  9. @Value("${jdbc.password}")
  10. String jdbcPassword;
  11. @Bean
  12. DataSource createDataSource() {
  13. HikariConfig config = new HikariConfig();
  14. config.setJdbcUrl(jdbcUrl);
  15. config.setUsername(jdbcUsername);
  16. config.setPassword(jdbcPassword);
  17. config.addDataSourceProperty("autoCommit", "true");
  18. config.addDataSourceProperty("connectionTimeout", "5");
  19. config.addDataSourceProperty("idleTimeout", "60");
  20. return new HikariDataSource(config);
  21. }
  22. @Bean
  23. JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
  24. return new JdbcTemplate(dataSource);
  25. }
  26. }

在上述配置中:

  1. 通过@PropertySource(“jdbc.properties”)读取数据库配置文件;
  2. 通过@Value(“${jdbc.url}”)注入配置文件的相关配置;
  3. 创建一个DataSource实例,它的实际类型是HikariDataSource,创建时需要用到注入的配置;
  4. 创建一个JdbcTemplate实例,它需要注入DataSource,这是通过方法参数完成注入的。

最后,针对HSQLDB写一个配置文件jdbc.properties:

  1. # 数据库文件名为testdb:
  2. jdbc.url=jdbc:hsqldb:file:testdb
  3. # Hsqldb默认的用户名是sa,口令是空字符串:
  4. jdbc.username=sa
  5. jdbc.password=

可以通过HSQLDB自带的工具来初始化数据库表,这里我们写一个Bean,在Spring容器启动时自动创建一个users表:

  1. @Component
  2. public class DatabaseInitializer {
  3. @Autowired
  4. JdbcTemplate jdbcTemplate;
  5. @PostConstruct
  6. public void init() {
  7. jdbcTemplate.update("CREATE TABLE IF NOT EXISTS users (" //
  8. + "id BIGINT IDENTITY NOT NULL PRIMARY KEY, " //
  9. + "email VARCHAR(100) NOT NULL, " //
  10. + "password VARCHAR(100) NOT NULL, " //
  11. + "name VARCHAR(100) NOT NULL, " //
  12. + "UNIQUE (email))");
  13. }
  14. }

现在,所有准备工作都已完毕。我们只需要在需要访问数据库的Bean中,注入JdbcTemplate即可:

  1. @Component
  2. public class UserService {
  3. @Autowired
  4. JdbcTemplate jdbcTemplate;
  5. ...
  6. }

1.JdbcTemplate用法

Spring提供的JdbcTemplate采用Template模式,提供了一系列以回调为特点的工具方法,目的是避免繁琐的try…catch语句。
我们以具体的示例来说明JdbcTemplate的用法。
首先我们看T execute(ConnectionCallback action)方法,它提供了Jdbc的Connection供我们使用:

  1. public User getUserById(long id) {
  2. // 注意传入的是ConnectionCallback:
  3. return jdbcTemplate.execute((Connection conn) -> {
  4. // 可以直接使用conn实例,不要释放它,回调结束后JdbcTemplate自动释放:
  5. // 在内部手动创建的PreparedStatement、ResultSet必须用try(...)释放:
  6. try (var ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
  7. ps.setObject(1, id);
  8. try (var rs = ps.executeQuery()) {
  9. if (rs.next()) {
  10. return new User( // new User object:
  11. rs.getLong("id"), // id
  12. rs.getString("email"), // email
  13. rs.getString("password"), // password
  14. rs.getString("name")); // name
  15. }
  16. throw new RuntimeException("user not found by id.");
  17. }
  18. }
  19. });
  20. }

也就是说,上述回调方法允许获取Connection,然后做任何基于Connection的操作。
我们再看T execute(String sql, PreparedStatementCallback action)的用法:

  1. public User getUserByName(String name) {
  2. // 需要传入SQL语句,以及PreparedStatementCallback:
  3. return jdbcTemplate.execute("SELECT * FROM users WHERE name = ?", (PreparedStatement ps) -> {
  4. // PreparedStatement实例已经由JdbcTemplate创建,并在回调后自动释放:
  5. ps.setObject(1, name);
  6. try (var rs = ps.executeQuery()) {
  7. if (rs.next()) {
  8. return new User( // new User object:
  9. rs.getLong("id"), // id
  10. rs.getString("email"), // email
  11. rs.getString("password"), // password
  12. rs.getString("name")); // name
  13. }
  14. throw new RuntimeException("user not found by id.");
  15. }
  16. });
  17. }

最后,我们看T queryForObject(String sql, Object[] args, RowMapper rowMapper)方法:

  1. public User getUserByEmail(String email) {
  2. // 传入SQL,参数和RowMapper实例:
  3. return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email = ?", new Object[] { email },
  4. (ResultSet rs, int rowNum) -> {
  5. // 将ResultSet的当前行映射为一个JavaBean:
  6. return new User( // new User object:
  7. rs.getLong("id"), // id
  8. rs.getString("email"), // email
  9. rs.getString("password"), // password
  10. rs.getString("name")); // name
  11. });
  12. }

在queryForObject()方法中,传入SQL以及SQL参数后,JdbcTemplate会自动创建PreparedStatement,自动执行查询并返回ResultSet,我们提供的RowMapper需要做的事情就是把ResultSet的当前行映射成一个JavaBean并返回。整个过程中,使用Connection、PreparedStatement和ResultSet都不需要我们手动管理。
RowMapper不一定返回JavaBean,实际上它可以返回任何Java对象。例如,使用SELECT COUNT(*)查询时,可以返回Long:

  1. public long getUsers() {
  2. return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", null, (ResultSet rs, int rowNum) -> {
  3. // SELECT COUNT(*)查询只有一列,取第一列数据:
  4. return rs.getLong(1);
  5. });
  6. }

如果我们期望返回多行记录,而不是一行,可以用query()方法:

  1. public List<User> getUsers(int pageIndex) {
  2. int limit = 100;
  3. int offset = limit * (pageIndex - 1);
  4. return jdbcTemplate.query("SELECT * FROM users LIMIT ? OFFSET ?", new Object[] { limit, offset },
  5. new BeanPropertyRowMapper<>(User.class));
  6. }

上述query()方法传入的参数仍然是SQL、SQL参数以及RowMapper实例。这里我们直接使用Spring提供的BeanPropertyRowMapper。如果数据库表的结构恰好和JavaBean的属性名称一致,那么BeanPropertyRowMapper就可以直接把一行记录按列名转换为JavaBean。
如果我们执行的不是查询,而是插入、更新和删除操作,那么需要使用update()方法:

  1. public void updateUser(User user) {
  2. // 传入SQL,SQL参数,返回更新的行数:
  3. if (1 != jdbcTemplate.update("UPDATE user SET name = ? WHERE id=?", user.getName(), user.getId())) {
  4. throw new RuntimeException("User not found by id");
  5. }
  6. }

只有一种INSERT操作比较特殊,那就是如果某一列是自增列(例如自增主键),通常,我们需要获取插入后的自增值。JdbcTemplate提供了一个KeyHolder来简化这一操作:

  1. public User register(String email, String password, String name) {
  2. // 创建一个KeyHolder:
  3. KeyHolder holder = new GeneratedKeyHolder();
  4. if (1 != jdbcTemplate.update(
  5. // 参数1:PreparedStatementCreator
  6. (conn) -> {
  7. // 创建PreparedStatement时,必须指定RETURN_GENERATED_KEYS:
  8. var ps = conn.prepareStatement("INSERT INTO users(email,password,name) VALUES(?,?,?)",
  9. Statement.RETURN_GENERATED_KEYS);
  10. ps.setObject(1, email);
  11. ps.setObject(2, password);
  12. ps.setObject(3, name);
  13. return ps;
  14. },
  15. // 参数2:KeyHolder
  16. holder)
  17. ) {
  18. throw new RuntimeException("Insert failed.");
  19. }
  20. // 从KeyHolder中获取返回的自增值:
  21. return new User(holder.getKey().longValue(), email, password, name);
  22. }

需要强调的是,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)方法。

2.使用声明式事务

如果要在Spring中操作事务,没必要手写JDBC事务,可以使用Spring提供的高级接口来操作事务。
Spring提供了一个PlatformTransactionManager来表示事务管理器,所有的事务都由它负责管理。而事务由TransactionStatus表示。如果手写事务代码,使用try…catch如下:

  1. TransactionStatus tx = null;
  2. try {
  3. // 开启事务:
  4. tx = txManager.getTransaction(new DefaultTransactionDefinition());
  5. // 相关JDBC操作:
  6. jdbcTemplate.update("...");
  7. jdbcTemplate.update("...");
  8. // 提交事务:
  9. txManager.commit(tx);
  10. } catch (RuntimeException e) {
  11. // 回滚事务:
  12. txManager.rollback(tx);
  13. throw e;
  14. }

Spring为啥要抽象出PlatformTransactionManager和TransactionStatus?原因是JavaEE除了提供JDBC事务外,它还支持分布式事务JTA(Java Transaction API)。分布式事务是指多个数据源(比如多个数据库,多个消息系统)要在分布式环境下实现事务的时候,应该怎么实现。分布式事务实现起来非常复杂,简单地说就是通过一个分布式事务管理器实现两阶段提交,但本身数据库事务就不快,基于数据库事务实现的分布式事务就慢得难以忍受,所以使用率不高。
Spring为了同时支持JDBC和JTA两种事务模型,就抽象出PlatformTransactionManager。因为我们的代码只需要JDBC事务,因此,在AppConfig中,需要再定义一个PlatformTransactionManager对应的Bean,它的实际类型是DataSourceTransactionManager:

  1. @Configuration
  2. @ComponentScan
  3. @PropertySource("jdbc.properties")
  4. public class AppConfig {
  5. ...
  6. @Bean
  7. PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
  8. return new DataSourceTransactionManager(dataSource);
  9. }
  10. }

使用编程的方式使用Spring事务仍然比较繁琐,更好的方式是通过声明式事务来实现。使用声明式事务非常简单,除了在AppConfig中追加一个上述定义的PlatformTransactionManager外,再加一个@EnableTransactionManagement就可以启用声明式事务:

  1. @Configuration
  2. @ComponentScan
  3. @EnableTransactionManagement // 启用声明式
  4. @PropertySource("jdbc.properties")
  5. public class AppConfig {
  6. ...
  7. }

然后,对需要事务支持的方法,加一个@Transactional注解:

  1. @Component
  2. public class UserService {
  3. // 此public方法自动具有事务支持:
  4. @Transactional
  5. public User register(String email, String password, String name) {
  6. ...
  7. }
  8. }

或者更简单一点,直接在Bean的class处加上,表示所有public方法都具有事务支持:

  1. @Component
  2. @Transactional
  3. public class UserService {
  4. ...
  5. }

Spring对一个声明式事务的方法,如何开启事务支持?原理仍然是AOP代理,即通过自动创建Bean的Proxy实现:

  1. public class UserService$$EnhancerBySpringCGLIB extends UserService {
  2. UserService target = ...
  3. PlatformTransactionManager txManager = ...
  4. public User register(String email, String password, String name) {
  5. TransactionStatus tx = null;
  6. try {
  7. tx = txManager.getTransaction(new DefaultTransactionDefinition());
  8. target.register(email, password, name);
  9. txManager.commit(tx);
  10. } catch (RuntimeException e) {
  11. txManager.rollback(tx);
  12. throw e;
  13. }
  14. }
  15. ...
  16. }

注意:声明了@EnableTransactionManagement后,不必额外添加@EnableAspectJAutoProxy。

1.回滚事务

默认情况下,如果发生了RuntimeException,Spring的声明式事务将自动回滚。在一个事务方法中,如果程序判断需要回滚事务,只需抛出RuntimeException,例如:

  1. @Transactional
  2. public buyProducts(long productId, int num) {
  3. ...
  4. if (store < num) {
  5. // 库存不够,购买失败:
  6. throw new IllegalArgumentException("No enough products");
  7. }
  8. ...
  9. }

如果要针对Checked Exception回滚事务,需要在@Transactional注解中写出来:

  1. @Transactional(rollbackFor = {RuntimeException.class, IOException.class})
  2. public buyProducts(long productId, int num) throws IOException {
  3. ...
  4. }

上述代码表示在抛出RuntimeException或IOException时,事务将回滚。
为了简化代码,我们强烈建议业务异常体系从RuntimeException派生,这样就不必声明任何特殊异常即可让Spring的声明式事务正常工作:

  1. public class BusinessException extends RuntimeException {
  2. ...
  3. }
  4. public class LoginException extends BusinessException {
  5. ...
  6. }
  7. public class PaymentException extends BusinessException {
  8. ...
  9. }

2.事务边界

在使用事务的时候,明确事务边界非常重要。对于声明式事务,例如,下面的register()方法:

  1. @Component
  2. public class UserService {
  3. @Transactional
  4. public User register(String email, String password, String name) { // 事务开始
  5. ...
  6. } // 事务结束
  7. }

它的事务边界就是register()方法开始和结束。
类似的,一个负责给用户增加积分的addBonus()方法:

  1. @Component
  2. public class BonusService {
  3. @Transactional
  4. public void addBonus(long userId, int bonus) { // 事务开始
  5. ...
  6. } // 事务结束
  7. }

它的事务边界就是addBonus()方法开始和结束。
在现实世界中,问题总是要复杂一点点。用户注册后,能自动获得100积分,因此,实际代码如下:

  1. @Component
  2. public class UserService {
  3. @Autowired
  4. BonusService bonusService;
  5. @Transactional
  6. public User register(String email, String password, String name) {
  7. // 插入用户记录:
  8. User user = jdbcTemplate.insert("...");
  9. // 增加100积分:
  10. bonusService.addBonus(user.id, 100);
  11. }
  12. }

现在问题来了:调用方(比如RegisterController)调用UserService.register()这个事务方法,它在内部又调用了BonusService.addBonus()这个事务方法,一共有几个事务?如果addBonus()抛出了异常需要回滚事务,register()方法的事务是否也要回滚?

3.事务传播

要解决上面的问题,我们首先要定义事务的传播模型。
假设用户注册的入口是RegisterController,它本身没有事务,仅仅是调用UserService.register()这个事务方法:

  1. @Controller
  2. public class RegisterController {
  3. @Autowired
  4. UserService userService;
  5. @PostMapping("/register")
  6. public ModelAndView doRegister(HttpServletRequest req) {
  7. String email = req.getParameter("email");
  8. String password = req.getParameter("password");
  9. String name = req.getParameter("name");
  10. User user = userService.register(email, password, name);
  11. return ...
  12. }
  13. }

因此,UserService.register()这个事务方法的起始和结束,就是事务的范围。
我们需要关心的问题是,在UserService.register()这个事务方法内,调用BonusService.addBonus(),我们期待的事务行为是什么:

  1. @Transactional
  2. public User register(String email, String password, String name) {
  3. // 事务已开启:
  4. User user = jdbcTemplate.insert("...");
  5. // ???:
  6. bonusService.addBonus(user.id, 100);
  7. } // 事务结束

对于大多数业务来说,我们期待BonusService.addBonus()的调用,和UserService.register()应当融合在一起,它的行为应该如下:
UserService.register()已经开启了一个事务,那么在内部调用BonusService.addBonus()时,BonusService.addBonus()方法就没必要再开启一个新事务,直接加入到BonusService.register()的事务里就好了。
其实就相当于:

  1. UserService.register()先执行了一条INSERT语句:INSERT INTO users …
  2. 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. @Controller
  2. public class LoginController {
  3. @Autowired
  4. BonusService bonusService;
  5. @PostMapping("/login")
  6. public ModelAndView doLogin(HttpServletRequest req) {
  7. User user = ...
  8. bonusService.addBonus(user.id, 5);
  9. }
  10. }

可见,BonusService.addBonus()方法必须要有@Transactional,否则,登录后积分就无法添加了。
默认的事务传播级别是REQUIRED,它满足绝大部分的需求。还有一些其他的传播级别:
SUPPORTS:表示如果有事务,就加入到当前事务,如果没有,那也不开启事务执行。这种传播级别可用于查询方法,因为SELECT语句既可以在事务内执行,也可以不需要事务;
MANDATORY:表示必须要存在当前事务并加入执行,否则将抛出异常。这种传播级别可用于核心更新逻辑,比如用户余额变更,它总是被其他事务方法调用,不能直接由非事务方法调用;
REQUIRES_NEW:表示不管当前有没有事务,都必须开启一个新的事务执行。如果当前已经有事务,那么当前事务会挂起,等新事务完成后,再恢复执行;
NOT_SUPPORTED:表示不支持事务,如果当前有事务,那么当前事务会挂起,等这个方法执行完成后,再恢复执行;
NEVER:和NOT_SUPPORTED相比,它不但不支持事务,而且在监测到当前有事务时,会抛出异常拒绝执行;
NESTED:表示如果当前有事务,则开启一个嵌套级别事务,如果当前没有事务,则开启一个新事务。
上面这么多种事务的传播级别,其实默认的REQUIRED已经满足绝大部分需求,SUPPORTS和REQUIRES_NEW在少数情况下会用到,其他基本不会用到,因为把事务搞得越复杂,不仅逻辑跟着复杂,而且速度也会越慢。
定义事务的传播级别也是写在@Transactional注解里的:

  1. @Transactional(propagation = Propagation.REQUIRES_NEW)
  2. public Product createProduct() {
  3. ...
  4. }

问题:Spring是如何传播事务的?
我们在JDBC中使用事务的时候,是这么个写法:

  1. Connection conn = openConnection();
  2. try {
  3. // 关闭自动提交:
  4. conn.setAutoCommit(false);
  5. // 执行多条SQL语句:
  6. insert(); update(); delete();
  7. // 提交事务:
  8. conn.commit();
  9. } catch (SQLException e) {
  10. // 回滚事务:
  11. conn.rollback();
  12. } finally {
  13. conn.setAutoCommit(true);
  14. conn.close();
  15. }

Spring使用声明式事务,最终也是通过执行JDBC事务来实现功能的,那么,一个事务方法,如何获知当前是否存在事务?
答案是使用ThreadLocal。Spring总是把JDBC相关的Connection和TransactionStatus实例绑定到ThreadLocal。如果一个事务方法从ThreadLocal未取到事务,那么它会打开一个新的JDBC连接,同时开启一个新的事务,否则,它就直接使用从ThreadLocal获取的JDBC连接以及TransactionStatus。
因此,事务能正确传播的前提是,方法调用是在一个线程内才行。如果像下面这样写:

  1. @Transactional
  2. public User register(String email, String password, String name) { // BEGIN TX-A
  3. User user = jdbcTemplate.insert("...");
  4. new Thread(() -> {
  5. // BEGIN TX-B:
  6. bonusService.addBonus(user.id, 100);
  7. // END TX-B
  8. }).start();
  9. } // END TX-A

在另一个线程中调用BonusService.addBonus(),它根本获取不到当前事务,因此,UserService.register()和BonusService.addBonus()两个方法,将分别开启两个完全独立的事务。
换句话说,事务只能在当前线程传播,无法跨线程传播。
那如果我们想实现跨线程传播事务呢?原理很简单,就是要想办法把当前线程绑定到ThreadLocal的Connection和TransactionStatus实例传递给新线程,但实现起来非常复杂,根据异常回滚更加复杂,不推荐自己去实现。
Spring提供的声明式事务极大地方便了在数据库中使用事务,正确使用声明式事务的关键在于确定好事务边界,理解事务传播级别。

3.使用DAO

在传统的多层应用程序中,通常是Web层调用业务层,业务层调用数据访问层。业务层负责处理各种业务逻辑,而数据访问层只负责对数据进行增删改查。因此,实现数据访问层就是用JdbcTemplate实现对数据库的操作。
编写数据访问层的时候,可以使用DAO模式。DAO即Data Access Object的缩写,它没有什么神秘之处,实现起来基本如下:

  1. public class UserDao {
  2. @Autowired
  3. JdbcTemplate jdbcTemplate;
  4. User getById(long id) {
  5. ...
  6. }
  7. List<User> getUsers(int page) {
  8. ...
  9. }
  10. User createUser(User user) {
  11. ...
  12. }
  13. User updateUser(User user) {
  14. ...
  15. }
  16. void deleteUser(User user) {
  17. ...
  18. }
  19. }

Spring提供了一个JdbcDaoSupport类,用于简化DAO的实现。这个JdbcDaoSupport没什么复杂的,核心代码就是持有一个JdbcTemplate:

  1. public abstract class JdbcDaoSupport extends DaoSupport {
  2. private JdbcTemplate jdbcTemplate;
  3. public final void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
  4. this.jdbcTemplate = jdbcTemplate;
  5. initTemplateConfig();
  6. }
  7. public final JdbcTemplate getJdbcTemplate() {
  8. return this.jdbcTemplate;
  9. }
  10. ...
  11. }

它的意图是子类直接从JdbcDaoSupport继承后,可以随时调用getJdbcTemplate()获得JdbcTemplate的实例。那么问题来了:因为JdbcDaoSupport的jdbcTemplate字段没有标记@Autowired,所以,子类想要注入JdbcTemplate,还得自己想个办法:

  1. @Component
  2. @Transactional
  3. public class UserDao extends JdbcDaoSupport {
  4. @Autowired
  5. JdbcTemplate jdbcTemplate;
  6. @PostConstruct
  7. public void init() {
  8. super.setJdbcTemplate(jdbcTemplate);
  9. }
  10. }

有的童鞋可能看出来了:既然UserDao都已经注入了JdbcTemplate,那再把它放到父类里,通过getJdbcTemplate()访问岂不是多此一举?
如果使用传统的XML配置,并不需要编写@Autowired JdbcTemplate jdbcTemplate,但是考虑到现在基本上是使用注解的方式,我们可以编写一个AbstractDao,专门负责注入JdbcTemplate:

  1. public abstract class AbstractDao extends JdbcDaoSupport {
  2. @Autowired
  3. private JdbcTemplate jdbcTemplate;
  4. @PostConstruct
  5. public void init() {
  6. super.setJdbcTemplate(jdbcTemplate);
  7. }
  8. }

这样,子类的代码就非常干净,可以直接调用getJdbcTemplate():

  1. @Component
  2. @Transactional
  3. public class UserDao extends AbstractDao {
  4. public User getById(long id) {
  5. return getJdbcTemplate().queryForObject(
  6. "SELECT * FROM users WHERE id = ?",
  7. new BeanPropertyRowMapper<>(User.class),
  8. id
  9. );
  10. }
  11. ...
  12. }

倘若肯再多写一点样板代码,就可以把AbstractDao改成泛型,并实现getById(),getAll(),deleteById()这样的通用方法:

  1. public abstract class AbstractDao<T> extends JdbcDaoSupport {
  2. private String table;
  3. private Class<T> entityClass;
  4. private RowMapper<T> rowMapper;
  5. public AbstractDao() {
  6. // 获取当前类型的泛型类型:
  7. this.entityClass = getParameterizedType();
  8. this.table = this.entityClass.getSimpleName().toLowerCase() + "s";
  9. this.rowMapper = new BeanPropertyRowMapper<>(entityClass);
  10. }
  11. public T getById(long id) {
  12. return getJdbcTemplate().queryForObject("SELECT * FROM " + table + " WHERE id = ?", this.rowMapper, id);
  13. }
  14. public List<T> getAll(int pageIndex) {
  15. int limit = 100;
  16. int offset = limit * (pageIndex - 1);
  17. return getJdbcTemplate().query("SELECT * FROM " + table + " LIMIT ? OFFSET ?",
  18. new Object[] { limit, offset },
  19. this.rowMapper);
  20. }
  21. public void deleteById(long id) {
  22. getJdbcTemplate().update("DELETE FROM " + table + " WHERE id = ?", id);
  23. }
  24. ...
  25. }

这样,每个子类就自动获得了这些通用方法:

  1. @Component
  2. @Transactional
  3. public class UserDao extends AbstractDao<User> {
  4. // 已经有了:
  5. // User getById(long)
  6. // List<User> getAll(int)
  7. // void deleteById(long)
  8. }
  9. @Component
  10. @Transactional
  11. public class BookDao extends AbstractDao<Book> {
  12. // 已经有了:
  13. // Book getById(long)
  14. // List<Book> getAll(int)
  15. // void deleteById(long)
  16. }

可见,DAO模式就是一个简单的数据访问模式,是否使用DAO,根据实际情况决定,因为很多时候,直接在Service层操作数据库也是完全没有问题的。
Spring提供了JdbcDaoSupport来便于我们实现DAO模式;
可以基于泛型实现更通用、更简洁的DAO模式。

4.集成Hibernate

使用JdbcTemplate的时候,我们用得最多的方法就是List query(String sql, Object[] args, RowMapper rowMapper)。这个RowMapper的作用就是把ResultSet的一行记录映射为Java Bean。
这种把关系数据库的表记录映射为Java对象的过程就是ORM:Object-Relational Mapping。ORM既可以把记录转换成Java对象,也可以把Java对象转换为行记录。
使用JdbcTemplate配合RowMapper可以看作是最原始的ORM。如果要实现更自动化的ORM,可以选择成熟的ORM框架,例如Hibernate
我们来看看如何在Spring中集成Hibernate。
Hibernate作为ORM框架,它可以替代JdbcTemplate,但Hibernate仍然需要JDBC驱动,所以,我们需要引入JDBC驱动、连接池,以及Hibernate本身。在Maven中,我们加入以下依赖项:

  1. <!-- JDBC驱动,这里使用HSQLDB -->
  2. <dependency>
  3. <groupId>org.hsqldb</groupId>
  4. <artifactId>hsqldb</artifactId>
  5. <version>2.5.0</version>
  6. </dependency>
  7. <!-- JDBC连接池 -->
  8. <dependency>
  9. <groupId>com.zaxxer</groupId>
  10. <artifactId>HikariCP</artifactId>
  11. <version>3.4.2</version>
  12. </dependency>
  13. <!-- Hibernate -->
  14. <dependency>
  15. <groupId>org.hibernate</groupId>
  16. <artifactId>hibernate-core</artifactId>
  17. <version>5.4.2.Final</version>
  18. </dependency>
  19. <!-- Spring ContextSpring ORM -->
  20. <dependency>
  21. <groupId>org.springframework</groupId>
  22. <artifactId>spring-context</artifactId>
  23. <version>5.2.0.RELEASE</version>
  24. </dependency>
  25. <dependency>
  26. <groupId>org.springframework</groupId>
  27. <artifactId>spring-orm</artifactId>
  28. <version>5.2.0.RELEASE</version>
  29. </dependency>

在AppConfig中,我们仍然需要创建DataSource、引入JDBC配置文件,以及启用声明式事务:

  1. @Configuration
  2. @ComponentScan
  3. @EnableTransactionManagement
  4. @PropertySource("jdbc.properties")
  5. public class AppConfig {
  6. @Bean
  7. DataSource createDataSource() {
  8. ...
  9. }
  10. }

为了启用Hibernate,我们需要创建一个LocalSessionFactoryBean:

  1. public class AppConfig {
  2. @Bean
  3. LocalSessionFactoryBean createSessionFactory(@Autowired DataSource dataSource) {
  4. var props = new Properties();
  5. props.setProperty("hibernate.hbm2ddl.auto", "update"); // 生产环境不要使用
  6. props.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
  7. props.setProperty("hibernate.show_sql", "true");
  8. var sessionFactoryBean = new LocalSessionFactoryBean();
  9. sessionFactoryBean.setDataSource(dataSource);
  10. // 扫描指定的package获取所有entity class:
  11. sessionFactoryBean.setPackagesToScan("com.itranswarp.learnjava.entity");
  12. sessionFactoryBean.setHibernateProperties(props);
  13. return sessionFactoryBean;
  14. }
  15. }

注意我们在定制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. public class AppConfig {
  2. @Bean
  3. HibernateTemplate createHibernateTemplate(@Autowired SessionFactory sessionFactory) {
  4. return new HibernateTemplate(sessionFactory);
  5. }
  6. @Bean
  7. PlatformTransactionManager createTxManager(@Autowired SessionFactory sessionFactory) {
  8. return new HibernateTransactionManager(sessionFactory);
  9. }
  10. }

这两个Bean的创建都十分简单。HibernateTransactionManager是配合Hibernate使用声明式事务所必须的,而HibernateTemplate则是Spring为了便于我们使用Hibernate提供的工具类,不是非用不可,但推荐使用以简化代码。
到此为止,所有的配置都定义完毕,我们来看看如何将数据库表结构映射为Java对象。
考察如下的数据库表:

  1. CREATE TABLE user
  2. id BIGINT NOT NULL AUTO_INCREMENT,
  3. email VARCHAR(100) NOT NULL,
  4. password VARCHAR(100) NOT NULL,
  5. name VARCHAR(100) NOT NULL,
  6. createdAt BIGINT NOT NULL,
  7. PRIMARY KEY (`id`),
  8. UNIQUE KEY `email` (`email`)
  9. );

其中,id是自增主键,email、password、name是VARCHAR类型,email带唯一索引以确保唯一性,createdAt存储整型类型的时间戳。用JavaBean表示如下:

  1. public class User {
  2. private Long id;
  3. private String email;
  4. private String password;
  5. private String name;
  6. private Long createdAt;
  7. // getters and setters
  8. ...
  9. }

这种映射关系十分易懂,但我们需要添加一些注解来告诉Hibernate如何把User类映射到表记录:

  1. @Entity
  2. public class User {
  3. @Id
  4. @GeneratedValue(strategy = GenerationType.IDENTITY)
  5. @Column(nullable = false, updatable = false)
  6. public Long getId() { ... }
  7. @Column(nullable = false, unique = true, length = 100)
  8. public String getEmail() { ... }
  9. @Column(nullable = false, length = 100)
  10. public String getPassword() { ... }
  11. @Column(nullable = false, length = 100)
  12. public String getName() { ... }
  13. @Column(nullable = false, updatable = false)
  14. public Long getCreatedAt() { ... }
  15. }

如果一个JavaBean被用于映射,我们就标记一个@Entity。默认情况下,映射的表名是user,如果实际的表名不同,例如实际表名是users,可以追加一个@Table(name=”users”)表示:

  1. @Entity
  2. @Table(name="users)
  3. public class User {
  4. ...
  5. }

每个属性到数据库列的映射用@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. @Entity
  2. public class Book {
  3. @Id
  4. @GeneratedValue(strategy = GenerationType.IDENTITY)
  5. @Column(nullable = false, updatable = false)
  6. public Long getId() { ... }
  7. @Column(nullable = false, length = 100)
  8. public String getTitle() { ... }
  9. @Column(nullable = false, updatable = false)
  10. public Long getCreatedAt() { ... }
  11. }

如果仔细观察User和Book,会发现它们定义的id、createdAt属性是一样的,这在数据库表结构的设计中很常见:对于每个表,通常我们会统一使用一种主键生成机制,并添加createdAt表示创建时间,updatedAt表示修改时间等通用字段。
不必在User和Book中重复定义这些通用字段,我们可以把它们提到一个抽象类中:

  1. @MappedSuperclass
  2. public abstract class AbstractEntity {
  3. private Long id;
  4. private Long createdAt;
  5. @Id
  6. @GeneratedValue(strategy = GenerationType.IDENTITY)
  7. @Column(nullable = false, updatable = false)
  8. public Long getId() { ... }
  9. @Column(nullable = false, updatable = false)
  10. public Long getCreatedAt() { ... }
  11. @Transient
  12. public ZonedDateTime getCreatedDateTime() {
  13. return Instant.ofEpochMilli(this.createdAt).atZone(ZoneId.systemDefault());
  14. }
  15. @PrePersist
  16. public void preInsert() {
  17. setCreatedAt(System.currentTimeMillis());
  18. }
  19. }

对于AbstractEntity来说,我们要标注一个@MappedSuperclass表示它用于继承。此外,注意到我们定义了一个@Transient方法,它返回一个“虚拟”的属性。因为getCreatedDateTime()是计算得出的属性,而不是从数据库表读出的值,因此必须要标注@Transient,否则Hibernate会尝试从数据库读取名为createdDateTime这个不存在的字段从而出错。
再注意到@PrePersist标识的方法,它表示在我们将一个JavaBean持久化到数据库之前(即执行INSERT语句),Hibernate会先执行该方法,这样我们就可以自动设置好createdAt属性。
有了AbstractEntity,我们就可以大幅简化User和Book:

  1. @Entity
  2. public class User extends AbstractEntity {
  3. @Column(nullable = false, unique = true, length = 100)
  4. public String getEmail() { ... }
  5. @Column(nullable = false, length = 100)
  6. public String getPassword() { ... }
  7. @Column(nullable = false, length = 100)
  8. public String getName() { ... }
  9. }

注意到使用的所有注解均来自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. @Component
  2. @Transactional
  3. public class UserService {
  4. @Autowired
  5. HibernateTemplate hibernateTemplate;
  6. }

1.Insert操作

要持久化一个User实例,我们只需调用save()方法。以register()方法为例,代码如下:

  1. public User register(String email, String password, String name) {
  2. // 创建一个User对象:
  3. User user = new User();
  4. // 设置好各个属性:
  5. user.setEmail(email);
  6. user.setPassword(password);
  7. user.setName(name);
  8. // 不要设置id,因为使用了自增主键
  9. // 保存到数据库:
  10. hibernateTemplate.save(user);
  11. // 现在已经自动获得了id:
  12. System.out.println(user.getId());
  13. return user;
  14. }

2.Delete操作

删除一个User相当于从表中删除对应的记录。注意Hibernate总是用id来删除记录,因此,要正确设置User的id属性才能正常删除记录:

  1. public boolean deleteUser(Long id) {
  2. User user = hibernateTemplate.get(User.class, id);
  3. if (user != null) {
  4. hibernateTemplate.delete(user);
  5. return true;
  6. }
  7. return false;
  8. }

通过主键删除记录时,一个常见的用法是先根据主键加载该记录,再删除。load()和get()都可以根据主键加载记录,它们的区别在于,当记录不存在时,get()返回null,而load()抛出异常。

3.Update操作

更新记录相当于先更新User的指定属性,然后调用update()方法:

  1. public void updateUser(Long id, String name) {
  2. User user = hibernateTemplate.load(User.class, id);
  3. user.setName(name);
  4. hibernateTemplate.update(user);
  5. }

前面我们在定义User时,对有的属性标注了@Column(updatable=false)。Hibernate在更新记录时,它只会把@Column(updatable=true)的属性加入到UPDATE语句中,这样可以提供一层额外的安全性,即如果不小心修改了User的email、createdAt等属性,执行update()时并不会更新对应的数据库列。但也必须牢记:这个功能是Hibernate提供的,如果绕过Hibernate直接通过JDBC执行UPDATE语句仍然可以更新数据库的任意列的值。

4.Query

最后,我们编写的大部分方法都是各种各样的查询。根据id查询我们可以直接调用load()或get(),如果要使用条件查询,有3种方法。
假设我们想执行以下查询:

  1. SELECT * FROM user WHERE email = ? AND password = ?

我们来看看可以使用什么查询。

1.使用Example查询

第一种方法是使用findByExample(),给出一个User实例,Hibernate把该实例所有非null的属性拼成WHERE条件:

  1. public User login(String email, String password) {
  2. User example = new User();
  3. example.setEmail(email);
  4. example.setPassword(password);
  5. List<User> list = hibernateTemplate.findByExample(example);
  6. return list.isEmpty() ? null : list.get(0);
  7. }

因为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条件!

2.使用Criteria查询

第二种查询方法是使用Criteria查询,可以实现如下:

  1. public User login(String email, String password) {
  2. DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
  3. criteria.add(Restrictions.eq("email", email))
  4. .add(Restrictions.eq("password", password));
  5. List<User> list = (List<User>) hibernateTemplate.findByCriteria(criteria);
  6. return list.isEmpty() ? null : list.get(0);
  7. }

DetachedCriteria使用链式语句来添加多个AND条件。和findByExample()相比,findByCriteria()可以组装出更灵活的WHERE条件,例如:

  1. SELECT * FROM user WHERE (email = ? OR name = ?) AND password = ?

上述查询没法用findByExample()实现,但用Criteria查询可以实现如下:

  1. DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
  2. criteria.add(
  3. Restrictions.and(
  4. Restrictions.or(
  5. Restrictions.eq("email", email),
  6. Restrictions.eq("name", email)
  7. ),
  8. Restrictions.eq("password", password)
  9. )
  10. );

只要组织好Restrictions的嵌套关系,Criteria查询可以实现任意复杂的查询。

3.使用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. @NamedQueries(
  2. @NamedQuery(
  3. // 查询名称:
  4. name = "login",
  5. // 查询语句:
  6. query = "SELECT u FROM User u WHERE u.email=?0 AND u.password=?1"
  7. )
  8. )
  9. @Entity
  10. public class User extends AbstractEntity {
  11. ...
  12. }

注意到引入的NamedQuery是javax.persistence.NamedQuery,它和直接传入HQL有点不同的是,占位符使用?0、?1,并且索引是从0开始的(真乱)。
使用NamedQuery只需要引入查询名和参数:

  1. public User login(String email, String password) {
  2. List<User> list = (List<User>) hibernateTemplate.findByNamedQuery("login", email, password);
  3. return list.isEmpty() ? null : list.get(0);
  4. }

直接写HQL和使用NamedQuery各有优劣。前者可以在代码中直观地看到查询语句,后者可以在User类统一管理所有相关查询。

4.使用Hibernate原生接口

如果要使用Hibernate原生接口,但不知道怎么写,可以参考HibernateTemplate的源码。使用Hibernate的原生接口实际上总是从SessionFactory出发,它通常用全局变量存储,在HibernateTemplate中以成员变量被注入。有了SessionFactory,使用Hibernate用法如下:

  1. void operation() {
  2. Session session = null;
  3. boolean isNew = false;
  4. // 获取当前Session或者打开新的Session:
  5. try {
  6. session = this.sessionFactory.getCurrentSession();
  7. } catch (HibernateException e) {
  8. session = this.sessionFactory.openSession();
  9. isNew = true;
  10. }
  11. // 操作Session:
  12. try {
  13. User user = session.load(User.class, 123L);
  14. }
  15. finally {
  16. // 关闭新打开的Session:
  17. if (isNew) {
  18. session.close();
  19. }
  20. }
  21. }

在Spring中集成Hibernate需要配置的Bean如下:

  • DataSource;
  • LocalSessionFactory;
  • HibernateTransactionManager;
  • HibernateTemplate(推荐)。

推荐使用Annotation配置所有的Entity Bean。

5.集成JPA

JPA就是JavaEE的一个ORM标准,它的实现其实和Hibernate没啥本质区别,但是用户如果使用JPA,那么引用的就是javax.persistence这个“标准”包,而不是org.hibernate这样的第三方包。因为JPA只是接口,所以,还需要选择一个实现产品,跟JDBC接口和MySQL驱动一个道理。
我们使用JPA时也完全可以选择Hibernate作为底层实现,但也可以选择其它的JPA提供方,比如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. @Configuration
  2. @ComponentScan
  3. @EnableTransactionManagement
  4. @PropertySource("jdbc.properties")
  5. public class AppConfig {
  6. @Bean
  7. DataSource createDataSource() { ... }
  8. }

使用Hibernate时,我们需要创建一个LocalSessionFactoryBean,并让它再自动创建一个SessionFactory。使用JPA也是类似的,我们需要创建一个LocalContainerEntityManagerFactoryBean,并让它再自动创建一个EntityManagerFactory:

  1. @Bean
  2. LocalContainerEntityManagerFactoryBean createEntityManagerFactory(@Autowired DataSource dataSource) {
  3. var entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
  4. // 设置DataSource:
  5. entityManagerFactoryBean.setDataSource(dataSource);
  6. // 扫描指定的package获取所有entity class:
  7. entityManagerFactoryBean.setPackagesToScan("com.itranswarp.learnjava.entity");
  8. // 指定JPA的提供商是Hibernate:
  9. JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
  10. entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
  11. // 设定特定提供商自己的配置:
  12. var props = new Properties();
  13. props.setProperty("hibernate.hbm2ddl.auto", "update");
  14. props.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
  15. props.setProperty("hibernate.show_sql", "true");
  16. entityManagerFactoryBean.setJpaProperties(props);
  17. return entityManagerFactoryBean;
  18. }

观察上述代码,除了需要注入DataSource和设定自动扫描的package外,还需要指定JPA的提供商,这里使用Spring提供的一个HibernateJpaVendorAdapter,最后,针对Hibernate自己需要的配置,以Properties的形式注入。
最后,我们还需要实例化一个JpaTransactionManager,以实现声明式事务:

  1. @Bean
  2. PlatformTransactionManager createTxManager(@Autowired EntityManagerFactory entityManagerFactory) {
  3. return new JpaTransactionManager(entityManagerFactory);
  4. }

这样,我们就完成了JPA的全部初始化工作。有些童鞋可能从网上搜索得知JPA需要persistence.xml配置文件,以及复杂的orm.xml文件。这里我们负责地告诉大家,使用Spring+Hibernate作为JPA实现,无需任何配置文件。
所有Entity Bean的配置和上一节完全相同,全部采用Annotation标注。我们现在只需关心具体的业务类如何通过JPA接口操作数据库。
还是以UserService为例,除了标注@Component和@Transactional外,我们需要注入一个EntityManager,但是不要使用Autowired,而是@PersistenceContext:

  1. @Component
  2. @Transactional
  3. public class UserService {
  4. @PersistenceContext
  5. EntityManager em;
  6. }

我们回顾一下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. public class EntityManagerProxy implements EntityManager {
  2. private EntityManagerFactory emf;
  3. }

Spring遇到标注了@PersistenceContext的EntityManager会自动注入代理,该代理会在必要的时候自动打开EntityManager。换句话说,多线程引用的EntityManager虽然是同一个代理类,但该代理类内部针对不同线程会创建不同的EntityManager实例。
简单总结一下,标注了@PersistenceContext的EntityManager可以被多线程安全地共享。
因此,在UserService的每个业务方法里,直接使用EntityManager就很方便。以主键查询为例:

  1. public User getUserById(long id) {
  2. User user = this.em.find(User.class, id);
  3. if (user == null) {
  4. throw new RuntimeException("User not found by id: " + id);
  5. }
  6. return user;
  7. }

JPA同样支持Criteria查询,比如我们需要的查询如下:

  1. SELECT * FROM user WHERE email = ?

使用Criteria查询的代码如下:

  1. public User fetchUserByEmail(String email) {
  2. // CriteriaBuilder:
  3. var cb = em.getCriteriaBuilder();
  4. CriteriaQuery<User> q = cb.createQuery(User.class);
  5. Root<User> r = q.from(User.class);
  6. q.where(cb.equal(r.get("email"), cb.parameter(String.class, "e")));
  7. TypedQuery<User> query = em.createQuery(q);
  8. // 绑定参数:
  9. query.setParameter("e", email);
  10. // 执行查询:
  11. List<User> list = query.getResultList();
  12. return list.isEmpty() ? null : list.get(0);

一个简单的查询用Criteria写出来就像上面那样复杂,太恐怖了,如果条件多加几个,这种写法谁读得懂?
所以,正常人还是建议写JPQL查询,它的语法和HQL基本差不多:

  1. public User getUserByEmail(String email) {
  2. // JPQL查询:
  3. TypedQuery<User> query = em.createQuery("SELECT u FROM User u WHERE u.email = :e", User.class);
  4. query.setParameter("e", email);
  5. List<User> list = query.getResultList();
  6. if (list.isEmpty()) {
  7. throw new RuntimeException("User not found by email.");
  8. }
  9. return list.get(0);
  10. }

同样的,JPA也支持NamedQuery,即先给查询起个名字,再按名字创建查询:

  1. public User login(String email, String password) {
  2. TypedQuery<User> query = em.createNamedQuery("login", User.class);
  3. query.setParameter("e", email);
  4. query.setParameter("p", password);
  5. List<User> list = query.getResultList();
  6. return list.isEmpty() ? null : list.get(0);
  7. }

NamedQuery通过注解标注在User类上,它的定义和上一节的User类一样:

  1. @NamedQueries(
  2. @NamedQuery(
  3. name = "login",
  4. query = "SELECT u FROM User u WHERE u.email=:e AND u.password=:p"
  5. )
  6. )
  7. @Entity
  8. public class User {
  9. ...
  10. }

对数据库进行增删改的操作,可以分别使用persist()、remove()和merge()方法,参数均为Entity Bean本身,使用非常简单,这里不再多述。
在Spring中集成JPA要选择一个实现,可以选择Hibernate或EclipseLink;
使用JPA与Hibernate类似,但注入的核心资源是带有@PersistenceContext注解的EntityManager代理类。

6.集成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. public class UserProxy extends User {
  2. boolean _isNameChanged;
  3. public void setName(String name) {
  4. super.setName(name);
  5. _isNameChanged = true;
  6. }
  7. }

这样,代理类可以跟踪到每个属性的变化。
针对一对多或多对一关系时,代理类可以直接通过getter方法查询数据库:

  1. public class UserProxy extends User {
  2. Session _session;
  3. boolean _isNameChanged;
  4. public void setName(String name) {
  5. super.setName(name);
  6. _isNameChanged = true;
  7. }
  8. /**
  9. * 获取User对象关联的Address对象:
  10. */
  11. public Address getAddress() {
  12. Query q = _session.createQuery("from Address where userId = :userId");
  13. q.setParameter("userId", this.getId());
  14. List<Address> list = query.list();
  15. return list.isEmpty() ? null : list(0);
  16. }
  17. }

为了实现这样的查询,UserProxy必须保存Hibernate的当前Session。但是,当事务提交后,Session自动关闭,此时再获取getAddress()将无法访问数据库,或者获取的不是事务一致的数据。因此,ORM框架总是引入了Attached/Detached状态,表示当前此Java Bean到底是在Session的范围内,还是脱离了Session变成了一个“游离”对象。很多初学者无法正确理解状态变化和事务边界,就会造成大量的PersistentObjectException异常。这种隐式状态使得普通Java Bean的生命周期变得复杂。
此外,Hibernate和JPA为了实现兼容多种数据库,它使用HQL或JPQL查询,经过一道转换,变成特定数据库的SQL,理论上这样可以做到无缝切换数据库,但这一层自动转换除了少许的性能开销外,给SQL级别的优化带来了麻烦。
最后,ORM框架通常提供了缓存,并且还分为一级缓存和二级缓存。一级缓存是指在一个Session范围内的缓存,常见的情景是根据主键查询时,两次查询可以返回同一实例:

  1. User user1 = session.load(User.class, 123);
  2. User user2 = session.load(User.class, 123);

二级缓存是指跨Session的缓存,一般默认关闭,需要手动配置。二级缓存极大的增加了数据的不一致性,原因在于SQL非常灵活,常常会导致意外的更新。例如:

  1. // 线程1读取:
  2. User user1 = session1.load(User.class, 123);
  3. ...
  4. // 一段时间后,线程2读取:
  5. User user2 = session2.load(User.class, 123);

当二级缓存生效的时候,两个线程读取的User实例是一样的,但是,数据库对应的行记录完全可能被修改,例如:

  1. -- 给老用户增加100积分:
  2. UPDATE users SET bonus = bonus + 100 WHERE createdAt <= ?

ORM无法判断id=123的用户是否受该UPDATE语句影响。考虑到数据库通常会支持多个应用程序,此UPDATE语句可能由其他进程执行,ORM框架就更不知道了。
我们把这种ORM框架称之为全自动ORM框架。
对比Spring提供的JdbcTemplate,它和ORM框架相比,主要有几点差别:

  1. 查询后需要手动提供Mapper实例以便把ResultSet的每一行变为Java对象;
  2. 增删改操作所需的参数列表,需要手动传入,即把User实例变为[user.id, user.name, user.email]这样的列表,比较麻烦。

但是JdbcTemplate的优势在于它的确定性:即每次读取操作一定是数据库操作而不是缓存,所执行的SQL是完全确定的,缺点就是代码比较繁琐,构造INSERT INTO users VALUES (?,?,?)更是复杂。
所以,介于全自动ORM如Hibernate和手写全部如JdbcTemplate之间,还有一种半自动的ORM,它只负责把ResultSet自动映射到Java Bean,或者自动填充Java Bean参数,但仍需自己写出SQL。MyBatis就是这样一种半自动化ORM框架。
我们来看看如何在Spring中集成MyBatis。
首先,我们要引入MyBatis本身,其次,由于Spring并没有像Hibernate那样内置对MyBatis的集成,所以,我们需要再引入MyBatis官方自己开发的一个与Spring集成的库:

  • org.mybatis:mybatis:3.5.4
  • org.mybatis:mybatis-spring:2.0.4

和前面一样,先创建DataSource是必不可少的:

  1. @Configuration
  2. @ComponentScan
  3. @EnableTransactionManagement
  4. @PropertySource("jdbc.properties")
  5. public class AppConfig {
  6. @Bean
  7. DataSource createDataSource() { ... }
  8. }

再回顾一下Hibernate和JPA的SessionFactory与EntityManagerFactory,MyBatis与之对应的是SqlSessionFactory和SqlSession:

JDBC Hibernate JPA MyBatis
DataSource SessionFactory EntityManagerFactory SqlSessionFactory
Connection Session EntityManager SqlSession

可见,ORM的设计套路都是类似的。使用MyBatis的核心就是创建SqlSessionFactory,这里我们需要创建的是SqlSessionFactoryBean:

  1. @Bean
  2. SqlSessionFactoryBean createSqlSessionFactoryBean(@Autowired DataSource dataSource) {
  3. var sqlSessionFactoryBean = new SqlSessionFactoryBean();
  4. sqlSessionFactoryBean.setDataSource(dataSource);
  5. return sqlSessionFactoryBean;
  6. }

因为MyBatis可以直接使用Spring管理的声明式事务,因此,创建事务管理器和使用JDBC是一样的:

  1. @Bean
  2. PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
  3. return new DataSourceTransactionManager(dataSource);
  4. }

和Hibernate不同的是,MyBatis使用Mapper来实现映射,而且Mapper必须是接口。我们以User类为例,在User类和users表之间映射的UserMapper编写如下:

  1. public interface UserMapper {
  2. @Select("SELECT * FROM users WHERE id = #{id}")
  3. User getById(@Param("id") long id);
  4. }

注意:这里的Mapper不是JdbcTemplate的RowMapper的概念,它是定义访问users表的接口方法。比如我们定义了一个User getById(long)的主键查询方法,不仅要定义接口方法本身,还要明确写出查询的SQL,这里用注解@Select标记。SQL语句的任何参数,都与方法参数按名称对应。例如,方法参数id的名字通过注解@Param()标记为id,则SQL语句里将来替换的占位符就是#{id}。
如果有多个参数,那么每个参数命名后直接在SQL中写出对应的占位符即可:

  1. @Select("SELECT * FROM users LIMIT #{offset}, #{maxResults}")
  2. List<User> getAll(@Param("offset") int offset, @Param("maxResults") int maxResults);

注意:MyBatis执行查询后,将根据方法的返回类型自动把ResultSet的每一行转换为User实例,转换规则当然是按列名和属性名对应。如果列名和属性名不同,最简单的方式是编写SELECT语句的别名:

  1. -- 列名是created_time,属性名是createdAt:
  2. SELECT id, name, email, created_time AS createdAt FROM users

执行INSERT语句就稍微麻烦点,因为我们希望传入User实例,因此,定义的方法接口与@Insert注解如下:

  1. @Insert("INSERT INTO users (email, password, name, createdAt) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.createdAt})")
  2. void insert(@Param("user") User user);

上述方法传入的参数名称是user,参数类型是User类,在SQL中引用的时候,以#{obj.property}的方式写占位符。和Hibernate这样的全自动化ORM相比,MyBatis必须写出完整的INSERT语句。
如果users表的id是自增主键,那么,我们在SQL中不传入id,但希望获取插入后的主键,需要再加一个@Options注解:

  1. @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
  2. @Insert("INSERT INTO users (email, password, name, createdAt) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.createdAt})")
  3. void insert(@Param("user") User user);

keyProperty和keyColumn分别指出JavaBean的属性和数据库的主键列名。
执行UPDATE和DELETE语句相对比较简单,我们定义方法如下:

  1. @Update("UPDATE users SET name = #{user.name}, createdAt = #{user.createdAt} WHERE id = #{user.id}")
  2. void update(@Param("user") User user);
  3. @Delete("DELETE FROM users WHERE id = #{id}")
  4. void deleteById(@Param("id") long id);

有了UserMapper接口,还需要对应的实现类才能真正执行这些数据库操作的方法。虽然可以自己写实现类,但我们除了编写UserMapper接口外,还有BookMapper、BonusMapper……一个一个写太麻烦,因此,MyBatis提供了一个MapperFactoryBean来自动创建所有Mapper的实现类。可以用一个简单的注解来启用它:

  1. @MapperScan("com.itranswarp.learnjava.mapper")
  2. ...其他注解...
  3. public class AppConfig {
  4. ...
  5. }

有了@MapperScan,就可以让MyBatis自动扫描指定包的所有Mapper并创建实现类。在真正的业务逻辑中,我们可以直接注入:

  1. @Component
  2. @Transactional
  3. public class UserService {
  4. // 注入UserMapper:
  5. @Autowired
  6. UserMapper userMapper;
  7. public User getUserById(long id) {
  8. // 调用Mapper方法:
  9. User user = userMapper.getById(id);
  10. if (user == null) {
  11. throw new RuntimeException("User not found by id.");
  12. }
  13. return user;
  14. }
  15. }

可见,业务逻辑主要就是通过XxxMapper定义的数据库方法来访问数据库。

1.XML配置

上述在Spring中集成MyBatis的方式,我们只需要用到注解,并没有任何XML配置文件。MyBatis也允许使用XML配置映射关系和SQL语句,例如,更新User时根据属性值构造动态SQL:

  1. <update id="updateUser">
  2. UPDATE users SET
  3. <set>
  4. <if test="user.name != null"> name = #{user.name} </if>
  5. <if test="user.hobby != null"> hobby = #{user.hobby} </if>
  6. <if test="user.summary != null"> summary = #{user.summary} </if>
  7. </set>
  8. WHERE id = #{user.id}
  9. </update>

编写XML配置的优点是可以组装出动态SQL,并且把所有SQL操作集中在一起。缺点是配置起来太繁琐,调用方法时如果想查看SQL还需要定位到XML配置中。这里我们不介绍XML的配置方式,需要了解的童鞋请自行阅读官方文档
使用MyBatis最大的问题是所有SQL都需要全部手写,优点是执行的SQL就是我们自己写的SQL,对SQL进行优化非常简单,也可以编写任意复杂的SQL,或者使用数据库的特定语法,但切换数据库可能就不太容易。好消息是大部分项目并没有切换数据库的需求,完全可以针对某个数据库编写尽可能优化的SQL。
MyBatis是一个半自动化的ORM框架,需要手写SQL语句,没有自动加载一对多或多对一关系的功能。

7.设计ORM

所谓ORM,也是建立在JDBC的基础上,通过ResultSet到JavaBean的映射,实现各种查询。有自动跟踪Entity修改的全自动化ORM如Hibernate和JPA,需要为每个Entity创建代理,也有完全自己映射,连INSERT和UPDATE语句都需要手动编写的MyBatis,但没有任何透明的Proxy。
而查询是涉及到数据库使用最广泛的操作,需要最大的灵活性。各种ORM解决方案各不相同,Hibernate和JPA自己实现了HQL和JPQL查询语法,用以生成最终的SQL,而MyBatis则完全手写,每增加一个查询都需要先编写SQL并增加接口方法。
还有一种Hibernate和JPA支持的Criteria查询,用Hibernate写出来类似:

  1. DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
  2. criteria.add(Restrictions.eq("email", email))
  3. .add(Restrictions.eq("password", password));
  4. List<User> list = (List<User>) hibernateTemplate.findByCriteria(criteria);

上述Criteria查询写法复杂,但和JPA相比,还是小巫见大巫了:

  1. var cb = em.getCriteriaBuilder();
  2. CriteriaQuery<User> q = cb.createQuery(User.class);
  3. Root<User> r = q.from(User.class);
  4. q.where(cb.equal(r.get("email"), cb.parameter(String.class, "e")));
  5. TypedQuery<User> query = em.createQuery(q);
  6. query.setParameter("e", email);
  7. List<User> list = query.getResultList();

此外,是否支持自动读取一对多和多对一关系也是全自动化ORM框架的一个重要功能。
如果我们自己来设计并实现一个ORM,应该吸取这些ORM的哪些特色,然后高效实现呢?

1.设计ORM接口

任何设计,都必须明确设计目标。这里我们准备实现的ORM并不想要全自动ORM那种自动读取一对多和多对一关系的功能,也不想给Entity加上复杂的状态,因此,对于Entity来说,它就是纯粹的JavaBean,没有任何Proxy。
此外,ORM要兼顾易用性和适用性。易用性是指能覆盖95%的应用场景,但总有一些复杂的SQL,很难用ORM去自动生成,因此,也要给出原生的JDBC接口,能支持5%的特殊需求。
最后,我们希望设计的接口要易于编写,并使用流式API便于阅读。为了配合编译器检查,还应该支持泛型,避免强制转型。
以User类为例,我们设计的查询接口如下:

  1. // 按主键查询: SELECT * FROM users WHERE id = ?
  2. User u = db.get(User.class, 123);
  3. // 条件查询唯一记录: SELECT * FROM users WHERE email = ? AND password = ?
  4. User u = db.from(User.class)
  5. .where("email=? AND password=?", "bob@example.com", "bob123")
  6. .unique();
  7. // 条件查询多条记录: SELECT * FROM users WHERE id < ? ORDER BY email LIMIT ?, ?
  8. List<User> us = db.from(User.class)
  9. .where("id < ?", 1000)
  10. .orderBy("email")
  11. .limit(0, 10)
  12. .list();
  13. // 查询特定列: SELECT id, name FROM users WHERE email = ?
  14. User u = db.select("id", "name")
  15. .from(User.class)
  16. .where("email = ?", "bob@example.com")
  17. .unique();

这样的流式API便于阅读,也非常容易推导出最终生成的SQL。
对于插入、更新和删除操作,就相对比较简单:

  1. // 插入User:
  2. db.insert(user);
  3. // 按主键更新更新User:
  4. db.update(user);
  5. // 按主键删除User:
  6. db.delete(User.class, 123);

对于Entity来说,通常一个表对应一个。手动列出所有Entity是非常麻烦的,一定要传入package自动扫描。
最后,ORM总是需要元数据才能知道如何映射。我们不想编写复杂的XML配置,也没必要自己去定义一套规则,直接使用JPA的注解就行。

2.实现ORM

我们并不需要从JDBC底层开始编写,并且,还要考虑到事务,最好能直接使用Spring的声明式事务。实际上,我们可以设计一个全局DbTemplate,它注入了Spring的JdbcTemplate,涉及到数据库操作时,全部通过JdbcTemplate完成,自然天生支持Spring的声明式事务,因为这个ORM只是在JdbcTemplate的基础上做了一层封装。
在AppConfig中,我们初始化所有Bean如下:

  1. @Configuration
  2. @ComponentScan
  3. @EnableTransactionManagement
  4. @PropertySource("jdbc.properties")
  5. public class AppConfig {
  6. @Bean
  7. DataSource createDataSource() { ... }
  8. @Bean
  9. JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
  10. return new JdbcTemplate(dataSource);
  11. }
  12. @Bean
  13. DbTemplate createDbTemplate(@Autowired JdbcTemplate jdbcTemplate) {
  14. return new DbTemplate(jdbcTemplate, "com.itranswarp.learnjava.entity");
  15. }
  16. @Bean
  17. PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
  18. return new DataSourceTransactionManager(dataSource);
  19. }
  20. }

以上就是我们所需的所有配置。
编写业务逻辑,例如UserService,写出来像这样:

  1. @Component
  2. @Transactional
  3. public class UserService {
  4. @Autowired
  5. DbTemplate db;
  6. public User getUserById(long id) {
  7. return db.get(User.class, id);
  8. }
  9. public User getUserByEmail(String email) {
  10. return db.from(User.class)
  11. .where("email = ?", email)
  12. .unique();
  13. }
  14. public List<User> getUsers(int pageIndex) {
  15. int pageSize = 100;
  16. return db.from(User.class)
  17. .orderBy("id")
  18. .limit((pageIndex - 1) * pageSize, pageSize)
  19. .list();
  20. }
  21. public User register(String email, String password, String name) {
  22. User user = new User();
  23. user.setEmail(email);
  24. user.setPassword(password);
  25. user.setName(name);
  26. user.setCreatedAt(System.currentTimeMillis());
  27. db.insert(user);
  28. return user;
  29. }
  30. ...
  31. }

上述代码给出了ORM的接口,以及如何在业务逻辑中使用ORM。下一步,就是如何实现这个DbTemplate。这里我们只给出框架代码,有兴趣的可以自己实现核心代码:

  1. public class DbTemplate {
  2. private JdbcTemplate jdbcTemplate;
  3. // 保存Entity Class到Mapper的映射:
  4. private Map<Class<?>, Mapper<?>> classMapping;
  5. public <T> T fetch(Class<T> clazz, Object id) {
  6. Mapper<T> mapper = getMapper(clazz);
  7. List<T> list = (List<T>) jdbcTemplate.query(mapper.selectSQL, new Object[] { id }, mapper.rowMapper);
  8. if (list.isEmpty()) {
  9. return null;
  10. }
  11. return list.get(0);
  12. }
  13. public <T> T get(Class<T> clazz, Object id) {
  14. ...
  15. }
  16. public <T> void insert(T bean) {
  17. ...
  18. }
  19. public <T> void update(T bean) {
  20. ...
  21. }
  22. public <T> void delete(Class<T> clazz, Object id) {
  23. ...
  24. }
  25. }

实现链式API的核心代码是第一步从DbTemplate调用select()或from()时实例化一个CriteriaQuery实例,并在后续的链式调用中设置它的字段:

  1. public class DbTemplate {
  2. ...
  3. public Select select(String... selectFields) {
  4. return new Select(new Criteria(this), selectFields);
  5. }
  6. public <T> From<T> from(Class<T> entityClass) {
  7. Mapper<T> mapper = getMapper(entityClass);
  8. return new From<>(new Criteria<>(this), mapper);
  9. }
  10. }

然后以此定义Select、From、Where、OrderBy、Limit等。在From中可以设置Class类型、表名等:

  1. public final class From<T> extends CriteriaQuery<T> {
  2. From(Criteria<T> criteria, Mapper<T> mapper) {
  3. super(criteria);
  4. // from可以设置class、tableName:
  5. this.criteria.mapper = mapper;
  6. this.criteria.clazz = mapper.entityClass;
  7. this.criteria.table = mapper.tableName;
  8. }
  9. public Where<T> where(String clause, Object... args) {
  10. return new Where<>(this.criteria, clause, args);
  11. }
  12. }

在Where中可以设置条件参数:

  1. public final class Where<T> extends CriteriaQuery<T> {
  2. Where(Criteria<T> criteria, String clause, Object... params) {
  3. super(criteria);
  4. this.criteria.where = clause;
  5. this.criteria.whereParams = new ArrayList<>();
  6. // add:
  7. for (Object param : params) {
  8. this.criteria.whereParams.add(param);
  9. }
  10. }
  11. }

最后,链式调用的尽头是调用list()返回一组结果,调用unique()返回唯一结果,调用first()返回首个结果。
ORM框架就是自动映射数据库表结构到JavaBean的工具,设计并实现一个简单高效的ORM框架并不困难。