SpringBoot 中 JPA 的使用

楊高超發表於2018-03-13

前言

第一次使用 Spring JPA 的時候,感覺這東西簡直就是神器,幾乎不需要寫什麼關於資料庫訪問的程式碼一個基本的 CURD 的功能就出來了。下面我們就用一個例子來講述以下 JPA 使用的基本操作。

新建專案,增加依賴

在 Intellij IDEA 裡面新建一個空的 SpringBoot 專案。具體步驟參考 SpringBoot 的第一次邂逅。根據本樣例的需求,我們要新增下面三個依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>6.0.6</version>
</dependency>
複製程式碼

準備資料庫環境

為這個專案,我們專門新建一個 springboot_jpa 的資料庫,並且給 springboot 使用者授權

create database springboot_jpa;

grant all privileges on springboot_jpa.* to 'springboot'@'%' identified by 'springboot';

flush privileges;
複製程式碼

專案配置

#通用資料來源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://10.110.2.56:3306/springboot_jpa?charset=utf8mb4&useSSL=false
spring.datasource.username=springboot
spring.datasource.password=springboot
# Hikari 資料來源專用配置
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
# JPA 相關配置
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
複製程式碼

這李前面的資料來源配置和前文《SpringBoot 中使用 JDBC Templet》中的一樣。後面的幾個配置需要解釋一下

  1. spring.jpa.show-sql=true 配置在日誌中列印出執行的 SQL 語句資訊。
  2. spring.jpa.hibernate.ddl-auto=create 配置指明在程式啟動的時候要刪除並且建立實體類對應的表。這個引數很危險,因為他會把對應的表刪除掉然後重建。所以千萬不要在生成環境中使用。只有在測試環境中,一開始初始化資料庫結構的時候才能使用一次。
  3. spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect 。在 SrpingBoot 2.0 版本中,Hibernate 建立資料表的時候,預設的資料庫儲存引擎選擇的是 MyISAM (之前好像是 InnoDB,這點比較詭異)。這個引數是在建表的時候,將預設的儲存引擎切換為 InnoDB 用的。

建立第一個資料實體類

資料庫實體類是一個 POJO Bean 物件。這裡我們先建立一個 UserDO 的資料庫實體。資料庫實體的原始碼如下

package com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * 使用者實體類
 *
 * @author 楊高超
 * @since 2018-03-12
 */
@Entity
@Table(name = "AUTH_USER")
public class UserDO {
    @Id
    private Long id;
    @Column(length = 32)
    private String name;
    @Column(length = 32)
    private String account;
    @Column(length = 64)
    private String pwd;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
}
複製程式碼

其中:

  1. @Entity 是一個必選的註解,宣告這個類對應了一個資料庫表。
  2. @Table(name = "AUTH_USER") 是一個可選的註解。宣告瞭資料庫實體對應的表資訊。包括表名稱、索引資訊等。這裡宣告這個實體類對應的表名是 AUTH_USER。如果沒有指定,則表名和實體的名稱保持一致。
  3. @Id 註解宣告瞭實體唯一標識對應的屬性。
  4. @Column(length = 32) 用來宣告實體屬性的表欄位的定義。預設的實體每個屬性都對應了表的一個欄位。欄位的名稱預設和屬性名稱保持一致(並不一定相等)。欄位的型別根據實體屬性型別自動推斷。這裡主要是宣告瞭字元欄位的長度。如果不這麼宣告,則系統會採用 255 作為該欄位的長度

以上配置全部正確,則這個時候執行這個專案,我們就可以看到日誌中如下的內容:

Hibernate: drop table if exists auth_user
Hibernate: create table auth_user (id bigint not null, account varchar(32), name varchar(32), pwd varchar(64), primary key (id)) engine=InnoDB
複製程式碼

系統自動將資料表給我們建好了。在資料庫中檢視錶及表結構

springboot_jpa 表及表結構

以上過程和我們前使用 Hibernate 的過程基本類似,無論是資料庫實體的宣告還是表的自動建立。下面我們才正式進入 Spring Data JPA 的世界,來看一看他有什麼驚豔的表現

實現一個持久層服務

在 Spring Data JPA 的世界裡,實現一個持久層的服務是一個非常簡單的事情。以上面的 UserDO 實體物件為例,我們要實現一個增加、刪除、修改、查詢功能的持久層服務,那麼我只需要宣告一個介面,這個介面繼承 org.springframework.data.repository.Repository<T, ID> 介面或者他的子介面就行。這裡為了功能的完備,我們繼承了 org.springframework.data.jpa.repository.JpaRepository<T, ID> 介面。其中 T 是資料庫實體類,ID 是資料庫實體類的主鍵。 然後再簡單的在這個介面上增加一個 @Repository 註解就結束了。

package com.yanggaochao.springboot.learn.springbootjpalearn.security.dao;


import com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao.UserDO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * 使用者服務資料介面類
 *
 * @author 楊高超
 * @since 2018-03-12
 */

@Repository
package com.yanggaochao.springboot.learn.springbootjpalearn.security.dao;


