Spring Boot(7)之 用 ORM 操作 SQL 数据库

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、配置基础依赖

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
        dependency>

        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        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.bootgroupId>
    <artifactId>spring-boot-starter-data-jpaartifactId>
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编码变得更简单。

Spring Boot(7)之 用 ORM 操作 SQL 数据库_第1张图片
Spring Boot(7)之 用 ORM 操作 SQL 数据库_第2张图片
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 数据库的依赖

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-jpaartifactId>
        dependency>

        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        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 ages) … where x.age in ?1
NotIn not in() findByAgeNotIn(Collection 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("张三", "[email protected]");
        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 protected]), User(id=5, name=赵四, pswd=123456, [email protected]), User(id=6, name=赵四, pswd=123456, [email protected]), User(id=7, name=赵四, pswd=123456, [email protected]), User(id=8, name=赵四, pswd=123456, [email protected]), User(id=9, name=赵四, pswd=123456, [email protected]), User(id=10, name=赵四, pswd=123456, [email protected]), User(id=11, name=赵四, pswd=123456, [email protected]), User(id=12, name=赵四, pswd=123456, [email protected]), User(id=13, name=赵四, pswd=123456, [email protected])]
当前页面的记录数: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.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        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:";
    }

}

你可能感兴趣的:(数据库,spring,boot,sql)