詳述 Spring Data JPA 的那些事兒

2021-03-02 Java後端
jpa 的全稱是 Java Persistence API  , 中文的字面意思就是 java 的持久層 API , jpa 就是定義了一系列標準,讓實體類和資料庫中的表建立一個對應的關係,當我們在使用 java 操作實體類的時候能達到操作資料庫中表的效果(不用寫sql ,就可以達到效果),jpa 的實現思想即是 ORM (Object Relation Mapping),對象關係映射,用於在關係型資料庫和業務實體對象之間作一個映射。jpa 並不是一個框架,是一類框架的總稱,持久層框架 Hibernate 是 jpa 的一個具體實現,本文要談的 spring data jpa  又是在 Hibernate 的基礎之上的封裝實現。當我們項目中使用 spring data jpa 的時候,你會發現有時候沒有 sql 語句,其實框架的底層已經幫我們實現了,我們只需要遵守規範使用就可以了,下面會詳細談到 spring data jpa 的各種規範細則。使用 jpa 是可以解決一些我們寫 sql 語句的煩惱,但是搞開發的如果 sql 寫不好,還是很頭疼的。當然本文並不是捧吹 spring data jpa , 另一個資料庫層的框架 mybatis 也是十分優秀的框架,該框架是專注 sql 語句的.spring data jpa常用的 jpa 的配置下面所有演示的代碼均來自我個人 github 的 spring-data-jpa 倉庫,倉庫地址:https://github.com/kickcodeman/spring-data-jpa, 讀者可以clone 下來運行本項目,驗證下面講的所有知識點。下面把spring boot 項目關於 jpa 的常用配置 application.properties 配置如下:

#項目埠的常用配置
server.port=8081

# 資料庫連接的配置
spring.datasource.url=jdbc:mysql:///jpa?useSSL=false
spring.datasource.username=root
spring.datasource.password=zempty123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

#資料庫連接池的配置,hikari 連接池的配置
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.auto-commit=true


#通過 jpa 自動生成資料庫中的表
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

下面重點分析一下 jpa 中的三個配置 :
spring.jpa.hibernate.ddl-auto=update
該配置比較常用,當服務首次啟動會在資料庫中生成相應表,後續啟動服務時如果實體類有增加屬性會在數據中添加相應欄位,原來數據仍在,該配置除了 update ,還有其他配置值,
create  :該值慎用,每次重啟項目的時候都會刪除表結構,重新生成,原來數據會丟失不見。
create-drop :慎用,當項目關閉,資料庫中的表會被刪掉。
validate :驗證資料庫和實體類的屬性是否匹配,不匹配將會報錯。
綜上:個人感覺還是使用 update 較為穩妥。spring.jpa.show-sql=true
該配置當在執行資料庫操作的時候會在控制臺列印 sql 語句,方便我們檢查排錯等。spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
資料庫的方言配置。類映射到資料庫表的常用註解分析spring data jpa 提供了很多註解,下面我們把日常常用註解總結如下:@Entity 是一個類註解,用來註解該類是一個實體類用來進行和資料庫中的表建立關聯關係,首次啟動項目的時候,默認會在數據中生成一個同實體類相同名字的表(table),也可以通過註解中的 name 屬性來修改表(table)名稱, 如@Entity(name=「stu」) , 這樣資料庫中表的名稱則是 stu 。該註解十分重要,如果沒有該註解首次啟動項目的時候你會發現資料庫沒有生成對應的表。@Table 註解也是一個類註解,該註解可以用來修改表的名字,該註解完全可以忽略掉不用,@Entity 註解已具備該註解的功能。@Id 類的屬性註解,該註解表明該屬性欄位是一個主鍵,該屬性必須具備,不可缺少。@GeneratedValue 該註解通常和 @Id 主鍵註解一起使用,用來定義主鍵的呈現形式,該註解通常有多種使用策略,先總結如下:@GeneratedValue(strategy= GenerationType.IDENTITY) 該註解由資料庫自動生成,主鍵自增型,在 mysql 資料庫中使用最頻繁,oracle 不支持。@GeneratedValue(strategy= GenerationType.AUTO)  主鍵由程序控制,默認的主鍵生成策略,oracle 默認是序列化的方式,mysql 默認是主鍵自增的方式。@GeneratedValue(strategy= GenerationType.SEQUENCE) 根據底層資料庫的序列來生成主鍵,條件是資料庫支持序列,Oracle支持,Mysql不支持。@GeneratedValue(strategy= GenerationType.TABLE) 使用一個特定的資料庫表格來保存主鍵,較少使用。以上的主鍵生成策略當中,在資料庫 mysql 當中 IDENTITY  和 AUTO 用的較多,二者當中 IDENTITY 用的多些,以下文章當中演示的 demo 主鍵均使用 @GeneratedValue(strategy= GenerationType.IDENTITY) 的生成策略。@Column 是一個類的屬性註解,該註解可以定義一個欄位映射到資料庫屬性的具體特徵,比如欄位長度,映射到資料庫時屬性的具體名字等。@Transient  是一個屬性註解,該註解標註的欄位不會被應射到資料庫當中。以上使用的註解是定義一個實體類的常用註解,通過上述的註解我們就可以通過實體類生成資料庫中的表,實體類和表建立一個對應的關係,下面貼出一個實體類的定義 demo :

@Setter
@Getter
@Accessors(chain = true)
@Entity(name = "stu")
//@Table
public class Student {

