Spring Boot 结合 ORM 可以非常方便快捷地进行数据库的配置、链接、查询等操作。

准备

先解决 Jackson 和 数据库的时区问题,在配置文件当中加入:

1
2
spring.jackson.time-zone=GMT+8
spring.datasource.url=jdbc:mysql://localhost:3306/dbname?serverTimezone=GMT%2B8

datasource

DataSource 是 Spring 简化数据库配置的方式,首先加入数据库的依赖

1
2
3
4
5
6
7
8
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

配置数据库连接,这里的是我习惯的配置方式(host, port, dbname 是自定义的),这里的配置不需要指定 spring.datasource.driver-class-name=com.mysql.jdbc.Driver,因为 spring boot 会自动配置。这里的配置仅仅是最小配置,诸如数据库连接细节、连接池等可以更加需求自行配置。

1
2
3
4
5
6
7
spring.datasource.host=127.0.0.1
spring.datasource.port=3306
spring.datasource.dbname=spring
spring.datasource.username=root
spring.datasource.password=wsad

spring.datasource.url=jdbc:mysql://${spring.datasource.host}:${spring.datasource.port}/${spring.datasource.dbname}

JdbcTemplate

典型 jdbc

典型的 jdbc 连接数据库通常可以这样做:

1
2
3
4
5
6
7
8
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1/spring", "root", "wsad");

String sql = "select id, name, address from user where id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, 0);
ResultSet rs = ps.getResultSet();
System.out.println(rs.getInt("id") + rs.getString("name") + rs.getString("address"));

其中获取连接的部分可以由注入 Datasource 和 DataSource.getConnection() 完成。但是这种传统方式的从结果集到 POJO 的转换都是些很重复的步骤利用 jdbcTemplate 的 RowMapper 可以简化。

利用 jdbcTemplate 实现映射

编写 UserService 的 CURDL 接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// service.JdbcTmplUserService.java
public interface JdbcTmplUserService {
    public User getUser(int id);

    public List<User> findUsers(String uname, String address);

    public int insertUser(User user);

    public int updateUser(User user);

    public int deleteUser(int id);
}

编写 UserService 的实现

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// service.impl.JdbcTmplUserServiceImpl.java
@Component
public class JdbcTmplUserServiceImpl implements JdbcTmplUserService {

    @Autowired
    private JdbcTemplate jdbcTemplate = null;

    private RowMapper<User> getUserMapper() {
        // 对于小于 java 8 的可能需要使用声明类或者匿名类
        RowMapper<User> userRowMapper = (ResultSet rs, int rownum) -> {
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setName(rs.getString("name"));
            user.setAddress(rs.getString("address"));
            return user;
        };
        return userRowMapper;
    }

    @Override
    public User getUser(int id) {
        String sql = "select id, name, address from user where id = ?";
        Object[] params = new Object[] {id};
        User user = jdbcTemplate.queryForObject(sql, params, getUserMapper());
        return user;
    }

    @Override
    public List<User> findUsers(String uname, String address) {
        String sql = "select id, name, address from user "
                    + "where name like concat ('%', ?, '%') "
                    + "and address like concat ('%', ?, '%') ";

        Object[] params = new Object[] {uname, address};
        List<User> users= jdbcTemplate.query(sql, params, getUserMapper());
        return users;
    }

    @Override
    public int insertUser(User user) {
        String sql = "insert into user (name, address) values (?, ?)";
        return jdbcTemplate.update(sql, user.getName(), user.getAddress());
    }

    @Override
    public int updateUser(User user) {
        String sql = "update user set name = ?, address = ? "
                    + "where id = ?";
        return jdbcTemplate.update(sql, user.getName(), user.getAddress(), user.getId());
    }

    @Override
    public int deleteUser(int id) {
        String sql = "delete from user where id = ? ";
        return jdbcTemplate.update(sql, id);
    }
}

