1、认识 Java 的数据库连接模板 JDBCTemplate

1.1、认识 JDBCTemplate

1.1.2、了解 JDBC

JDBC(Java Database Connectivity),它是 Java 用于连接数据库的规范,也就是用于执行数据库 SQL 语句的 Java API
JDBC 可以连接多种数据库,原因在于提供了统一访问的接口,这也符合 Java 程序接口设计的模式。

JDBC 需要每次进行数据库连接,然后出了 SQL 语句、传值、关闭数据库。这样的流程操作,一旦忘记某一步,就会出现很多问题,于是 JDBCTemplate 被设计出来了。

1.1.3、了解 JDBCTemplate

JDBCTemplate = JDBC + Template 的组合,是对 JDBC 的封装。它更便于程序实现,替我们完成所有的 JDBC 底层操作。因此,对于数据库的操作,再不需要每次都进行连接、打开、关闭了。

JDBC 和 JDBCTemplate 就像是仓库管理员,负责从仓库(数据库)中存取物品。而后者采用的是“电动门自动控制”

JDBCTemplate 实现起来比 ORM 繁琐,所以大部分开发使用的是 ORM(JPAMyBatis)。但是 JDBCTemplate 依然有市场,因为学习成本低。

1.2、实例:用 JDBCTemplate 实现数据的 增删改查

1.2.1、配置基础依赖

<!-- JDBCTemplate 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!-- MYSQL 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

添加完成后,还需要配置数据库连接信息。这样 JDBCTemplate 才能正常连接数据库。
application.properties 配置文件中配置数据库的地址和用户信息

# MySql 数据库信息
    # 驱动地址
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    # 配置 IP 地址、编码、时区
spring.datasource.url=jdbc:mysql://localhost:3306/book?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
    # 用户名
spring.datasource.username=root
    # 密码 此处没有密码是因为 数据库本身没有设置密码
spring.datasource.password=

1.2.2、新建实体类

新建一个测试实体类 User,实现 RowMapper 类,重新 mapRow 方法,以便实体字段和数据表字段映射。(下面有创建数据表的 SQL 命令)

package com.example.model;

import lombok.Data;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

@Data
public class User implements RowMapper<User> {
    private Integer id;
    private String username;
    private String password;

    @Override
    public User mapRow(ResultSet resultSet, int i) throws SQLException {
        User user = new User();
        user.setId(resultSet.getInt("id"));
        user.setUsername(resultSet.getString("username"));
        user.setPassword(resultSet.getString("password"));
        return user;
    }
}

1.2.3、操作数据

JDBCTemplate 提供了以下操作数据的 3 个方法。

  • execute:表示“执行”,用于直接执行 SQL 语句。
  • updata:表示“更新”,包括 新增、修改、删除 操作。
  • query:表示“查询”。
1.2.3.1、创建数据表、新增、查询、删除

在使用 JDBCTemplate 之前,需要在控制器中注入 JDBCTemplate,然后就可以通过 execute 方法执行 SQL 操作了。

package com.example.test;

import com.example.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.List;

@SpringBootTest
public class UserControllerTest {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 创建表
    @Test
    public void createUserTable() throws Exception {
        String sql = "CREATE TABLE `user`(\n" +
                "`id` int(10) NOT NULL AUTO_INCREMENT,\n" +
                "`username` varchar(100) DEFAULT NULL,\n" +
                "`password` varchar(100) DEFAULT NULL,\n" +
                "PRIMARY KEY(`id`)\n" +
                ") ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;\n" +
                "\n";
        jdbcTemplate.execute(sql);
    }

    // 添加数据
    @Test
    public void saveUserTest() throws Exception {
        String sql = "INSERT INTO user (USERNAME, PASSWORD) VALUES ('张三', '123456');";
        Integer rows = jdbcTemplate.update(sql);
        System.out.println(rows);
    }

    // 查询数据
    @Test
    public void getUserByName() throws Exception {
        String name = "张三";
        String sql = "SELECT * FROM user WHERE USERNAME = ?";
        List<User> list = jdbcTemplate.query(sql, new User(), name);
        for (User user : list) {
            System.out.println(user);
        }
    }

    // 查询所有数据
    @Test
    public void list() throws Exception {
        String sql = "SELECT * FROM user LIMIT 0,1000";
        List<User> userList = jdbcTemplate.query(sql, new User());
        for (Object user : userList) {
            System.out.println(user);
        }
    }

    // 数据修改
    @Test
    public void updateUserPassword() throws Exception {
        Integer id = 1;
        String password = "99998888";
        String sql = "UPDATE user SET PASSWORD = ? WHERE ID = ?";
        Integer rows = jdbcTemplate.update(sql, password, id);
        System.out.println(rows);
    }

    // 数据删除
    @Test
    public void deleteUserById() throws Exception {
        Integer id = 1;
        String sql = "DELETE FROM user WHERE ID = ?";
        Integer rows = jdbcTemplate.update(sql, id);
        System.out.println(rows);
    }
}

1.3、认识 ORM

ORM(Object Relational Mapping)是 对象/关系映射。它提供了概念性的、易于理解的数据模型,将数据库中的表内存中的对象建立映射关系。它是随着面向对象的软件开发方法的发展而产生的,面向对象的开发方法依然是当前主流的开发方法。

