持久层是 Java EE 中访问数据库的核心操作,SpringBoot 对常见的持久层框架都提供了自动化配置,例如 JdbcTemplate,JPA 等,MyBatis 的自动化配置由官方提供

SpringBoot 版本 2.3.1

一、整合 JdbcTemplate

我们在学习 Spring 的时候,也接触过 JdbcTemplate,该模板框架利用了 AOP 技术解决直接使用 JDBC 的大量重复代码的问题。 JdbcTemplate 并没有 MyBaits 那么灵活,但是比纯 JDBC 会好一些

1.1 如何使用 JdbcTemplate 呢

  1. 提供 JDBCTemplate 的依赖
  2. 提供 DataSource 的依赖

1.2 创建项目

创建 SpringBoot 项目,引入 pom 依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <!-- 配置 JDBCTemplate -->
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-jdbc</artifactId>
  10. </dependency>
  11. <!-- 数据库连接驱动 -->
  12. <dependency>
  13. <groupId>mysql</groupId>
  14. <artifactId>mysql-connector-java</artifactId>
  15. <scope>runtime</scope>
  16. </dependency>
  17. <!-- 配置阿里巴巴的数据库连接池 -->
  18. <dependency>
  19. <groupId>com.alibaba</groupId>
  20. <artifactId>druid</artifactId>
  21. <version>1.1.22</version>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-test</artifactId>
  26. <scope>test</scope>
  27. <exclusions>
  28. <exclusion>
  29. <groupId>org.junit.vintage</groupId>
  30. <artifactId>junit-vintage-engine</artifactId>
  31. </exclusion>
  32. </exclusions>
  33. </dependency>
  34. </dependencies>

spring-boot-starter-jdbc 中提供了 spring寸idbc, 另外还加入了数据库驱动依赖和数据库连接池 依赖。

1.3 数据库信息配置

打开 application.yml (默认是 properties, 改成 yml) 文件。加入如下数据库配置

server:
  port: 8080
spring:
  datasource:
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8

我会用这张表
image.png

1.4 创建实体类 Student

根据数据结构创建对应的实体结构

public class Student {    
    private Integer id;
    private String sname;
    private Integer sage;
    private String sgencder;

    // 数据库 id 是主键,会自增,所以构造方法可以不需要主键
    public Student(String sname, Integer sage, String sgencder) {
        this.sname = sname;
        this.sage = sage;
        this.sgencder = sgencder;
    }

    getter setter 省略,无参构造,toString 省略...
}

1.5 创建持久层 StudentDao

package com.example.dao;

import com.example.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class StudentDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    // 增加学生
    public int addStudent(Student student) {
        return jdbcTemplate.update("insert into student(sname,sage,sgender) values (?,?,?)",student.getSname(),student.getSage(),student.getSgender());
    }

        // 查询所有学生
    public List<Student> queryStudents() {
        return jdbcTemplate.query("select * from student",new BeanPropertyRowMapper<>(Student.class));
    }
}

1.6 创建业务层 StudentService

package com.example.service;

import com.example.dao.StudentDao;
import com.example.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class StudentService {

    @Autowired
    StudentDao studentDao;

    public int addStudent(Student student) {
        return studentDao.addStudent(student);
    }

    public List<Student> getAllStudents() {
        return studentDao.queryStudents();
    }
}

1.7 编写控制层 StudentController

package com.example.controller;

import com.example.entity.Student;
import com.example.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
public class StudentController {

    @Autowired
    StudentService studentService;

    @RequestMapping("/")
    public String index() {
        return "Hello, 项目启动成了!";
    }

    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public String addStu(@ModelAttribute("stu") Student stu) {
        Student student = new Student(stu.getSname(),stu.getSage(),stu.getSgender());
        System.out.println(student.toString());
        int res = studentService.addStudent(student);
        if (res > 0) {
            return "添加成功";
        }
        return "添加失败";
    }


    @RequestMapping("/getall")
    public List<Student> allStudents() {
        return studentService.getAllStudents();
    }
}

1.8 编写静态页面 index.html

该页面主要通过表单完成学生的添加,需要在 static 目录下创建 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h3>Hello 我是首页</h3>
    <form action="/add" method="post">
        <label for="stuName">姓名</label>
        <input type="text" id="stuName" name="sname"></br>
        <label for="stuAge">年龄</label>
        <input type="text" id="stuAge" name="sage"></br>
        <label for="stuGender">性别</label>
        <input type="text" id="stuGender" name="sgender"></br>
        <input type="submit" value="提交">
    </form>
</body>
</html>

1.9 项目运行测试

打开游览器发现能正常看到返回的字符串
image.png
进入表单提交页
image.png

