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_create
到 gmtCreate
这样的转换可以直接由配置来完成,如:
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 嵌套事务
连接池