对象关系型数据是业务实体的两种表现形式。业务实体在内存中的表现为对象,在数据库中表现为关系型数据。内存中的对象不会被永久保存,只有关系型数据库(或 NoSQL 数据库,或文件)中的对象会被永久保存。

对象/关系映射(ORM)系统一般以中间件的形式存在,因为内存中的对象之间存在关联和继承关系,而在数据库中,关系型数据无法直接表达多对多的关联和继承关系。对象、数据库通过 ORM 映射的关系图。

目前比较常用的 ORM 是国外流行的 JPA 和国内流行的 MyBatis

2、JPA ---- Java 持久层 API

2.1、引入 JPA

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

2.2、认识 Spring Data

Spring Data: Spring 的一个子项目。用于简化数据库访问,支持NoSQL和关系数据库存储。其主要目标是使数据库的访问变得方便快捷。

Spring Data 提供了基于 增删改查、排序、分页 等层面的统一接口,已实现持久化的储存。

2.3、Spring Data 核心模块

  • Spring Data Commons - 支持每个Spring Data模块的Core Spring概念。
  • Spring Data JDBC - 对JDBC的Spring Data存储库支持。
  • Spring Data JDBC Ext - 支持标准JDBC的数据库特定扩展,包括对Oracle RAC快速连接故障转移的支持,AQ JMS支持以及对使用高级数据类型的支持。
  • Spring Data JPA - JPA的Spring Data存储库支持。
  • Spring Data KeyValue - 基于映射的存储库和SPI,可轻松构建用于键值存储的Spring Data模块。
  • Spring Data LDAP - 对Spring LDAP的Spring Data存储库支持。
  • Spring Data MongoDB - 基于Spring的对象文档支持和MongoDB的存储库。
  • Spring Data Redis - 从Spring应用程序轻松配置和访问Redis。
  • Spring Data REST - 将Spring Data存储库导出为超媒体驱动的RESTful资源。
  • Spring Data Apache Cassandra - 轻松配置和访问Apache Cassandra或大规模,高可用性。
  • Spring Data Apache Geode - 轻松配置和访问Apache Geode。
  • Spring Data Apache Solr - 为面向搜索的Spring应用程序轻松配置和访问Apache Solr。
  • Spring Data Pivotal GemFire - 轻松配置和访问Pivotal GemFire。

2.4、认识 JPA

JPA 是 Java Persistence API 的简称,中文名 Java持久层API,是 JDK 5.0注解XML描述 对象/关系表的映射关系,并将运行期的实体对象持久化到数据库中。

JPA 通过简单约定好接口方法的规则自动生成相应的 JPQL 语句,然后映射成 POJO 对象。

JPA 是一个规范化的接口,封装了 Hibernate 的操作作为默认实现,让用户不通过任何配置即可完成数据库的操作。

JPA是一种规范,Hibernate实现了JPA规范,即Hibernate为JPA的一种实现;而Spring Data JPA是对JPA进行更高级的封装,让其dao编码变得更简单。

sqllit集成springboot_数据库


sqllit集成springboot_sqllit集成springboot_02


Hibernate 主要通过 hibernate-annotation、hibernate-entitymanager 和 hibernate-core 三个组件来操作数据。

  • hibernate-annotation:是 Hibernate 支持 annotation 方式配置的基础,它包括标准的 JPA annotation、Hibernate 自身特殊功能的 annotation。
  • hibernate-entitymanager:实现了标准的 JPA。它是 hibernate-core 和 JPA 之间的适配器,它不直接提供 ORM 功能,而是对 hibernate-core 进行封装,使得 Hibernate 符合 JPA 的规范。
  • hibernate-core:是 Hibernate 的核心实现,提供了 Hibernate 所有的核心功能。

2.4.1、如果要使用 JPA 创建 1.2.2 里的实体,可以用一下代码:

  • JPA 创建
package com.example.test3.jpa;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
public class User {
    @Id // id 的自增由数据库自动管理
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String username;
    private String password;
}
  • JDBCTemplate 创建
package com.example.model;

import lombok.Data;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

@Data
public class User implements RowMapper<User> {
    private Integer id;
    private String username;
    private String password;

    @Override
    public User mapRow(ResultSet resultSet, int i) throws SQLException {
        User user = new User();
        user.setId(resultSet.getInt("id"));
        user.setUsername(resultSet.getString("username"));
        user.setPassword(resultSet.getString("password"));
        return user;
    }
}

对比 JPA 与 JDBCTemplate 创建实体的方式可以看出:JPA 的实现方式简单明了,不需要重新映射(支持自动映射),只需要设置好属性即可。id 的自增由数据库自动管理,也可以由程序管理,其他的工作 JPA 自动处理好了。

2.5、使用 JPA

要使用 JPA,只要加入它的 Starter 依赖,然后配置数据库连接信息。

2.5.1、添加 JPA 和 MySQL 数据库的依赖

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

        <!-- MYSQL 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

2.5.2、配置数据库连接信息

Spring Boot 项目使用 MySQL 等关系型数据库,需要配置连接信息,可以在 application.properties 文件中进行配置。

# MySql 数据库信息
    # 驱动地址
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    # 配置 IP 地址、编码、时区
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
    # 用户名