image.png
查看数据库的数据
image.png
image.png

整合完成

二、整合 MyBatis

MyBatis 是一个优秀的半自动 ORM 框架,它可以通过 Mapper.xml 映射 或者 注解 的方式简化 CRUD 操作。这里和上面的 JDBCTemplate 区别就在将 spring-boot-starter-jdbc 的依赖替换成 MyBatis 的依赖即可

这里依然沿用上面的数据库,如果完成了上面的配置,那么这个配置基本可以不用怎么大改

如果要写 xml 的方式,只需要在配置文件中指定 MyBatis xml 配置文件的路径即可,这样就能 SpringBoot 就能识别到了 MyBatis

2.1 项目基本环境搭建

搭建一个 SpringBoot 项目,加入如下 pom 依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mybatis 依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <!-- 群里巴巴数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.22</version>
        </dependency>
        <!-- 数据库驱动,不指定版本就会去下载 8.0 版本的 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.10</version>
            <scope>runtime</scope>
        </dependency>

        <!-- 测试,这里我没有用到,可以自行删除 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

我的项目结构如下,这里使用了 mapper 代替了 dao 层的操作
image.png

2.2 配置文件编写 (application.yml)

这里和上面的几乎没有太大区别

server:
  port: 8080
spring:
  datasource:
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8

2.3 编写实体类 (Student)

package com.example.entity;

public class Student {
    private Integer id;
    private String sname;
    private Integer sage;
    private String sgender;

    getter 和 setter toString,构造方法省略...
}

2.4 编写映射类 StudentMapper

这里就直接完成了 dao 层的操作,使用 MyBatis 提供的四个基本注解,可以解决大多数 CRUD 操作,用户也可以根据实际情况自定义

package com.example.mapper;

import com.example.entity.Student;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Component;

import java.util.List;

/***
 *  mapper 就代替了 dao 层完成了操作
 *  使用 mapper 注解之后,就可以不必再去写对应的映射 xml 了
 *  使用 Component 注解标识这个类,表明它是被 Spring 所管理的
 */

@Mapper
@Component("studentMapper")
public interface StudentMapper {
    @Update("delete from student where id = #{id}")
    int deleteById(@Param("id") Integer id);

    @Insert("insert into student(sname,sage,sgender) values(#{name}, #{age}, #{gender})")
    int addStudent(@Param("name") String name, @Param("age") Integer age, @Param("gender") String gender);

    @Update("update student set sname = #{name} where id = #{id}")
    int updateStudentNameById(@Param("name") String name,@Param("id") Integer id);

    @Select("select * from student where id = #{id}")
    Student selectByPrimaryKey(@Param("id") Integer id);

    @Select("select * from student")
    List<Student> getAllStudents();
}

2.5 编写业务层方法 StudentService

StudentService

package com.example.service;


import com.example.entity.Student;

import java.util.List;

public interface StudentService {

    int deleteById(Integer id);

    int addStudent(String name, Integer age,  String gender);

    int updateStudentNameById(String name, Integer id);

    Student selectById(Integer id);

    List<Student> getAllStudents();

}

StudentServiceImpl 实现类

package com.example.service.Impl;

import com.example.entity.Student;
import com.example.mapper.StudentMapper;
import com.example.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentMapper studentMapper;

    @Override
    public int deleteById(Integer id) {
        return studentMapper.deleteById(id);
    }

    @Override
    public int addStudent(String name, Integer age, String gender) {
        return studentMapper.addStudent(name, age, gender);
    }

    @Override
    public int updateStudentNameById(String name, Integer id) {
        return studentMapper.updateStudentNameById(name, id);
    }

    @Override
    public Student selectById(Integer id) {
        return studentMapper.selectByPrimaryKey(id);
    }

    @Override
    public List<Student> getAllStudents() {
        return studentMapper.getAllStudents();
    }
}

2.6 编写控制层 StudentController

我只完成了几个方法,其实都是大同小异的,主要是实现 Restful 风格的 API

package com.example.controller;

