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(JPA
和MyBatis
)。但是 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编码变得更简单。
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 的常用注解
注解 | 说明 |
| 声明 |
| 声明 |
| 指定非约束明确的各个字段,即 |
| 用于注释属性,表明该属性的 |
| 指定类的属性,一个表中的 |
| 指定标注 |
| 表示该属性并非一个数据库表的字段的映射,ORM 框架将忽略该属性,即它不是持久的,为 |
| 指定持久属性,即 |
| 指定在 @GeneratedValue 注解中指定的属性的值。 |
| 在数据库生成一张表来 |
| 这种类型的注释用于 |
| 指定的字段和用于主要或辅助表的 |
| 可以参考使用 select 子句的 SQL 查询中的列名 |
| 指定命名查询的列表 |
| 指定使用静态名称的查询 |
| 作用是 JSON 序列化时将 Java Bean 中的一些属性忽略掉,序列化和反序列化都受影响。name 映射外键的字段名(映射的表直接把表的实体类作为数据类型,如 private 实体类 属性名) |
| 声明一个属性映射为数据库表的主键列,可以标记在属性上也可以标记在set方法上. |
| TemporalType:TIMESTAMP(年月日时分秒) DATE(年月日) |
2.6.2、映射关系的注解
注解 | 说明 |
| 指定一个实体组织或实体集合。用在“多对一”和“一对多”的关联中 |
| 单向 |
| 单向 |
| 单向 |
| 单向 |
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 |
| findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or |
| findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals |
| findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between |
| 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 |
| findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull |
| findByAge(Is)NotNull | … where x.age not null |
Like |
| findByFirstnameLike | … where x.firstname like ?1 |
NotLike |
| findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith |
| findByFirstnameStartingWith | … where x.firstname like ?1(parameter bound with appended %) |
EndingWith |
| findByFirstnameEndingWith | … where x.firstname like ?1(parameter bound with prepended %) |
Containing |
| findByFirstnameContaining | … where x.firstname like ?1(parameter bound wrapped in %) |
OrderBy |
| findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not |
| findByLastnameNot | … where x.lastname <> ?1 |
In |
| findByAgeIn(Collection | … where x.age in ?1 |
NotIn |
| findByAgeNotIn(Collection | … where x.age not in ?1 |
TRUE |
| findByActiveTrue() | … where x.active = true |
FALSE |
| findByActiveFalse() | … where x.active = false |
IgnoreCase |
| 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:";
}
}