Spring Data作为Spring全家桶中重要的一员,在Spring项目全球使用市场份额排名中多次居前位,而在Spring Data子项目的使用份额排名中,Spring Data JPA也一直名列前茅。Spring Boot为Spring Data JPA提供了启动器,使Spring Data JPA在Spring Boot项目中的使用更加便利。
Spring Data JPA概述
对象关系映射(Object Relational Mapping,ORM)框架在运行时可以参照映射文件的信息,把对象持久化到数据库中,可以解决面向对象与关系数据库存在的互不匹配的现象,常见的ORM框架有Hibernate、OpenJPA等。ORM框架的出现,使开发者从数据库编程中解脱出来,把更多的精力放在业务模型与业务逻辑上,但各ORM框架之间的API差别很大,使用了某种ORM框架的系统会严重受限于该ORM的标准,基于此,SUN公司提出JPA(Java Persistence API,Java持久化API)。
JPA是Sun官方提出的Java持久化规范,用于描述对象和关系表的映射关系,并将运行期的实体对象持久化到数据库中。JPA规范本质上是一套规范,它提供了一些编程的API接口,但具体实现则由服务厂商来提供基于JPA的数据访问。
Spring Data JPA是Spring基于ORM框架、JPA规范的基础上封装的一套JPA应用框架,它提供了增删改查等常用功能,使开发者可以用较少的代码实现数据操作,同时还易于扩展。
基于JPA的数据访问如下:
Spring Data JPA整体处理逻辑如下:
Spring Data JPA快速入门
Spring Data JPA提供了很多模板代码,易于扩展,可以大幅提高开发效率,使开发者用极简的代码即可实现对数据的访问。使用Spring Data JPA可以通过Repository接口中的方法对数据库中的数据进行增删改查,也可以根据方法命名规则定义的方法、JPQL,以及原生SQL的方式进行操作。
Repository接口
自定义Repository接口,必须继承XXRepository<T, ID>接口,T:实体类,ID:实体类中主键对应的属性的数据类型。
Repository继承关系如下:
CrudRepository
在Spring Data JPA中,CrudRepository
是一个接口,它提供了基本的 CRUD(创建、读取、更新、删除)操作。虽然 CrudRepository
并没有直接提供一个专门用于更新的方法,但你可以通过 save()
方法来实现更新的功能。
当你调用 save()
方法并传递一个实体对象时,Spring Data JPA 会检查该实体是否已经存在于数据库中(通常是通过主键来判断,所以实体对象设置了主键值时会出现一条查询语句)。
- 如果实体不存在,
save()
方法会创建一个新的记录。 - 如果实体已经存在,
save()
方法会更新该实体的状态。
JpaRepository接口提供的方法
使用Spring Data JPA进行数据操作的多种实现方式
1、如果自定义接口继承了JpaRepository接口,则默认包含了一些常用的CRUD方法。
2、自定义Repository接口中,可以使用@Query注解配合SQL语句进行数据的查、改、删操作。
3、自定义Repository接口中,可以直接使用关键字构成的方法名进行查询操作。
4、在自定义的Repository接口中,使用@Query注解方式执行数据变更操作(修改、删除)时,必须添加@Modifying注解表示数据变更和@Transactional注解表示事务管理。
- 在自定义的Repository接口中,针对数据的变更操作(修改、删除),无论是否使用了@Query注解,都必须在方法上方添加@Transactional注解进行事务管理,否则程序执行就会出现InvalidDataAccessApiUsageException异常。
- 如果在调用Repository接口方法的业务层Service类上已经添加了@Transactional注解进行事务管理,那么Repository接口文件中就可以省略@Transactional注解。
5、使用Example实例进行复杂条件查询
根据方法命名规则定义方法
Spring Data中按照框架的规范自定义了Repository接口,除了可以使用接口提供的默认方法外,还可以按特定规则来定义查询方法,只要这些查询方法的方法名遵守特定的规则,不需要提供方法实现体,Spring Data就会自动为这些方法生成查询语句。Spring Data对这种特定的查询方法的定义规范如下:
以find、read、get、query、count开头。
涉及查询条件时,条件的属性使用条件关键字连接,并且条件属性的首字母大写。
支持属性的级联查询:
若当前类有符合条件的属性,则优先使用,而不使用级联属性。
若需要使用级联属性,则属性之间使用_连接。
条件关键字如下:
关键字 | 方法名示例 | 对应的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 |
关键字 | 方法名示例 | 对应的JPQL片段 |
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 | findByAgeIsNotNull() | … where x.ageisnot null |
NotNull | findByAgeNotNull() | … where x.age not null |
关键字 | 方法名示例 | 对应的JPQL片段 |
Like | findByFirstnameLike() | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike() | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith() | … where x.firstname like ?1 (绑定参数 %) |
EndingWith | findByFirstnameEndingWith() | … where x.firstname like ?1 (绑定参数 %) |
Containing | findByFirstnameContaining() | … where x.firstname like ?1 (绑定参数 %) |
OrderBy | findByAgeOrderByLastnameDesc() | … where x.age = ?1 order by x.lastname desc |
关键字 | 方法名示例 | 对应的JPQL片段 |
Not | findByLastnameNot() | … where x.lastname <> ?1 |
In | findByAgeIn(Collection<Age> ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection<Age> ages) | … 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) |
JPQL
使用Spring Data JPA提供的查询方法已经可以满足大部分应用场景的需求,但是有些业务需要更灵活的查询条件,这时就可以使用@Query注解,结合JPQL的方式来完成查询。 JPQL是JPA中定义的一种查询语言,此种语言旨在让开发者忽略数据库表和表中的字段,而关注实体类及实体类中的属性。 JPQL语句的写法和SQL语句的写法十分类似,但是要把查询的表名换成实体类名称,把表中的字段名换成实体类的属性名称。
JPQL支持命名参数和位置参数两种查询参数。
命名参数:在方法的参数列表中,使用@Param注解标注参数的名称,在@Query注解的查询语句中,使用“:参数名称” 匹配参数名称。
位置参数:在@Query注解的查询语句中,使用“?位置编号的数值” 匹配参数,查询语句中参数标注的编号需要和方法的参数列表中参数的顺序依次对应。
示例:
//命名参数绑定
@Query("from Book b where b.author=:author and b.name=:name")
List<Book> findByCondition1(@Param("author") String author,@Param("name") String name);
//位置参数绑定
@Query("from Book b where b.author=?1 and b.name=?2")
List<Book> findByCondition2(String author, String name);
JPQL中使用like模糊查询、排序查询、分页查询子句时,其用法与SQL中的用法相同,区别在于JPQL处理的类的实例不同。
示例:
//like模糊查询
@Query("from Book b where b.name like %:name%")List<Book> findByCondition3(@Param("name") String name);
//排序查询@Query("from Book b where b.name like %:name% order by id desc")
List<Book> findByCondition4(@Param("name") String name);
//分页查询
@Query("from Book b where b.name like %:name%")
Page<Book> findByCondition5(Pageable pageable, @Param("name") String name);
JPQL中除了可以使用字符串和基本数据类型的数据作为参数外,还可以使用集合和Bean作为参数,传入Bean进行查询时可以在JPQL中使用SpEL表达式接收变量。
示例:
//传入集合参数查询
@Query("from Book b where b.id in :ids")
List<Book> findByCondition6(@Param("ids") Collection<String> ids);
//传入Bean进行查询(使用SPEL表达式)
@Query("from Book b where b.author=:#{#Book.author} and " +" b.name=:#{#Book.name}")
Book findByCondition7(@Param("Book") Book Book);
原生SQL
如果出现非常复杂的业务情况,导致JPQL和其他查询都无法实现对应的查询,需要自定义SQL进行查询时,可以在@Query注解中定义该SQL。@Query注解中定义的是原生SQL时,需要在注解使用nativeQuery=true指定执行的查询语句为原生SQL,否则会将其当作JPQL执行。
示例:
@Query(value="SELECT * FROM book WHERE id = :id",nativeQuery=true)
Book findByCondition8(@Param("id") Integer id);
小提示
使用@Query注解可以执行JPQL和原生SQL查询,但是@Query注解无法进行DML数据操纵语言,主要语句有INSERT、DELETE和UPDATE操作,如果需要更新数据库中的数据,需要在对应的方法上标注@Modifying注解,以通知Spring Data当前需要进行的是DML操作。需要注意的是JPQL只支持DELETE和UPDATE操作,不支持INSERT操作。
整合Spring Data JPA
0.在全局配置文件中添加数据库配置
server.port=8088spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=zptc1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driverspring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto
是 Spring Boot 应用程序中与 JPA(Java Persistence API)和 Hibernate 相关的配置属性。Hibernate 是一个流行的 JPA 实现,用于将 Java 对象映射到关系数据库中的表。
ddl-auto
属性用于控制 Hibernate 在启动时如何自动处理数据库架构(DDL,即数据定义语言)。具体来说,它定义了 Hibernate 是否应该基于实体类自动创建、更新或验证数据库表结构。
以下是 ddl-auto
的几个常用值:
- create:Hibernate 会在启动时创建数据库表。如果表已经存在,它会被删除并重新创建。这是一个非常危险的设置,因为它会丢失所有现有数据。通常,这仅用于开发环境或测试数据库。
- create-drop:在
create
的基础上,当 Hibernate 的 SessionFactory 关闭时,它会删除所有创建的表。这同样适用于开发或测试环境。 - update:Hibernate 会根据实体类更新数据库表结构。它只会添加、修改或删除必要的列,以保持与实体类的同步。这是一个相对安全的设置,但仍然建议在生产环境中谨慎使用,因为自动模式可能会引入难以预料的问题。
- validate:Hibernate 验证数据库表结构是否与实体类匹配。如果不匹配,它会抛出异常。这不会创建或修改任何表,只用于验证。
注意:尽管 ddl-auto
在开发过程中可能非常方便,但在生产环境中使用它通常是不推荐的。在生产环境中,建议使用迁移工具(如 Flyway 或 Liquibase)来管理数据库架构的版本控制。这样可以确保架构更改的清晰、可预测和可审计。
1.在pom文件中添加Spring Data JPA依赖启动器
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>
2.新建ORM实体类
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;@Entity(name = "t_comment")
public class Discuss {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Integer id;private String content;private String author;@Column(name = "a_id")private Integer aId;//补上get、set、toString方法}
3.新建Repository接口,添加增删改查等方法
创建子包repository,及接口DiscussRepository
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.transaction.annotation.Transactional;public interface DiscussRepository extends JpaRepository<Discuss, Integer>{//查询author非空的Discuss评论信息public List<Discuss> findByAuthorNotNull();//通过文章id分页查询出Discuss评论信息。JPQL@Query("SELECT c FROM t_comment c WHERE c.aId = ?1")public List<Discuss> getDiscussPaged(Integer aid,Pageable pageable); //通过文章id分页查询出Discuss评论信息。原生sql@Query(value = "SELECT * FROM t_comment WHERE a_Id = ?1",nativeQuery = true)public List<Discuss> getDiscussPaged2(Integer aid,Pageable pageable);//使用命名参数:bb@Query("SELECT c FROM t_comment c WHERE c.aId = :bb")public List<Discuss> getDiscussPaged3(@Param(value = "bb") Integer aid,Pageable pageable);//对数据进行更新和删除操作@Transactional@Modifying@Query("UPDATE t_comment c SET c.author = ?1 WHERE c.id = ?2")public int updateDiscuss(String author,Integer id); @Transactional@Modifying@Query("DELETE t_comment c WHERE c.id = ?1")public int deleteDiscuss(Integer id);
}
注:
- getDiscussPaged2与getDiscussPaged()方法的参数和作用完全一样。
- 区别是该方法上方的@Query注解将nativeQuery属性设置为了true,用来编写原生SQL语句。
4.编写单元测试
import java.util.List;
import java.util.Optional;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.PageRequest;
import org.springframework.data.domain.Pageable;@SpringBootTest
class JpaTests {@Autowiredprivate DiscussRepository repository;//使用JpaRepository内部方法@Testpublic void selectComment() {Optional<Discuss>optional = repository.findById(1);if (optional.isPresent()) {System.out.println(optional.get());}}//使用方法名关键字进行数据操作@Testpublic void selectCommentByKeys() {List<Discuss>list = repository.findByAuthorNotNull();for (Discuss discuss : list) {System.out.println(discuss);}}//使用@Query注解@Testpublic void selectCommentPaged() {Pageable page = PageRequest.of(0, 3);List<Discuss>list = repository.getDiscussPaged(1, page);list.forEach(t -> System.out.println(t)); }//使用Example封装参数,精确匹配查询条件@Testpublic void selectCommentByExample() {Discuss discuss=new Discuss();discuss.setAuthor("张三");Example<Discuss> example = Example.of(discuss);List<Discuss> list = repository.findAll(example);list.forEach(t -> System.out.println(t));}//使用ExampleMatcher模糊匹配查询条件@Testpublic void selectCommentByExampleMatcher() {Discuss discuss=new Discuss();discuss.setAuthor("张");ExampleMatcher matcher = ExampleMatcher.matching().withMatcher("author",ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING));Example<Discuss> example = Example.of(discuss, matcher);List<Discuss> list = repository.findAll(example);System.out.println(list);}//保存评论@Testpublic void saveDiscuss() {Discuss discuss=new Discuss();discuss.setContent("张某的评论xxxx");discuss.setAuthor("张某");Discuss newDiscuss = repository.save(discuss);System.out.println(newDiscuss);}//更新作者@Testpublic void updateDiscuss() {int i = repository.updateDiscuss("更新者", 1);System.out.println("discuss:update:"+i);}//删除评论@Testpublic void deleteDiscuss() {int i = repository.deleteDiscuss(7);System.out.println("discuss:delete:"+i);}}