    @Id
    @GeneratedValue(strategy= GenerationType.TABLE)
    private long id;    @Column(length = 100)
    private String name;

    @Transient
    private String test;
    private int age;
    private LocalTime onDuty;
    private LocalDate onPosition;
    private LocalDateTime birthdayTime;
}

該實體類中的 @Setter ,@Getter, @Accessors(chain =true) 均是 lombok 的註解,不了解的建議了解一下 lombok。
使用上述實體類的註解,當運行項目的時候就會在資料庫中生成一個表名是 stu 的表。類的繼承分析下面來研究一下類之間存在繼承關係的時候,jpa 又是如何處理繼承關係的呢?這個是很值得了解清楚的,這個搞明白了我們在使用 spring data jpa 的時候可能會事半功倍。多類一表:多個類之間的屬性相同,唯一的區別就是類型上的差異(類名不同),這個時候我們可以為這個共同屬性的類建立一個父類,只讓父類應射到資料庫。多類多表:把多個類之間公有的屬性提取出來放在它們公有的父類中,各個類之間可以定義自己特有的屬性,子類和父類在資料庫中都有相應的表和其對應。子類建表:把多個類之間公有的屬性提取出來放在它們公有的父類中,各個類之間可以定義自己特有的屬性,僅僅子類和資料庫中的表建立關聯關係,父類中的屬性延續到每一個子類中,在資料庫中每一個子類對應的表都有父類中定義的屬性。jpa 是如何處理上述的三種情況呢?通過一個註解:@Inheritance 該註解僅使用在父類當中,該註解有三種策略分別對應上述的三種情況,該部分可以參考本人github 倉庫 https://github.com/kickcodeman/spring-data-jpa 測試了解 :@Inheritance(strategy = InheritanceType.SINGLE_TABLE) 該註解從字面來理解即可大致看出含義,只生成一個 table。現在先給出結論:該註解的使用場景是幾個實體類的屬性大致相同,沒有什麼區別,唯一區別的可能也就是類名了,這樣的話我們可以考慮使用該註解,使用該註解的話我們多個實體類公用一個table ,該表由父類生成,父類中默認會生成一個 dtype 欄位,用來表明該條數據是屬於哪一個實體類的數據。詳細使用可以參考項目包com.zempty.springbootjpa.entity.inheritance.single_table 中的三個類,A1,B1, Group1 三個類的使用,類中的 Group1 是 A1 和 B1 的子類,A1 和 B1 中通常會使用如下的一個註解:@DiscriminatorValue 該註解只有一個 value 值用來標註在插入數據的時候 dtype 欄位的值。在包 com.zempty.springbootjpa. inheritance. controller 中的 SingleController 有幾個詳細的測試案例,可以運行項目,測試幾個接口,查看一下資料庫查看使用細則。@Inheritance(strategy = InheritanceType.JOINED)該註解使用後,會生成多張表。現在先給出結論性總結如下:當有一個這樣的需求,一些屬性是多數類都有的,比如,username,password … ,那麼我們可以考慮把共有的屬性給提取出來,單獨做成一個表,類中特殊屬性定義在各自的類中。詳細使用可以參項目包 com.zempty. springbootjpa. entity.inheritance.joined 中的三個類 A2, B2, Group2 ,三個類的使用, Group2 是 A2 和 B2 的類,該案例將會把三個實體類都生成各自的表,當我們在添加 A2 或者 B2 數據進入資料庫的時候 ,Group2 對用也會相應的添加一條數據, 子類中有一個註解 @PrimaryKeyJoinColumn 可以用來定義一個子類生成表的主鍵的名字,如果沒有默認使用 id。 在包 com.zempty .springbootjpa. inheritance. controller 中的 JoinedController 中有幾個測試案例,運行項目,即可查看感受一下使用細則。@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)只有子類生成 table 。現在先給出一個結論:父類中的屬性是共有屬性,父類不會生成 table ,子類定義自己特有的屬性,子類生成的 table 會有父類中定義的屬性欄位。這裡需要注意使用該註解的時候父類中的主鍵生成策略不可以是@GeneratedValue(strategy = GenerationType.IDENTITY) ,這裡我定義成了 @GeneratedValue(strategy = GenerationType.AUTO) ,否則會報錯。詳細使用可以參考包 com.zempty .springbootjpa..entity.inheritance.per_table 中的三個類 A3,B3,Group3 的註解使用,Group3 是 A3 和 B3 的 父類,該案例,Group3 將不會被生成 table,但是其中的屬性將會出現在每一個子類生成的 table 當中。在包 com.zempty .springbootjpa. inheritance. controller 中的 PerController 中有幾個測試案例,運行項目,即可查看感受一下使用細則。還有一個註解在繼承這裡需要提到一下,那就是 @MappedSuperclass 這個註解,該註解也是使用在父類當中,父類不需要使用 @Entity 註解,該註解效果是同上面提到的第三種 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)  使用類似,這裡就不再過多解釋了。類之間的關係分析在資料庫當中表和表之間都是有一定的關聯關係的,jpa 是如何在實體類之間建立和資料庫表中類似的關聯關係呢?jpa 是通過一系列的註解來實現類之間的關聯關係的,下面我們就來透徹的分析一下如何使用註解來表明類之間的關係,類之間的關係大致可以有一下幾種情況:一對一的關係,jpa 使用的註解是 @OneToOne一對多的關係,jpa 使用的註解是 @OneToMany多對一的關係,jpa 使用的註解是 @ManyToOne多對多的關係,jpa 使用的註解是 @ManyToMany在使用 jpa 的時候,這幾個註解應該如何使用呢?下面結合實例進行展示說明,建議克隆遠程的倉庫的 https://github.com/kickcodeman/spring-data-jpa ,案例代碼都在倉庫中可見。一個學生通常只有一個課桌,一個課桌通常給一個學生作,這裡學生和課桌的關係就是互為 @OneToOne一個教室通常可以容納很多的學生,教室到學生的關係就可以定義為 @OneToMany很多學生容納在一個教室當中,學生到教室的關係可以定義為@ManyToOne一個學生可以有很多的老師,一個老師可以有很多的學生,這裡學生和老師的關係就互為 @ManyToMany在 java 的實體類當中應該如何描述上述關係呢?Talk is simple ,now let me show code:@OneToOne