spring.datasource.username=root
    # 密码 此处没有密码是因为 数据库本身没有设置密码
spring.datasource.password=rootroot
    # 打印 SQL 语句
spring.jpa.show-sql=true
    # hibernate 的配置属性,主要用于:自动创建、更新、验证数据库表结构。
spring.jpa.properties.hibernate.hbm2ddl.auto=update
    # hibernate 的方言设置
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
    # 控制 Session 生命周期,不添加后面使用可能会报错
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.enable_lazy_load_no_true=true
  • spring.jpa.properties.hibernate.hbm2ddl.auto:有四种配置方式,分别如下:

是hibernate的配置属性,其主要作用是:自动创建、更新、验证数据库表结构。该参数的几种配置如下:

  • create:每次加载hibernate时都会删除上一次的生成的表,然后根据你的model类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。
  • create-drop:每次加载hibernate时根据model类生成表,但是sessionFactory一关闭,表就自动删除。
  • update:最常用的属性,第一次加载hibernate时根据model类会自动建立起表的结构(前提是先建立好数据库),以后加载hibernate时根据model类自动更新表结构,即使表结构改变了但表中的行仍然存在不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等应用第一次运行起来后才会。
  • validate:每次加载hibernate时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。

2.6、了解 JPA 注解和属性

2.6.1、JPA 的常用注解

注解

说明

@Entity

声明类为实体,将该类标记为实体类,映射到指定的数据库的表

@Table

声明表名,name:数据库的表名,@Entity 和 @Table 注解一般一起使用,如果表明和实体类名相同,那么 @Table 可以省略

@Basic

指定非约束明确的各个字段,即普通字段,简单的属性到表字段的映射, getXxx() 方法会默认加上这个注解

@Embedded

用于注释属性,表明该属性的类是嵌入类(@Embeddable 用于注释 Java类,表示类是嵌入类)

@Id

指定类的属性,一个表中的主键

@GeneratedValue

指定标注主键生成策略, 例如:strategy:GenerationType.AUTO(默认自动) GenerationType.IDENTITY(数据库id自增长)

@Transient

表示该属性并非一个数据库表的字段的映射,ORM 框架将忽略该属性,即它不是持久的,为虚拟字段。如果不标记则会默认标记为@Basic

@Column

指定持久属性,即字段名。如果字段名与列名相同,则可以省略。name:字段名,unique:唯一约束,nullable:非空约束,length:长度

@SequenceGenerator

指定在 @GeneratedValue 注解中指定的属性的值。创建一个序列

@TableGenerator

在数据库生成一张表来管理主键生成策略

@AccessType

这种类型的注释用于设置访问类型。如果设置 @AccessType(FIELD),则可以直接访问变量,不需要使用Getter和Setter方法,但必须是 public 属性。如果设置@AccessType(PROPERTY),则需要使用Getter和Setter方法访问 Entity 的变量

@UniqueConstraint

指定的字段和用于主要或辅助表的唯一约束

@ColumnResult

可以参考使用 select 子句的 SQL 查询中的列名

@NamedQueries

指定命名查询的列表

@NamedQuery

指定使用静态名称的查询

@Jsonlgnore

作用是 JSON 序列化时将 Java Bean 中的一些属性忽略掉,序列化和反序列化都受影响。name 映射外键的字段名(映射的表直接把表的实体类作为数据类型,如 private 实体类 属性名)

@IDENTITY

声明一个属性映射为数据库表的主键列,可以标记在属性上也可以标记在set方法上.

@Temporal

TemporalType:TIMESTAMP(年月日时分秒) DATE(年月日)

2.6.2、映射关系的注解

注解

说明

@JoinColumn

指定一个实体组织或实体集合。用在“多对一”和“一对多”的关联中

@ManyToMany

单向多对多关系

@ManyToOne

单向多对一关系,fetch:FetchType.LAZY(修改关联属性加载策略为懒加载)

@OneToMany

单向一对多关系

@OneToOne

单向一对一关系,@JoinColumn 可以加上一条 unique=true 保持唯一性

2.6.3、注解详解

2.6.3.1、@Entity

@Entity 标记在类名上面,作为实体类的标识

2.6.3.2、@Table

当实体类与其映射的数据库表名不同名时需要使用 @Table 标注说明,该标注与 @Entity 标注并列使用,置于实体类声明语句之前,可写于单独语句行,也可与声明语句同行。

  • @Table 标注的常用选项是 name,用于指明数据库的表名
  • @Table标注还有一个两个选项 catalog 和 schema 用于设置表所属的数据库目录或模式,通常为数据库名。uniqueConstraints 选项用于设置约束条件,通常不须设置。
2.6.3.3、@Id

@Id 设置对象表示符,标识的实体类的属性映射对应表中的主键

2.6.3.4、@GeneratedValue

设置标识符的生成策略,常与@Id一起使用
参数:strategy 指定具体的生成策略

  • 方式一:@GeneratedValue(strategy=GenerationType.AUTO) 也是默认策略, 即写成 @GeneratedValue 也可;
    类似于hibernate的native策略,生成方式取决于底层的数据库。
  • 方式二:@GeneratedValue(strategy = GenerationType.IDENTITY)指定“自动增长”策略,适用于MySQL;
  • 方式三:@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = “seq_tbl_person”)指定“序列”策略,常用于Oracle,其中generator表示生成器的名字。而且还要指定@SequenceGenerator(name = “seq_tbl_person”, sequenceName = “seq_tbl_person”, allocationSize = 1)注解配合使用