上面的需要说明的几点

  • 利用 @Autowired 注入 JdbcTemplate 类型 bean (由 spring 在配置里获取 datasource)以供后面使用,Template 里屏蔽了数据库连接的打开关闭等等操作。
  • 在 JdbcTemplate 里的映射关系是需要自己实现 RowMapper 接口,也就是抽取从结果集到 POJO 的逻辑,统一应用。

随后相应的 Controller 里简单的处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@RestController
@RequestMapping("/user")
public class UserController {
   @GetMapping("/{id}")
    public User getUser(@PathVariable int id) {
        return userService.getUser(id);
    }

    @PostMapping(consumes = "application/json")
    public int createUser(@RequestBody User user) {
        return userService.insertUser(user);
    }
}

JPA

JPA(Java Persistence API)是定义了 ORM 以及实体对象持久化的标准接口。JPA 是 JSR-220 的一部分,在 Spring boot 中的 JPA 是依靠 Hibernate 来实现的。

JPA 所维护的核心是实体(Entity Bean),而他是通过一个持久化上下文来使用的,该上下文包含以下 3 部分:

  • 对象关系映射(Object Relational Mapping,简称ORM,或O/RM,或O/R映射)描述,JPA 支持注解或 XML 两种形式的描述,在 SpringBoot 中主要通过注解实现;

  • 实体操作 API, 通过这节规范可以实现对实体对象的 CRUD 操作,来完成对象的持久化和查询;

  • 查询语言,约定了面向对象的查询语言 JPQL(JavaPersistenceQueryLanguage),通过这层关系可以实现比较灵活的查询。

下面对 JPA 的简单使用作介绍。

JPA 映射实体类

先添加 JPA 的依赖。

1
2
3
4
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

需要定义实体类,并将该类与数据库中的表做对应。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// entity.User.java
// Entity 标明为实体类,Table 定义映射的表
@Entity(name="user")
@Table(name="user")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    // 定义属性和表字段关系,默认为字段名
    @Column(name = "name")
    private String name;
    private String address;

    // getter and setter
}

JPA 接口定义

使用 JPA 的话,默认附带一些简单的方法,也有两种方式实现复杂场景下的查询:JPA 查询语言 和 JPA 命名查询,这两种方式都可以在定义接口时候完成的,其接口的具体实现由 JPA 来动态完成。

1
2
3
4
5
6
7
8
9
// dao.JpaUserRepository.java
public interface JpaUserRepository extends JpaRepository<User, Integer> {
    // JAP 查询语言(JPQL)
    @Query("select u.address from user u where u.id = ?1 ")
    public String getUserAddress(int id);

    // 命名查询
    public List<User> getUsersByAddressIn(List<String> address);
}

getUserAddress 使用注解和 JPQL 语言来完成查询;而命名查询使用接口方法的名字来组成响应的语义,如这里的 getUsersByAddressIn 的方法名其实是有实际含义的,以 get/find 动词开头,以 by 定义用什么字段查询,in 表示使用的是范围等等。在 IDEA 中编写时候会有相应的提示,这里仅仅是对这两种方式简单的解释下。

JPA 的配置

如需注册 JPA 的实体类和接口,不能以简单的 Spring 方式加入上下文中(无具体的含义),需要在 Spring boot 的启动类中加上两个 @EnableJpaRepositories @EntityScan 注解实现扫描。数据库的配置,使用先前的 spring.datasource 即可。

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableJpaRepositories(basePackages = "contacts.dao")
@EntityScan(basePackages = "contacts.entity")
public class ContactsApplication {
    public static void main(String[] args) {
        SpringApplication.run(ContactsApplication.class, args);
    }
}

之后就可以在编写相应的 web 服务,将由 JPA 生成的实现了自定义的 JpaUserRepository 接口的实现注入到 jpaUserRepository 中。JPA 的 update 和 create 可以由 save 方法来实现。

 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
@RestController
@RequestMapping("/jpa")
public class JpaController {

    @Autowired
    private JpaUserRepository jpaUserRepository = null;

    @PostMapping("/user")
    public User createUser(@RequestBody User user) {
        return jpaUserRepository.save(user);
    }