學生實體類如下:
@Setter
@Getter
@Accessors(chain = true)
@Entity(name = "stu")
public class Student {
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private long id;

    @Column(length = 100)
    private String name;

// 這裡是定義學生和課桌的一對一的關係
    @OneToOne
// 下面的這個註解用來生成第三張表,來維護學生和課桌的關係
    @JoinTable( name = "stu_desk",joinColumns = @JoinColumn(name="student_id"),inverseJoinColumns = @JoinColumn(name="desk_id") )
    private Desk desk;

    @Transient
    private String test;
    private int age;
    private LocalTime onDuty;
    private LocalDate onPosition;
    private LocalDateTime birthdayTime;
}

@Setter
@Getter
@Accessors(chain = true)
@Entity
public class Desk {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private Integer deskNum;
    @OneToOne(mappedBy = "desk")
    private Student student;
}

上述的兩個實體類展示了一對一的關聯關係,彼此實體類中互相關聯彼此,這裡有一點需要提出:在一對一的關係維護中通常需要一個第三張表來維護這個關聯關係,在 Student 類中定義了一個 @JoinTable 註解 ,該註解是用來生成第三張表的,如果沒有該註解,就不會有第三張表,僅僅只是在 stu 表中生成一個外鍵
desk_id 用來維護關係。 在 Desk 類,@OneToOne 註解中有一個 mappedBy = 「desk」 的屬性,該欄位說明  Desk 類放棄主鍵的維護,關於 mappedBy 這個屬性下文也會重點談到。@OneToMany

@Setter
@Getter
@Accessors(chain = true)
@Entity(name="class_room")
public class ClassRoom {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    @OneToMany(mappedBy = "classRoom")
    private Set<Student> students;
}

使用 @OneToMany 的時候通常會緊跟 mappedBy 屬性,因為一的一方通常是不需要維護主鍵的,主鍵在 @ManyToOne 的一方。@ManyToOne

    @ManyToOne
    private ClassRoom classRoom;

一對多和多對一的關係維護中,通常在多的一方進行外鍵的維護,運行程序我們會發現在 stu 表中新增了一個 class_room_id 的外鍵。@ManyToMany

@Setter
@Getter
@Accessors(chain = true)
@Entity
public class Teacher {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private String subject;

    @ManyToMany(mappedBy = "teachers")
    private Set<Student> students;
}

該 Teacher 類的 @ManyToMany 的屬性使用了 mappedBy 表示該類放棄主鍵的維護,如果沒有該屬性會產生一個多餘的表 teacher_students 表,通常我們會在對多多的其中一方添加一個 mappedBy 屬性,避免多餘的表的產生 。

   @ManyToMany
    @JoinTable(name="stu_teacher",joinColumns = @JoinColumn(name ="stu_id"),inverseJoinColumns = @JoinColumn(name="teacher_id"))
   private Set<Teacher> teachers;

在多對多的關係維護中,通常是需要第三種表去維護彼此的關係,這裡使用了 @JoinTable 註解來定義第三種表的表名,屬性欄位等等。小結:類和類之間的關係在 jpa 中很重要,務必要搞清楚,以上展示代碼可參考個人倉庫  https://github.com/kickcodeman/spring-data-jpa,可以 clone 下來,詳細的分析一下應該如何使用上面所述的四種關係。進一步剖析級聯操作(cascade)這裡級聯可能不好理解,你可以理解成關聯,我在操作 Student 類的增刪改查的時候, Student 類的關聯類會受到相應的影響。在使用 spring data jpa 進行增刪改查的時候一定要弄清彼此的級聯關係,這很重要,很重要,很重要。。。在上面講解的四種類之間的關係的時候,四個關係註解 @OneToMany , @ManyToOne, @OneToOne , @ManyToMany 中都有一個屬性叫 cascade 該屬性值是一個 CascadeType 的數組,下面來重點解釋和分析各種 CascadeType, 沒有使用 cascade ,默認是 default, 就是不存在級聯。CascadeType.PERSIST 該級聯是級聯保存。CascadeType.MERGE  該級聯是級聯更新CascadeType.REMOVE 該級聯是級聯刪除CascadeType.REFRESH 該級聯是級聯刷新(不常用)CascadeType.DETACH  該級聯是級聯託管(不常用)CascadeType.ALL 具有上述五個級聯的功能下面我們用實例來說明上述的常用級聯(保存,更新,刪除)的使用情況: 以下展示的案例代碼均在 https://github.com/kickcodeman/spring-data-jpa 可見,如果你使用的是 idea 運行項目,resource 文件夾下面有 test.http 測試接口文件,裡面有案例展示的所有接口可以運行測試。保存學生的時候,級聯保存課桌

 // 設置級聯保存,保存學生的時候也會保存課桌
    @OneToOne(cascade = CascadeType.PERSIST)
    @JoinTable( name = "stu_desk",joinColumns = @JoinColumn(name="student_id"),inverseJoinColumns = @JoinColumn(name="desk_id") )
    private Desk desk;

  @PostMapping("/save_student")
    public Student saveStudent() {
        Student student = new Student()
                .setAge(19)
                .setName("zempty");

        Desk desk = new Desk()
                .setDeskNum(10);
// 學生分配座位,如果設置了級聯保存,保存學生的時候也會保存座位,如果沒設置級聯保存,添加課桌會報錯
        student.setDesk(desk);
        return studentRepository.save(student);
    }