其中 name 指定生成器的名字(与 generator 的值一样),sequenceName 指定数据库中定义序列的名字,allocationSize 指定序列每次增长1

2.6.3.5、@Column

描述数据库表中该字段的定义,具有一下属性

  • name:表示数据库表中该字段的名称,默认情形属性名称一致。
  • nullable:表示该字段是否允许为 null,默认为 true。
  • unique:表示该字段是否是唯一标识,默认为 false。
  • length:表示该字段的大小,仅对 String 类型的字段有效。
  • insertable:表示在ORM框架执行插入操作时,该字段是否应出现 INSETRT 语句中,默认为 true。
  • updateable:表示在ORM框架执行更新操作时,该字段是否应该出现在 UPDATE 语句中,默认为 true。对于一经创建就不可以更改的字段,该属性非常有用,如对于 birthday 字段。
  • columnDefinition:表示该字段在数据库中的实际类型。通常ORM框架可以根据属性类型自动判断数据库中字段的类型,但是对于 Date 类型仍无法确定数据库中字段类型究竟是 DATE,TIME 还是 TIMESTAMP。此外,String 的默认映射类型为 VARCHAR,如果要将 String 类型映射到特定数据库的 BLOB 或 TEXT 字段类型,该属性非常有用。
2.6.3.6、@OrderBy

在加载数据的时候可以为其指定顺序。

2.6.3.7、@Transient

表示该属性并非一个到数据库表的字段的映射,ORM 框架将忽略该属性。
如果一个属性并非数据库表的字段映射。就务必将其标示为 @Transient。否则。ORM框架默认其注解为 @Basic

2.6.3.8、@OneToOne

描述一个一对一的关联

  • fetch:表示抓取策略,默认为 FetchType.LAZY
  • cascade:表示级联操作策略
2.6.3.9、@ManyToOne

表示一个多对一的映射,该注解标注的属性通常是数据库表的外键

  • optional:是否允许该字段为 null,该属性应该根据数据库表的外键约束来确定,默认为 true
  • fetch:表示抓取策略,默认为 FetchType.EAGER
  • cascade:表示默认的级联操作策略,可以指定为 ALL,PERSIST,MERGE,REFRESH和REMOVE 中的若干组合,默认为无级联操作
  • targetEntity:表示该属性关联的实体类型。该属性通常不必指定,ORM框架根据属性类型自动判断 targetEntity。
2.6.3.10、@OneToMany

描述一个一对多的关联,该属性应该为集体类型,在数据库中并没有实际字段。

  • fetch:表示抓取策略,默认为 FetchType.LAZY ,因为关联的多个对象通常不必从数据库预先读取到内存
  • cascade:表示级联操作策略,对于 OneToMany 类型的关联非常重要,通常该实体更新或删除时,其关联的实体也应当被更新或删除

例如:实体 User 和 Order 是 OneToMany 的关系,则实体 User 被删除时,其关联的实体 Order 也应该被全部删除

2.6.3.11、@ManyToMany

描述一个多对多的关联.多对多关联上是两个一对多关联,但是在 ManyToMany 描述中,中间表是由 ORM 框架自动处理

  • targetEntity:表示多对多关联的另一个实体类的全名,例如: package.Book.class
  • mappedBy:表示多对多关联的另一个实体类的对应集合属性名称

两个实体间相互关联的属性必须标记为 @ManyToMany ,并相互指定 targetEntity 属性,
需要注意的是,有且只有一个实体的 @ManyToMany 注解需要指定 mappedBy 属性,指向 targetEntity 的集合属性名称
利用 ORM 工具自动生成的表除了 User 和 Book 表外,还自动生成了一个 User_Book 表,用于实现多对多关联

2.6.3.12、@JoinColumn

@JoinColumn 和 @Column 类似,介量描述的不是一个简单字段,而一一个关联字段,例如.描述一个 @ManyToOne 的字段.

  • name:该字段的名称.由于@JoinColumn描述的是一个关联字段,如ManyToOne,则默认的名称由其关联的实体决定.

例如,实体Order有一个user属性来关联实体User,则Order的user属性为一个外键,其默认的名称为实体User的名称+下划线+实体User的主键名称
@JoinTable(name = “student_teacher”, inverseJoinColumns = @JoinColumn(name = “tid”), joinColumns = @JoinColumn(name = “sid”))
由第三张表来维护两张表的关系

  • name:是关系表的名字
  • joinColumns:自己这一端的主键
  • inverseJoinColumns:对方的主键
2.6.3.13、@MappedSuperclass

@MappedSuperclass 可以将超类的 JPA 注解传递给子类,使子类能够继承超类的 JPA 注解

2.6.3.14、@Embedded

@Embedded 将几个字段组合成一个类,并作为整个 Entity 的一个属性.

例如 User 包括 id,name,city,street,zip 属性.
我们希望 city,street,zip 属性映射为 Address 对象.这样,User 对象将具有 id,name和address 这三个属性.
Address 对象必须定义为 @Embededable

2.7、实例:用 JPA 构建实体数据表

package com.example.test3.jpa;