import com.example.entity.Student;
import com.example.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class StudentController {

    @Autowired
    StudentService studentService;

    @RequestMapping("/")
    public String index() {
        return "Hello, 项目启动成功了";
    }


    @RequestMapping("/getall")
    public List<Student> queryAllStudents() {
        return studentService.getAllStudents();
    }


    // 使用 ModelAttribute 接收一个 对象参数
    @RequestMapping("/add")
    public String addStudents(@ModelAttribute("stu") Student student) {
        System.out.println(student.getSname() + " " + student.getSage() + " " + student.getSgender());
        int res = studentService.addStudent(student.getSname(),student.getSage(),student.getSgender());
        System.out.println(res);
        if (res > 0) {
            return "添加成功";
        } else {
            return "添加失败";
        }
    }

    // 根据 id 查找学生信息,使用 restfull 风格的 API
    @RequestMapping("/student/{id}")
    public Student queryStudentById(@PathVariable("id") Integer id) {
        return studentService.selectById(id);
    }

    @RequestMapping(value = "/student/{id}",method = RequestMethod.DELETE)
    public Map<String, Object> deleteStudent(@PathVariable("id") Integer id) {
        System.out.println("接收到了 delete 方法,删除学生信息");
        int res = studentService.deleteById(id);
        Map<String,Object> map = new HashMap<>();
        if (res > 0) {
            map.put("code",200);
            map.put("msg", "删除成功");
            return map;
        } else {
            map.put("code",500);
            map.put("msg", "删除失败");
            return map;
        }
    }


}

2.7 编写静态页面 (添加用户的方法)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>我是首页</title>
</head>
<body>
<form action="/add" method="post">
    <label for="stuName">姓名</label>
    <input type="text" id="stuName" name="sname"></br>
    <label for="stuAge">年龄</label>
    <input type="text" id="stuAge" name="sage"></br>
    <label for="stuGender">性别</label>
    <input type="text" id="stuGender" name="sgender"></br>
    <input type="submit" value="提交">
</form>
</body>
</html>

2.8 运行结果

2.8.1 运行主页

image.png

2.8.2 执行添加操作

image.png
添加成功
image.png

2.8.3 查询所有学生

image.png

2.8.4 根据 id 查询学生(restful API)

image.png

2.8.5 根据 id 执行删除操作 (restful API)

image.png

2.9 总结

使用 MyBatis 还是挺香的,后面其实还有更香的。比如 Mybatis-plus (增强版),codeGenerator (代码生成工具)等等一系列非常好用的方法

三、Spring Data JPA

3.1 什么是 JPA

JPA (Java Persistence API)和 Spring Data 是两个范畴概念。

我们学习 Java EE 的过程中,基本都有听过 Hibernate 框架,Hibernate 作为一个全自动的 ORM 框架,可以让我们不写任何 SQL 语句,从而完成对数据库的增删改查操作。 JPA 则是一种 ORM 规范, JPA 和 ORM 的关系像 JDBC 和 JDBC 驱动关系,即 JPA 制定了 ORM 规范,而 hibernate 是这些规范的实现。JPA 是在 hibernate 出现之后才出现的。因此从功能上来看,JPA 是 hibernate 的一个子集。

3.2 什么是 Spring Data

Spring Data 是 Spring 的一个子项目,致力于简化数据库访问。通过规范的方法名来分析开发者的意图,进而减少数据库访问层的代码量。

Spring Data 支持

  • 关系型数据库
  • 非关系型数据库

Spring Data 可以有效的简化关系型数据库访问代码。

3.3 使用 JPA 开发

3.3.1 创建数据库,不用建任何表

create table 'jpa' default character set utf8;

3.3.2 创建一个 SpringBoot 项目

添加 MySQL, Druid 数据库连接池,Spring Data JPA 依赖

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

                <!--    JPA 依赖    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.10</version>
            <scope>runtime</scope>
        </dependency>

        <!-- 开启 阿里巴巴数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.22</version>
        </dependency>

3.3.3 配置文件 application.yml 编写

server:
  port: 80

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/jpa?useUnicode=true&characterEncoding=utf8
    driver-class-name: com.mysql.jdbc.Driver
  jpa:
    show-sql: true  # 项目运行时打印 SQL
    database: mysql
    hibernate:
      ddl-auto: update  # 项目启动时更新数据库中的表,可选值 create、 create-drop、 validate、 no

3.3.4 编写实体类 entity

package cn.gorit.entity;

import javax.persistence.*;
/**
 * @Entity 注解表示该类是一个实体类,在项目启动时,会根据该类自动生成一张表,表的名称及 name 的名称,没有默认为类名
 * @Id 表示是一个主键,@GeneratedValue 表示主键自动生成,strategy  则表示生成策略
 * 默认情况下,生成的表的字段的名称就是实体类中属性的名称,通过 @Column 自定义生成字段的属性,name 表示该字段在数据库的名称, nullable 表示是否为空
 * @Transient 表示数据库生成表时,忽略该字段,即不生成字段。
 * */
@Entity(name = "t_book")
public class Book {

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

    @Column(name = "book_name",nullable = false)
    private String name;

    private String author;

    private Double price;

    @Transient
    private  String description;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", author='" + author + '\'' +
                ", price=" + price +
                ", description='" + description + '\'' +
                '}';
    }
}