從上面的代碼可以看到在保存 Student 的時候,Desk 也保存到了資料庫,這就是級聯保存的妙用。更新學生,級聯更新教室數據

    //設置級聯更新,在跟新 student 的時候如果更新 classroom , 會級聯更新 classroom
    @ManyToOne(cascade = CascadeType.MERGE)
    private ClassRoom classRoom;

   @PutMapping("/update_student/{id}")
    public Student updateStudent(@PathVariable("id") Integer id) {
        Optional<Student> optional = studentRepository.findById(id);
        optional.ifPresent(student->{
            student.setName("zempty_zhao");

            ClassRoom room = student.getClassRoom();
            room.setName("IT 666999");
            studentRepository.save(student);
        });
        return optional.get();
    }

該案例展示了在跟新 Student  的時候順便更新了 ClassRoom 。刪除學生,把老師也刪除了

    //設置級聯刪除操作,這是多對對的級聯刪除,
    // 刪除學生的同時會刪除關聯的老師,如果老師還有關聯其他的學生,就會報錯,除非老師類也要級聯刪除,這個刪除是要慎重的
    @ManyToMany(cascade = {CascadeType.REMOVE})
    @JoinTable(name="stu_teacher",joinColumns = @JoinColumn(name = "stu_id"),inverseJoinColumns = @JoinColumn(name="teacher_id"))
    private Set<Teacher> teachers;

@DeleteMapping("/delete_student/{id}")
public void deleteStudent(@PathVariable("id") Integer id) {
    Optional<Student> optional = studentRepository.findById(id);
    optional.ifPresent(student -> {
        studentRepository.delete(student);
    });
}

運行測試,你會發現刪除學生的同時把老師也給刪除了,這個很容易出問題,可能會出現如下的兩個問題:如果老師那裡也配置了級聯刪除,刪除老師的同時,老師的關聯學生都會連帶刪除。
如果不清楚級聯刪除的功能,可能會造成很嚴重的後果,建議讀者一定反覆測試該案例弄清楚級聯刪除的使用。教室裡有學生,如何刪除教室如果資料庫中教室和學生存在綁定關係,如果刪除這個教室就會出現問題,無法正常刪除因為存在外鍵,如何解決這個問題呢?當刪除數據的時候,如果該數據存在外鍵是無法直接刪除的,這是在日常使用當中很容易遇到的一個問題,現在就這個問題給出一些解決方案:

    @OneToMany(mappedBy = "classRoom")
    @JsonIgnore
    private Set<Student> students;

ClassRoom 類和學生的關係是一對多,我們使用了 mappedBy 屬性,表示該類放棄主鍵的維護,由學生類來進行主鍵的維護,直接刪除學生是無法清掉主鍵的,必須找到該教師的所有學生,把學生的教師置空,然後在刪除教師,關鍵性代碼如下所示:

   @DeleteMapping("delete_room/{id}")
    public void deleteClassRoom(@PathVariable("id") Integer id) {
        Optional<ClassRoom> optional= classRoomRepository.findById(id);
        optional.ifPresent(classRoom ->{
            // 先找到所有的學生,把教室置空,然後刪除教室
            Set<Student> students = classRoom.getStudents();
            students.forEach(student -> student.setClassRoom(null));
            classRoomRepository.delete(classRoom);
        });;
    }

下面講一下另一種解決辦法,該方法不推薦:
教室類在@OneToMany 中添加一個屬性 orphanRemoval = true  
教師類關鍵代碼如下所示:

   @OneToMany(mappedBy = "classRoom",orphanRemoval = true)
    @JsonIgnore
    private Set<Student> students;

下面改掉刪除的測試代碼 ,注釋掉學生部分代碼,直接刪除教室,如下所示:

 @DeleteMapping("delete_room/{id}")
    public void deleteClassRoom(@PathVariable("id") Integer id) {
        Optional<ClassRoom> optional= classRoomRepository.findById(id);
        optional.ifPresent(classRoom ->{
            // 先找到所有的學生,把教室置空,然後刪除教室
//            Set<Student> students = classRoom.getStudents();
//            students.forEach(student -> student.setClassRoom(null));
            classRoomRepository.delete(classRoom);
        });;
    }