import com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao.UserDO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * 使用者服務資料介面類
 *
 * @author 楊高超
 * @since 2018-03-12
 */

@Repository
public interface UserDao extends JpaRepository<UserDO, Long> {
}
複製程式碼

一行程式碼也不用寫。那麼針對 UserDO 這個實體類,我們已經擁有下下面的功能

UserDao 儲存實體功能

UserDao 儲存實體刪除功能

UserDao 查詢實體刪除功能

例如,我們用下面的程式碼就將一些使用者實體儲存到資料庫中了。

package com.yanggaochao.springboot.learn.springbootjpalearn;

import com.yanggaochao.springboot.learn.springbootjpalearn.security.dao.UserDao;
import com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao.UserDO;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Optional;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserDOTest {

    @Autowired
    private UserDao userDao;

    @Before
    public void before() {
        UserDO userDO = new UserDO();
        userDO.setId(1L);
        userDO.setName("風清揚");
        userDO.setAccount("fengqy");
        userDO.setPwd("123456");
        userDao.save(userDO);
        userDO = new UserDO();
        userDO.setId(3L);
        userDO.setName("東方不敗");
        userDO.setAccount("bubai");
        userDO.setPwd("123456");
        userDao.save(userDO);
        userDO.setId(5L);
        userDO.setName("向問天");
        userDO.setAccount("wentian");
        userDO.setPwd("123456");
        userDao.save(userDO);
    }
    @Test
    public void testAdd() {
        UserDO userDO = new UserDO();
        userDO.setId(2L);
        userDO.setName("任我行");
        userDO.setAccount("renwox");
        userDO.setPwd("123456");
        userDao.save(userDO);
        userDO = new UserDO();
        userDO.setId(4L);
        userDO.setName("令狐沖");
        userDO.setAccount("linghuc");
        userDO.setPwd("123456");
        userDao.save(userDO);
    }

    @After
    public void after() {
        userDao.deleteById(1L);
        userDao.deleteById(3L);
        userDao.deleteById(5L);
    }

}
複製程式碼

這個是採用 Junit 來執行測試用例。@Before 註解在測試用例之前執行準備的程式碼。這裡先插入三個使用者資訊。 執行執行這個測試,完成後,檢視資料庫就可以看到資料庫中有了 5 條記錄:

資料庫記錄

我們還可以通過測試用例驗證通過標識查詢物件功能,查詢所有資料功能的正確性,查詢功能甚至可以進行排序和分頁

    @Test
    public void testLocate() {
        Optional<UserDO> userDOOptional = userDao.findById(1L);
        if (userDOOptional.isPresent()) {
            UserDO userDO = userDOOptional.get();
            System.out.println("name = " + userDO.getName());
            System.out.println("account = " + userDO.getAccount());
        }
    }

    @Test
    public void testFindAll() {
        List<UserDO> userDOList = userDao.findAll(new Sort(Sort.Direction.DESC,"account"));
        for (UserDO userDO : userDOList) {
            System.out.println("name = " + userDO.getName());
            System.out.println("account = " + userDO.getAccount());
        }
    }
複製程式碼

可以看到,我們所做的全部事情僅僅是在 SpingBoot 工程裡面增加資料庫配置資訊,宣告一個 UserDO 的資料庫實體物件,然後宣告瞭一個持久層的介面,改介面繼承自 org.springframework.data.jpa.repository.JpaRepository<T, ID> 介面。然後,系統就自動擁有了豐富的增加、刪除、修改、查詢功能。查詢功能甚至還擁有了排序和分頁的功能。

這就是 JPA 的強大之處。除了這些介面外,使用者還會有其他的一些需求, JPA 也一樣可以滿足你的需求。

擴充套件查詢

從上面的截圖 “UserDao 查詢實體刪除功能” 中,我們可以看到,查詢功能是不盡人意的,很多我們想要的查詢功能還沒有。不過放心。JPA 有非常方便和優雅的方式來解決

根據屬性來查詢

如果想要根據實體的某個屬性來進行查詢我們可以在 UserDao 介面中進行介面宣告。例如,如果我們想根據實體的 account 這個屬性來進行查詢(在登入功能的時候可能會用到)。我們在 com.yanggaochao.springboot.learn.springbootjpalearn.security.dao.UserDao 中增加一個介面宣告就可以了

  UserDO findByAccount(String account);
複製程式碼

然後增加一個測試用例

@Test
    public void testFindByAccount() {
        UserDO userDO = userDao.findByAccount("wentian");
        if (userDO != null) {
            System.out.println("name = " + userDO.getName());
            System.out.println("account = " + userDO.getAccount());
        }
    }
複製程式碼

執行之後,會在日誌中列印出

name = 向問天
account = wentian
複製程式碼

這種方式非常強大,不經能夠支援單個屬性,還能支援多個屬性組合。例如如果我們想查詢賬號和密碼同時滿足查詢條件的介面。那麼我們在 UserDao 介面中宣告

UserDO findByAccountAndPwd(String account, String pwd);
複製程式碼