import lombok.Data;

import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;

@Data
@Entity
public class Article implements Serializable {

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

    @Column(nullable = false, unique = true)
    @NotEmpty(message = "标题不能为空")
    private String title;

    // 枚举类型
    @Column(columnDefinition = "enum('图', '图文', '文')")
    private String type;

    // 默认值 false
    private Boolean available = Boolean.FALSE;

    @Size(min = 0, max = 20)
    private String keyword;

    @Size(max = 255)
    private String description;

    @Column(nullable = false)
    private String body;

    // 创建虚拟字段
    @Transient
    private List keywordlists;

    public List getKeywordlists() {
        return Arrays.asList(this.keyword.trim().split("\\|"));
    }

    public void setKeywordlists(List keywordlists) {
        this.keywordlists = keywordlists;
    }
}

3、认识 JPA 的接口

JPA 提供了操作数据库的接口。在自定义接口过程中,可以不写相关的 SQL 操作,由代理类自动生成。

3.1、JPA 接口 JpaRepository

JpaRepository 继承自 PagingAndSortingRepository。该接口提供了 JPA 的相关实用功能,以及通过 Example 进行查询操作。Example 对象是 JPA 提供用来构造查询条件的对象。该接口源码如下:

package org.springframework.data.jpa.repository;
// 省略...

@NoRepositoryBean
// T:表示实体对象;ID:表示主键;ID必须实现序列化,即 实体对象必须继承 Serializable 接口。
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

   // 查询所有实体
    @Override
    List<T> findAll();

   // 排序、查询所有实体
    @Override
    List<T> findAll(Sort sort);

   // 返回指定一组 ID 的实体
    @Override
    List<T> findAllById(Iterable<ID> ids);

   // 保存集合
    @Override
    <S extends T> List<S> saveAll(Iterable<S> entities);

   // 执行缓存与数据库同步
    void flush();

   // 强制执行持久化
    <S extends T> S saveAndFlush(S entity);

   // 删除一个实体集合
    void deleteInBatch(Iterable<T> entities);

   // 删除所有实体
    void deleteAllInBatch();

   // 返回 ID 对应的实体,如果不存在,则返回空置
    T getOne(ID id);

   // 查询满足 Example 的所有对象
    @Override
    <S extends T> List<S> findAll(Example<S> example);

   // 查询满足 Example 的所有对象,并进行排序返回
    @Override
    <S extends T> List<S> findAll(Example<S> example, Sort sort);
}

3.2、分页排序接口 PagingAndSortingRepository

PagingAndSortingRepository 继承自 CrudRepository 提供的 分页和排序方法。源码如下:

package org.springframework.data.repository;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {

    // 排序功能,按照 sort 制定的排序返回数据
    Iterable<T> findAll(Sort sort);

    // 分页查询(含排序功能)
    Page<T> findAll(Pageable pageable);
}

3.3、数据操作接口 CrudRepository

CrudRepository 继承自 Repository 接口,并新增了 增加、删除、修改和查询方法。源码如下:

package org.springframework.data.repository;

import java.util.Optional;

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {

    // 保存实体。当实体中包含主键时,JPA会进行更新操作
    <S extends T> S save(S entity);

    // 保存所有实体。实体必须不为空
    <S extends T> Iterable<S> saveAll(Iterable<S> entities);

    // 根据主键 ID 检索实体
    Optional<T> findById(ID id);

    // 根据主键 ID 检索实体,返回是否存在
    boolean existsById(ID id);

    // 返回所有实体
    Iterable<T> findAll();

    // 根据给定的一组 ID 值返回一组ID
    Iterable<T> findAllById(Iterable<ID> ids);

    // 返回实体的数量
    long count();

    // 根据 ID 删除数据
    void deleteById(ID id);

    // 删除实体
    void delete(T entity);

    // 删除所有实体
    void deleteAll(Iterable<? extends T> entities);

    // 删除全部实体
    void deleteAll();
}

3.4、分页接口、排序类 Pageable、Page、Sort

Pageable 接口用于构造翻页查询,返回 Page 对象。Page 从 0 开始分页。

此处仅部分展示使用代码,其他关联文件未展示。

注意:排序忽略大小写

package com.example.test3.controller;

import com.example.test3.jpa.Article;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import java.util.ArrayList;
import java.util.List;

@RestController
public class JpaController {

    @RequestMapping("/article")
    public ModelAndView articleList(
            @RequestParam(value = "start", defaultValue = "0") Integer start,
            @RequestParam(value = "limit", defaultValue = "10") Integer limit) {
        start = start < 0 ? 0 : start;
//        Sort sort = new Sort(Sort.Direction.DESC, "id"); 不推荐这种写法
        // 多列排序
        List<Sort.Order> orders = new ArrayList<>();
        orders.add(new Sort.Order(Sort.Direction.DESC, "id"));
        orders.add(new Sort.Order(Sort.Direction.ASC, "view"));
//        Pageable pageable = new PageRequest(start, limit, sort); 不推荐这种写法
        Pageable pageable = PageRequest.of(start, limit, Sort.by(orders));
        // articleRepository 是自定的数据库操作类,这里没有写
        Page<Article> page = articleRepository.findAll(pageable);
        ModelAndView mav = new ModelAndView("admin/article/list");
        mav.addObject("page", page);
        return mav;
    }
}