這個刪除效果是可以把教室刪除的,但是你你查詢資料庫的時候你會發現,教室內的學生都不見了,學生也跟著一起刪除了,這個使用時一定要慎重的。orphanRemoval 使用細則orphanRemoval 這個屬性只存在兩類關係註解中 @OneToOne 和 @OneToManyjpa 為什麼把這個註解僅僅只放在這兩個關係類註解中呢?
個人分析是使用 @OneToOne 和 @OneToMany 的實體類是存在外鍵的,操作存在外鍵的類,尤其是刪除的時候就會很頭痛,於是就提供了這樣的一個屬性,來消除外鍵帶來的煩惱。在使用該屬性的時候,也就是該屬性設置成 true 的時候一定要慎重,從上面的例子可以看出來,當我在刪除教室的時候,教室裡的學生也都被刪除了,該屬性會有一個級聯的效果。進一步剖析 mappedBy在 jpa 中的關係屬性中,mappedBy 是很重要的一個屬性存在,做為一個使用者我們一定要清楚 mappedBy 的使用細則,下面根據個人的使用經驗總結如下:當一個實體類使用了 mappedBy 屬性,是可以避免多餘的表生成的,如果沒有使用該屬性,程序運行後在資料庫會多生成一個關係表。當一個實體類使用了 mappedBy 屬性,表示該類放棄主鍵的維護,該類生成的表中不存放和它關聯類的外鍵。mappedBy 細節分析使用 mappedBy 的一方是放棄主鍵的維護的,當在使用 mappedBy 的一方進行級聯操作的時候有些細節你應該知道:通常 mappedBy 在處理級聯刪除的時候使用 orphanRemoval 屬性就好,當然在@ManyToMany 這個註解當中是沒有 orphanRemoval 這個屬性的,還是需要使用自己的級聯刪除屬性的。級聯保存和級聯更新的時候你需要知道在保存和更新關聯數據的時候是沒有關聯到外鍵的,你需要藉助關聯類去維護外鍵,下面看代碼展示:

   @OneToMany(mappedBy = "classRoom",cascade = CascadeType.PERSIST)
    @JsonIgnore
    private Set<Student> students;

   @PostMapping("/save_room")
    public ClassRoom saveClassRoom() {
        ClassRoom room = new ClassRoom()
                .setName("IT 教室");
        Set<Student> students = new HashSet<>();
        Student student = new Student().setName("test123");
        students.add(student);
        room.setStudents(students);
        return classRoomRepository.save(room);
    }

以上這種處理方式是可以在保存教室的時候,把學生也保存到資料庫當中,但是因為教室類不進行外鍵的維護,雖然學生類保存成功,但是是失敗的,因為它們之間的關係並沒有建立起來,查看學生表的新增數據我們會發現新增的學生並沒有教室的外鍵存在我們如何有效的級聯保存呢?
我們必須弄清楚誰是維護彼此關係的,上面的教室使用了 mappedBy 屬性放棄了主鍵的維護,因此我們需要藉助學生類來維護彼此的關係,我們在測試代碼中需要在學生類中把教室給設置進去,這樣問題就解決了:
改進測試代碼:

@PostMapping("/save_room")
public ClassRoom saveClassRoom() {
    ClassRoom room = new ClassRoom()
            .setName("IT 教室");
    Set<Student> students = new HashSet<>();
    Student stu = new Student().setName("test123");
    students.add(stu);
    //改進代碼,學生類維護關係,把教室設置到每一個學生當中
    students.forEach(student -> student.setClassRoom(room));
    room.setStudents(students);
    return classRoomRepository.save(room);
}

上面的這個案例開發者很容易忽略,在此希望該案例能夠幫到你。使用spring data jpa關鍵字進行增刪改查在使用 spring data jpa 進行資料庫的增刪改查的時候,基本上我們無需寫 sql 語句的,但是我們必須要遵守它的規則,下面就來聊一聊:如何定義 DAO 層spring data jpa 的數據層,我們只需要定義一個接口繼承 JpaRepository 就好,
JpaRepository 接口中定義了豐富的查詢方法供我們使用,足以供我們進行增刪改查的工作,參考代碼如下:定義一個 Student 的 dao 層,這樣我們的增刪改查就已經有了

public interface StudentRepository extends JpaRepository<Student,Integer> {
}

在 spring boot 項目中在 dao 層我們不需要寫 @Repository 註解 ,我們在使用的時候直接注入使用就好,這裡需要說明一點, 我們在更新數據的時候,可以先查詢,然後更改屬性,使用 save 方法保存就好。使用關鍵字自定義查詢我們可以使用 jpa 提供的 find  和 get 關鍵字完成常規的查詢操作,使用 delete 關鍵字完成刪除,使用 count 關鍵字完成統計等

public interface StudentRepository extends JpaRepository<Student,Integer> {
    // 查詢資料庫中指定名字的學生
    List<Student> findByName(String name);
    // 根據名字和年齡查詢學生
    List<Student> getByNameAndAge(String name, Integer age);
    //刪除指定名字的學生
    Long deleteByName(String name);
    // 統計指定名字學生的數量
    Long countByName(String name);
}

  //通過find 關鍵字進行名字查詢
    @GetMapping("/find/{name}")
    public List<Student> findStudentsByName(@PathVariable("name") String name) {
        return studentRepository.findByName(name);
    }

