javascript
SpringBoot学习笔记:Spring Data Jpa的使用
更多請(qǐng)關(guān)注公眾號(hào)
?
?
Spring Data Jpa 簡(jiǎn)介
JPA
JPA(Java Persistence API)意即Java持久化API,是Sun官方在JDK5.0后提出的Java持久化規(guī)范(JSR 338,這些接口所在包為javax.persistence,詳細(xì)內(nèi)容可參考https://github.com/javaee/jpa-spec)
JPA的出現(xiàn)主要是為了簡(jiǎn)化持久層開發(fā)以及整合ORM技術(shù),結(jié)束Hibernate、TopLink、JDO等ORM框架各自為營的局面。JPA是在吸收現(xiàn)有ORM框架的基礎(chǔ)上發(fā)展而來,易于使用,伸縮性強(qiáng)??偟膩碚f,JPA包括以下3方面的技術(shù):
- ORM映射元數(shù)據(jù): 支持XML和注解兩種元數(shù)據(jù)的形式,元數(shù)據(jù)描述對(duì)象和表之間的映射關(guān)系
- API: 操作實(shí)體對(duì)象來執(zhí)行CRUD操作
- 查詢語言: 通過面向?qū)ο蠖敲嫦驍?shù)據(jù)庫的查詢語言(JPQL)查詢數(shù)據(jù),避免程序的SQL語句緊密耦合
Spring Data Jpa
來看一下Spring官方的解釋https://spring.io/projects/spring-data-jpa#overview
Spring Data Jpa官方解釋?
Spring Data JPA是Spring Data家族的一部分,可以輕松實(shí)現(xiàn)基于JPA的存儲(chǔ)庫。 此模塊處理對(duì)基于JPA的數(shù)據(jù)訪問層的增強(qiáng)支持。 它使構(gòu)建使用數(shù)據(jù)訪問技術(shù)的Spring驅(qū)動(dòng)應(yīng)用程序變得更加容易。
在相當(dāng)長(zhǎng)的一段時(shí)間內(nèi),實(shí)現(xiàn)應(yīng)用程序的數(shù)據(jù)訪問層一直很麻煩。 必須編寫太多樣板代碼來執(zhí)行簡(jiǎn)單查詢以及執(zhí)行分頁和審計(jì)。 Spring Data JPA旨在通過減少實(shí)際需要的工作量來顯著改善數(shù)據(jù)訪問層的實(shí)現(xiàn)。 作為開發(fā)人員,您編寫repository接口,包括自定義查找器方法,Spring將自動(dòng)提供實(shí)現(xiàn)。
?
Spring Data生態(tài)Jpa、Hibernate、Spring Data Jpa三者之間的關(guān)系
這個(gè)問題可參考https://stackoverflow.com/questions/16148188/spring-data-jpa-versus-jpa-whats-the-difference及https://blog.csdn.net/u014421556/article/details/52635000
總的來說JPA是ORM規(guī)范,Hibernate、TopLink等是JPA規(guī)范的具體實(shí)現(xiàn),這樣的好處是開發(fā)者可以面向JPA規(guī)范進(jìn)行持久層的開發(fā),而底層的實(shí)現(xiàn)則是可以切換的。Spring Data Jpa則是在JPA之上添加另一層抽象(Repository層的實(shí)現(xiàn)),極大地簡(jiǎn)化持久層開發(fā)及ORM框架切換的成本。
?
Jpa、Hibernate、Spring Data Jpa三者之間的關(guān)系Spring Data Jpa的java配置方案
在Spring Boot沒出來之前如果要采用Java Configuration來配置Spring Data Jpa你需要配置如下的Bean
參考自Spring In Action及Spring Data Jpa官方文檔5.1.2. Annotation-based Configuration
?
啟用web支持還需要在Spring MVC配置類上添加@EnableSpringDataWebSupport注解
@Configuration @ComponentScan(basePackages = {"cn.fulgens.controller"}) @EnableWebMvc // 啟用spring mvc @EnableSpringDataWebSupport // 啟用springmvc對(duì)spring data的支持 public class WebMvcConfig extends WebMvcConfigurerAdapter {}?
Spring Boot整合Spring Data Jpa
導(dǎo)入依賴
<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-devtools</artifactId><optional>true</optional> </dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope> </dependency>相關(guān)配置
server:port: 8080servlet:context-path: / spring:datasource:url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false username: rootpassword: mysql123jpa:database: MySQLdatabase-platform: org.hibernate.dialect.MySQL5InnoDBDialectshow-sql: truehibernate:ddl-auto: updateddl-auto
- create:每次運(yùn)行程序時(shí),都會(huì)重新創(chuàng)建表,故而數(shù)據(jù)會(huì)丟失
- create-drop:每次運(yùn)行程序時(shí)會(huì)先創(chuàng)建表結(jié)構(gòu),然后待程序結(jié)束時(shí)清空表
- upadte:每次運(yùn)行程序,沒有表時(shí)會(huì)創(chuàng)建表,如果對(duì)象發(fā)生改變會(huì)更新表結(jié)構(gòu),原有數(shù)據(jù)不會(huì)清空,只會(huì)更新(推薦使用)
- validate:運(yùn)行程序會(huì)校驗(yàn)數(shù)據(jù)與數(shù)據(jù)庫的字段類型是否相同,字段不同會(huì)報(bào)錯(cuò)
- none: 禁用DDL處理
注意:
Spring Data Jpa的使用
Spring Data Jpa UML類圖
Spring Data Jpa UML簡(jiǎn)單的REST CRUD示例
實(shí)體類
/src/main/java/com/example/springbootjpa/entity/Userpackage com.example.springbootjpa.entity;import lombok.Data; import org.hibernate.annotations.GenericGenerator;import javax.persistence.*;@Entity @Table(name = "tb_user") @Data public class User {@Id@GenericGenerator(name = "idGenerator", strategy = "uuid")@GeneratedValue(generator = "idGenerator")private String id;@Column(name = "username", unique = true, nullable = false, length = 64)private String username;@Column(name = "password", nullable = false, length = 64)private String password;@Column(name = "email", length = 64)private String email;}主鍵采用UUID策略
@GenericGenerator是Hibernate提供的主鍵生成策略注解,注意下面的@GeneratedValue(JPA注解)使用generator = "idGenerator"引用了上面的name = "idGenerator"主鍵生成策略
一般簡(jiǎn)單的Demo示例中只會(huì)使用@GeneratedValue(strategy = GenerationType.IDENTITY)這種主鍵自增的策略,而實(shí)際數(shù)據(jù)庫中表字段主鍵類型很少是int型的
JPA自帶的幾種主鍵生成策略
- TABLE: 使用一個(gè)特定的數(shù)據(jù)庫表格來保存主鍵
- SEQUENCE: 根據(jù)底層數(shù)據(jù)庫的序列來生成主鍵,條件是數(shù)據(jù)庫支持序列。這個(gè)值要與generator一起使用,generator 指定生成主鍵使用的生成器(可能是orcale中自己編寫的序列)
- IDENTITY: 主鍵由數(shù)據(jù)庫自動(dòng)生成(主要是支持自動(dòng)增長(zhǎng)的數(shù)據(jù)庫,如mysql)
- AUTO: 主鍵由程序控制,也是GenerationType的默認(rèn)值
Dao層
/src/main/java/com/example/springbootjpa/repository/UserRepositorypackage com.example.springbootjpa.repository;import com.example.springbootjpa.entity.User; import org.springframework.data.jpa.repository.JpaRepository;public interface UserRepository extends JpaRepository<User, String> { }Controller層
這里簡(jiǎn)單起見省略Service層
/src/main/java/com/example/springbootjpa/controller/UserControllerpackage com.example.springbootjpa.controller;import com.example.springbootjpa.entity.User; import com.example.springbootjpa.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.*;import java.util.HashMap; import java.util.Optional;@RestController @RequestMapping("/users") public class UserController {@Autowiredprivate UserRepository userRepository;@PostMapping()public User saveUser(@RequestBody User user) {return userRepository.save(user);}@DeleteMapping("/{id}")public void deleteUser(@PathVariable("id") String userId) {userRepository.deleteById(userId);}@PutMapping("/{id}")public User updateUser(@PathVariable("id") String userId, @RequestBody User user) {user.setId(userId);return userRepository.saveAndFlush(user);}@GetMapping("/{id}")public User getUserInfo(@PathVariable("id") String userId) {Optional<User> optional = userRepository.findById(userId);return optional.orElseGet(User::new);}@GetMapping("/list")public Page<User> pageQuery(@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {return userRepository.findAll(PageRequest.of(pageNum - 1, pageSize));}}Spring Data Jpa使用詳解
Spring Data查詢方法
使用Spring Data創(chuàng)建查詢只需四步:
3.1 JavaConfig參見上文
3.2 使用Xml配置,可以像下面這樣使用jpa命名空間進(jìn)行配置:
順帶一提,對(duì)于不同的Spring Data子項(xiàng)目Spring提供了不同的xml命名空間,如對(duì)于Spring Data MongoDB可以將上面的jpa改為mongodb
當(dāng)然,使用Spring Boot這一步基本可以省略,我們需要做的就是在application.properties或application.yml文件中配置幾個(gè)屬性即可
定義Repository接口
選擇性暴露CRUD方法
一種方法是定義一個(gè)BaseRepository接口繼承Repository接口,并從CrudRepository中copy你想暴露的CRUD方法
src/main/java/com/example/springbootjpa/repository/MyBaseRepository
注意:MyBaseRepository上面加了@NoRepositoryBean注解
src/main/java/com/example/springbootjpa/repository/UserRepository2
package com.example.springbootjpa.repository;import com.example.springbootjpa.entity.User; import org.springframework.stereotype.Repository;public interface UserRepository2 extends MyBaseRepository<User, String> { }Junit測(cè)試
package com.example.springbootjpa.repository;import org.junit.Assert; 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 UserRepository2Test {@Autowiredprivate UserRepository2 userRepositoy;@Testpublic void findByIdTest() {Optional optional = userRepositoy.findById("40289f0c65674a930165674d54940000");Assert.assertNotNull(optional.get());}}這里啟動(dòng)Junit測(cè)試時(shí)報(bào)了一個(gè)錯(cuò),記錄一下
java.lang.IllegalStateException: Failed to load ApplicationContext ... Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBExceptionat java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:190)at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:499)... 50 more錯(cuò)誤很明顯Spring應(yīng)用上下文加載失敗,原因是找不到j(luò)avax.xml.bind.JAXBException
手賤從java8升級(jí)到j(luò)ava10,JAXB API是java EE 的API,在java SE 9.0 中已經(jīng)不再包含這個(gè) Jar 包。java9 中引入了模塊的概念,默認(rèn)情況下,Java SE中將不再包含java EE 的Jar包,而在 java 6/7 / 8 時(shí)關(guān)于這個(gè)API 都是捆綁在一起的,解決方法添加如下jar包
解決方法參考自:https://www.cnblogs.com/newcaoguo/p/8831690.html
另一種方法是使用@RepositoryDefinition注解,并從CrudRepository中copy你想暴露的CRUD方法
src/main/java/com/example/springbootjpa/repository/UserRepository3
Repository方法的Null值處理
從Spring Data2.0開始對(duì)于返回單個(gè)聚合實(shí)例的CRUD方法可以使用java8 Optional接口作為方法返回值來表明可能存在的缺省值,典型示例為CrudRepository的findById方法
另外Spring也提供了幾個(gè)注解來處理Null值
-
@NonNullApi: 在包級(jí)別使用來聲明參數(shù)和返回值不能為Null
-
@NonNull: 在參數(shù)或返回值上使用,當(dāng)它們不能為Null時(shí)(如果在包級(jí)別上使用了@NonNullApi注解則沒有必要再使用@NonNull注解了)
-
@Nullable: 在參數(shù)或返回值上使用,當(dāng)它們可以為Null時(shí)
查詢方法
查詢創(chuàng)建Query Creation
Spring Data Jpa通過解析方法名創(chuàng)建查詢,框架在進(jìn)行方法名解析時(shí),會(huì)先把方法名多余的前綴find…By, read…By, query…By, count…By以及get…By截取掉,然后對(duì)剩下部分進(jìn)行解析,第一個(gè)By會(huì)被用作分隔符來指示實(shí)際查詢條件的開始。 我們可以在實(shí)體屬性上定義條件,并將它們與And和Or連接起來,從而創(chuàng)建大量查詢:
User findByUsername(String username);List<User> findByUsernameIgnoreCase(String username);List<User> findByUsernameLike(String username);User findByUsernameAndPassword(String username, String password);User findByEmail(String email);List<User> findByEmailLike(String email);List<User> findByIdIn(List<String> ids);List<User> findByIdInOrderByUsername(List<String> ids);void deleteByIdIn(List<String> ids);Long countByUsernameLike(String username);支持的關(guān)鍵字、示例及JPQL片段如下表所示:
| And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
| Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
| Is,Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
| Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
| LessThan | findByAgeLessThan | … where x.age < ?1 |
| LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
| GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
| GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
| After | findByStartDateAfter | … where x.startDate > ?1 |
| Before | findByStartDateBefore | … where x.startDate < ?1 |
| IsNull | findByAgeIsNull | … where x.age is null |
| IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
| Like | findByFirstnameLike | … where x.firstname like ?1 |
| NotLike | findByFirstnameNotLike | ... findByFirstnameNotLike |
| StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
| EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
| Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
| OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
| Not | findByLastnameNot | … where x.lastname <> ?1 |
| In | findByAgeIn(Collection<Age> ages) | … where x.age in ?1 |
| NotIn | findByAgeNotIn(Collection<Age> ages) | … where x.age not in ?1 |
| True | findByActiveTrue() | … where x.active = true |
| False | findByActiveFalse() | … where x.active = false |
| IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
具體Spring Data Jpa對(duì)方法名的解析規(guī)則可參看官方文檔4.4.3. Property Expressions
限制查詢結(jié)果
Spring Data Jpa支持使用first、top以及Distinct?關(guān)鍵字來限制查詢結(jié)果,如:
User findFirstByUsernameOrderByUsernameAsc(String username);List<User> findTop10ByUsername(String username, Sort sort);List<User> findTop10ByUsername(String username, Pageable pageable);自定義查詢Using @Query
@Query 注解的使用非常簡(jiǎn)單,只需在聲明的方法上面標(biāo)注該注解,同時(shí)提供一個(gè) JPQL 查詢語句即可
@Query("select u from User u where u.email = ?1") User getByEmail(String eamil);@Query("select u from User u where u.username = ?1 and u.password = ?2") User getByUsernameAndPassword(String username, String password);@Query("select u from User u where u.username like %?1%") List<User> getByUsernameLike(String username);使用命名參數(shù)Using Named Parameters
默認(rèn)情況下,Spring Data JPA使用基于位置的參數(shù)綁定,如前面所有示例中所述。 這使得查詢方法在重構(gòu)參數(shù)位置時(shí)容易出錯(cuò)。 要解決此問題,可以使用@Param注解為方法參數(shù)指定具體名稱并在查詢中綁定名稱,如以下示例所示:
@Query("select u from User u where u.id = :id") User getById(@Param("id") String userId);@Query("select u from User u where u.username = :username or u.email = :email") User getByUsernameOrEmail(@Param("username") String username, @Param("email") String email);Using SpEL Expressions
從Spring Data JPA release 1.4開始,Spring Data JPA支持名為entityName的變量。 它的用法是select x from #{#entityName} x。 entityName的解析方式如下:如果實(shí)體類在@Entity注解上設(shè)置了name屬性,則使用它。 否則,使用實(shí)體類的簡(jiǎn)單類名。為避免在@Query注解使用實(shí)際的實(shí)體類名,就可以使用#{#entityName}進(jìn)行代替。如以上示例中,@Query注解的查詢字符串里的User都可替換為#{#entityName}
@Query("select u from #{#entityName} u where u.email = ?1") User getByEmail(String eamil);原生查詢Native Queries
@Query注解還支持通過將nativeQuery標(biāo)志設(shè)置為true來執(zhí)行原生查詢,同樣支持基于位置的參數(shù)綁定及命名參數(shù),如:
@Query(value = "select * from tb_user u where u.email = ?1", nativeQuery = true) User queryByEmail(String email);@Query(value = "select * from tb_user u where u.email = :email", nativeQuery = true) User queryByEmail(@Param("email") String email);注意:Spring Data Jpa目前不支持對(duì)原生查詢進(jìn)行動(dòng)態(tài)排序,但可以通過自己指定計(jì)數(shù)查詢countQuery來使用原生查詢進(jìn)行分頁、排序,如:
@Query(value = "select * from tb_user u where u.username like %?1%",countQuery = "select count(1) from tb_user u where u.username = %?1%",nativeQuery = true) Page<User> queryByUsernameLike(String username, Pageable pageable);分頁查詢及排序
Spring Data Jpa可以在方法參數(shù)中直接傳入Pageable或Sort來完成動(dòng)態(tài)分頁或排序,通常Pageable或Sort會(huì)是方法的最后一個(gè)參數(shù),如:
@Query("select u from User u where u.username like %?1%") Page<User> findByUsernameLike(String username, Pageable pageable);@Query("select u from User u where u.username like %?1%") List<User> findByUsernameAndSort(String username, Sort sort);那調(diào)用repository方法時(shí)傳入什么參數(shù)呢?
對(duì)于Pageable參數(shù),在Spring Data 2.0之前我們可以new一個(gè)org.springframework.data.domain.PageRequest對(duì)象,現(xiàn)在這些構(gòu)造方法已經(jīng)廢棄,取而代之Spring推薦我們使用PageRequest的of方法
注意:Spring Data PageRequest的page參數(shù)是從0開始的 zero-based page index
對(duì)于Sort參數(shù),同樣可以new一個(gè)org.springframework.data.domain.Sort,但推薦使用Sort.by方法
自定義修改、刪除 Modifying Queries
單獨(dú)使用@Query注解只是查詢,如涉及到修改、刪除則需要再加上@Modifying注解,如:
@Transactional() @Modifying @Query("update User u set u.password = ?2 where u.username = ?1") int updatePasswordByUsername(String username, String password);@Transactional() @Modifying @Query("delete from User where username = ?1") void deleteByUsername(String username);注意:Modifying queries can only use void or int/Integer as return type!
多表查詢
這里使用級(jí)聯(lián)查詢進(jìn)行多表的關(guān)聯(lián)查詢
多對(duì)多
/src/main/java/com/example/springbootjpa/entity/Userpackage com.example.springbootjpa.entity;import lombok.Data; import org.hibernate.annotations.GenericGenerator;import javax.persistence.*; import java.util.Date; import java.util.Set; import java.util.UUID;@Entity @Table(name = "tb_user") @Data public class User {@Id@GenericGenerator(name = "idGenerator", strategy = "uuid")@GeneratedValue(generator = "idGenerator")private String id;@Column(name = "username", unique = true, nullable = false, length = 64)private String username;@Column(name = "password", nullable = false, length = 64)private String password;@Column(name = "email", unique = true, length = 64)private String email;@ManyToMany(targetEntity = Role.class, cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)@JoinTable(name = "tb_user_role", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})private Set<Role> roles;} /src/main/java/com/example/springbootjpa/entity/Rolepackage com.example.springbootjpa.entity;import lombok.Data; import org.hibernate.annotations.GenericGenerator;import javax.persistence.*;@Entity @Table(name = "tb_role") @Data public class Role {@Id@GenericGenerator(name = "idGenerator", strategy = "uuid")@GeneratedValue(generator = "idGenerator")private String id;@Column(name = "role_name", unique = true, nullable = false, length = 64)private String roleName;}測(cè)試
@Test public void findByIdTest() {Optional<User> optional = userRepository.findById("40289f0c65674a930165674d54940000");Set<Role> roles = optional.get().getRoles();System.out.println(optional.get()); }不出意外會(huì)報(bào)Hibernate懶加載異常,無法初始化代理類,No Session:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.springbootjpa.entity.User.roles, could not initialize proxy - no Session原因:Spring Boot整合JPA后Hibernate的Session就交付給Spring去管理。每次數(shù)據(jù)庫操作后,會(huì)關(guān)閉Session,當(dāng)我們想要用懶加載方式去獲得數(shù)據(jù)的時(shí)候,原來的Session已經(jīng)關(guān)閉,不能獲取數(shù)據(jù),所以會(huì)拋出這樣的異常。
解決方法:
在application.yml中做如下配置:
一對(duì)多(多對(duì)一)
/src/main/java/com/example/springbootjpa/entity/Departmentpackage com.example.springbootjpa.entity;import lombok.Data; import org.hibernate.annotations.GenericGenerator;import javax.persistence.*; import java.util.Set;@Entity @Table(name = "tb_dept") @Data public class Department {@Id@GenericGenerator(name = "idGenerator", strategy = "uuid")@GeneratedValue(generator = "idGenerator")private String id;@Column(name = "dept_name", unique = true, nullable = false, length = 64)private String deptName;@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, fetch = FetchType.LAZY)private Set<Employee> employees;} /src/main/java/com/example/springbootjpa/entity/Employeepackage com.example.springbootjpa.entity;import lombok.Data; import org.hibernate.annotations.GenericGenerator;import javax.persistence.*; import java.util.UUID;@Entity @Table(name = "tb_emp") @Data public class Employee {@Id@GenericGenerator(name = "idGenerator", strategy = "uuid")@GeneratedValue(generator = "idGenerator")private String id;@Column(name = "emp_name", nullable = false, length = 64)private String empName;@Column(name = "emp_job", length = 64)private String empJob;@Column(name = "dept_id", insertable = false, updatable = false)private String deptId;@ManyToOne(targetEntity = Department.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)@JoinColumn(name = "dept_id")private Department department;} 測(cè)試@Test public void findByIdTest() {Optional<Employee> optional = employeeRepository.findById("93fce66c1ef340fa866d5bd389de3d79");System.out.println(optional.get()); }?
結(jié)果報(bào)錯(cuò)了...
java.lang.StackOverflowErrorat java.base/java.lang.Exception.<init>(Exception.java:102)at java.base/java.lang.ReflectiveOperationException.<init>(ReflectiveOperationException.java:89)at java.base/java.lang.reflect.InvocationTargetException.<init>(InvocationTargetException.java:73)at jdk.internal.reflect.GeneratedConstructorAccessor54.newInstance(Unknown Source)at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:488)at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)at com.mysql.jdbc.PreparedStatement.getInstance(PreparedStatement.java:761)at com.mysql.jdbc.ConnectionImpl.clientPrepareStatement(ConnectionImpl.java:1404)at com.mysql.jdbc.ConnectionImpl.prepareStatement(ConnectionImpl.java:4121)at com.mysql.jdbc.ConnectionImpl.prepareStatement(ConnectionImpl.java:4025)at com.zaxxer.hikari.pool.ProxyConnection.prepareStatement(ProxyConnection.java:318)at com.zaxxer.hikari.pool.HikariProxyConnection.prepareStatement(HikariProxyConnection.java)at org.hibernate.engine.jdbc.internal.StatementPreparerImpl$5.doPrepare(StatementPreparerImpl.java:145)at org.hibernate.engine.jdbc.internal.StatementPreparerImpl$StatementPreparationTemplate.prepareStatement(StatementPreparerImpl.java:171)at org.hibernate.engine.jdbc.internal.StatementPreparerImpl.prepareQueryStatement(StatementPreparerImpl.java:147)at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.prepareQueryStatement(AbstractLoadPlanBasedLoader.java:226)at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeQueryStatement(AbstractLoadPlanBasedLoader.java:190)at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:121)at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:86)at org.hibernate.loader.collection.plan.AbstractLoadPlanBasedCollectionInitializer.initialize(AbstractLoadPlanBasedCollectionInitializer.java:87)at org.hibernate.persister.collection.AbstractCollectionPersister.initialize(AbstractCollectionPersister.java:688)at org.hibernate.event.internal.DefaultInitializeCollectionEventListener.onInitializeCollection(DefaultInitializeCollectionEventListener.java:75)at org.hibernate.internal.SessionImpl.initializeCollection(SessionImpl.java:2223)at org.hibernate.collection.internal.AbstractPersistentCollection$4.doWork(AbstractPersistentCollection.java:565)at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:247)at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:561)at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:132)at org.hibernate.collection.internal.PersistentSet.hashCode(PersistentSet.java:430)at com.example.springbootjpa.entity.Department.hashCode(Department.java:14)?
通過日志看sql的輸出,發(fā)現(xiàn)了sql重復(fù)執(zhí)行了好多次。以下我截取了前10條sql記錄。
Hibernate: select employee0_.id as id1_1_0_, employee0_.dept_id as dept_id2_1_0_, employee0_.emp_job as emp_job3_1_0_, employee0_.emp_name as emp_name4_1_0_ from tb_emp employee0_ where employee0_.id=? Hibernate: select department0_.id as id1_0_0_, department0_.dept_name as dept_nam2_0_0_ from tb_dept department0_ where department0_.id=? Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=? Hibernate: select department0_.id as id1_0_0_, department0_.dept_name as dept_nam2_0_0_ from tb_dept department0_ where department0_.id=? Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=? Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=? Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=? Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=? Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=? Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=?通過觀察發(fā)現(xiàn),第一條sql是執(zhí)行查詢Employee的sql,第二條sql是執(zhí)行查詢Department的sql,第三條sql是執(zhí)行Department里面所有員工的sql,第四條sql是執(zhí)行查詢Department的sql,后面所有的sql都是執(zhí)行查詢Department里面所有員工的sql。
很明顯發(fā)生了循環(huán)依賴的情況。這是Lombok的@Data注解的鍋。Lombok的@Data注解相當(dāng)于@Getter、@Setter、@RequiredArgsConstructor、@ToString、@EqualsAndHashCode這幾個(gè)注解。
我們可以通過反編譯看一下Lombok生成的toString()方法
可以發(fā)現(xiàn)Lombok為我們生成的toString()方法覆蓋了整個(gè)類的所有屬性
現(xiàn)在將@Data注解去掉,替換為@Setter、@Getter、@EqualsAndHashCode,重寫toString()方法
再次運(yùn)行測(cè)試用例,測(cè)試通過,以上Employee toString()方法打印的department會(huì)觸發(fā)懶加載,最終日志輸出的sql如下:
Hibernate: select employee0_.id as id1_1_0_, employee0_.dept_id as dept_id2_1_0_, employee0_.emp_job as emp_job3_1_0_, employee0_.emp_name as emp_name4_1_0_ from tb_emp employee0_ where employee0_.id=? Hibernate: select department0_.id as id1_0_0_, department0_.dept_name as dept_nam2_0_0_ from tb_dept department0_ where department0_.id=? Employee{id='93fce66c1ef340fa866d5bd389de3d79', empName='jack', empJob='hr', deptId='0a4fe7234fff42afad34f6a06a8e1821', department=Department{id='0a4fe7234fff42afad34f6a06a8e1821', deptName='人事部'}}再來測(cè)試查詢Department
@Test public void findByIdTest() {Optional<Department> optional = departmentRepository.findById("0a4fe7234fff42afad34f6a06a8e1821");Set<Employee> employees = optional.get().getEmployees();Assert.assertNotEquals(0, employees.size()); }同樣還是報(bào)了堆棧溢出,錯(cuò)誤定位在Department和Employee的hashCode()方法上
java.lang.StackOverflowErrorat com.mysql.jdbc.Util.handleNewInstance(Util.java:439)at com.mysql.jdbc.ResultSetImpl.getInstance(ResultSetImpl.java:342)at com.mysql.jdbc.MysqlIO.buildResultSetWithRows(MysqlIO.java:3132)at com.mysql.jdbc.MysqlIO.getResultSet(MysqlIO.java:477)at com.mysql.jdbc.MysqlIO.readResultsForQueryOrUpdate(MysqlIO.java:3115)at com.mysql.jdbc.MysqlIO.readAllResults(MysqlIO.java:2344)at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2739)at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2486)at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1858)at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1966)at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.extract(ResultSetReturnImpl.java:60)at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.getResultSet(AbstractLoadPlanBasedLoader.java:419)at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeQueryStatement(AbstractLoadPlanBasedLoader.java:191)at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:121)at org.hibernate.loader.plan.exec.internal.AbstractLoadPlanBasedLoader.executeLoad(AbstractLoadPlanBasedLoader.java:86)at org.hibernate.loader.collection.plan.AbstractLoadPlanBasedCollectionInitializer.initialize(AbstractLoadPlanBasedCollectionInitializer.java:87)at org.hibernate.persister.collection.AbstractCollectionPersister.initialize(AbstractCollectionPersister.java:688)at org.hibernate.event.internal.DefaultInitializeCollectionEventListener.onInitializeCollection(DefaultInitializeCollectionEventListener.java:75)at org.hibernate.internal.SessionImpl.initializeCollection(SessionImpl.java:2223)at org.hibernate.collection.internal.AbstractPersistentCollection$4.doWork(AbstractPersistentCollection.java:565)at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:247)at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:561)at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:132)at org.hibernate.collection.internal.PersistentSet.hashCode(PersistentSet.java:430)at com.example.springbootjpa.entity.Department.hashCode(Department.java:17)依舊是Lombok的鍋,@EqualsAndHashCode為我們生成的equals()和hashCode()方法會(huì)使用所有屬性,注意,Department中employees是Set集合,當(dāng)我們調(diào)用department.getEmployees()時(shí),Employee的hashCode()方法會(huì)被調(diào)用,Employee中的hashCode()又依賴于Department的HashCode()方法,這樣又形成了循環(huán)引用...
// Department public int hashCode() {int i = 43;String $id = getId();int result = ($id == null ? 43 : $id.hashCode()) + 59;String $deptName = getDeptName();result = (result * 59) + ($deptName == null ? 43 : $deptName.hashCode());Set $employees = getEmployees();int i2 = result * 59;if ($employees != null) {i = $employees.hashCode();}return i2 + i; } // Employee public int hashCode() {int i = 43;String $id = getId();int result = ($id == null ? 43 : $id.hashCode()) + 59;String $empName = getEmpName();result = (result * 59) + ($empName == null ? 43 : $empName.hashCode());String $empJob = getEmpJob();result = (result * 59) + ($empJob == null ? 43 : $empJob.hashCode());String $deptId = getDeptId();result = (result * 59) + ($deptId == null ? 43 : $deptId.hashCode());Department $department = getDepartment();int i2 = result * 59;if ($department != null) {i = $department.hashCode();}return i2 + i; }自己動(dòng)手重寫equals()和hashCode()方法,去掉@EqualsAndHashCode注解
// Department @Override public boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Department that = (Department) o;return Objects.equals(id, that.id) &&Objects.equals(deptName, that.deptName); }@Override public int hashCode() {return Objects.hash(id, deptName); } // Employee @Override public boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Employee employee = (Employee) o;return Objects.equals(id, employee.id) &&Objects.equals(empName, employee.empName) &&Objects.equals(empJob, employee.empJob) &&Objects.equals(deptId, employee.deptId); }@Override public int hashCode() {return Objects.hash(id, empName, empJob, deptId); }再次運(yùn)行測(cè)試用例,測(cè)試通過
總結(jié):慎用@Data注解,使用@Getter、@Setter注解,需要時(shí)自己重寫toString()、equals()以及hashCode()方法
審計(jì)Auditing
參考自官方文檔5.9Auditing
一般數(shù)據(jù)庫表在設(shè)計(jì)時(shí)都會(huì)添加4個(gè)審計(jì)字段,Spring Data Jpa同樣支持審計(jì)功能。Spring Data提供了@CreatedBy,@LastModifiedBy,@CreatedDate,@LastModifiedDate4個(gè)注解來記錄表中記錄的創(chuàng)建及修改信息。
實(shí)體類
package com.example.springbootjpa.entity;import lombok.Data; import org.hibernate.annotations.GenericGenerator; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener;import javax.persistence.*; import java.util.Date; import java.util.Set;@Entity @EntityListeners(AuditingEntityListener.class) @Table(name = "tb_user") @Data public class User {@Id@GenericGenerator(name = "idGenerator", strategy = "uuid")@GeneratedValue(generator = "idGenerator")private String id;@Column(name = "username", unique = true, nullable = false, length = 64)private String username;@Column(name = "password", nullable = false, length = 64)private String password;@Column(name = "email", unique = true, length = 64)private String email;@ManyToMany(targetEntity = Role.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)@JoinTable(name = "tb_user_role", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})private Set<Role> roles;@CreatedDate@Column(name = "created_date", updatable = false)private Date createdDate;@CreatedBy@Column(name = "created_by", updatable = false, length = 64)private String createdBy;@LastModifiedDate@Column(name = "updated_date")private Date updatedDate;@LastModifiedBy@Column(name = "updated_by", length = 64)private String updatedBy;}實(shí)體類上還添加了@EntityListeners(AuditingEntityListener.class),而AuditingEntityListener是由Spring Data Jpa提供的
實(shí)現(xiàn)AuditorAware接口
光添加了4個(gè)審計(jì)注解還不夠,得告訴程序到底是誰在創(chuàng)建和修改表記錄
/src/main/java/com/example/springbootjpa/auditing/AuditorAwareImpl
這里簡(jiǎn)單的返回了一個(gè)"admin"字符串來代表當(dāng)前用戶名
啟用Jpa審計(jì)功能
在Spring Boot啟動(dòng)類上添加@EnableJpaAuditing注解用于啟用Jpa的審計(jì)功能
package com.example.springbootjpa;import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing;@SpringBootApplication @EnableJpaAuditing public class SpringBootJpaApplication {public static void main(String[] args) {SpringApplication.run(SpringBootJpaApplication.class, args);} }更多關(guān)于Jpa?Specifications、Example查詢請(qǐng)查閱官方文檔
參考資料:
Spring Data Jpa官方文檔
使用Hibernate、JPA、Lombok遇到的有趣問題
SpringData Jpa、Hibernate、Jpa 三者之間的關(guān)系
Spring Data-JPA versus JPA: What's the difference?
?
原文鏈接??https://www.jianshu.com/p/c23c82a8fcfc
轉(zhuǎn)載于:https://www.cnblogs.com/mzdljgz/p/11591880.html
總結(jié)
以上是生活随笔為你收集整理的SpringBoot学习笔记:Spring Data Jpa的使用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: idea插件手动安装
- 下一篇: Maven私服搭建(Nexus Repo