一文搞懂如何在Spring Boot中正确使用JPA

开发 架构
本文已经整理进 JavaGuide 开源的 springboot-guide(SpringBoot 核心知识点总结。 基于 Spring Boot 2.19+),地址:https://github.com/Snailclimb/springboot-guide 。

JPA 这部分内容上手很容易,但是涉及到的东西还是挺多的,网上大部分关于 JPA 的资料都不是特别齐全,大部分用的版本也是比较落后的。另外,我下面讲到了的内容也不可能涵盖所有 JPA 相关内容,我只是把自己觉得比较重要的知识点总结在了下面。很多地方我自己也是参考着官方文档写的,官方文档非常详细了,非常推荐阅读一下。这篇文章可以帮助对 JPA 不了解或者不太熟悉的人来在实际项目中正确使用 JPA。

[[279563]]

另外,我发现网上关于连表查询这一块并没有太多比较有参考价值的博客,所以对这部分也做了详细的总结,以供大家学习参考。

项目代码基于 Spring Boot 最新的 2.1.9.RELEASE 版本构建(截止到这篇文章写完),另外,新建项目的过程就不多说了。

一 JPA 基础:常见操作

1.相关依赖

我们需要下面这些依赖支持我们完成这部分内容的学习:

  1. <dependencies> 
  2.         <dependency> 
  3.             <groupId>org.springframework.boot</groupId> 
  4.             <artifactId>spring-boot-starter-web</artifactId> 
  5.         </dependency> 
  6.         <dependency> 
  7.             <groupId>org.springframework.boot</groupId> 
  8.             <artifactId>spring-boot-starter-data-jpa</artifactId> 
  9.         </dependency> 
  10.         <dependency> 
  11.             <groupId>mysql</groupId> 
  12.             <artifactId>mysql-connector-java</artifactId> 
  13.             <scope>runtime</scope> 
  14.         </dependency> 
  15.         <dependency> 
  16.             <groupId>org.projectlombok</groupId> 
  17.             <artifactId>lombok</artifactId> 
  18.             <optional>true</optional> 
  19.         </dependency> 
  20.         <dependency> 
  21.             <groupId>org.springframework.boot</groupId> 
  22.             <artifactId>spring-boot-starter-test</artifactId> 
  23.             <scope>test</scope> 
  24.         </dependency> 
  25.     </dependencies> 

2.配置数据库连接信息和JPA配置

下面的配置中需要单独说一下 spring.jpa.hibernate.ddl-auto=create这个配置选项。

这个属性常用的选项有四种:

  1. create:每次重新启动项目都会重新创新表结构,会导致数据丢失
  2. create-drop:每次启动项目创建表结构,关闭项目删除表结构
  3. update:每次启动项目会更新表结构
  4. validate:验证表结构,不对数据库进行任何更改

但是,一定要不要在生产环境使用 ddl 自动生成表结构,一般推荐手写 SQL 语句配合 Flyway 来做这些事情。

  1. spring.datasource.url=jdbc:mysql://localhost:3306/springboot_jpa?useSSL=false&serverTimezone=CTT 
  2. spring.datasource.username=root 
  3. spring.datasource.password=123456 
  4. # 打印出 sql 语句 
  5. spring.jpa.show-sql=true 
  6. spring.jpa.hibernate.ddl-auto=create 
  7. spring.jpa.open-in-view=false 
  8. # 创建的表的 ENGINE 为 InnoDB 
  9. spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL55Dialect 

3.实体类