3.3.5 编写 dao 层

创建 BookDao,继承 JpaRepository

package cn.gorit.dao;

import cn.gorit.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

/**
 * 自定义的 BookDao 继承 JpaRepository,JpaRepository中提供了一些基本的数据库方法,
 * 有基本的增删改查,分页查询,排序查询
 *
 * 在 Spring Data Jpa 中,只要方法的定义符合既定规范,Spring Data 就能分析出开发者的意图,就能避免开发者定义 SQL
 * nativeQuery = true 表示使用原生的 SQL 查询
 *
 * */
public interface BookDao extends JpaRepository<Book,Integer> {


    List<Book> getBooksByAuthorStartingWith(String author);

    List<Book> getBooksByPriceGreaterThan(Double price);

    @Query(value = "select * from t_book where id = (select max(id) from t_book )", nativeQuery = true)
    Book getMaxIdBook();

    @Query(value = "select * from t_book where id = (select min(id) from t_book )", nativeQuery = true)
    Book getMinIdBook();

    /**
     *      JPQL 查询
     *      根据 id 和 author 查询,这里使用默认的 JPQL,JPQL 是一种面向对象的表达式语言。可以将 SQL 语法和简单查询语义绑定在一起
     *      这种语句是可以被编译成主流数据库的 SQL 语句,这里和 HQL 语句比较相似
     *      下面使用 :id, :author 实现了数据的绑定,这里的字段并非数据库中的字段
     */


    @Query(value = "select b from t_book b where b.id>:id and b.author=:author")
    List<Book> getBooksByIdAndAuthor(@Param("author") String author, @Param("id") Integer id);

    @Query("select b from t_book b where b.id<?2 and b.name like %?1%")
    List<Book> getBooksByIdAndName(String name,Integer id);

    // 如果 BookDao 中的方法涉及修改操作,需要添加 @Modifying 注解添加事物
}

3.3.6 编写 Service 方法

创建 BookService,service 方法比较简单,调用 dao 层的方法即可,代码如下。

package cn.gorit.service;

import cn.gorit.dao.BookDao;
import cn.gorit.entity.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BookService {

    @Autowired
    BookDao bd;

    // 添加书籍,使用 save 方法存对象进入数据库,该方法是 JpaRepository 提供的
    public void addBook(Book book) {
        bd.save(book);
    }

    // 分页查询数据,返回值为 Page<Book>, 该对象中包含有分页常 用数据,例如总记录数、总页数、每页记录数、 当前页记录数等。
    public Page<Book> getBookByPage(Pageable pageable) {
        return bd.findAll(pageable);
    }

    // 根据作者名称查询书籍
    public List<Book> getBooksByAuthorStartingWith(String author) {
        return bd.getBooksByAuthorStartingWith(author);
    }

    // 查询比数据加个大于等于的书籍
    public List<Book> getBooksByPriceGreaterThan(Double price) {
        return bd.getBooksByPriceGreaterThan(price);
    }

    public Book getBookByMaxId() {
        return bd.getMaxIdBook();
    }

    public Book getBookByMinId() {
        return bd.getMinIdBook();
    }

    public List<Book> getBookByIdAndAuthor(String author, Integer id) {
        return bd.getBooksByIdAndAuthor(author, id);
    }

    public List<Book> getBooksByIdAndName(String name, Integer id) {
        return bd.getBooksByIdAndName(name, id);
    }
}

3.3.7 编写 控制层 Controller 方法

package cn.gorit.controller;

import cn.gorit.entity.Book;
import cn.gorit.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
public class BookController {

    @Autowired
    BookService bookService;

    @RequestMapping("/getmaxbook")
    @ResponseBody
    public Book getMaxIdBook() {
        return bookService.getBookByMaxId();
    }

    @RequestMapping("/getmixbook")
    @ResponseBody
    public Book getMinIdBook() {
        return bookService.getBookByMinId();
    }

    @RequestMapping("/findAll")
    @ResponseBody
    public Map<String,Object> findAll() {
        // 该方法中,findAll 接口中,首先通过调用 PageRequest 中的 of 方法构造 PageRequest 对象。 of 方法接收两个参数
        // 第一个参数是页数,从0开始计数,第二个参数是每页显示的页数
        Map<String,Object> map = new HashMap<String, Object>();
        PageRequest pageable = PageRequest.of(1,5);
        Page<Book> page = bookService.getBookByPage(pageable);
        System.out.println("总页数:" +page.getTotalPages());
        System.out.println("总记录数:"+page.getTotalElements());
        System.out.println("查询结果:"+page.getContent());
        System.out.println("当前页数:"+(page.getNumber()+1));
        System.out.println("当前页记录数:"+page.getNumberOfElements());
        System.out.println("每页记录数:"+page.getSize());

        map.put("总页数:" ,page.getTotalPages());
        map.put("查询结果:",page.getContent());
        map.put("当前页数",(page.getNumber()+1));

        return map;
    }

