重学Springboot系列之整合数据库开发框架---上

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重学Springboot系列之整合数据库开发框架---上相关的知识,希望对你有一定的参考价值。


整合Spring JDBC操作数据

jdbc简介

JDBC(Java DataBase Connectivity)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序,

术语:什么是持久层:持久层就是指对数据进行持久化操作的代码,比如将数据保存到数据库、文件、磁盘等操作都是持久层操作。所谓持久就是保存起来的意思。对于web应用最常用的持久层框架就是JDBC、Mybatis、JPA。


使用jdbc操作数据库的步骤

直接在 Java 程序中使用 JDBC 比较复杂,需要 7 步才能完成数据库的操作:

  • 加载数据库驱动
  • 建立数据库连接
  • 创建数据库操作对象
  • 定义操作的 SQL 语句
  • 执行数据库操作
  • 获取并操作结果集
  • 关闭对象,回收资源

关键代码如下:

try 

    // 1、加载数据库驱动
    Class.forName(driver);

    // 2、获取数据库连接
    conn = DriverManager.getConnection(url, username, password);

    // 3、获取数据库操作对象
    stmt = conn.createStatement();

    // 4、定义操作的 SQL 语句
    String sql = "select * from user where id = 6";

    // 5、执行数据库操作
    rs = stmt.executeQuery(sql);

    // 6、获取并操作结果集
    while (rs.next()) 

    // 解析结果集

    

 catch (Exception e) 
    // 日志信息
 finally 
    // 7、关闭资源

通过上面的示例可以看出直接使用 JDBC 来操作数据库比较复杂。为此,Spring Boot 针对 JDBC 的使用提供了对应的 Starter 包:spring-boot-starter-jdbc,它其实就是在 Spring JDBC 上做了进一步的封装,方便在 Spring Boot 生态中更好的使用 JDBC,下面进行示例演示。

Spring JDBC详细教程

不论是JDBC,还是封装之后的Spring JDBC,直接操作数据库都比较麻烦。如果企业有成熟的ORM知识积累,并且无特殊需求,不建议直接使用JDBC操作数据库


将Spring JDBC集成到Spring boot项目

第一步:引入maven依赖包,包括spring JDBC和mysql驱动。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

第二步:修改application.yml,增加数据库连接、用户名、密码相关的配置。driver-class-name请根据自己使用的数据库和数据库版本准确填写。

spring:
  datasource:
    url: jdbc:mysql://192.168.161.3:3306/testdb?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: dhy
    password: 12346
    driver-class-name: com.mysql.cj.jdbc.Driver
  • mysql-connector-java 5版本及其以下,使用com.mysql.jdbc.Driver
  • mysql-connector-java 6版本及其以上,使用com.mysql.cj.jdbc.Driver

spring boot jdbc 基础代码

spring jdbc集成完毕之后,我们来写代码做一个基本的测试。首先我们新建一张测试表article