我们为这个类添加了 @Entity 注解代表它是数据库持久化类,还配置了主键 id。

  1. import lombok.Data; 
  2. import lombok.NoArgsConstructor; 
  3.  
  4. import javax.persistence.Column
  5. import javax.persistence.Entity; 
  6. import javax.persistence.GeneratedValue; 
  7. import javax.persistence.GenerationType; 
  8. import javax.persistence.Id; 
  9.  
  10. @Entity 
  11. @Data 
  12. @NoArgsConstructor 
  13. public class Person { 
  14.      
  15.     @Id 
  16.     @GeneratedValue(strategy = GenerationType.IDENTITY) 
  17.     private Long id; 
  18.     @Column(unique = true
  19.     private String name
  20.     private Integer age; 
  21.  
  22.     public Person(String nameInteger age) { 
  23.         this.name = name
  24.         this.age = age; 
  25.     } 
  26.  

如何检验你是否正确完成了上面 3 步?很简单,运行项目,查看数据如果发现控制台打印出创建表的 sql 语句,并且数据库中表真的被创建出来的话,说明你成功完成前面 3 步。

控制台打印出来的 sql 语句类似下面这样:

  1. drop table if exists person 
  2. CREATE TABLE `person` ( 
  3.   `id` bigint(20) NOT NULL AUTO_INCREMENT, 
  4.   `age` int(11) DEFAULT NULL
  5.   `namevarchar(255) DEFAULT NULL, 
  6.    PRIMARY KEY (`id`) 
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
  8. alter table person add constraint UK_p0wr4vfyr2lyifm8avi67mqw5 unique (name

4.创建操作数据库的 Repository 接口

  1. @Repository 
  2. public interface PersonRepository extends JpaRepository<Person, Long> { 

首先这个接口加了 @Repository 注解,代表它和数据库操作有关。另外,它继承了 JpaRepository接口,而JpaRepository长这样:

  1. @NoRepositoryBean 
  2. public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> { 
  3.     List<T> findAll(); 
  4.  
  5.     List<T> findAll(Sort var1); 
  6.  
  7.     List<T> findAllById(Iterable<ID> var1); 
  8.  
  9.     <S extends T> List<S> saveAll(Iterable<S> var1); 
  10.  
  11.     void flush(); 
  12.  
  13.     <S extends T> S saveAndFlush(S var1); 
  14.  
  15.     void deleteInBatch(Iterable<T> var1); 
  16.  
  17.     void deleteAllInBatch(); 
  18.  
  19.     T getOne(ID var1); 
  20.  
  21.     <S extends T> List<S> findAll(Example<S> var1); 
  22.  
  23.     <S extends T> List<S> findAll(Example<S> var1, Sort var2); 

这表明我们只要继承了JpaRepository 就具有了 JPA 为我们提供好的增删改查、分页查询以及根据条件查询等方法。

4.1 JPA 自带方法实战

1) 增删改查

1.保存用户到数据库

  1. Person person = new Person("SnailClimb", 23); 
  2.    personRepository.save(person); 

save()方法对应 sql 语句就是:insert into person (age, name) values (23,"snailclimb")

2.根据 id 查找用户

  1. Optional<Person> personOptional = personRepository.findById(id); 

findById()方法对应 sql 语句就是:select * from person p where p.id = id

3.根据 id 删除用户

  1. personRepository.deleteById(id); 

deleteById()方法对应 sql 语句就是:delete from person where id=id

4.更新用户

更新操作也要通过 save()方法来实现,比如:

  1. Person person = new Person("SnailClimb", 23); 
  2.     Person savedPerson = personRepository.save(person); 
  3.     // 更新 person 对象的姓名 
  4.     savedPerson.setName("UpdatedName"); 
  5.     personRepository.save(savedPerson); 

在这里 save()方法相当于 sql 语句:update person set name="UpdatedName" where id=id

2) 带条件的查询

下面这些方法是我们根据 JPA 提供的语法自定义的,你需要将下面这些方法写到 PersonRepository 中。

假如我们想要根据 Name 来查找 Person ,你可以这样:

  1. Optional<Person> findByName(String name); 

如果你想要找到年龄大于某个值的人,你可以这样:

  1. List<Person> findByAgeGreaterThan(int age); 

4.2 自定义 SQL 语句实战

很多时候我们自定义 sql 语句会非常有用。

根据 name 来查找 Person:

  1. @Query("select p from Person p where p.name = :name"
  2.     Optional<Person> findByNameCustomeQuery(@Param("name") String name); 

Person 部分属性查询,避免 select *操作:

  1. @Query("select p.name from Person p where p.id = :id"
  2.     String findPersonNameById(@Param("id") Long id); 

根据 id 更新Person name:

  1. @Modifying 
  2.     @Transactional 
  3.     @Query("update Person p set p.name = ?1 where p.id = ?2"
  4.     void updatePersonNameById(String name, Long id); 

4.3 创建异步方法

如果我们需要创建异步方法的话,也比较方便。

异步方法在调用时立即返回,然后会被提交给TaskExecutor执行。当然你也可以选择得出结果后才返回给客户端。如果对 Spring Boot 异步编程感兴趣的话可以看这篇文章:《新手也能看懂的 SpringBoot 异步编程指南》 。

  1. @Async 
  2. Future<User> findByName(String name); 
  3.  
  4. @Async 
  5. CompletableFuture<User> findByName(String name); 

5.测试类和源代码地址

测试类:

  1. @SpringBootTest 
  2. @RunWith(SpringRunner.class) 
  3. public class PersonRepositoryTest { 
  4.     @Autowired 
  5.     private PersonRepository personRepository; 
  6.     private Long id; 
  7.  
  8.     /** 
  9.      * 保存person到数据库 
  10.      */ 
  11.     @Before 
  12.     public void setUp() { 
  13.         assertNotNull(personRepository); 
  14.         Person person = new Person("SnailClimb", 23); 
  15.         Person savedPerson = personRepository.saveAndFlush(person);// 更新 person 对象的姓名 
  16.         savedPerson.setName("UpdatedName"); 
  17.         personRepository.save(savedPerson); 
  18.  
  19.         id = savedPerson.getId(); 
  20.     } 
  21.  
  22.     /** 
  23.      * 使用 JPA 自带的方法查找 person 
  24.      */ 
  25.     @Test 
  26.     public void should_get_person() { 
  27.         Optional<Person> personOptional = personRepository.findById(id); 
  28.         assertTrue(personOptional.isPresent()); 
  29.         assertEquals("SnailClimb", personOptional.get().getName()); 
  30.         assertEquals(Integer.valueOf(23), personOptional.get().getAge()); 
  31.  
  32.         List<Person> personList = personRepository.findByAgeGreaterThan(18); 
  33.         assertEquals(1, personList.size()); 
  34.         // 清空数据库 
  35.         personRepository.deleteAll(); 
  36.     } 
  37.  
  38.     /** 
  39.      * 自定义 query sql 查询语句查找 person 
  40.      */ 
  41.  
  42.     @Test 
  43.     public void should_get_person_use_custom_query() { 
  44.         // 查找所有字段 
  45.         Optional<Person> personOptional = personRepository.findByNameCustomeQuery("SnailClimb"); 
  46.         assertTrue(personOptional.isPresent()); 
  47.         assertEquals(Integer.valueOf(23), personOptional.get().getAge()); 
  48.         // 查找部分字段 
  49.         String personName = personRepository.findPersonNameById(id); 
  50.         assertEquals("SnailClimb", personName); 
  51.         System.out.println(id); 
  52.         // 更新 
  53.         personRepository.updatePersonNameById("UpdatedName", id); 
  54.         Optional<Person> updatedName = personRepository.findByNameCustomeQuery("UpdatedName"); 
  55.         assertTrue(updatedName.isPresent()); 
  56.         // 清空数据库 
  57.         personRepository.deleteAll(); 
  58.     } 
  59.  

源代码地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/jpa-demo

6. 总结

本文主要介绍了 JPA 的基本用法:

使用 JPA 自带的方法进行增删改查以及条件查询。

自定义 SQL 语句进行查询或者更新数据库。

创建异步的方法。

在下一篇关于 JPA 的文章中我会介绍到非常重要的两个知识点:

基本分页功能实现

多表联合查询以及多表联合查询下的分页功能实现。

二 JPA 连表查询和分页

对于连表查询,在 JPA 中还是非常常见的,由于 JPA 可以在 respository 层自定义 SQL 语句,所以通过自定义 SQL 语句的方式实现连表还是挺简单。这篇文章是在上一篇入门 JPA的文章的基础上写的,不了解 JPA 的可以先看上一篇文章。

在上一节的基础上我们新建了两个实体类,如下:

1.相关实体类创建

  1. Company.java 
  2. @Entity 
  3. @Data 
  4. @NoArgsConstructor 
  5. public class Company { 
  6.     @Id 
  7.     @GeneratedValue(strategy = GenerationType.IDENTITY) 
  8.     private Long id; 
  9.     @Column(unique = true
  10.     private String companyName; 
  11.     private String description; 
  12.  
  13.     public Company(String name, String description) { 
  14.         this.companyName = name
  15.         this.description = description; 
  16.     } 
  17. School.java 
  18. @Entity 
  19. @Data 
  20. @NoArgsConstructor 
  21. @AllArgsConstructor 
  22. public class School { 
  23.     @Id 
  24.     @GeneratedValue(strategy = GenerationType.IDENTITY) 
  25.     private Long id; 
  26.     @Column(unique = true
  27.     private String name
  28.     private String description; 

2.自定义 SQL语句实现连表查询

假如我们当前要通过 person 表的 id 来查询 Person 的话,我们知道 Person 的信息一共分布在Company、School、Person这三张表中,所以,我们如果要把 Person 的信息都查询出来的话是需要进行连表查询的。

首先我们需要创建一个包含我们需要的 Person 信息的 DTO 对象,我们简单第将其命名为 UserDTO,用于保存和传输我们想要的信息。

  1. @Data 
  2. @NoArgsConstructor 
  3. @Builder(toBuilder = true
  4. @AllArgsConstructor 
  5. public class UserDTO { 
  6.     private String name
  7.     private int age; 
  8.     private String companyName; 
  9.     private String schoolName; 

下面我们就来写一个方法查询出 Person 的基本信息。

  1. /** 
  2.    * 连表查询 
  3.    */ 
  4.   @Query(value = "select new github.snailclimb.jpademo.model.dto.UserDTO(p.name,p.age,c.companyName,s.name) " + 
  5.           "from Person p left join Company c on  p.companyId=c.id " + 
  6.           "left join School s on p.schoolId=s.id " + 
  7.           "where p.id=:personId"
  8.   Optional<UserDTO> getUserInformation(@Param("personId") Long personId); 

可以看出上面的 sql 语句和我们平时写的没啥区别,差别比较大的就是里面有一个 new 对象的操作。

3.自定义 SQL 语句连表查询并实现分页操作

假如我们要查询当前所有的人员信息并实现分页的话,你可以按照下面这种方式来做。可以看到,为了实现分页,我们在@Query注解中还添加了 countQuery 属性。

  1. @Query(value = "select new github.snailclimb.jpademo.model.dto.UserDTO(p.name,p.age,c.companyName,s.name) " + 
  2.         "from Person p left join Company c on  p.companyId=c.id " + 
  3.         "left join School s on p.schoolId=s.id "
  4.         countQuery = "select count(p.id) " + 
  5.                 "from Person p left join Company c on  p.companyId=c.id " + 
  6.                 "left join School s on p.schoolId=s.id "
  7. Page<UserDTO> getUserInformationList(Pageable pageable); 

实际使用:

  1. //分页选项 
  2. PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC"age"); 
  3. Page<UserDTO> userInformationList = personRepository.getUserInformationList(pageRequest); 
  4. //查询结果总数 
  5. System.out.println(userInformationList.getTotalElements());// 6 
  6. //按照当前分页大小,总页数 
  7. System.out.println(userInformationList.getTotalPages());// 2 
  8. System.out.println(userInformationList.getContent()); 

4.加餐:自定以SQL语句的其他用法

下面我只介绍两种比较常用的:

IN 查询

BETWEEN 查询

当然,还有很多用法需要大家自己去实践了。

4.1 IN 查询

在 sql 语句中加入我们需要筛选出符合几个条件中的一个的情况下,可以使用 IN 查询,对应到 JPA 中也非常简单。比如下面的方法就实现了,根据名字过滤需要的人员信息。

  1. @Query(value = "select new github.snailclimb.jpademo.model.dto.UserDTO(p.name,p.age,c.companyName,s.name) " + 
  2.         "from Person p left join Company c on  p.companyId=c.id " + 
  3.         "left join School s on p.schoolId=s.id " + 
  4.         "where p.name IN :peopleList"
  5. List<UserDTO> filterUserInfo(List peopleList); 

实际使用:

List personList=new ArrayList<>(Arrays.asList("person1","person2"));List userDTOS = personRepository.filterUserInfo(personList);

4.2 BETWEEN 查询

查询满足某个范围的值。比如下面的方法就实现查询满足某个年龄范围的人员的信息。

  1. @Query(value = "select new github.snailclimb.jpademo.model.dto.UserDTO(p.name,p.age,c.companyName,s.name) " + 
  2.             "from Person p left join Company c on  p.companyId=c.id " + 
  3.             "left join School s on p.schoolId=s.id " + 
  4.             "where p.age between :small and :big"
  5.     List<UserDTO> filterUserInfoByAge(int small,int big); 

实际使用:

List userDTOS = personRepository.filterUserInfoByAge(19,20);

5.测试类和源代码地址

  1. @SpringBootTest 
  2. @RunWith(SpringRunner.class) 
  3. public class PersonRepositoryTest2 { 
  4.     @Autowired 
  5.     private PersonRepository personRepository; 
  6.  
  7.     @Sql(scripts = {"classpath:/init.sql"}) 
  8.     @Test 
  9.     public void find_person_age_older_than_18() { 
  10.         List<Person> personList = personRepository.findByAgeGreaterThan(18); 
  11.         assertEquals(1, personList.size()); 
  12.     } 
  13.  
  14.     @Sql(scripts = {"classpath:/init.sql"}) 
  15.     @Test 
  16.     public void should_get_user_info() { 
  17.         Optional<UserDTO> userInformation = personRepository.getUserInformation(1L); 
  18.         System.out.println(userInformation.get().toString()); 
  19.     } 
  20.  
  21.     @Sql(scripts = {"classpath:/init.sql"}) 
  22.     @Test 
  23.     public void should_get_user_info_list() { 
  24.         PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC"age"); 
  25.         Page<UserDTO> userInformationList = personRepository.getUserInformationList(pageRequest); 
  26.         //查询结果总数 
  27.         System.out.println(userInformationList.getTotalElements());// 6 
  28.         //按照当前分页大小,总页数 
  29.         System.out.println(userInformationList.getTotalPages());// 2 
  30.         System.out.println(userInformationList.getContent()); 
  31.     } 
  32.  
  33.     @Sql(scripts = {"classpath:/init.sql"}) 
  34.     @Test 
  35.     public void should_filter_user_info() { 
  36.         List<String> personList=new ArrayList<>(Arrays.asList("person1","person2")); 
  37.         List<UserDTO> userDTOS = personRepository.filterUserInfo(personList); 
  38.         System.out.println(userDTOS); 
  39.     } 
  40.  
  41.     @Sql(scripts = {"classpath:/init.sql"}) 
  42.     @Test 
  43.     public void should_filter_user_info_by_age() { 
  44.         List<UserDTO> userDTOS = personRepository.filterUserInfoByAge(19,20); 
  45.         System.out.println(userDTOS); 
  46.     } 

六 总结

本节我们主要学习了下面几个知识点:

自定义 SQL 语句实现连表查询;

自定义 SQL 语句连表查询并实现分页操作;

条件查询:IN 查询,BETWEEN查询。

我们这一节是把 SQl 语句连表查询的逻辑放在 Dao 层直接写的,这样写的好处是比较方便,也比较简单明了。但是可能会不太好维护,很多时候我们会选择将这些逻辑放到 Service 层去做,这样也是可以实现的,后面章我就会介绍到如何将这些写在 Dao 层的逻辑转移到 Service 层去。

代码地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/jpa-demo 

 

责任编辑:武晓燕 来源: JavaGuide
相关推荐

2023-10-21 20:50:00

Python项目PyCharm

2023-11-18 23:39:37

JavaSpringHTTP

2015-08-05 09:33:21

Javawaitnotify

2024-04-12 12:19:08

语言模型AI

2023-11-26 18:31:41

Linux信号

2021-10-25 09:00:37

Node.jsJS前端

2021-03-22 10:05:59

netstat命令Linux

2023-09-15 12:00:01

API应用程序接口

2023-09-08 08:20:46

ThreadLoca多线程工具

2021-02-22 09:44:03

KubernetesDNSLinux

2022-03-24 08:51:48

Redis互联网NoSQL

2024-02-04 16:40:11

LLM人工智能AI

2021-01-13 05:21:59

参数

2023-08-24 16:50:45

2023-04-03 15:04:00

RPCPHP语言

2020-03-18 14:00:47

MySQL分区数据库

2022-08-15 15:39:23

JavaScript面向对象数据

2023-10-16 08:16:31

Bean接口类型

2021-06-30 08:45:02

内存管理面试

2019-11-19 08:00:00

神经网络AI人工智能
点赞
收藏

51CTO技术栈公众号