一文搞懂如何在Spring Boot中正確使用JPA
JPA 這部分內(nèi)容上手很容易,但是涉及到的東西還是挺多的,網(wǎng)上大部分關(guān)于 JPA 的資料都不是特別齊全,大部分用的版本也是比較落后的。另外,我下面講到了的內(nèi)容也不可能涵蓋所有 JPA 相關(guān)內(nèi)容,我只是把自己覺得比較重要的知識(shí)點(diǎn)總結(jié)在了下面。很多地方我自己也是參考著官方文檔寫的,官方文檔非常詳細(xì)了,非常推薦閱讀一下。這篇文章可以幫助對(duì) JPA 不了解或者不太熟悉的人來在實(shí)際項(xiàng)目中正確使用 JPA。
另外,我發(fā)現(xiàn)網(wǎng)上關(guān)于連表查詢這一塊并沒有太多比較有參考價(jià)值的博客,所以對(duì)這部分也做了詳細(xì)的總結(jié),以供大家學(xué)習(xí)參考。
項(xiàng)目代碼基于 Spring Boot 最新的 2.1.9.RELEASE 版本構(gòu)建(截止到這篇文章寫完),另外,新建項(xiàng)目的過程就不多說了。
一 JPA 基礎(chǔ):常見操作
1.相關(guān)依賴
我們需要下面這些依賴支持我們完成這部分內(nèi)容的學(xué)習(xí):
- <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.配置數(shù)據(jù)庫連接信息和JPA配置
下面的配置中需要單獨(dú)說一下 spring.jpa.hibernate.ddl-auto=create這個(gè)配置選項(xiàng)。
這個(gè)屬性常用的選項(xiàng)有四種:
- create:每次重新啟動(dòng)項(xiàng)目都會(huì)重新創(chuàng)新表結(jié)構(gòu),會(huì)導(dǎo)致數(shù)據(jù)丟失
- create-drop:每次啟動(dòng)項(xiàng)目創(chuàng)建表結(jié)構(gòu),關(guān)閉項(xiàng)目刪除表結(jié)構(gòu)
- update:每次啟動(dòng)項(xiàng)目會(huì)更新表結(jié)構(gòu)
- validate:驗(yàn)證表結(jié)構(gòu),不對(duì)數(shù)據(jù)庫進(jìn)行任何更改
但是,一定要不要在生產(chǎn)環(huán)境使用 ddl 自動(dòng)生成表結(jié)構(gòu),一般推薦手寫 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
- # 創(chuàng)建的表的 ENGINE 為 InnoDB
- spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL55Dialect
3.實(shí)體類
我們?yōu)檫@個(gè)類添加了 @Entity 注解代表它是數(shù)據(jù)庫持久化類,還配置了主鍵 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;
- }
- }
如何檢驗(yàn)?zāi)闶欠裾_完成了上面 3 步?很簡(jiǎn)單,運(yùn)行項(xiàng)目,查看數(shù)據(jù)如果發(fā)現(xiàn)控制臺(tái)打印出創(chuàng)建表的 sql 語句,并且數(shù)據(jù)庫中表真的被創(chuàng)建出來的話,說明你成功完成前面 3 步。
控制臺(tái)打印出來的 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.創(chuàng)建操作數(shù)據(jù)庫的 Repository 接口
- @Repository
- public interface PersonRepository extends JpaRepository<Person, Long> {
- }
首先這個(gè)接口加了 @Repository 注解,代表它和數(shù)據(jù)庫操作有關(guān)。另外,它繼承了 JpaRepository接口,而JpaRepository長(zhǎng)這樣:
- @NoRepositoryBean
- public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
- List<T> findAll();
- List<T> findAll(Sort var1);
- List<T> findAllById(Iterable<ID> var1);
- <S extends T> List<S> saveAll(Iterable<S> var1);
- void flush();
- <S extends T> S saveAndFlush(S var1);
- void deleteInBatch(Iterable<T> var1);
- void deleteAllInBatch();
- T getOne(ID var1);
- <S extends T> List<S> findAll(Example<S> var1);
- <S extends T> List<S> findAll(Example<S> var1, Sort var2);
- }
這表明我們只要繼承了JpaRepository 就具有了 JPA 為我們提供好的增刪改查、分頁查詢以及根據(jù)條件查詢等方法。
4.1 JPA 自帶方法實(shí)戰(zhàn)
1) 增刪改查
1.保存用戶到數(shù)據(jù)庫
- Person person = new Person("SnailClimb", 23);
- personRepository.save(person);
save()方法對(duì)應(yīng) sql 語句就是:insert into person (age, name) values (23,"snailclimb")
2.根據(jù) id 查找用戶
- Optional<Person> personOptional = personRepository.findById(id);
findById()方法對(duì)應(yīng) sql 語句就是:select * from person p where p.id = id
3.根據(jù) id 刪除用戶
- personRepository.deleteById(id);
deleteById()方法對(duì)應(yīng) sql 語句就是:delete from person where id=id
4.更新用戶
更新操作也要通過 save()方法來實(shí)現(xiàn),比如:
- Person person = new Person("SnailClimb", 23);
- Person savedPerson = personRepository.save(person);
- // 更新 person 對(duì)象的姓名
- savedPerson.setName("UpdatedName");
- personRepository.save(savedPerson);
在這里 save()方法相當(dāng)于 sql 語句:update person set name="UpdatedName" where id=id
2) 帶條件的查詢
下面這些方法是我們根據(jù) JPA 提供的語法自定義的,你需要將下面這些方法寫到 PersonRepository 中。
假如我們想要根據(jù) Name 來查找 Person ,你可以這樣:
- Optional<Person> findByName(String name);
如果你想要找到年齡大于某個(gè)值的人,你可以這樣:
- List<Person> findByAgeGreaterThan(int age);
4.2 自定義 SQL 語句實(shí)戰(zhàn)
很多時(shí)候我們自定義 sql 語句會(huì)非常有用。
根據(jù) name 來查找 Person:
- @Query("select p from Person p where p.name = :name")
- Optional<Person> findByNameCustomeQuery(@Param("name") String name);
Person 部分屬性查詢,避免 select *操作:
- @Query("select p.name from Person p where p.id = :id")
- String findPersonNameById(@Param("id") Long id);
根據(jù) 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 創(chuàng)建異步方法
如果我們需要?jiǎng)?chuàng)建異步方法的話,也比較方便。
異步方法在調(diào)用時(shí)立即返回,然后會(huì)被提交給TaskExecutor執(zhí)行。當(dāng)然你也可以選擇得出結(jié)果后才返回給客戶端。如果對(duì) Spring Boot 異步編程感興趣的話可以看這篇文章:《新手也能看懂的 SpringBoot 異步編程指南》 。
- @Async
- Future<User> findByName(String name);
- @Async
- CompletableFuture<User> findByName(String name);
5.測(cè)試類和源代碼地址
測(cè)試類:
- @SpringBootTest
- @RunWith(SpringRunner.class)
- public class PersonRepositoryTest {
- @Autowired
- private PersonRepository personRepository;
- private Long id;
- /**
- * 保存person到數(shù)據(jù)庫
- */
- @Before
- public void setUp() {
- assertNotNull(personRepository);
- Person person = new Person("SnailClimb", 23);
- Person savedPerson = personRepository.saveAndFlush(person);// 更新 person 對(duì)象的姓名
- 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());
- // 清空數(shù)據(jù)庫
- 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());
- // 清空數(shù)據(jù)庫
- personRepository.deleteAll();
- }
- }
源代碼地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/jpa-demo
6. 總結(jié)
本文主要介紹了 JPA 的基本用法:
使用 JPA 自帶的方法進(jìn)行增刪改查以及條件查詢。
自定義 SQL 語句進(jìn)行查詢或者更新數(shù)據(jù)庫。
創(chuàng)建異步的方法。
在下一篇關(guān)于 JPA 的文章中我會(huì)介紹到非常重要的兩個(gè)知識(shí)點(diǎn):
基本分頁功能實(shí)現(xiàn)
多表聯(lián)合查詢以及多表聯(lián)合查詢下的分頁功能實(shí)現(xiàn)。
二 JPA 連表查詢和分頁
對(duì)于連表查詢,在 JPA 中還是非常常見的,由于 JPA 可以在 respository 層自定義 SQL 語句,所以通過自定義 SQL 語句的方式實(shí)現(xiàn)連表還是挺簡(jiǎn)單。這篇文章是在上一篇入門 JPA的文章的基礎(chǔ)上寫的,不了解 JPA 的可以先看上一篇文章。
在上一節(jié)的基礎(chǔ)上我們新建了兩個(gè)實(shí)體類,如下:
1.相關(guān)實(shí)體類創(chuàng)建
- 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語句實(shí)現(xiàn)連表查詢
假如我們當(dāng)前要通過 person 表的 id 來查詢 Person 的話,我們知道 Person 的信息一共分布在Company、School、Person這三張表中,所以,我們?nèi)绻?Person 的信息都查詢出來的話是需要進(jìn)行連表查詢的。
首先我們需要?jiǎng)?chuàng)建一個(gè)包含我們需要的 Person 信息的 DTO 對(duì)象,我們簡(jiǎn)單第將其命名為 UserDTO,用于保存和傳輸我們想要的信息。
- @Data
- @NoArgsConstructor
- @Builder(toBuilder = true)
- @AllArgsConstructor
- public class UserDTO {
- private String name;
- private int age;
- private String companyName;
- private String schoolName;
- }
下面我們就來寫一個(gè)方法查詢出 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);
可以看出上面的 sql 語句和我們平時(shí)寫的沒啥區(qū)別,差別比較大的就是里面有一個(gè) new 對(duì)象的操作。
3.自定義 SQL 語句連表查詢并實(shí)現(xiàn)分頁操作
假如我們要查詢當(dāng)前所有的人員信息并實(shí)現(xiàn)分頁的話,你可以按照下面這種方式來做。可以看到,為了實(shí)現(xiàn)分頁,我們?cè)贎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);
實(shí)際使用:
- //分頁選項(xiàng)
- PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "age");
- Page<UserDTO> userInformationList = personRepository.getUserInformationList(pageRequest);
- //查詢結(jié)果總數(shù)
- System.out.println(userInformationList.getTotalElements());// 6
- //按照當(dāng)前分頁大小,總頁數(shù)
- System.out.println(userInformationList.getTotalPages());// 2
- System.out.println(userInformationList.getContent());
4.加餐:自定以SQL語句的其他用法
下面我只介紹兩種比較常用的:
IN 查詢
BETWEEN 查詢
當(dāng)然,還有很多用法需要大家自己去實(shí)踐了。
4.1 IN 查詢
在 sql 語句中加入我們需要篩選出符合幾個(gè)條件中的一個(gè)的情況下,可以使用 IN 查詢,對(duì)應(yīng)到 JPA 中也非常簡(jiǎn)單。比如下面的方法就實(shí)現(xiàn)了,根據(jù)名字過濾需要的人員信息。
- @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);
實(shí)際使用:
List personList=new ArrayList<>(Arrays.asList("person1","person2"));List userDTOS = personRepository.filterUserInfo(personList);
4.2 BETWEEN 查詢
查詢滿足某個(gè)范圍的值。比如下面的方法就實(shí)現(xiàn)查詢滿足某個(gè)年齡范圍的人員的信息。
- @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);
實(shí)際使用:
List userDTOS = personRepository.filterUserInfoByAge(19,20);
5.測(cè)試類和源代碼地址
- @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);
- //查詢結(jié)果總數(shù)
- System.out.println(userInformationList.getTotalElements());// 6
- //按照當(dāng)前分頁大小,總頁數(shù)
- 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);
- }
- }
六 總結(jié)
本節(jié)我們主要學(xué)習(xí)了下面幾個(gè)知識(shí)點(diǎn):
自定義 SQL 語句實(shí)現(xiàn)連表查詢;
自定義 SQL 語句連表查詢并實(shí)現(xiàn)分頁操作;
條件查詢:IN 查詢,BETWEEN查詢。
我們這一節(jié)是把 SQl 語句連表查詢的邏輯放在 Dao 層直接寫的,這樣寫的好處是比較方便,也比較簡(jiǎn)單明了。但是可能會(huì)不太好維護(hù),很多時(shí)候我們會(huì)選擇將這些邏輯放到 Service 層去做,這樣也是可以實(shí)現(xiàn)的,后面章我就會(huì)介紹到如何將這些寫在 Dao 層的邏輯轉(zhuǎn)移到 Service 層去。
代碼地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/jpa-demo