get 關鍵字測試:
  //根據名字和年齡進行查詢

    @GetMapping("/get/{name}/{age}")
    public List<Student> getStudentByNameAndAge(@PathVariable("name") String name,@PathVariable("age") Integer age) {
        return studentRepository.getByNameAndAge(name, age);
    }

   //根據名字進行刪除操作
    @DeleteMapping("/delete/{name}")
    //刪除的時候一定要添加事務註解
    @Transactional
    public Long deleteStudentByName(@PathVariable("name") String name) {
        return studentRepository.deleteByName(name);
    }

count 關鍵字統計:
 //統計指定名字學生的數量

    @GetMapping("/count/{name}")
    public Long countStudentByName(@PathVariable("name") String name) {
        return studentRepository.countByName(name);
    }

可以看到上述使用 find關鍵字加上By 和要查詢的欄位,這樣一條查詢語句就寫好了,可以說相當的方便,下面附上通過關鍵字查詢的圖表,方便我們使用關鍵字查詢:基本上我們操作數據的時候,參考上述表格的關鍵字就可以解決了。jpa 使用 sql 增刪改查有時候我們不習慣使用上述的關鍵字去操作數據,就是喜歡寫 sql , spring data jpa 也是支持寫 sql 語句的,如何使用呢?案例代碼如下所示:

public interface ClassRoomRepository extends JpaRepository<ClassRoom,Integer> {

    //使用的 JPQL 的 sql 形式 from 後面是類名
    // ?1 代表是的是方法中的第一個參數
    @Query("select s from ClassRoom s where s.name =?1")
    List<ClassRoom> findClassRoom1(String name);

    //這是使用正常的 sql 語句去查詢
    // :name 是通過 @Param 註解去確定的
    @Query(nativeQuery = true,value = "select * from class_room c where c.name =:name")
    List<ClassRoom> findClassRoom2(@Param("name")String name);
}

參考上述的案例我們可以發現,sql 有兩種呈現形式:JPQL 形式的 sql 語句,from 後面是以類名呈現的。原生的 sql 語句,需要使用 nativeQuery = true 指定使用原生 sql使用問號 ?,緊跟數字序列,數字序列從1 開始,如 ?1 接收第一個方法參數的值。使用冒號:,緊跟參數名,參數名是通過@Param 註解來確定。使用 Sort 來對數據進行一個排序spring data jpa 提供了一個 Sort 類來進行分類排序,下面通過代碼來說明 Sort 的使用:

public interface TeacherRepositoty extends JpaRepository<Teacher,Integer> {

    // 正常使用,只是多加了一個 sort 參數而已
    @Query("select t from Teacher t where t.subject = ?1")
    List<Teacher> getTeachers(String subject, Sort sort);
}

上述代碼正常的寫 sql 語句,只是多加了一個 Sort 參數而已,如何實例化 Sort 應該是我們關注的重點,現在看測試代碼如下:

  @GetMapping("/sort/{subject}")
    public List<Teacher> getTeachers(@PathVariable("subject") String subject) {
        // 第一種方法實例化出 Sort 類,根據年齡進行升序排列
        Sort sort1 = Sort.by(Sort.Direction.ASC, "age");

        //定義多個欄位的排序
        Sort sort2 = Sort.by(Sort.Direction.DESC, "id", "age");

        // 通過實例化 Sort.Order 來排序多個欄位
        List<Sort.Order> orders = new ArrayList<>();
        Sort.Order order1 = new Sort.Order(Sort.Direction.DESC, "id");
        Sort.Order order2 = new Sort.Order(Sort.Direction.DESC, "age");
        orders.add(order1);
        orders.add(order2);
        Sort sort3 = Sort.by(orders);

        //可以傳不同的 sort1,2,3 去測試效果
        return teacherRepositoty.getTeachers(subject, sort1);
    }

Sort 是用來用來給欄位排序的,可以根據一個欄位進行排序,也可以給多個欄位設置排序規則,但是個人之見使用Sort 對一個欄位排序就好。Sort 類的實例化可以通過 Sort 的 by 靜態方法實例化就好,這裡就不一一列舉了,參考上述案例就好。jpa 的分頁操作數據多的時候就需要分頁,spring data jpa 對分頁提供了很好的支持,下面通過一個 demo 來展示如何使用分頁:

public interface TeacherRepositoty extends JpaRepository<Teacher,Integer> {

    //正常使用,只是多加了一個 Pageable 參數而已
    @Query("select t from Teacher t where t.subject = :subject")
    Page<Teacher> getPage(@Param("subject") String subject, Pageable pageable);
}

按照正常的查詢步驟,多加一個 Pageable 的參數而已,如何獲取 Pageable 呢?Pageable 是一個接口類,它的實現類是 PageRequest ,下面就通過測試代碼來研究一下如何來實例化 PageRequest:

    @GetMapping("page/{subject}")
    public Page<Teacher> getPage(@PathVariable("subject") String subject) {
        // 第一種方法實例化 Pageable
        Pageable pageable1 = PageRequest.of(1, 2);

        //第二種實例化 Pageable
        Sort sort = Sort.by(Sort.Direction.ASC, "age");
        Pageable pageable2 = PageRequest.of(1, 2, sort);

        //第三種實例化 Pageable
        Pageable pageable3 = PageRequest.of(1, 2, Sort.Direction.DESC, "age");


        //可以傳入不同的 Pageable,測試效果
        Page page = teacherRepositoty.getPage(subject, pageable3);
        System.out.println(page.getTotalElements());
        System.out.println(page.getTotalPages());
        System.out.println(page.hasNext());
        System.out.println(page.hasPrevious());
        System.out.println(page.getNumberOfElements());
        System.out.println(page.getSize());
        return page;
    }

