Spring Boot 正確中使用JPA

本文已經整理進 JavaGuide 開源的 springboot-guide(SpringBoot 核心知識點總結。 基於 Spring Boot 2.19+),地址:https://github.com/Snailclimb/springboot-guide 。

JPA 這部分內容上手很容易,但是涉及到的東西還是挺多的,網上大部分關於 JPA 的資料都不是特別齊全,大部分用的版本也是比較落後的。另外,我下面講到了的內容也不可能涵蓋所有 JPA 相關內容,我只是把自己覺得比較重要的知識點總結在了下面。很多地方我自己也是參考著官方文檔寫的,官方文檔非常詳細了,非常推薦閱讀一下。這篇文章可以幫助對 JPA 不瞭解或者不太熟悉的人來在實際項目中正確使用 JPA。

另外,我發現網上關於連表查詢這一塊並沒有太多比較有參考價值的博客,所以對這部分也做了詳細的總結,以供大家學習參考

項目代碼基於 Spring Boot 最新的 2.1.9.RELEASE 版本構建(截止到這篇文章寫完),另外,新建項目的過程就不多說了。

一 JPA 基礎:常見操作

1.相關依賴

我們需要下面這些依賴支持我們完成這部分內容的學習:

 <dependencies>
<dependency>

<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-web/<artifactid>
/<dependency>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-data-jpa/<artifactid>
/<dependency>
<dependency>
<groupid>mysql/<groupid>
<artifactid>mysql-connector-java/<artifactid>
<scope>runtime/<scope>
/<dependency>
<dependency>
<groupid>org.projectlombok/<groupid>
<artifactid>lombok/<artifactid>
<optional>true/<optional>
/<dependency>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-test/<artifactid>
<scope>test/<scope>
/<dependency>
/<dependencies>

2.配置數據庫連接信息和JPA配置

下面的配置中需要單獨說一下 spring.jpa.hibernate.ddl-auto=create這個配置選項。

這個屬性常用的選項有四種:

  1. create:每次重新啟動項目都會重新創新表結構,會導致數據丟失
  2. create-drop:每次啟動項目創建表結構,關閉項目刪除表結構
  3. update:每次啟動項目會更新表結構
  4. validate:驗證表結構,不對數據庫進行任何更改

但是,一定要不要在生產環境使用 ddl 自動生成表結構,一般推薦手寫 SQL 語句配合 Flyway 來做這些事情。

spring.datasource.url=jdbc:mysql://localhost:3306/springboot_jpa?useSSL=false&serverTimezone=CTT
spring.datasource.username=root
spring.datasource.password=123456
# 打印出 sql 語句
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
spring.jpa.open-in-view=false
# 創建的表的 ENGINE 為 InnoDB
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL55Dialect

3.實體類

我們為這個類添加了 @Entity 註解代表它是數據庫持久化類,還配置了主鍵 id。

import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Data
@NoArgsConstructor
public class Person {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)

private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}

如何檢驗你是否正確完成了上面 3 步?很簡單,運行項目,查看數據如果發現控制檯打印出創建表的 sql 語句,並且數據庫中表真的被創建出來的話,說明你成功完成前面 3 步。

控制檯打印出來的 sql 語句類似下面這樣:

drop table if exists person
CREATE TABLE `person` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`age` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
alter table person add constraint UK_p0wr4vfyr2lyifm8avi67mqw5 unique (name)

4.創建操作數據庫的 Repository 接口

@Repository
public interface PersonRepository extends JpaRepository<person> {
}
/<person>

首先這個接口加了 @Repository 註解,代表它和數據庫操作有關。另外,它繼承了 JpaRepository<person>接口,而JpaRepository<person>長這樣:/<person>/<person>


@NoRepositoryBean
public interface JpaRepository
extends PagingAndSortingRepository, QueryByExampleExecutor {
List findAll();
List findAll(Sort var1);
List findAllById(Iterable var1);
List saveAll(Iterable var1);
void flush();
S saveAndFlush(S var1);
void deleteInBatch(Iterable var1);
void deleteAllInBatch();
T getOne(ID var1);
List findAll(Example var1);
List findAll(Example var1, Sort var2);
}

這表明我們只要繼承了JpaRepository 就具有了 JPA 為我們提供好的增刪改查、分頁查詢以及根據條件查詢等方法。

4.1 JPA 自帶方法實戰

1) 增刪改查

1.保存用戶到數據庫

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

save()方法對應 sql 語句就是:insert into person (age, name) values (23,"snailclimb")

2.根據 id 查找用戶

 Optional<person> personOptional = personRepository.findById(id);
/<person>