    @RequestMapping("/search")
    public void search() {
        List<Book> bs1 = bookService.getBookByIdAndAuthor("鲁迅",7);
        List<Book> bs2 = bookService.getBooksByAuthorStartingWith("吴");
        Book b = bookService.getBookByMinId();
        System.out.println(bs1.toString());
        System.out.println(bs2.toString());
        System.out.println(b.toString());
    }

    @RequestMapping(value = "/save", method = RequestMethod.POST )
    public String save(@ModelAttribute Book book) {
        bookService.addBook(book);
        return "redirect:success.html";
    }

 }

3.3.8 测试

添加测试数据
image.png

image.png

http://localhost/findAll
这个 findAll 为分页查询
image.png

save 保存方法,我自己编写了一个前端界面,用来保存数据
image.png
image.png

四、Spring Data JPA 构建 REST 服务

使用来了 REST 服务之后,就需要开发者自己编写 Controller 了

4.1 什么是 Rest ?

是一种 Web 软件架构风格,是风格,而不是标准。匹配或者兼容这种架构风格的网络服务成为 REST 服务,匹配或者兼容这种架构风格的网络服务成为 REST 服务。REST 服务简洁并且有层次,REST 通常基于 HTTP,URI 和 XML 以及 HTML 这些现有的广泛流行的协议和标准。

对资源的增删改查操作可以通过 HTTP 协议提供的 GET POST PUT DELETE 等方法实现。使用 REST 可以更高效地利用缓存来提高响应速度,同时 REST 中的通信会话状态由客户端来维护,这可以让不同的服务器处理一系列请求中的不同请求,进而提高服务器的扩展性。

4.2 使用 JPA 实现 REST

在 SpringBoot 中,使用 Spring Data JPA 和 Spring Data Rest 可以快速开发一个 Restful 应用

4.2.1 搭建一个 SpringBoot 项目,编写相关配置文件

  1. pom 坐标依赖

    <!--    JPA 依赖    -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-jpa</artifactId>
         </dependency>
         <!-- rest 风格 -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-rest</artifactId>
         </dependency>
    
         <!-- 开启热部署 -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-devtools</artifactId>
             <scope>runtime</scope>
             <optional>true</optional>
         </dependency>
    
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
    
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <version>5.1.10</version>
             <scope>runtime</scope>
         </dependency>
    
         <!-- 开启 阿里巴巴数据库连接池 -->
         <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>druid</artifactId>
             <version>1.1.22</version>
         </dependency>
    
  2. application.yml 配置文件

和上一节内容基本差不多

server:
  port: 80

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/jpa?useUnicode=true&characterEncoding=utf8
    driver-class-name: com.mysql.jdbc.Driver
  jpa:
    show-sql: true
    database: mysql
    hibernate:
      ddl-auto: update
  1. 实体类创建

和上一期配置基本一致

package cn.gorit.entity;

import javax.persistence.*;

/**
 * @Entity 注解表示该类是一个实体类,在项目启动时,会根据该类自动生成一张表,表的名称及 name 的名称,没有默认为类名
 * @Id 表示是一个主键,@GeneratedValue 表示主键自动生成,strategy  则表示生成策略
 * 默认情况下,生成的表的字段的名称就是实体类中属性的名称,通过 @Column 自定义生成字段的属性,name 表示该字段在数据库的名称, nullable 表示是否为空
 * @Transient 表示数据库生成表时,忽略该字段
 * */
@Entity(name = "t_book")
public class Book {

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

    @Column(name = "book_name",nullable = false)
    private String name;

    private String author;

    private Double price;