CREATE TABLE `article` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`author` VARCHAR(32) NOT NULL,
	`title` VARCHAR(32) NOT NULL,
	`content` VARCHAR(512) NOT NULL,
	`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	PRIMARY KEY (`id`)
)
COMMENT='文章'
ENGINE=InnoDB;

MySQL5.6之后的版本才支持CURRENT_TIMESTAMP

mysql中对于UPDATE_TIME字段我们有时候会设置ON UPDATE CURRENT_TIMESTAMP,表示在数据库数据有更新的时候UPDATE_TIME的时间会自动更新

DAO层代码:

  • jdbcTemplate.update适合于insert 、update和delete操作;
  • jdbcTemplate.queryForObject用于查询单条记录返回结果
  • jdbcTemplate.query用于查询结果列表
  • BeanPropertyRowMapper可以将数据库字段的值向Article对象映射,满足驼峰标识也可以自动映射。如:数据库create_time字段映射到createTime属性。
@Repository  //持久层依赖注入注解
public class ArticleJDBCDAO 

    @Resource
    private JdbcTemplate jdbcTemplate;

    //保存文章
    public void save(Article article) 
        //jdbcTemplate.update适合于insert 、update和delete操作;
        jdbcTemplate.update("INSERT INTO article(author, title,content) values(?, ?, ?)",
                article.getAuthor(),
                article.getTitle(),
                article.getContent());

    

    //删除文章
    public void deleteById(Long id) 
        //jdbcTemplate.update适合于insert 、update和delete操作;
        jdbcTemplate.update("DELETE FROM article WHERE id = ?",id);

    

    //更新文章
    public void updateById(Article article) 
        //jdbcTemplate.update适合于insert 、update和delete操作;
        jdbcTemplate.update("UPDATE article SET author = ?, title = ? ,content = ? WHERE id = ?",
                article.getAuthor(),
                article.getTitle(),
                article.getContent(),
                article.getId());

    

    //根据id查找文章
    public Article findById(Long id) 
        //queryForObject用于查询单条记录返回结果
        return (Article) jdbcTemplate.queryForObject("SELECT * FROM article WHERE id=?",
                new Object[]id,new BeanPropertyRowMapper<>(Article.class));
    

    //查询所有
    public List<Article> findAll()
        //query用于查询结果列表
        return (List<Article>) jdbcTemplate.query("SELECT * FROM article ",  new BeanPropertyRowMapper<>(Article.class));
    





service层接口

public interface ArticleService 

     void saveArticle(Article article);

     void deleteArticle(Long id);

     void updateArticle(Article article);

     Article getArticle(Long id);

     List<Article> getAll();

service层操作JDBC持久层

@Slf4j
@Service   //服务层依赖注入注解
public class ArticlleJDBCService  implements  ArticleService  

    @Resource
    private
    ArticleJDBCDAO articleJDBCDAO;

    @Transactional
    public void saveArticle( Article article) 
        articleJDBCDAO.save(article);
        //int a = 2/0;  //人为制造一个异常,用于测试事务
    

    public void deleteArticle(Long id)
        articleJDBCDAO.deleteById(id);
    

    public void updateArticle(Article article)
        articleJDBCDAO.updateById(article);
    

    public Article getArticle(Long id)
        return articleJDBCDAO.findById(id);
    

    public List<Article> getAll()
        return articleJDBCDAO.findAll();
    


最后,在我们之前的章节为大家实现的ArticleController中调用ArticleRestJDBCService 实现方法,进行从Controller 到 Service 到 DAO层的全流程测试。

@RestController
public class AtricleController

    @Autowired
    private ArticleService articleService;

    @PostMapping("/articles")
    public Object save(@RequestBody Article article)
    
    articleService.saveArticle(article);
    return article;
    

    @PutMapping("/articles")
    public Object update(@RequestBody Article article)
    
        articleService.updateArticle(article);
        return article;
    

    @GetMapping("/articles")
    public Object get(@RequestParam Long id)
    
        Article article = articleService.getArticle(id);
        return article;
    

    @DeleteMapping("/articles")
    public Object delete(@RequestParam Long id)
    
        articleService.deleteArticle(id);
        return id;
    

  • 重点测试一下事务的回滚,人为制造一个被除数为0的异常。
  • 在saveArticle方法上使用了@Trasactional注解,该注解基本功能为事务管理,保证saveArticle方法一旦有异常,所有的数据库操作就回滚。

Spring JDBC多数据源的实现

随着应用的数据量增多,很可能会采用数据分库存储的方案,所以说对于我们的持久层代码可能面临在一个服务函数中操作多个数据库的场景。我们该如何通过配置去满足这样的场景?本节来为大家介绍。

配置多个数据源

application.yml配置2个数据源,第一个叫做primary,第二个叫做secondary。注意两个数据源连接的是不同的库,testdb和testdb2.

spring:
  datasource:
    primary:
      jdbc-url: jdbc:mysql://192.168.161.3:3306/testdb?useUnicode=true&characterEncoding=utf-8&useSSL=false
      username: test
      password: 4rfv$RFV
      driver-class-name: com.mysql.cj.jdbc.Driver
    secondary:
      jdbc-url: jdbc:mysql://192.168.161.3:3306/testdb2?useUnicode=true&characterEncoding=utf-8&useSSL=false
      username: test
      password: 4rfv$RFV
      driver-class-name: com.mysql.cj.jdbc.Driver

通过Java Config将数据源注入到Spring上下文。

  • primaryJdbcTemplate使用primaryDataSource数据源操作数据库testdb。
  • secondaryJdbcTemplate使用secondaryDataSource数据源操作数据库testdb2。
@Configuration
public class DataSourceConfig 
    @Primary
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix="spring.datasource.primary")   //testdb
    public DataSource primaryDataSource() 
            return DataSourceBuilder.create().build();
    

    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties(prefix="spring.datasource.secondary")   //testdb2
    public DataSource secondaryDataSource() 
        return DataSourceBuilder.create().build();
    

    @Bean(name="primaryJdbcTemplate")
    public JdbcTemplate primaryJdbcTemplate (@Qualifier("primaryDataSource") DataSource dataSource ) 
        return new JdbcTemplate(dataSource);
    

    @Bean(name="secondaryJdbcTemplate")
    public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource dataSource) 
        return new JdbcTemplate(dataSource);
    

  • primaryDataSource和secondaryDataSource都是DataSource接口的实例化对象(Bean)
  • @Primary注解的作用是当一个接口有多个实现类的时候,我们在主实现类对象的上面加上这个注解。表示当Spring如果只能选一个实现进行依赖注入的时候,就选@Primary标识的这个Bean。(如果这个项目只使用一个数据源,那就是primaryDataSource)
  • @Qualifier明确通过编码的形式说明,当一个接口有多个实现类对象Bean的时候,我要使用哪一个Bean。

Spring实现多数据源配置的思想和使用方式


ArticleJDBCDAO改造

  • 注入primaryJdbcTemplate作为默认的数据库操作对象。
  • 将jdbcTemplate作为参数传入ArticleJDBCDAO的方法,不同的template操作不同的库。
    @Resource
    private JdbcTemplate primaryJdbcTemplate;


    //以保存文章为例,新增一个参数:jdbcTemplate ,其他的方法照做
    public void save(Article article,JdbcTemplate jdbcTemplate ) 
        if(jdbcTemplate == null)  //判断新增参数不能为空,如果为空使用primaryJdbcTemplate
            jdbcTemplate= primaryJdbcTemplate;
        

        //jdbcTemplate.update适合于insert 、update和delete操作;
        jdbcTemplate.update("INSERT INTO article(author, title,content) values(?, ?, ?)",
                article.getAuthor(),
                article.getTitle(),
                article.getContent());

    

测试同时向两个数据库保存数据

在src/test/java目录下,加入如下单元测试类,并进行测试。正常情况下,在testdb和testdb2数据库的article表,将分别插入一条数据,表示多数据源测试成功

// @RunWith(SpringRunner.class)  Junit4
@ExtendWith(SpringExtension.class)  //Junit5
@SpringBootTest
public class SpringJdbcTest 

    @Resource
    private ArticleJDBCDAO articleJDBCDAO;
    @Resource
    private JdbcTemplate primaryJdbcTemplate;
    @Resource
    private JdbcTemplate secondaryJdbcTemplate;


    @Test
    public void testJdbc() 
        articleJDBCDAO.save(
                Article.builder()
                .author("zimug").title("primaryJdbcTemplate").content("ceshi").createTime(new Date())
                .build(),
                primaryJdbcTemplate);
        articleJDBCDAO.save(
                Article.builder()
               .author("zimug").title("secondaryJdbcTemplate").content("ceshi").createTime(new Date())
               .build(),
                secondaryJdbcTemplate);
    




Spring JDBC JTA实现分布式事务

举例说明分布式事务

在上一节代码的的Service层做一下测试,人为制造一个被除数为0的异常。然后对该服务对应的Controller方法发送请求。(postman)


@Resource
private JdbcTemplate primaryJdbcTemplate;
@Resource
private JdbcTemplate secondaryJdbcTemplate;

@Transactional  //表示两个数据库操作放在一个事务里面执行
public Article saveArticle( Article article) 
    articleJDBCDAO.save(article,primaryJdbcTemplate);
    articleJDBCDAO.save(article,secondaryJdbcTemplate);
    int a = 2/0;    //人为制造一个被除数为0的异常
    return article;


secondaryJdbcTemplate的数据插入数据成功,primaryJdbcTemplate的数据插入数据失败,这显然不是我们想要的结果。这是因为:数据库事务不能跨连接, 当然也就不能跨数据源,更不能跨库。一旦出现跨连接的情况,也就成了分布式事务,分布式事务不能单纯依赖于数据库去处理。

我们期望的事务效果是:正常情况下数据库操作都成功,如果出现异常,操作必须都回滚、都失败。A向B进行银行转账,你不能A账户的钱少了,B账户的钱没增加。

大家要注意数据库事务和分布式事务区别:数据库事务是由单一的数据库实例来控制事务的提交与回滚。而分布式事务至少涉及到两个数据库实例,不能单一的由某一方自己控制事务的提交与回滚。 我们这一节的实现方式,是通过JTA来实现“分布式事务”。

举个例子:你自驾游想几点出发就几点出发,想几点回来就几点回来;如果你是跟团游,就得听导游的安排,不然你就赶不上车。JTA就是跟团游的团长、导游,负责管理团体的集合与返回。


通过整合JTA实现跨库分布式事务

  • XA是X/Open DTP组织(X/Open DTP group)定义的两阶段提交协议,XA被许多数据库(如Oracle、DB2、SQL Server、MySQL)和中间件等工具(如CICS 和 Tuxedo)支持 。
  • JTA规范:JTA(Java Transaction API),是J2EE的编程接口规范,它是XA协议的JAVA实现。某种程度上,可以认为JTA规范是XA规范的Java版。
  • Atomikos是一个分布式事务管理器,是JTA / XA的具体实现,提供的功能比JTA / XA所要求的更多。

大家先不用忙着去理解JTA这个名词,我们先实现分布式事务。用最简单的话说,分布式事务就是跨数据库操作的事务。一个事务的多数据库操作,要么都成功,要么都失败回滚。后面我们专门做一个章节讲解事务与分布式事务。


引入maven依赖包

atomikos是对JTA规范的一个标准实现。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

修改application配置文件

双数据源配置testdb和testdb2。删掉原有数据库连接配置.

primarydb:
  uniqueResourceName: primary
  xaDataSourceClassName: com.mysql.cj.jdbc.MysqlXADataSource
  xaProperties:
    url: jdbc:mysql://192.168.161.3:3306/testdb?useUnicode=true&characterEncoding=utf-8&useSSL=false
    user: test
    password: 4rfv$RFV
  exclusiveConnectionMode: true
  minPoolSize: 3
  maxPoolSize: 10
  testQuery: SELECT 1 from dual #由于采用HikiriCP,用于检测数据库连接是否存活。

secondarydb:
  uniqueResourceName: secondary
  xaDataSourceClassName: com.mysql.cj.jdbc.MysqlXADataSource
  xaProperties:
    url: jdbc:mysql://192.168.161.3:3306/testdb2?useUnicode=true&characterEncoding=utf-8&useSSL=false
    user: test
    password: 4rfv$RFV
 

以上是关于重学Springboot系列之整合数据库开发框架---上的主要内容,如果未能解决你的问题,请参考以下文章

重学Springboot系列之整合数据库开发框架---下

重学SpringBoot系列之整合静态资源与模板引擎

重学SpringBoot系列之日志框架与全局日志管理

重学Springboot系列之邮件发送的整合与使用

重学SpringBoot系列之EhCache缓存,缓存问题

重学SpringBoot系列之整合分布式文件系统