findById()方法對應 sql 語句就是:select * from person p where p.id = id

3.根據 id 刪除用戶

 personRepository.deleteById(id);

deleteById()方法對應 sql 語句就是:delete from person where id=id

4.更新用戶

更新操作也要通過 save()方法來實現,比如:

 Person person = new Person("SnailClimb", 23);
Person savedPerson = personRepository.save(person);
// 更新 person 對象的姓名
savedPerson.setName("UpdatedName");
personRepository.save(savedPerson);

在這裡 save()方法相當於 sql 語句:update person set name="UpdatedName" where id=id

2) 帶條件的查詢

下面這些方法是我們根據 JPA 提供的語法自定義的,你需要將下面這些方法寫到 PersonRepository 中。

假如我們想要根據 Name 來查找 Person ,你可以這樣:

 Optional<person> findByName(String name);
/<person>

如果你想要找到年齡大於某個值的人,你可以這樣:

 List<person> findByAgeGreaterThan(int age);
/<person>

4.2 自定義 SQL 語句實戰

很多時候我們自定義 sql 語句會非常有用。

根據 name 來查找 Person:

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

Person 部分屬性查詢,避免 select *操作:

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

根據 id 更新Person name:


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

4.3 創建異步方法

如果我們需要創建異步方法的話,也比較方便。

異步方法在調用時立即返回,然後會被提交給TaskExecutor執行。當然你也可以選擇得出結果後才返回給客戶端。如果對 Spring Boot 異步編程感興趣的話可以看這篇文章:《新手也能看懂的 SpringBoot 異步編程指南》 。

@Async
Future<user> findByName(String name);
@Async
CompletableFuture<user> findByName(String name);
/<user>/<user>

5.測試類和源代碼地址

測試類:


@SpringBootTest
@RunWith(SpringRunner.class)
public class PersonRepositoryTest {
@Autowired
private PersonRepository personRepository;
private Long id;
/**
* 保存person到數據庫
*/
@Before
public void setUp() {
assertNotNull(personRepository);
Person person = new Person("SnailClimb", 23);
Person savedPerson = personRepository.saveAndFlush(person);// 更新 person 對象的姓名
savedPerson.setName("UpdatedName");
personRepository.save(savedPerson);
id = savedPerson.getId();
}
/**
* 使用 JPA 自帶的方法查找 person
*/
@Test
public void should_get_person() {
Optional<person> personOptional = personRepository.findById(id);
assertTrue(personOptional.isPresent());
assertEquals("SnailClimb", personOptional.get().getName());
assertEquals(Integer.valueOf(23), personOptional.get().getAge());
List<person> personList = personRepository.findByAgeGreaterThan(18);
assertEquals(1, personList.size());
// 清空數據庫
personRepository.deleteAll();
}
/**
* 自定義 query sql 查詢語句查找 person
*/
@Test
public void should_get_person_use_custom_query() {
// 查找所有字段
Optional<person> personOptional = personRepository.findByNameCustomeQuery("SnailClimb");
assertTrue(personOptional.isPresent());
assertEquals(Integer.valueOf(23), personOptional.get().getAge());

// 查找部分字段
String personName = personRepository.findPersonNameById(id);
assertEquals("SnailClimb", personName);
System.out.println(id);
// 更新
personRepository.updatePersonNameById("UpdatedName", id);
Optional<person> updatedName = personRepository.findByNameCustomeQuery("UpdatedName");
assertTrue(updatedName.isPresent());
// 清空數據庫
personRepository.deleteAll();
}
}
/<person>/<person>/<person>/<person>

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

6. 總結

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

  1. 使用 JPA 自帶的方法進行增刪改查以及條件查詢。
  2. 自定義 SQL 語句進行查詢或者更新數據庫。
  3. 創建異步的方法。

在下一篇關於 JPA 的文章中我會介紹到非常重要的兩個知識點:

  1. 基本分頁功能實現
  2. 多表聯合查詢以及多表聯合查詢下的分頁功能實現。

二 JPA 連表查詢和分頁

對於連表查詢,在 JPA 中還是非常常見的,由於 JPA 可以在 respository 層自定義 SQL 語句,所以通過自定義 SQL 語句的方式實現連表還是挺簡單。這篇文章是在上一篇入門 JPA的文章的基礎上寫的,不瞭解 JPA 的可以先看上一篇文章。

在上一節的基礎上我們新建了兩個實體類,如下:

1.相關實體類創建

Company.java

@Entity
@Data
@NoArgsConstructor
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String companyName;
private String description;
public Company(String name, String description) {
this.companyName = name;
this.description = description;
}
}

School.java

@Entity
@Data
@NoArgsConstructor