PageRequest 一共有三個可以實例化的靜態方法:public static PageRequest of(int page, int size)public static PageRequest of(int page, int size, Sort sort) 分頁的同時還可以針對分頁後的結果進行一個排序。public static PageRequest of(int page, int size, Direction direction, String… properties) 直接針對欄位進行排序。jpa  使用 Specification上面提供的各種 jpa 的使用方法已經相當的豐富了,可以根據自己的需求去選擇,下面我們在來分析另一種 spring data jpa 查詢數據的方法,使用 Specification 去處理數據:接口繼承 JpaSpecificationExecutor

public interface TeacherRepositoty extends JpaRepository<Teacher,Integer> , JpaSpecificationExecutor {
}

TeacherRepository 除了繼承 JpaRepository 以外,還繼承了 JpaSpecificationExecutor  接口,下面就來詳細分析一下該接口,JpaSpecificationExecutor  提供了如下的幾個方法供我們使用:

public interface JpaSpecificationExecutor<T> {
    Optional<T> findOne(@Nullable Specification<T> var1);
    List<T> findAll(@Nullable Specification<T> var1);
    Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);
    List<T> findAll(@Nullable Specification<T> var1, Sort var2);
    long count(@Nullable Specification<T> var1);
}

該接口一共有五個方法,還提供了排序,分頁的功能,分析方法的參數我們會發現方法中的參數 Specification 是我們使用的一個門檻,下面來具體分析如何實例化 Specification 。分析 SpecificationSpecification 是一個函數式接口,裡面有一個抽象的方法:

 Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);

實現該方法我們不需要弄清楚 Predicate , Root , CriteriaQuery 和 CriteriaBuilder 四個類的使用規則:現在有這樣的一條 sql 語句 :select  *  from teacher where age > 20Predicate 是用來建立 where 後的查尋條件的相當於上述sql語句的 age > 20。Root 使用來定位具體的查詢欄位,比如 root.get(「age」) ,定位 age欄位,CriteriaBuilder是用來構建一個欄位的範圍,相當於 > ,= ,<,and …. 等等CriteriaQuery 可以用來構建整個 sql 語句,可以指定sql 語句中的 select 後的查詢欄位,也可以拼接 where , groupby 和 having 等複雜語句。上面的分析是十分抽象的,關於這四個類的詳細使用,自己也可以上網查詢多參考幾個案例分析即可。

   @GetMapping("/specification/{subject}")
    public List<Teacher> specification(@PathVariable("subject") String subject) {
        //實例化 Specification 類
        Specification specification = ((root, criteriaQuery, criteriaBuilder) -> {
            // 構建查詢條件
            Predicate predicate = criteriaBuilder.equal(root.get("subject"), subject);
            // 使用 and 連接上一個條件
            predicate = criteriaBuilder.and(predicate, criteriaBuilder.greaterThan(root.get("age"), 21));
            return predicate;
        });
        //使用查詢
        return teacherRepositoty.findAll(specification);
    }

這裡我演示了一個很簡單的查詢demo,希望可以幫助你打開使用 Specification 的大門。使用spring data  jpa 的 Projection (投影映射)該部分是很有趣的一部分,簡單容易操作, Projection 是要解決什麼問題呢?當我們使用 spring data jpa 查詢數據的時候,有時候不需要返回所有欄位的數據,我們只需要個別欄位數據,這樣使用 Projection 也是不錯的選擇,下面講一下使用細則。定義一個接口現在的需求是我只需要 Teacher 類對應的表 teacher 中的 name 和 age 的數據,其他數據不需要。定義一個如下的接口:

public interface TeacherProjection {
    String getName();
    Integer getAge();
    @Value("#{target.name +' and age is' + target.age}")
    String getTotal();
}

接口中的方法以 get 開頭 + 屬性名,屬性名首字母大寫, 例如 getName(), 也可以通過 @Value 註解中使用 target.屬性名獲取屬性,也可以把多個屬性值拼接成一個字符串。使用自定義接口定義好一個接口後,在查詢方法中指定返回接口類型的數據即可,參考代碼如下:

public interface TeacherRepositoty extends JpaRepository<Teacher,Integer>, JpaSpecificationExecutor {

   // 返回 TeacherProjection 接口類型的數據
    @Query("select t from Teacher t ")
    List<TeacherProjection> getTeacherNameAndAge();
}

  @GetMapping("/projection")
    public List<TeacherProjection> projection() {
       // 返回指定欄位的數據
        List<TeacherProjection> projections = teacherRepositoty.getTeacherNameAndAge();

       // 列印欄位值
        projections.forEach(teacherProjection -> {
            System.out.println(teacherProjection.getAge());
            System.out.println(teacherProjection.getName());
            System.out.println(teacherProjection.getTotal());
        });
        return projections;
    }

運行測試,查看結果我們會發現,我們僅僅只是拿到 name 和 age 的欄位值而已。繼續學習,求一波關注這篇文章很長,也寫了很久,文中表達的觀點個人都經過反覆的驗證,力求確保準確,如果文中表達有錯誤之處,歡迎指正,本文案例代碼來自本人 github 倉庫 https://github.com/kickcodeman/spring-data-jpa ,可以 clone 下來運行測試即可。路漫漫其修遠矣,學習的路還很長,期待和你做朋友,一起探討,一起進步。

 