4、JPA 的查询方式

约定方法名一定要根据命名规范来书写,Spring Data 会根据前缀、中间连接词(Or、And、Like、NotNull 等类似 SQL 中的关键字)、内部拼接 SQL 代理生成方法的实现。约定方法名的方法见表:

关键词

SQL符号

样例

对应JPQL 语句片段

And

and

findByLastnameAndFirstname

… where x.lastname = ?1 and x.firstname = ?2

Or

or

findByLastnameOrFirstname

… where x.lastname = ?1 or x.firstname = ?2

Is,Equals

=

findByFirstname,findByFirstnameIs,findByFirstnameEquals

… where x.firstname = ?1

Between

between xxx and xxx

findByStartDateBetween

… where x.startDate between ?1 and ?2

LessThan

<

findByAgeLessThan

… where x.age < ?1

LessThanEqual

<=

findByAgeLessThanEqual

… where x.age <= ?1

GreaterThan

>

findByAgeGreaterThan

… where x.age > ?1

GreaterThanEqual

>=

findByAgeGreaterThanEqual

… where x.age >= ?1

After

>

findByStartDateAfter

… where x.startDate > ?1

Before

<

findByStartDateBefore

… where x.startDate < ?1

IsNull

is null

findByAgeIsNull

… where x.age is null

IsNotNull,NotNull

is not null

findByAge(Is)NotNull

… where x.age not null

Like

like

findByFirstnameLike

… where x.firstname like ?1

NotLike

not like

findByFirstnameNotLike

… where x.firstname not like ?1

StartingWith

like 'xxx%'

findByFirstnameStartingWith

… where x.firstname like ?1(parameter bound with appended %)

EndingWith

like 'xxx%'

findByFirstnameEndingWith

… where x.firstname like ?1(parameter bound with prepended %)

Containing

like '%xxx%'

findByFirstnameContaining

… where x.firstname like ?1(parameter bound wrapped in %)

OrderBy

order by

findByAgeOrderByLastnameDesc

… where x.age = ?1 order by x.lastname desc

Not

<>

findByLastnameNot

… where x.lastname <> ?1

In

in()

findByAgeIn(Collection<Age> ages)

… where x.age in ?1

NotIn

not in()

findByAgeNotIn(Collection<Age> ages)

… where x.age not in ?1

TRUE

=true

findByActiveTrue()

… where x.active = true

FALSE

=false

findByActiveFalse()

… where x.active = false

IgnoreCase

upper(xxx)=upper(yyyy)

findByFirstnameIgnoreCase

… where UPPER(x.firstame) = UPPER(?1)

接口方法的命名规则也很简单,明白And、Or、Is、Equal、Greater、StartingWith等英文单词的含义,就可以写接口方法了。
具体用法如下:

package com.example.repository;

import com.example.jpa.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.Repository;

import java.util.List;

public interface UserRepository extends Repository<User, Long> {
    User findFirstByOrderByNameAsc();

    List<User> findByEmailOrName(String email, String name);

    Page<User> queryFirst100ByName(String name, Pageable pageable);

    Slice<User> findTop100ByName(String name, Pageable pageable);

    List<User> findFirst100ByName(String name, Sort sort);
}

5、用 JPQL 进行查询

JPQL 语言(Java Persistence Query Language)是一种和 SQL 非常类似的中间性和对象化的查询语言,它最终会被编译成针对不同底层数据库的 SQL 语言,从而屏蔽不同数据库的差异。

JPQL 语言通过 Query 接口封装执行,Query 接口封装了执行数据库查询的相关方法。调用 EntityManager 的 Query、NamedQuery 及 NativeQuery 方法可以获得查询对象,进而可调用 Query 接口的相关方法来执行查询操作。

JPQL 是面向对象进行查询的语言,可以通过自定义的 JPQL 完成 UPDATE 和 DELETE 操作。JPQL 不支持使用 INSERT。对于 UPDATE 或 DELETE 操作,必须使用注解 @Modifying 进行修饰。

5.1、JPQL 的用法:

package com.example.repository;

import com.example.jpa.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface UserRepository2 extends JpaRepository<User, Long> {

    // 根据名称查询
    @Query("select u from User u where u.name = ?1")
    User fingByName(String name);

    // 根据名称模糊查询
    @Query("select u from User u where u.name like %?1")
    List<User> findByName(String name);
}

5.2、用原生 SQL 进行查询

package com.example.repository;

import com.example.jpa.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface UserRepository2 extends JpaRepository<User, Long> {

    // 根据 ID 查询
    @Query(value = "select * from User u where u.id = :id", nativeQuery = true)
    Optional<User> findById(@Param("id") Long id);

    // 查询所有用户
    @Query(value = "select * from User", nativeQuery = true)
    List<User> findAllNative();

    // 根据 Email 查询
    @Query(value = "select * from User where email = ?1", nativeQuery = true)
    User findByEmail(String email);

    // 根据 Name 查询,并返回分页对象 Page
    @Query(value = "select * from User where name = ?1",
            countQuery = "select count(*) from User where name = ?1",
            nativeQuery = true)
    Page<User> findByName(String name, Pageable pageable);

    // 根据 Name 来修改 Email 的值
    @Modifying
    @Transactional
    @Query("update User set email = :email where name = :name")
    Integer updateUserEmailByName(@Param("name") String name, @Param("email") String email);
}

5.2.1、使用事务

UPDATE 或 DELETE 操作需要使用事务。此时需要先定义 Service 层,然后在 Service 层的方法上添加事务操作。对于自定义的方法,如果需要改变 Spring Data 提供的事务默认方法,则可以在方法上使用注解 @Transactional

// 根据 Name 来修改 Email 的值
    @Modifying
    @Transactional
    @Query("update User set email = :email where name = :name")
    Integer updateUserEmailByName(@Param("name") String name, @Param("email") String email);

5.2.2、测试

package com.example.bean;

import com.example.jpa.User;
import com.example.repository.UserRepository2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class UserServiceTest {

    @Autowired
    UserRepository2 userRepository2;

    @Test
    void TestUpdateEmailByName() {
        Integer i = userRepository2.updateUserEmailByName("张三", "sdfgsgsdfg@163.com");
        System.out.println(i);
    }

    @Test
    void TestFindById() {
        List<User> list = userRepository2.findAllNative();
        System.out.println(list);
    }
}

5.3、用 Specifications 查询

如果想使 Repository 支持 Specification 查询,则需要在 Repository 中继承 JpaSpecificationExecutor 接口

package com.example.repository;

import com.example.jpa.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.*;

@SpringBootTest
class UserRepository3Test {

    @Autowired
    private UserRepository3 userRepository3;

    @Test
    public void testJpa() {
        PageRequest pageable = PageRequest.of(0, 10);
        // 通常使用 Specification 的匿名内部类

        Specification<User> specification = new Specification<User>() {
            @Override
            public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                Path path = root.get("id");
                // gt 是大于的意思,这里表示 ID 大于 2
                Predicate predicate1 = criteriaBuilder.gt(path, 2);
                // equal 是等于的意思,代表查询 name 值为 赵四 的数据记录
                Predicate predicate2 = criteriaBuilder.equal(root.get("name"), "赵四");
                // 构建组合的 Predicate
                Predicate predicate = criteriaBuilder.and(predicate1, predicate2);
                return predicate;
            }
        };

        Page<User> page = userRepository3.findAll(specification, pageable);
        System.out.println("总记录数:" + page.getTotalElements());
        System.out.println("当前第:" + (page.getNumber() + 1) + " 页");
        System.out.println("总页数:" + page.getTotalPages());
        System.out.println("当前页面的 List:" + page.getContent());
        System.out.println("当前页面的记录数:" + page.getNumberOfElements());
    }
}

Hibernate: select user0_.id as id1_0_, user0_.email as email2_0_, user0_.name as name3_0_, user0_.pswd as pswd4_0_ from user user0_ where user0_.id>2 and user0_.name=? limit ?
Hibernate: select count(user0_.id) as col_0_0_ from user user0_ where user0_.id>2 and user0_.name=?
总记录数:100
当前第:1 页
总页数:10
当前页面的 List:[User(id=4, name=赵四, pswd=123456, email=345634@qq.com), User(id=5, name=赵四, pswd=123456, email=345634@qq.com), User(id=6, name=赵四, pswd=123456, email=345634@qq.com), User(id=7, name=赵四, pswd=123456, email=345634@qq.com), User(id=8, name=赵四, pswd=123456, email=345634@qq.com), User(id=9, name=赵四, pswd=123456, email=345634@qq.com), User(id=10, name=赵四, pswd=123456, email=345634@qq.com), User(id=11, name=赵四, pswd=123456, email=345634@qq.com), User(id=12, name=赵四, pswd=123456, email=345634@qq.com), User(id=13, name=赵四, pswd=123456, email=345634@qq.com)]
当前页面的记录数:10

5.4、用 ExampleMatcher 进行查询

Spring Data 可以通过 Example 对象来构造 JPQL 查询

package com.example.repository;

import com.example.jpa.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.*;
import java.util.List;

@SpringBootTest
class UserRepository3Test {

    @Autowired
    private UserRepository3 userRepository3;

    @Test
    public void testExample() {
        User user = new User();
        // 构建查询条件
        user.setName("张三");
        // 创建一个 ExampleMatcher
        ExampleMatcher matcher = ExampleMatcher.matching()
                // 不区分大小写匹配 Name
                .withIgnorePaths("name")
                // 包含 null 值
                .withIncludeNullValues();
        // 通过 Example 构建查询
        Example<User> example = Example.of(user, matcher);
        List<User> list = userRepository3.findAll(example);
        System.out.println(list);
    }
}

6、实例:用 JPA 开发文章管理模块

6.1、定义自动填充字段

package com.example.test3.jpa;

import lombok.Data;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;

/**
 * Description: ENTITY基类,让实体类去继承时间字段
 * 1.实体头加注解@EntityListeners(AuditingEntityListener.class)
 * 2.启动类加@EnableJpaAuditing
 *
 * 数据库添加相应控制也可以CURRENT_TIMESTAMP , CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
 */
@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    // 创建时间
    @CreatedDate
//    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Long createTime;

    // 最后修改时间
    @LastModifiedDate
//    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Long updateTime;

    // 创建人
    @Column(name = "create_by")
    @CreatedBy
    private Long createBy;

    // 修改人
    @Column(name = "lastModified_by")
    @LastModifiedBy
    private String lastModifiedBy;
}