@AllArgsConstructor
public class School {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private String description;
}

2.自定義 SQL語句實現連表查詢

假如我們當前要通過 person 表的 id 來查詢 Person 的話,我們知道 Person 的信息一共分佈在Company、School、Person這三張表中,所以,我們如果要把 Person 的信息都查詢出來的話是需要進行連表查詢的。

首先我們需要創建一個包含我們需要的 Person 信息的 DTO 對象,我們簡單第將其命名為 UserDTO,用於保存和傳輸我們想要的信息。

@Data
@NoArgsConstructor
@Builder(toBuilder = true)
@AllArgsConstructor
public class UserDTO {
private String name;
private int age;
private String companyName;
private String schoolName;
}

下面我們就來寫一個方法查詢出 Person 的基本信息。

 /**
* 連表查詢

*/
@Query(value = "select new github.snailclimb.jpademo.model.dto.UserDTO(p.name,p.age,c.companyName,s.name) " +
"from Person p left join Company c on p.companyId=c.id " +
"left join School s on p.schoolId=s.id " +
"where p.id=:personId")
Optional<userdto> getUserInformation(@Param("personId") Long personId);
/<userdto>

可以看出上面的 sql 語句和我們平時寫的沒啥區別,差別比較大的就是裡面有一個 new 對象的操作。

3.自定義 SQL 語句連表查詢並實現分頁操作

假如我們要查詢當前所有的人員信息並實現分頁的話,你可以按照下面這種方式來做。可以看到,為了實現分頁,我們在@Query註解中還添加了 countQuery 屬性。

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

實際使用:

//分頁選項
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "age");
Page<userdto> userInformationList = personRepository.getUserInformationList(pageRequest);
//查詢結果總數
System.out.println(userInformationList.getTotalElements());// 6
//按照當前分頁大小,總頁數
System.out.println(userInformationList.getTotalPages());// 2

System.out.println(userInformationList.getContent());
/<userdto>

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

下面我只介紹兩種比較常用的:

  1. IN 查詢
  2. BETWEEN 查詢

當然,還有很多用法需要大家自己去實踐了。

4.1 IN 查詢

在 sql 語句中加入我們需要篩選出符合幾個條件中的一個的情況下,可以使用 IN 查詢,對應到 JPA 中也非常簡單。比如下面的方法就實現了,根據名字過濾需要的人員信息。

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

實際使用:

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

4.2 BETWEEN 查詢

查詢滿足某個範圍的值。比如下面的方法就實現查詢滿足某個年齡範圍的人員的信息。

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

實際使用:

List<userdto> userDTOS = personRepository.filterUserInfoByAge(19,20);
/<userdto>

5.測試類和源代碼地址

@SpringBootTest
@RunWith(SpringRunner.class)
public class PersonRepositoryTest2 {
@Autowired
private PersonRepository personRepository;
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void find_person_age_older_than_18() {
List<person> personList = personRepository.findByAgeGreaterThan(18);
assertEquals(1, personList.size());
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_get_user_info() {
Optional<userdto> userInformation = personRepository.getUserInformation(1L);
System.out.println(userInformation.get().toString());
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_get_user_info_list() {
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "age");
Page<userdto> userInformationList = personRepository.getUserInformationList(pageRequest);
//查詢結果總數
System.out.println(userInformationList.getTotalElements());// 6
//按照當前分頁大小,總頁數

System.out.println(userInformationList.getTotalPages());// 2
System.out.println(userInformationList.getContent());
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_filter_user_info() {
List<string> personList=new ArrayList<>(Arrays.asList("person1","person2"));
List<userdto> userDTOS = personRepository.filterUserInfo(personList);
System.out.println(userDTOS);
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_filter_user_info_by_age() {
List<userdto> userDTOS = personRepository.filterUserInfoByAge(19,20);
System.out.println(userDTOS);
}
}
/<userdto>/<userdto>/<string>/<userdto>/<userdto>/<person>

六 總結

本節我們主要學習了下面幾個知識點:

  1. 自定義 SQL 語句實現連表查詢;
  2. 自定義 SQL 語句連表查詢並實現分頁操作;
  3. 條件查詢:IN 查詢,BETWEEN查詢。

我們這一節是把 SQl 語句連表查詢的邏輯放在 Dao 層直接寫的,這樣寫的好處是比較方便,也比較簡單明瞭。但是可能會不太好維護,很多時候我們會選擇將這些邏輯放到 Service 層去做,這樣也是可以實現的,後面章我就會介紹到如何將這些寫在 Dao 層的邏輯轉移到 Service 層去。

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


分享到:


相關文章: