我们知道,如果采用领域驱动开发(DDD)的话,采用JPA技术,会非常方便。但是对于复杂的多表联合查询,使用JPA技术就比较费力了。为了解决复杂SQL查询问题,很多项目采用了MyBatis。但是Spring提倡大家使用JPA,对MyBatis技术实际上是有一点儿抵制的。我们在实际项目中,采用数据库增删改采用JPA,而复杂数据库SQL查询,直接采用JDBC来实现。采用这种方式,也符合大容量、高并发网站架构,因为在处理大容量、高并发时,数据库读写分离是普遍采用技术。我们采用JPA与JDBC相结合的方式,也可以完美地用于数据库读写分离场景。
我们首先需要向工程中添加依赖库:
......
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
......
接着我们在src/main/resource/application.properties中声明项目中需要用到的数据源,我们采用Spring Boot内置的数据库连接池,采用读、写分离原则,分别定义read-ds和write-ds,如下所示:
# JdbcTemplate
spring.datasource.read-ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.read-ds.jdbc-url=jdbc:mysql://localhost:3306/MseDb?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.read-ds.username=mse
spring.datasource.read-ds.password=mse2018
spring.datasource.read-ds.max-active=40
spring.datasource.read-ds.max-idle=5
spring.datasource.read-ds.min-idle=5
spring.datasource.read-ds.initial-size=5
spring.datasource.read-ds.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.write-ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.write-ds.jdbc-url=jdbc:mysql://localhost:3306/MseDb?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.write-ds.username=mse
spring.datasource.write-ds.password=mse2018
spring.datasource.write-ds.max-active=10
spring.datasource.write-ds.max-idle=5
spring.datasource.write-ds.min-idle=5
spring.datasource.write-ds.initial-size=5
spring.datasource.write-ds.type=com.zaxxer.hikari.HikariDataSource
接着我们在程序中对数据库进行配置,如下所示:
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class MseDatasource {
@Bean(name = "readDataSource")
@Qualifier("readDataSource")
@ConfigurationProperties(prefix = "spring.datasource.read-ds")
public DataSource readDataSource(){
return DataSourceBuilder.create().build();
}
@Bean(name = "writeDataSource")
@Qualifier("writeDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.write-ds")
public DataSource secondaryDataSource(){
return DataSourceBuilder.create().build();
}
@Bean(name = "readJdbcTemplate")
public JdbcTemplate readJdbcTemplate(@Qualifier("readDataSource")DataSource readDataSource){
return new JdbcTemplate(readDataSource);
}
@Bean(name = "writeJdbcTemplate")
public JdbcTemplate writeJdbcTemplate(@Qualifier("writeDataSource")DataSource writeDataSource){
return new JdbcTemplate(writeDataSource);
}
}
我们在需要进行数据库操作的DAO文件中,引入JdbcTemplate对象,进行数据库操作,如下所示:
import java.util.List;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class ProductMysqlDao implements ProductDao {
@Autowired
@Qualifier("readJdbcTemplate")
private JdbcTemplate jdbcTemplate;
public List<ProductVo> getProducts() {
Object[] params = new Object[1];
params[0] = 1;
List<ProductVo> recs = jdbcTemplate.query("select product_name, image_url, price from t_product where product_category_id=?", params, (rs, num) -> {
ProductVo vo = new ProductVo();
return vo;
});
return recs;
}
}
在服务的调用处,引入DAO对象调用其方法,如下所示:
// ProductController.java
@Autowired
private ProductMysqlDao dao;
@GetMapping("/products/jdbc")
public List<ProductVo> testJdbc() {
//ProductDao dao = DaoFactory.getProductDao(DaoFactory.DB_MYSQL);
return dao.getProducts();
}
到此为止,我们就成功配置出了能够满足读写分离需求的数据库连接。我们可以将增删改查操作委托给JPA,而复杂的多表查询用JdbcTemplate来实现。
在实际应用中,由于需要保证数据的一致性,因此事务是非常重要的。我们在设计时,尽量要让事务封装在一个微服务之内,如果是跨微服务的事务,由于是分布式事务,处理起来非常复杂,我们一般应该尽量避免。
我们首先在ProductMysqlDao中生成一个需要插入两个产品的方法,并将该方法注解为需要事务,如下所示:
@Transactional
public HttpSimpleResponse testTransaction() {
HttpSimpleResponse resp = new HttpSimpleResponse(0, "Ok", "Success");
Object[] args = new Object[5];
args[0] = "新1001";
args[1] = "x1001.jpg";
args[2] = 1333.3;
args[3] = 3;
args[4] = 1;
int[] argTypes = new int[5];
argTypes[0] = Types.VARCHAR;
argTypes[1] = Types.VARCHAR;
argTypes[2] = Types.DOUBLE;
argTypes[3] = Types.INTEGER;
argTypes[4] = Types.INTEGER;
int affectedRows = writeJdbcTemplate.update("insert into t_product(product_name, image_url, price, quantity, product_category_id) values(?, ?, ?, ?, ?)", args, argTypes);
System.out.println("ar=" + affectedRows + "!");
args[0] = "事务001";
int r2 = writeJdbcTemplate.update("insert into t_product(product_name, image_url, price, quantity, product_category_id) values(?, ?, ?, ?, ?)", args, argTypes);
System.out.println("r2=" + r2 + "!");
return resp;
}
我们用@Transactional来表明需要使用事务,其后是两条数据库插入操作。
我们在服务定义类ProductController中,调用此方法:
@GetMapping("/products/testTransaction")
public HttpSimpleResponse testTransaction() {
try {
return dao.testTransaction();
} catch (Exception ex) {
return new HttpSimpleResponse(4, "事务失败", ex.getMessage());
}
}
如果发生任何失败情况,直接返回失败原因。
如果我们直运行上面的代码,会成功向数据库中插入两条产品记录。我们将第2个SQL语句改错,则会提示如下信息:
如上图所示,出现了数据操作错误,并且虽然第一条可以正常插入,但是整个事务的状态是失败,所以数据库中也没有插入任何记录。由此可见,只需要一个简单的@Transactional注解,就可以实现对事务的支持,非常强大。