    @GetMapping("/user/byAddress")
    public List<User> getUserInAddress(@RequestParam List<String> address) {
        return jpaUserRepository.getUsersByAddressIn(address);
    }

    @GetMapping("/address/{id}")
    public String getUserAddress(@PathVariable int id) {
        return jpaUserRepository.getUserAddress(id);
    }

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable int id) {
        return jpaUserRepository.findById(id).get();
    }
}

Mybatis

Mybatis 相比 JPA 和 Hibernate 更为简单、灵活。避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集,采用 SQL 语句来制定功能,无需定义类似 JPA 的 Entity,POJO(Plain Old Java Objects) 即可。

添加 mybatis 的依赖,注意这里要指明下版本

1
2
3
4
5
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>1.3.2</version>
</dependency>

Mapper

和 JPA 一样,只需要定义接口即可,类似 JPQL 的方式注解来指明动作,只不过是使用的 SQL 语句,如这里的采用的 MySQL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// mapper.UserMapper.java
@Repository
@Mapper
public interface UserMapper {

    @Select("select id, name, address from user where id = #{id}")
    User getUserById(@Param("id") int id);

    @Insert("insert into user (name, address) " +
            "values(#{u.name}, #{u.address})")
    void createUser(@Param("u") User user);
}

上面的几点说明:

  • 接口使用 @Mapper 注解,就无需配置了。这里的 @Repository 不是必须的,加了之后可以避免注入时候 IDEA 里报找不到 bean 的错误。
  • @Select@Insert 里的为改接口的 SQL 语句,#{} 的形式指定对应接口参数指定的名称(使用 @Param 注解的),可以使用 . 访问对象参数里的成员(通过 get 方法)
  • 结果集由 mybatis 自动获取、填充到接口方法的返回值当中。

编写简单的 REST 控制器(这里省略掉 service 接口和 service 实现,正确的方式是应当在 service 里 注入 UserMapper):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/mybatis")
public class MybaitsController {

    @Autowired
    private UserMapper userMapper = null;

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable int id) {
        return userMapper.getUserById(id);
    }

    @PostMapping(value = "/user", consumes = "application/json")
    public String createUser(@RequestBody User user) {
        userMapper.createUser(user);
        return "Success";
    }
}

返回的结果默认是以名称绑定的,如果字段名和 POJO 的不一致需要使用 @Results@Result 指定下:

1
2
3
4
5
6
@Results({
        @Result(property = "name", column = "name"),
        @Result(property = "address", column = "address"),
})
@Select("select id, name, address from user where id = #{id}")
User getUserById(@Param("id") int id);

一般的在数据库中,由于不同平台大小写敏感程度不一样,一般字段和表名都是以下划线分隔的,在 java 中一般是以驼峰风格命名的,如 gmt_creategmtCreate 这样的转换可以直接由配置来完成,如:

1
mybatis.configuration.map-underscore-to-camel-case=true

更多功能和特性参考 mybatis 文档。不过,由于 Java 注解的一些限制加之某些 MyBatis 映射的复杂性,XML 映射对于大多数高级映射(比如:嵌套 Join 映射)来说仍然是必须的。

获取创建的数据

在 REST 接口中常常需要在 create 之后返回资源对象本身或者 id,但是在上面的 createUser Mapper 当中,返回值除了 void 之外还可以是 int (SQL 返回),如果创建成功的话,POJO 对象里的数据肯定是照样存入数据库当中,但是有些自动生成的字段,在创建成功之后存在于数据库中,但是没有反映到传入的 POJO 中。这时候直接返回 POJO 会缺少哪些字段更新/创建字段的信息,比如比较致命的 ID。

还好在 mybatis 中可以使用 @SelectKey 或者 @Options 这两个注解完成这个需求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Repository
@Mapper
public interface UserMapper {
    // before=false 即 after,在运行完 SQL 之后再执行 select @@IDENTITY 并 set 到传入的 POJO u.id 之中
    @SelectKey(statement = "select @@IDENTITY", keyProperty = "u.id", before = false, resultType = Integer.class)
    int createUser1(@Param("u") User user);