    @Transient
    private  String description;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", author='" + author + '\'' +
                ", price=" + price +
                ", description='" + description + '\'' +
                '}';
    }
}
  1. 创建 BookRepository 并继承 JpaRepository ```java package cn.gorit.mapper;

import cn.gorit.entity.Book; import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository { }

JpaRepository 方法默认提供了一些基本的增删改查,分页查询方法等都提供好了。<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/473179/1594106929469-6bc34646-203b-471a-9e7e-981b84d10e5d.png#align=left&display=inline&height=634&margin=%5Bobject%20Object%5D&name=image.png&originHeight=634&originWidth=1016&size=476358&status=done&style=none&width=1016)

<a name="jBPhX"></a>
#### 4.2.2 项目说明
到这里为止,整个项目基本搭建完毕,一个 RESTful  服务构建成功了。看起来没有写 Controller,但是我们的项目确实也搭建完毕了。这句诗 SpringBoot 的魅力所在了。

项目编写好了,接下来需要的是一个测试工具,这里我使用 postman 进行接口测试。

<a name="QJ7dF"></a>
#### 4.2.3 添加数据测试
RESTful 服务构建成功后,默认的请求路径是实体类名小写加上后缀。此时向数据库添加一条数据非常容易,发送一个 post 请求,服务器会返回添加后的结果

[http://localhost/books](http://localhost/books?page=0&size=5)<br />post 请求<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/473179/1594109537064-ac962a7d-60e7-4bc9-a534-94167f5912c3.png#align=left&display=inline&height=595&margin=%5Bobject%20Object%5D&name=image.png&originHeight=595&originWidth=953&size=53112&status=done&style=none&width=953)

<a name="wzcBZ"></a>
#### 4.2.4 查询数据测试
查询一般使用 get 请求就可以了

查询所有数据<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/473179/1594110034098-ea00e3ec-efd4-4fd2-a231-f042348dd8d8.png#align=left&display=inline&height=686&margin=%5Bobject%20Object%5D&name=image.png&originHeight=686&originWidth=914&size=61846&status=done&style=none&width=914)<br />根据 id 查询指定数据,我们只需要在后面添加一个 id 即可,以下为查询 id 为 6 的记录。<br />[http://localhost/books](http://localhost/books?page=0&size=5)/6<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/473179/1594110059553-13edd31a-1632-424d-b688-814444419078.png#align=left&display=inline&height=402&margin=%5Bobject%20Object%5D&name=image.png&originHeight=402&originWidth=883&size=34127&status=done&style=none&width=883)

分页查询,根据 page  和 size 指定页数,和当前页数多少条记录<br />[http://localhost/books?page=0&size=5](http://localhost/books?page=0&size=5)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/473179/1594110162919-e58d4c5f-cd37-4882-b08f-2d22e0672858.png#align=left&display=inline&height=687&margin=%5Bobject%20Object%5D&name=image.png&originHeight=687&originWidth=893&size=64161&status=done&style=none&width=893)

分页之余,还支持排序,例如查第三页数据,每条记录数为 4,根据 id 倒序排列<br />[http://localhost/books?page=2&size=4&sort=id,desc](http://localhost/books?page=2&size=4&sort=id,desc)

![image.png](https://cdn.nlark.com/yuque/0/2020/png/473179/1594110404529-74b071a9-939b-4d1f-8640-4b343a9e504e.png#align=left&display=inline&height=588&margin=%5Bobject%20Object%5D&name=image.png&originHeight=588&originWidth=919&size=54926&status=done&style=none&width=919)

<a name="NPKnS"></a>
#### 4.2.5 数据修改测试
发送 put 请求即可实现对数据的修改,对数据的修改是通过 id 实现的,因此请求路径中要有 id

我们修改这条记录 [http://localhost/books/5](http://localhost/books/5)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/473179/1594110523316-4787f429-81af-4b06-986d-4b37d2a19a1c.png#align=left&display=inline&height=542&margin=%5Bobject%20Object%5D&name=image.png&originHeight=542&originWidth=907&size=47714&status=done&style=none&width=907)

以下为 PUT 要发送的消息
```json
{
    "name": "Python 案例驱动学习",
    "author": "Gorit",
    "price": 88
}

修改成功
image.png

4.2.6 数据删除操作

发送 DELETE 请求即可实现对数据的删除操作,例如 id 为 12 的记录

发送 DELETE 请求
http://localhost/books/12

删掉了在查询,就没有任何记录
image.png

4.3 自定义请求路径

默认情况下,请求路径都是实体类名小写加s,如果开发者想要对请求路径重新定义。默认最好不要改,因为也不好记忆。

通过 @RepositoryRestResource 注解即可实现,下面我们就来测试一下

package cn.gorit.mapper;

import cn.gorit.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource(path = "bs",collectionResourceRel = "bs", itemResourceRel = "b")
public interface BookRepository extends JpaRepository<Book,Integer> {
}

image.png

4.4 自定义查询方法

默认的查询方法支持分页查询,排序查询以及按照 ID 查询,如果开发者想要按照某个属性查询,只需要在 BookRepository 中定义相关方法并暴露出去即可

package cn.gorit.mapper;

import cn.gorit.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;

import java.util.List;

//@RepositoryRestResource(path = "bs",collectionResourceRel = "bs", itemResourceRel = "b")
public interface BookRepository extends JpaRepository<Book,Integer> {