6.2、实现当前用户自填

上面可以解决时间的自动填写,但是没有实现 @CreatedBy@LastModifiedBy 的自动填写,所以需要实现 AuditorAware 接口来返回需要插入的值。

这里使用到后面要学习的 安全框架,这里先引入:

<!-- 安全框架,安全认证与授权 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

package com.example.test3.jpa;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.Optional;

@Configuration // 表示配置类,让 Spring 来加载该类配置
public class InjectAuditor implements AuditorAware<String> {
    @Override
    public Optional<String> getCurrentAuditor() {
        // SecurityContextHolder 用于获取 SecurityContext,其存放了 Authentication 和特定于请求的安全信息。
        SecurityContext securityContext = SecurityContextHolder.getContext();
        // 这里判断用户是否登录
        if (securityContext == null) {
            return Optional.empty();
        }
        // 这里判断是否登录成功,如果成功则获取并返回用户名
        if (securityContext.getAuthentication() == null) {
            return Optional.empty();
        } else {
            String loginUserName = securityContext.getAuthentication().getName();
            Optional<String> name = Optional.ofNullable(loginUserName);
            return name;
        }
    }
}

6.3、实现文章实体

package com.example.test3.jpa;

import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;

@Data
@Entity
@EqualsAndHashCode(callSuper = true) // lombok 继承父类
// 这里继承了 BaseEntity 自填充字段
public class Article extends BaseEntity implements Serializable {

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

    @Column(nullable = false, unique = true)
    @NotEmpty(message = "标题不能为空")
    private String title;

    // 枚举类型
    @Column(columnDefinition = "enum('图', '图文', '文')")
    private String type;

    // 默认值 false
    private Boolean available = Boolean.FALSE;

    @Size(min = 0, max = 20)
    private String keyword;

    @Size(max = 255)
    private String description;

    @Column(nullable = false)
    private String body;

    // 创建虚拟字段
    @Transient
    private List<String> keyWordLists;

    public List<String> getKeyWordLists() {
        return Arrays.asList(this.keyword.trim().split("\\|"));
    }

    public void setKeyWordLists(List<String> keyWordLists) {
        this.keyWordLists = keyWordLists;
    }
}

6.4、实现数据持久层

package com.example.test3.jpa;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface ArticleRepository extends JpaRepository<Article,Long>, JpaSpecificationExecutor<Article> {
    Article findById(long id);
}

6.5、实现服务接口

package com.example.test3.jpa;

import java.util.List;
import java.util.Optional;

public interface ArticleService {

    public List<Article> getArticleList();

    public Optional<Article> findArticleById(Long id);

}

6.6、实现服务接口的实现类

package com.example.test3.jpa;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service // 标注为服务类
public class ArticleServiceImpl implements ArticleService {

    @Autowired
    private ArticleRepository articleRepository;

    // 重写service接口的实现,实现列表功能
    @Override
    public List<Article> getArticleList() {
        return articleRepository.findAll();
    }

    // 重写service接口的实现,实现根据id查询对象功能。
    @Override
    public Optional<Article> findArticleById(Long id) {
        return articleRepository.findById(id);
    }
}

6.7、实现增、删、改和查的控制层 API 功能

package com.example.test3.jpa;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("article")
public class ArticleController {

    @Autowired
    private ArticleRepository articleRepository;

    // 文章列表
    @RequestMapping("")
    public ModelAndView articleList(@RequestParam(value = "start", defaultValue = "0") Integer start,
                                    @RequestParam(value = "limit", defaultValue = "5") Integer limit) {
        start = start < 0 ? 0 : start;
        Sort sort = Sort.by(Sort.Direction.DESC, "id");
        Pageable pageable = PageRequest.of(start, limit, sort);
        Page<Article> page = articleRepository.findAll(pageable);
        ModelAndView mav = new ModelAndView("article/list");
        mav.addObject("page", page);
        return mav;
    }

    // 根据id获取文章对象
    @GetMapping("/{id}")
    public ModelAndView getArticle(@PathVariable("id") Integer id) throws Exception {
        Article articles = articleRepository.findById(id);
        ModelAndView mav = new ModelAndView("article/show");
        mav.addObject("article", articles);
        return mav;
    }

    // 新增操作视图
    @GetMapping("/add")
    public String addArticle() throws Exception {
        return "article/add";
    }

    // 新增保存方法
    @PostMapping("")
    public String saveArticle(Article model) throws Exception {
        articleRepository.save(model);
        return "redirect:/article/";
    }

    // 删除
    @DeleteMapping("/{id}")
    public String del(@PathVariable("id") long id) throws Exception {
        articleRepository.deleteById(id);
        return "redirect:";
    }

    // 编辑视图
    @GetMapping("/edit/{id}")
    public ModelAndView editArticle(@PathVariable("id") long id) throws Exception {
        Article model = articleRepository.findById(id);
        ModelAndView mav = new ModelAndView("article/edit");
        mav.addObject("article", model);
        return mav;
    }

    // 修改方法
    @PutMapping("/{id}")
    public String editArticleSave(Article model, long id) throws Exception {
        model.setId(id);
        articleRepository.save(model);
        return "redirect:";
    }

}