JDBC(Java Database Connectivity),它是 Java 用于连接数据库的规范
,也就是用于执行数据库 SQL 语句的 Java API
。
JDBC 可以连接多种数据库,原因在于提供了统一访问的接口
,这也符合 Java 程序接口设计的模式。
JDBC 需要每次进行数据库连接,然后出了 SQL 语句、传值、关闭数据库。这样的流程操作,一旦忘记某一步,就会出现很多问题,于是 JDBCTemplate 被设计出来了。
JDBCTemplate = JDBC + Template
的组合,是对 JDBC 的封装。它更便于程序实现,替我们完成所有的 JDBC 底层操作。因此,对于数据库的操作,再不需要每次都进行连接、打开、关闭了。
JDBC 和 JDBCTemplate 就像是仓库管理员,负责从仓库(数据库)中存取物品。而后者采用的是“电动门自动控制”
JDBCTemplate
实现起来比ORM
繁琐,所以大部分开发使用的是 ORM(JPA
和MyBatis
)。但是 JDBCTemplate 依然有市场,因为学习成本低。
<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=
新建一个测试实体类 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;
}
}
JDBCTemplate 提供了以下操作数据的 3 个方法。
在使用 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);
}
}
ORM(Object Relational Mapping)是 对象/关系映射
。它提供了概念性的、易于理解的数据模型
,将数据库中的表
和内存中的对象
建立映射关系
。它是随着面向对象的软件开发方法的发展而产生的,面向对象的开发方法依然是当前主流的开发方法。
对象
和关系型数据
是业务实体的两种表现形式。业务实体在内存中
的表现为对象
,在数据库中
表现为关系型数据
。内存中的对象不会被永久保存,只有关系型数据库(或 NoSQL 数据库,或文件)中的对象会被永久保存。
对象/关系映射(ORM)系统一般以中间件的形式存在,因为内存中的对象之间存在关联和继承关系,而在数据库中,关系型数据无法直接表达多对多的关联和继承关系。对象、数据库通过 ORM 映射的关系图。
目前比较常用的 ORM 是国外流行的 JPA
和国内流行的 MyBatis
。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
Spring Data: Spring 的一个子项目。用于简化数据库访问,支持NoSQL和关系数据库存储。其主要目标是使数据库的访问变得方便快捷。
Spring Data 提供了基于 增删改查、排序、分页 等层面的统一接口,已实现持久化的储存。
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 三个组件来操作数据。
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;
}
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 自动处理好了。
要使用 JPA,只要加入它的 Starter 依赖,然后配置数据库连接信息。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
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
是hibernate的配置属性,其主要作用是:自动创建、更新、验证数据库表结构。该参数的几种配置如下:
注解 | 说明 |
---|---|
@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(年月日) |
注解 | 说明 |
---|---|
@JoinColumn |
指定一个实体组织或实体集合。用在“多对一”和“一对多”的关联中 |
@ManyToMany |
单向多对多 关系 |
@ManyToOne |
单向多对一 关系,fetch:FetchType.LAZY(修改关联属性加载策略为懒加载) |
@OneToMany |
单向一对多 关系 |
@OneToOne |
单向一对一 关系,@JoinColumn 可以加上一条 unique=true 保持唯一性 |
@Entity 标记在类名上面,作为实体类的标识
当实体类与其映射的数据库表名不同名时需要使用 @Table 标注说明,该标注与 @Entity 标注并列使用,置于实体类声明语句之前,可写于单独语句行,也可与声明语句同行。
@Id 设置对象表示符,标识的实体类的属性映射对应表中的主键
设置标识符的生成策略,常与@Id一起使用
参数:strategy 指定具体的生成策略
其中 name 指定生成器的名字(与 generator 的值一样),sequenceName 指定数据库中定义序列的名字,allocationSize 指定序列每次增长1
描述数据库表中该字段的定义,具有一下属性
在加载数据的时候可以为其指定顺序。
表示该属性并非一个到数据库表的字段的映射,ORM 框架将忽略该属性。
如果一个属性并非数据库表的字段映射。就务必将其标示为 @Transient。否则。ORM框架默认其注解为 @Basic
描述一个一对一的关联
表示一个多对一的映射,该注解标注的属性通常是数据库表的外键
描述一个一对多的关联,该属性应该为集体类型,在数据库中并没有实际字段。
例如:实体 User 和 Order 是 OneToMany 的关系,则实体 User 被删除时,其关联的实体 Order 也应该被全部删除
描述一个多对多的关联.多对多关联上是两个一对多关联,但是在 ManyToMany 描述中,中间表是由 ORM 框架自动处理
两个实体间相互关联的属性必须标记为 @ManyToMany ,并相互指定 targetEntity 属性,
需要注意的是,有且只有一个实体的 @ManyToMany 注解需要指定 mappedBy 属性,指向 targetEntity 的集合属性名称
利用 ORM 工具自动生成的表除了 User 和 Book 表外,还自动生成了一个 User_Book 表,用于实现多对多关联
@JoinColumn 和 @Column 类似,介量描述的不是一个简单字段,而一一个关联字段,例如.描述一个 @ManyToOne 的字段.
例如,实体Order有一个user属性来关联实体User,则Order的user属性为一个外键,其默认的名称为实体User的名称+下划线+实体User的主键名称
@JoinTable(name = “student_teacher”, inverseJoinColumns = @JoinColumn(name = “tid”), joinColumns = @JoinColumn(name = “sid”))
由第三张表来维护两张表的关系
@MappedSuperclass 可以将超类的 JPA 注解传递给子类,使子类能够继承超类的 JPA 注解
@Embedded 将几个字段组合成一个类,并作为整个 Entity 的一个属性.
例如 User 包括 id,name,city,street,zip 属性.
我们希望 city,street,zip 属性映射为 Address 对象.这样,User 对象将具有 id,name和address 这三个属性.
Address 对象必须定义为 @Embededable
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;
}
}
JPA 提供了操作数据库的接口。在自定义接口过程中,可以不写相关的 SQL 操作,由代理类自动生成。
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);
}
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);
}
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();
}
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;
}
}
约定方法名一定要根据命名规范来书写,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);
}
JPQL 语言(Java Persistence Query Language)是一种和 SQL 非常类似的中间性和对象化的查询语言,它最终会被编译成针对不同底层数据库的 SQL 语言,从而屏蔽不同数据库的差异。
JPQL 语言通过 Query 接口封装执行,Query 接口封装了执行数据库查询的相关方法。调用 EntityManager 的 Query、NamedQuery 及 NativeQuery 方法可以获得查询对象,进而可调用 Query 接口的相关方法来执行查询操作。
JPQL 是面向对象进行查询的语言,可以通过自定义的 JPQL 完成 UPDATE 和 DELETE 操作。JPQL 不支持使用 INSERT。对于 UPDATE 或 DELETE 操作,必须使用注解 @Modifying
进行修饰。
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);
}
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);
}
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);
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);
}
}
如果想使 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
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);
}
}
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;
}
上面可以解决时间的自动填写,但是没有实现 @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;
}
}
}
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;
}
}
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);
}
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);
}
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);
}
}
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:";
}
}