    @Options(useGeneratedKeys = true, keyProperty = "u.id", keyColumn = "id")
    int createUser2(@Param("u") User user);
}

可以通过配置 mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl 来输出原生 SQL,上面的两个方式打印出 SQL 看球来是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
==>  Preparing: insert into user (name, address) values( ?, ?)
==> Parameters: Scott(String), Jingzhou(String)
<==    Updates: 1
==>  Preparing: select @@IDENTITY
==> Parameters:
<==    Columns: @@IDENTITY
<==        Row: 20
<==      Total: 1

==>  Preparing: insert into user (name, address) values( ?, ?)
==> Parameters: Scott(String), Jingzhou(String)
<==    Updates: 1

很明显 @SelectKey 会再执行一条 SQL 语句,而使用 useGeneratedKeys 的方式貌似只会使用一条语句,但是是怎么获取到插入的 ID 的还有待探究(直接看 MySQL 的 general_log 在前期也是看不到获取 @@IDENTITY 的这样操作的)。所以平时可能使用 useGeneratedKeys 可能效率更好些。

MyBatis-Plus

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,国人的一个开源程序,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

h2 database

事务

Spring AOP 的约定使得事务处理更加简化,不必编写大量的重复的事务的处理如:setAutoCommit, setTransactionIsolation, roolback, try catch 等等,利用 @Transactional 即可将业务切面独立出来,将其它的枯燥的代码隐藏于 Spring 切面之中。

@Transactional 可以注解接口,也可以注解实现类。通常情况下推荐在实现类里面注解,方便使用 CGLIB 动态代理,不然就只能使用 JDK 的动态代理(?)。

Spring 中事务管理器的顶层接口为 PlatformTransactionManager,使用 mybatis-spring-boot-starter 之后会自动创建一个 DataSourceTransactionManager 作为事务管理器。

1
2
3
4
5
6
7
8
9
@Service
public class MybatisUserServiceImpl implements UserService {
    // Transactional 一般放在 service 层,而不是 dao 层
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1)
    public User insertUser(User user) {
        userMapper.createUser(user);
    // ...
}

可以在名为 spring.datasource.* 的配置项中设置各个连接的默认事务级别,数据库隔离级别参考 Transaction

造成成回滚的 exception 可以过滤定义,如 @Transactional( rollbackFor={Exception.class, ...}),或者过滤掉某些 @Transactional(notRollbackFor=RunTimeException.class),一般来说需要时 Runtime Exception 的子类。

传播行为

在大规模的业务代码中,有可能分为几个小的独立步骤,这时候如果将事务边界设置为整段代码,那么独立的步骤即使成功也将回滚。例如批量操作插入,为了更好的处理这种情况就需要涉及到事务的传播。

可以在 @Transactional 注解中使用 propagation 参数指定事务传播方式,总的来说有一下的几种方式(有注释的为较为重要的):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public enum Propagation {
    REQUIRED(0),        // 默认,需要事务,如果当前存在事务就沿用当前事务,否则新建
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),    // 无论事务存在与否,都会创建新事务(非嵌套子事务),即则把当前事务挂起。
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);          // 嵌套事务,只会回滚当前方法调用出错的子事务的 SQL
    // ...
}

然后可以使用 @Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1, propagation = Propagation.REQUIRED)

需要注意的是,对于一个 service 内的事务方法的组合相互调用,propagation 是无效的,因为 AOP 是通过动态代理完成的,类里的方法间的调用是没有经过代理的处理的。要想完成既定的需求可以从 context 中获取本 class 的实例然后在调用:

 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
@Service
public class MybatisUserServiceImpl implements UserService, ApplicationContextAware{
    @Autowired
    private UserMapper userMapper;

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public User updateUser(User user) {
        // ...
    }

    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED, timeout = 1, propagation = Propagation.REQUIRED)
    public User insertUser(User user) {
        UserService userService = applicationContext.getBean(UserService.class);
        // ...
        return user;
    }
}

更多的关于事务传播的知识可以参考 事务之六:spring 嵌套事务

连接池