    /**
     * 自定义查询只需要在该类中定义相关的查询方法即可,定义好的方法可以不用添加 @RestResource 注解,
     * 默认路径就是 方法名,以第一个方法为例:
     *      不加注解:路径为: http://localhost/books/search/findByAuthorContains?author=鲁迅
     *      添加注解:路径为:http://localhost/books/search/author?author=鲁迅
     *
     *      用户可以直接访问:http://localhost/books/search 路径查看哪些暴露出来的方法
     * */
    @RestResource(path = "author", rel = "author")
    List<Book> findByAuthorContains(@Param("author") String author);

    @RestResource(path = "name",rel = "name")
    Book findByNameEquals(@Param("name") String name);
}

image.png

4.5 不暴露方法

如果不想对外暴露 BookRepository 中的方法,可以通过 @RepositoryRestResource 注解实现

如果我们将该注解定义在类上,并且设置 exported 为 false ,那么前面用过的增删改查的方法统统都会失效。因此不建议这么用
image.png

但是我们可以将该注解放在方法上,就可以不对外暴露一些方法了。

4.6 配置跨域 CORS

配置 CORS 直接配置 全局的比较省心,但是我们也可以对局部配置 CORS,配置的方式是在 类 或者方法中添加该注解 @CrossOrigin

image.png

这样的话,BookRepository 中的所有方法都支持跨域了。

默认的 RESTful 工程不需要开发者自己提供 Controller,因此添加在 Controller 中的注解可以直接卸载 BookRepository 上

package cn.gorit.mapper;

import cn.gorit.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.web.bind.annotation.CrossOrigin;

import java.util.List;

@CrossOrigin
//@RepositoryRestResource(path = "bs",collectionResourceRel = "bs", itemResourceRel = "b")
@RepositoryRestResource(exported = true)
public interface BookRepository extends JpaRepository<Book,Integer> {

    /**
     * 自定义查询只需要在该类中定义相关的查询方法即可,定义好的方法可以不用添加 @RestResource 注解,
     * 默认路径就是 方法名,以第一个方法为例:
     *      不加注解:路径为: http://localhost/books/search/findByAuthorContains?author=鲁迅
     *      添加注解:路径为:http://localhost/books/search/author?author=鲁迅
     *
     *      用户可以直接访问:http://localhost/books/search 路径查看哪些暴露出来的方法
     * */
    @RestResource(path = "author", rel = "author")
    List<Book> findByAuthorContains(@Param("author") String author);

    @RestResource(path = "name",rel = "name")
    Book findByNameEquals(@Param("name") String name);
}

4.7 其他配置

我们可以自己配置一些常用属性。查询参数相关配置

在 application.yml 中配置

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/jpa?useUnicode=true&characterEncoding=utf8
    driver-class-name: com.mysql.jdbc.Driver
  jpa:
    show-sql: true
    database: mysql
    hibernate:
      ddl-auto: update
  data:
    rest:
      default-page-size: 20 # 每页默认记录数,默认为 20
      page-param-name: page  # 分页查询的参数,  page  代表页数
      limit-param-name: size # 分页记录数参数名,默认为 size
      sort-param-name: sort  # 分页查询排序参数名
      base-path: /  # 表示给所有请求路径增加前缀
      return-body-on-create: true # 添加数据陈宫时,返回数据
      return-body-on-update: true # 更新数据成功是返回数据

也可以自己编写配置类(配置类的优先级要高于 文件配置)

package cn.gorit.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter;

@Configuration
public class RestConfig extends RepositoryRestConfigurerAdapter {

    /**
     *      配置每页记录数为 5条
     *      分页查询页码参数名,默认为 page
     *        默认查询路径为 /api/vi/
     * */
    @Override
    public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
          config.setDefaultPageSize(5)
                  .setBasePath("/api/v1/")
                  .setPageParamName("page")
                  .setLimitParamName("size")
                  .setSortParamName("sort")
                  .setReturnBodyOnCreate(true)
                  .setReturnBodyOnUpdate(true);
    }
}

image.png

五、SpringBoot 整合多数据源

大家在写项目的时候有没有遇到过这样的情况,当你需要你的好基友电脑上,服务器上的数据的时候,是不是就得依赖他给你的接口。这样做是不是局限性太大。假如有一种方式可以直接通过你的小伙伴的服务器上的账号密码,就可以自己定制化的操作你的小伙伴的服务器上的数据了,这样是不是很酷呢?

5.1 项目环境准备

  1. MySQL 5.5 版本以上
  2. JDK 1.8 以上
  3. 开发工具:idea 2020, maven 3.5.2 版本及以上
  4. SpringBoot 版本 2.2.10

5.2 项目配置初始化