相關焦點

  • SpringBoot(五) :spring data jpa 的使用
    的使用.html如有好文章投稿,請點擊 → 這裡了解詳情在上篇文章《 springboot(二):web綜合開發 》中簡單介紹了一下spring data jpa的基礎性使用,這篇文章將更加全面的介紹spring data jpa 常見用法以及注意事項。
  • springboot(五):spring data jpa的使用
    在上篇文章springboot(二):web綜合開發中簡單介紹了一下spring data jpa的基礎性使用,這篇文章將更加全面的介紹spring
  • 一文搞懂 Spring JPA
    了解了什麼是 JPA,我們來看看本文的主角——spring data jpa。spring data jpaSpring Data JPA 是 Spring 基於 ORM 框架、JPA 規範的基礎上封裝的一套 JPA 應用框架,底層使用了 Hibernate 的 JPA 技術實現,可使開發者用極簡的代碼即可實現對數據的訪問和操作。
  • Spring Boot的JPA / Hibernate複合主鍵示例
    該屬性spring.jpa.hibernate.ddl-auto = update使應用程式中的實體類型和映射的資料庫表保持同步。每當更新域實體時,下次重新啟動應用程式時,資料庫中相應的映射表也將更新。這非常適合開發,因為您不需要手動創建或更新表。它們將根據應用程式中的Entity類自動創建/更新。
  • 一文搞懂如何在Spring Boot 正確中使用JPA
    <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId
  • springboot+jpa+thymeleaf實現信息增刪改查功能
    前端:thymeleaf後端:springboot+jpa資料庫:mysql5.6jdk:1.8及以上</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId>        </dependency> <dependency> <groupId>mysql</groupId>
  • MyBatis JPA Extra,MyBatis JPA 擴展 v2.2 發布
    MyBatis JPA ExtraMyBatis JPA Extra對MyBatis進行了JPA擴展,旨在基於JPA 2.1的注釋簡化對單表CUID操作,根據JPA注釋動態生成SQL語句;使用Interceptor實現資料庫SELECT分頁查詢,適配多種資料庫;另外提供mybatis-jpa-extra-spring-boot-starter
  • mongoHelper 0.3.9 發布,spring-data-mongodb 增強工具包
    mongoHelper 是基於 spring-data-mongodb 的增強工具包,簡化 CRUD 操作,提供類 jpa 的資料庫操作。
  • SpringData JPA就是這麼簡單
    遇到的問題以及解決資料SpringData JPA遇到的問題有:參考資料:https://www.cnblogs.com/sevenlin/p/sevenlin_sprindatajpa20150725.htmlhttp://blog.csdn.net/qq_35797610/article/details/78737211CascadeType jpa
  • SpringBoot從入門到放棄之配置Spring-Data-JPA自動建表
    pom文件配置引入依賴配置文件進行jpa配置這裡有兩個配置需要說明一下;show_sql: true 在控制臺顯示jpa生成的s
  • Spring Cloud Connectors 1.2.0 發布
    ConfigurationThere was an issue with nested @Configuration classes that extend AbstractCloudConfig triggering ClassNotFound exceptions when spring-data-jpa
  • Spring Data Redis使用
    <dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version></dependency><dependency><groupId>org.springframework.data
  • Postgres、R2DBC、Spring Data JDBC和Spring WebFlux的響應式API簡介
    <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId
  • Spring Boot 實現RESTful webservice服務端示例
    createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true username: root password: 123456 jpa: hibernate: ddl-auto
  • 詳解|Spring Boot 最核心的 3 個註解詳解
    context.getBean(SomeBean.class); System.out.println(sb1); SomeBean sb2 = context.getBean(SomeBean.class); System.out.println(sb2); context.close(); }}輸出結果為init...com.spring.SomeBean
  • Spring Boot 和 Spring 到底有啥區別?
    ;org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.1.0.RELEASE</version></dependency><dependency> <groupId
  • Spring事務管理
    跨不同事務API的統一的編程模型,無論你使用的是jdbc、jta、jpa、hibernate。2. 支持聲明式事務3. 簡單的事務管理API4.四、Spring事務管理API學習Spring為事務管理提供了統一的抽象建模,這樣我們使用Spring來進行事務管理時,就只需要學會這套API即可,無論底下使用的是何種事務管理方式,jdbc也好,jpa也好,hibernate
  • 深入淺出 spring-data-elasticsearch 之 ElasticSearch 架構初探(一)
    如何健康檢查集群名,集群的健康狀態GET http:{   "cluster_name":          "elasticsearch",   "status":                "green",   "timed_out":             false,   "number_of_nodes":       1,   "number_of_data_nodes
  • 修為進階之Spring Data API
    RepositorySpring Data JPA 可以用來簡化data access的實現,藉助JPA我們可以快速的實現一些簡單的查詢,分頁,排序不在話下。querydsl.version}</version>  <scope>provided</scope></dependency><dependency>  <groupId>com.querydsl</groupId>  <artifactId>querydsl-jpa
  • Spring 中那些讓你愛不釋手的代碼技巧
    有些讀者私信我說希望後面多分享spring方面的文章,這樣能夠在實際工作中派上用場。正好我對spring源碼有過一定的研究,並結合我這幾年實際的工作經驗,把spring中我認為不錯的知識點總結一下,希望對您有所幫助。