再例如,我們要查詢 id 大於某個條件的使用者列表,則可以宣告如下的介面

List<UserDO> findAllByIdGreaterThan(Long id);
複製程式碼

這個語句結構可以用下面的表來說明

JPA 關鍵字說明

自定義查詢

如果上述的情況還無法滿足需要。那麼我們就可以通過通過 import org.springframework.data.jpa.repository.Query 註解來解決這個問題。例如我們想查詢名稱等於某兩個名字的所有使用者列表,則宣告如下的介面即可

@Query("SELECT O FROM UserDO O WHERE O.name = :name1  OR O.name = :name2 ")
List<UserDO> findTwoName(@Param("name1") String name1, @Param("name2") String name2);
複製程式碼

這裡是用 PQL 的語法來定義一個查詢。其中兩個引數名字有語句中的 : 後面的支付來決定

如果你習慣編寫 SQL 語句來完成查詢,還可以在用下面的方式實現

@Query(nativeQuery = true, value = "SELECT * FROM AUTH_USER WHERE name = :name1  OR name = :name2 ")
List<UserDO> findSQL(@Param("name1") String name1, @Param("name2") String name2);
複製程式碼

這裡在 @Query 註解中增加一個 nativeQuery = true 的屬性,就可以採用原生 SQL 語句的方式來編寫查詢。

聯合主鍵

從 org.springframework.data.jpa.repository.JpaRepository<T, ID> 介面定義來看,資料實體的主鍵是一個單獨的物件,那麼如果一個資料庫的表的主鍵是兩個或者兩個以上欄位聯合組成的怎麼解決呢。

我們擴充一下前面的場景。假如我們有一個角色 Role 物件,有兩個屬性 一個 id ,一個 name ,對應了 auth_role 資料表,同時有一個角色使用者關係物件 RoleUser,說明角色和使用者對應關係,有兩個屬性 roleId,userId 對應 auth_role_user 表。那麼我們需要宣告一個 RoleDO 物件如下

package com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * 角色實體類
 *
 * @author 楊高超
 * @since 2018-03-12
 */
@Entity
@Table(name = "AUTH_ROLE")
public class RoleDO {
    @Id
    private Long id;
    @Column(length = 32)
    private String name;
    @Column(length = 64)
    private String note;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}
複製程式碼

對於有多個屬性作為聯合主鍵的情況,我們一般要新建一個單獨的主鍵類,他的屬性和資料庫實體主鍵的欄位一樣,要實現 java.io.Serializable 介面,類宣告如下

package com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao;

import java.io.Serializable;

/**
 * 聯合主鍵物件
 *
 * @author 楊高超
 * @since 2018-03-12
 */
public class RoleUserId implements Serializable {
    private Long roleId;
    private Long userId;
}

複製程式碼

同樣的,我們宣告一個 RoleUserDO 物件如下

package com.yanggaochao.springboot.learn.springbootjpalearn.security.domain.dao;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import java.io.Serializable;

/**
 * 角色使用者關係實體類
 *
 * @author 楊高超
 * @since 2018-03-12
 */
@Entity
@IdClass(RoleUserId.class)
@Table(name = "AUTH_ROLE_USER")
public class RoleUserDO  {
    @Id
    private Long roleId;
    @Id
    private Long userId;

    public Long getRoleId() {
        return roleId;
    }

    public void setRoleId(Long roleId) {
        this.roleId = roleId;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }
}

複製程式碼

這裡因為資料實體類和資料實體主鍵類的屬性一樣,所以我們可以刪除掉這個資料實體主鍵類,然後將資料實體類的主鍵類宣告為自己即可。當然,自己也要實現 java.io.Serializable 介面。

這樣,我們如果要查詢某個角色下的所有使用者列表,就可以宣告如下的介面

@Query("SELECT U FROM UserDO U ,RoleUserDO RU WHERE U.id = RU.userId AND RU.roleId = :roleId")
List<UserDO> findUsersByRole(@Param("roleId") Long roleId);
複製程式碼

當然了,這種情況下,我們會看到系統自動建立了 AUTH_ROLE 和 AUTH_ROLE_USER 表。他們的表結構如下所示

auth_role 和 auth_role_user 表結構

注意這裡 auth_role_user 表中,屬性名 userId 轉換為了 user_id, roleId 轉換為了 role_id.

如果我們要用 SQL 語句的方式實現上面的功能,那麼我們就把這個介面宣告修改為下面的形式。

@Query("SELECT U.* FROM AUTH_USER U ,AUTH_ROLE_USER RU WHERE U.id = RU.user_id AND RU.role_id = :roleId")
List<UserDO> findUsersByRole(@Param("roleId") Long roleId);
複製程式碼

後記

這個樣例基本上講述了 JPA 使用過程中的一些細節。我們可以看出。使用 JPA 來完成關於關聯式資料庫增刪改查的功能是非常的方便快捷的。所有程式碼已經上傳到 github 的倉庫 springboot-jpa-learn 上了

原文發表在 簡書

相關文章