经过上一节的讨论,相信你已经有了一套可运维的MySQL服务器了,接下来的两节,我们来讨论如何在Spring Boot中整合MySQL。
在Spring Boot中整合MySQL有很多方式,常见的有:
- Spring JDBC Template直接集成
- Spring Data JPA集成
- Hibernate集成
- MyBatis集成
使用过Spring框架开发的同学,可能对后两种比较熟悉。但是这两种方法过于重量级,本书将专注于前两种,其中:
- JDBC Template需要直接编写SQL语句,更加接近数据库底层,开发效率低、性能高。
- Spring Data JPA可以自动生成部分参数、解析结果的语句,开发效率高,性能低一些。 上述两种方法各有优略,大家可以根据实际情况作出选择。
数据源配置
无论是选用哪种集成方式,数据源的集成都是必不可少的。
为了提升性能,一般会使用数据库连接池,我们采用Spring Boot默认的Tomcat连接池,只需要如下依赖配置即可生效:
compile 'org.springframework.boot:spring-boot-starter-jdbc'
compile 'mysql:mysql-connector-java:5.1.9'
接下来我们看一下数据源的配置,在application.yaml中添加:
spring.datasource:
url: jdbc:mysql://mysql/lmsia_abc?rewriteBatchedStatements=true
username: lmsia
password: pass
testOnBorrow: true
validationQuery: SELECT 1
tomcat:
max-active: 500
如上所示,除了基本的url和用户名、密码外,还设定了一系列额外参数,这些都是生产环境建议设置的,解释一下:
- testOnBorrow / validationQuery: 从连接池取出连接后,先检查是否可用。这主要是解决长时间空闲情况下MySQL Server的[Gone Away问题]https://dev.mysql.com/doc/refman/8.0/en/gone-away.html)
- tomcat.max-active: 连接池最大连接数设定为500,默认的100在高并发场景下可能不够。
- rewriteBatchedStatements: 只有设置为true,才会默认启用batch模式,可提升批量写入的性能。
添加了上述配置后,Spring Boot会自动生成DataSource以及NamedParameterJdbcTemplate。我们可以通过后者直接操作数据库。
通过JDBCTemplate操作数据库
在操作数据库前,我们先来看一下数据表结构:
CREATE TABLE IF NOT EXISTS `user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(256) NOT NULL,
`createdTime` BIGINT(20) NOT NULL,
`updatedTime` BIGINT(20) NOT NULL,
PRIMARY KEY (`id`)
);
在微服务开发中,一般会将表的一行映射成一个实体:
import lombok.Data;
@Data
public class User {
private int id;
private String name;
private long createdTime;
private long updatedTime;
}
其中上面的@Data是lombok的注解,用于帮我们自动生成getter和setter,感兴趣的同学可以看这lombok官方文档,这里不再详述。
来看一下数据库操作,我们将其封装在了Repository中:
@Repository
public class UserRepositoryImpl implements UserRepository {
protected Logger LOG = LoggerFactory.getLogger(getClass());
@Autowired
protected NamedParameterJdbcTemplate db;
private RowMapper<User> ROW_MAPPER = new BeanPropertyRowMapper(User.class);
@Override
public void add(User user) {
String sql = "INSERT INTO `user`(`name`, `createdTime`, `updatedTime`) VALUES " +
"(:name, :createdTime, :updatedTime)";
SqlParameterSource param = new BeanPropertySqlParameterSource(user);
KeyHolder keyHolder = new GeneratedKeyHolder();
db.update(sql, param, keyHolder);
LOG.info("insert succ, id = {}", keyHolder.getKey().longValue());
}
@Override
public Optional<User> getUserById(int id) {
String sql = "SELECT * FROM `user` WHERE `id` = :id";
SqlParameterSource param = new MapSqlParameterSource("id", id);
try {
return Optional.ofNullable(db.queryForObject(sql, param, ROW_MAPPER));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
}
解读一下上面的代码:
- 通过Autowired自动注入NamedParameterJdbcTemplate
- JdbcTemplate上执行update和query来完成插入或查询
- 查询参数通过SqlParameterSource传入,返回值的对象映射通过RowMapper完成。
两个数据源
在上面的数据源配置、数据库操作中,都存在一个假设:只有一个数据源。
如果一个微服务要同时依赖多个数据库,需要做如下事情:
- 配置不同的数据源,建议不要采用默认的spring.datasource前缀,这主要是为了避免@Autowired时命名冲突。
- 为多个数据源手动声明Configuration,包含多组DataSource和JdbcTemplate
例如我们现在要添加2个数据库的数据源,那么配置文件要变成:
db1.datasource:
url: jdbc:mysql://mysql/db1?rewriteBatchedStatements=true
username: db1
password: pass
testOnBorrow: true
validationQuery: SELECT 1
tomcat:
max-active: 500
db2.datasource:
url: jdbc:mysql://mysql/db2?rewriteBatchedStatements=true
username: db2
password: pass
testOnBorrow: true
validationQuery: SELECT 1
tomcat: max-active: 500
由于不采用默认的spring.datasource前缀了,Spring Boot默认不会激活自动配置,需要手动编写:
@Configuration
@EnableTransactionManagement
public class DataSourceConfiguration {
@Bean(name = "db1JdbcTemplate")
@Primary
public NamedParameterJdbcTemplate initDb1JdbcTemplate(
@Autowired @Qualifier("db1DataSource") DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
@Bean
@Primary
@ConfigurationProperties(prefix = "db1.datasource")
public DataSource db1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "db2JdbcTemplate")
public NamedParameterJdbcTemplate initDb2JdbcTemplate(
@Autowired @Qualifier("db2DataSource") DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
@Bean
@ConfigurationProperties(prefix = "db2.datasource")
public DataSource db2DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public PlatformTransactionManager txManager() {
return new DataSourceTransactionManager(tutorClockinWriterDataSource());
}
}
简单说明一下:
- 根据自定义的前缀生成对应DataSource。
- 根据DataSource生成对应的NamedParameterJdbcTemplate。
- 因为要生成两组Datasource和NamedParameterJdbcTemplate,所以有一组要设置为@Primary,这是Spring Boot的要求。
在使用时,因为有两个NamedParameterJdbcTemplate了,所以要补充一下名字以做区分,如下:
@Autowired
@Qualifier("db1")
protected NamedParameterJdbcTemplate db1;
@Autowired
@Qualifier("db2")
protected NamedParameterJdbcTemplate db2;
区分了不同的NamedParameterJdbcTemplate后,其余的数据库操作和一个Datasource时是完全相同的,这里不再赘述。
通过JPA操纵数据库
前面提到了,除了JdbcTemplate外,还可以使用JPA来操作数据库。
由于spring-boot-starer-data-jpa显示以依赖了spring-boot-starter-jdbc,所以我们可以直接替换依赖:
compile 'org.springframework.boot:spring-boot-starter-data-jpa'
这一步替换,将不会影响DataSource、JdbcTemplate的自动注入,JPA也是需要Datasource和JdbcTemplate才能正常完成工作的。
Spring Boot JPA的默认实现是通过Hibernete完成的(JPA只是一套接口,Hibernete是接口的一种实现)。
jpa需要在YAML中添加一些特殊配置:
spring.jpa.properties.hibernate.dialect: org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto: validate
spring.jpa.hibernate.naming.physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
其中:
- hibernate.dialect让Hibernete可以更高效的生成SQL
- ddl-auto设置为validate,不自动创建表但是会验证表与实体是否符合
- naming.physical-strategy表字段名映射为驼峰命名
针对要操作的实体,需要做一些特殊注解,以让JPA可以关联到对应的表上,为了对比说明,我们单独创建了一个对象UserForJpa:
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
/**
* @author coder4
*/
@Data
@Entity
@Table(name = "user")
public class UserForJpa {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(nullable = false)
private String name;
@Column(name = "createdTime", nullable = false, updatable = false)
private long createdTime;
@Column(name = "updatedTime", nullable = false)
private long updatedTime;
}
说明一下:
- 实体通过@Entity标注
- @Table关联实体和表
- @Id和@GeneratedValue完成自增主键的声明
- @Column是普通列的声明,可设置是否nullable以及是否可更新
在数据库操作中,我们可以完全让JPA帮我们生成SQL,如下:
@Repository
public interface UserJpaRepository extends JpaRepository<UserForJpa, Integer> {
}
是的,你没有看错,我们不需要编写任何方法,就能自动获得save(), findOne(), findAll(), count(), delete()等接口,具体可以参见JpaRepository的源代码。
我们看一下调用方式:
userJpaRepository.findOne(userId);
userJpaRepository.save(user);
JpaRepository提供的都是较为基础的操作,有事无法完全满足我们的需求。我们可以自行定义SQL,如下:
@Query(
value = "SELECT * FROM `user` ORDER BY `id` DESC LIMIT 1",
nativeQuery = true)
UserForJpa findLatestUser();
如上所示,我们通过@Query注解实现了通过指定SQL查找最新注册的用户。
小结
在本小节中,我们首先介绍了Sping Boot中MySQL数据源的配置,随后,介绍了如何配置多个数据源并手动注入DataSource、JdbcTemplate。
接下来,我们介绍了两种数据库操作方法:
- JdbcTemplate更接近数据库底层,需要编写较多代码,性能较好
- Spring JPA Data可以自动生成部分代码,开发效率高,性能稍差,且对POJO具有一定的侵入性
上述两种方法各有优劣,大家可以根据实际需求进行选择。
拓展阅读
- Tomcat数据库连接池的详细配置参数可以参考官方参数文档
- Spring JPA Data更详细的用法可以参考Spring JPA Data官方文档
- Spring JDBC更详细的用法可以参考Spring JDBC官方文档
下一节:如果业务进一步发展,通过"读写分离"、"分库分表"后,数据库的性能依然无法满足高并发读请求,此时就需要缓存出马了。