5.2.0 项目结构

image.png

5.2.1 坐标依赖

 <dependencies>
        <!-- 这是一个 Web 项目 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <!-- mysql 5.5 版本依然可以使用 8.0的驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
    </dependencies>

5.2.2 application.yml

因为配置了两个数据源,所以为了区分它们,我们会自定义配置,同时数据库也会创建两个

spring:
  datasource:
    druid:
      db1:
        url: jdbc:mysql://localhost:3306/dts1?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
        driverClassName: com.mysql.cj.jdbc.Driver
        username: root
        password: root
      db2:
        url: jdbc:mysql://localhost:3306/dst2?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
        driverClassName: com.mysql.cj.jdbc.Driver
        username: root
        password: root

5.2.3 创建数据库

为了简化操作,我只是保证两个数据库的名称不一致,其它的数据库名称,字段都是一致的

我们创建了一个名为 book 的表,并随机插入几条数据

DROP TABLE IF EXISTS `book`;

CREATE TABLE `book` (
  `id` tinyint(3) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `author` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

/*Data for the table `book` */

insert  into `book`(`id`,`name`,`author`) values (1,'时间简史','霍金'),(2,'放学后','东野圭吾'),(3,'白白夜行','东野圭吾'),(4,'红楼梦','清 曹雪芹');

5.3 MVC 三层编写

5.3.1 mapper 编写

在 MyBatis 中,我没有使用注解开发,而是使用的 xml 方式,所以我们还需要在 SpringBoot 的 Resources 目录下创建 mapper 目录 和 map1,map2 两个子目录

dao 层相关的项目结构如下,然后下面我只给出 map1 中的相关代码,map2 中只是相对应的修改

- mapper
    - map1
        - BookMapper1
    - map2
        - BookMapper2

- resources
    - mapper
        - map1
            - BookMapper1.xml
        - map2
            - BookMapper2.xml

BookMapper1代码如下:

package cn.gorit.mapper.map1;

import cn.gorit.entity.Book;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface BookMapper1 {

    List<Book> getAllBooks();
}

BookMapper1.xml 配置如下

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.gorit.mapper.map1.BookMapper1">

    <select id="getAllBooks" resultType="cn.gorit.entity.Book">
        select * from book
    </select>

</mapper>

5.3.2 entity 实体类编写

package cn.gorit.entity;

public class Book {
    private Integer id;
    private String name;
    private String author;

    // getter 和 setter 、构造方法省略
}

5.3.3 config 配置类

DataSourceConfig 配置类编写

package cn.gorit.config;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

/**
 * DataSourceConfig 中提供了两个数据源:database1 和 database2,默认方法名即实例名
 * @ConfigurationProperties  使用不同的前缀配置文件
 */
@Configuration
public class DataSourceConfig {

    //  标识为第一个数据源
    @Bean("dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.druid.db1")
    DataSource database1() {
        return DruidDataSourceBuilder.create().build();
    }

    // 标识为第二个数据源
    @Bean("dataSource2")
    @ConfigurationProperties(prefix = "spring.datasource.druid.db2")
    DataSource database2() {
        return DruidDataSourceBuilder.create().build();
    }
}

MyBatisConfigOne 配置类 (第一个数据源对应的配置类)

package cn.gorit.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
* 第二个数据源同理,创建一个新的配置类,将 MapperScan 的路径改为 map2,bean 的注入也要对那个的修改
*/

@Configuration
@MapperScan(value = "cn.gorit.mapper.map1",sqlSessionFactoryRef = "sqlSessionFactoryBean1")
public class MyBatisConfigOne {

    @Autowired
    @Qualifier("dataSource1")
    DataSource db1;

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean1() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(db1);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:./mapper/map1/*.xml"));
        return factoryBean.getObject();
    }

    @Bean
    SqlSessionTemplate sqlSessionTemplate1() throws Exception {
        return new SqlSessionTemplate(sqlSessionFactoryBean1());
    }
}

5.3.4 控制层 controller

package cn.gorit.controller;

import cn.gorit.mapper.map1.BookMapper1;
import cn.gorit.mapper.map2.BookMapper2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class BookController {

    @Autowired
    BookMapper1 bookMapper1;

    @Autowired
    BookMapper2 bookMapper2;

    @GetMapping("/book1")
    public Map<String,Object> test1() {
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","获取成功");
        map.put("data",bookMapper1.getAllBooks());
        return map;
    }

    @GetMapping("/book2")
    public Map<String,Object> test2() {
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","获取成功");
        map.put("data",bookMapper2.getAllBooks());
        return map;
    }
}

5.4 测试运行

image-20200920203924589.pngimage-20200920203944240.png