日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

SpringBoot学习笔记:Spring Data Jpa的使用

發(fā)布時間:2025/3/20 javascript 52 豆豆
生活随笔 收集整理的這篇文章主要介紹了 SpringBoot学习笔记:Spring Data Jpa的使用 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

更多請關(guān)注公眾號

?

?

Spring Data Jpa 簡介

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)主要是為了簡化持久層開發(fā)以及整合ORM技術(shù),結(jié)束Hibernate、TopLink、JDO等ORM框架各自為營的局面。JPA是在吸收現(xiàn)有ORM框架的基礎(chǔ)上發(fā)展而來,易于使用,伸縮性強。總的來說,JPA包括以下3方面的技術(shù):

  • ORM映射元數(shù)據(jù): 支持XML和注解兩種元數(shù)據(jù)的形式,元數(shù)據(jù)描述對象和表之間的映射關(guān)系
  • API: 操作實體對象來執(zhí)行CRUD操作
  • 查詢語言: 通過面向?qū)ο蠖敲嫦驍?shù)據(jù)庫的查詢語言(JPQL)查詢數(shù)據(jù),避免程序的SQL語句緊密耦合
JPA架構(gòu)

Spring Data Jpa

來看一下Spring官方的解釋https://spring.io/projects/spring-data-jpa#overview

Spring Data Jpa官方解釋

?

Spring Data JPA是Spring Data家族的一部分,可以輕松實現(xiàn)基于JPA的存儲庫。 此模塊處理對基于JPA的數(shù)據(jù)訪問層的增強支持。 它使構(gòu)建使用數(shù)據(jù)訪問技術(shù)的Spring驅(qū)動應(yīng)用程序變得更加容易。

在相當(dāng)長的一段時間內(nèi),實現(xiàn)應(yīng)用程序的數(shù)據(jù)訪問層一直很麻煩。 必須編寫太多樣板代碼來執(zhí)行簡單查詢以及執(zhí)行分頁和審計。 Spring Data JPA旨在通過減少實際需要的工作量來顯著改善數(shù)據(jù)訪問層的實現(xiàn)。 作為開發(fā)人員,您編寫repository接口,包括自定義查找器方法,Spring將自動提供實現(xiàn)。

?

Spring Data生態(tài)

Jpa、Hibernate、Spring Data Jpa三者之間的關(guān)系

這個問題可參考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ī)范的具體實現(xiàn),這樣的好處是開發(fā)者可以面向JPA規(guī)范進行持久層的開發(fā),而底層的實現(xiàn)則是可以切換的。Spring Data Jpa則是在JPA之上添加另一層抽象(Repository層的實現(xià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

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.persistence.EntityManagerFactory; import javax.sql.DataSource;/*** 注意:spring-data-jpa2.x版本需要spring版本為5.x* 否則會報Initialization of bean failed; nested exception is java.lang.AbstractMethodError錯誤* 參考:https://stackoverflow.com/questions/47558017/error-starting-a-spring-application-initialization-of-bean-failed-nested-excep* 搭配方案:spring4+spring-data-jpa1.x或spring5+spring-data-jpa2.x*/ @Configuration // 借助spring data實現(xiàn)自動化的jpa repository,只需編寫接口無需編寫實現(xiàn)類 // 相當(dāng)于xml配置的<jpa:repositories base-package="com.example.repository" /> // repositoryImplementationPostfix默認(rèn)就是Impl // entityManagerFactoryRef默認(rèn)就是entityManagerFactory // transactionManagerRef默認(rèn)就是transactionManager @EnableJpaRepositories(basePackages = {"com.example.repository"},repositoryImplementationPostfix = "Impl",entityManagerFactoryRef = "entityManagerFactory",transactionManagerRef = "transactionManager") @EnableTransactionManagement // 啟用事務(wù)管理器 public class SpringDataJpaConfig {// 配置jpa廠商適配器(參見spring實戰(zhàn)p320) @Beanpublic JpaVendorAdapter jpaVendorAdapter() {HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();// 設(shè)置數(shù)據(jù)庫類型(可使用org.springframework.orm.jpa.vendor包下的Database枚舉類) jpaVendorAdapter.setDatabase(Database.MYSQL);// 設(shè)置打印sql語句jpaVendorAdapter.setShowSql(true);// 設(shè)置不生成ddl語句jpaVendorAdapter.setGenerateDdl(false);// 設(shè)置hibernate方言jpaVendorAdapter.setDatabasePlatform("org.hibernate.dialect.MySQL5Dialect");return jpaVendorAdapter;}// 配置實體管理器工廠 @Beanpublic LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) {LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();// 注入數(shù)據(jù)源 emfb.setDataSource(dataSource);// 注入jpa廠商適配器 emfb.setJpaVendorAdapter(jpaVendorAdapter);// 設(shè)置掃描基本包emfb.setPackagesToScan("com.example.entity");return emfb;}// 配置jpa事務(wù)管理器 @Beanpublic PlatformTransactionManager transactionManager(EntityManagerFactory emf) {JpaTransactionManager transactionManager = new JpaTransactionManager();// 配置實體管理器工廠 transactionManager.setEntityManagerFactory(emf);return transactionManager;}}

?

啟用web支持還需要在Spring MVC配置類上添加@EnableSpringDataWebSupport注解

@Configuration @ComponentScan(basePackages = {"cn.fulgens.controller"}) @EnableWebMvc // 啟用spring mvc @EnableSpringDataWebSupport // 啟用springmvc對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: update

ddl-auto

  • create:每次運行程序時,都會重新創(chuàng)建表,故而數(shù)據(jù)會丟失
  • create-drop:每次運行程序時會先創(chuàng)建表結(jié)構(gòu),然后待程序結(jié)束時清空表
  • upadte:每次運行程序,沒有表時會創(chuàng)建表,如果對象發(fā)生改變會更新表結(jié)構(gòu),原有數(shù)據(jù)不會清空,只會更新(推薦使用)
  • validate:運行程序會校驗數(shù)據(jù)與數(shù)據(jù)庫的字段類型是否相同,字段不同會報錯
  • none: 禁用DDL處理

注意:

Spring Data Jpa的使用

Spring Data Jpa UML類圖

Spring Data Jpa UML

簡單的REST CRUD示例

實體類

/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"主鍵生成策略

一般簡單的Demo示例中只會使用@GeneratedValue(strategy = GenerationType.IDENTITY)這種主鍵自增的策略,而實際數(shù)據(jù)庫中表字段主鍵類型很少是int型的

JPA自帶的幾種主鍵生成策略

  • TABLE: 使用一個特定的數(shù)據(jù)庫表格來保存主鍵
  • SEQUENCE: 根據(jù)底層數(shù)據(jù)庫的序列來生成主鍵,條件是數(shù)據(jù)庫支持序列。這個值要與generator一起使用,generator 指定生成主鍵使用的生成器(可能是orcale中自己編寫的序列)
  • IDENTITY: 主鍵由數(shù)據(jù)庫自動生成(主要是支持自動增長的數(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層

這里簡單起見省略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)建查詢只需四步:

  • 聲明一個接口繼承自Repository或Repositoy的一個子接口,對于Spring Data Jpa通常是JpaRepository,如:
  • interface PersonRepository extends Repository<Person, Long> { … }
  • 在接口中聲明查詢方法,如:
  • interface PersonRepository extends Repository<Person, Long> {List<Person> findByLastname(String lastname); }
  • 使用?JavaConfig?或?XML configuration配置Spring,讓 Spring 為聲明的接口創(chuàng)建代理對象
    3.1 JavaConfig參見上文
    3.2 使用Xml配置,可以像下面這樣使用jpa命名空間進行配置:
  • <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:jpa="http://www.springframework.org/schema/data/jpa"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/data/jpahttp://www.springframework.org/schema/data/jpa/spring-jpa.xsd"><jpa:repositories base-package="com.acme.repositories"/></beans>

    順帶一提,對于不同的Spring Data子項目Spring提供了不同的xml命名空間,如對于Spring Data MongoDB可以將上面的jpa改為mongodb
    當(dāng)然,使用Spring Boot這一步基本可以省略,我們需要做的就是在application.properties或application.yml文件中配置幾個屬性即可

  • 注入Repository實例并使用,如:
  • class SomeClient {private final PersonRepository repository;SomeClient(PersonRepository repository) {this.repository = repository;}void doSomething() {List<Person> persons = repository.findByLastname("Matthews");} }

    定義Repository接口

    選擇性暴露CRUD方法

    一種方法是定義一個BaseRepository接口繼承Repository接口,并從CrudRepository中copy你想暴露的CRUD方法
    src/main/java/com/example/springbootjpa/repository/MyBaseRepository

    package com.example.springbootjpa.repository;import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.Repository;import java.util.Optional;/*** 自定義Repository,選擇性暴露CRUD方法* @param <T>* @param <ID>*/ @NoRepositoryBean public interface MyBaseRepository<T, ID> extends Repository<T, ID> {Optional<T> findById(ID id);<S extends T> S save(S entity);}

    注意: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測試

    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());}}

    這里啟動Junit測試時報了一個錯,記錄一下

    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

    錯誤很明顯Spring應(yīng)用上下文加載失敗,原因是找不到j(luò)avax.xml.bind.JAXBException
    手賤從java8升級到j(luò)ava10,JAXB API是java EE 的API,在java SE 9.0 中已經(jīng)不再包含這個 Jar 包。java9 中引入了模塊的概念,默認(rèn)情況下,Java SE中將不再包含java EE 的Jar包,而在 java 6/7 / 8 時關(guān)于這個API 都是捆綁在一起的,解決方法添加如下jar包

    <dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.0</version> </dependency> <dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-impl</artifactId><version>2.3.0</version> </dependency> <dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-core</artifactId><version>2.3.0</version> </dependency> <dependency><groupId>javax.activation</groupId><artifactId>activation</artifactId><version>1.1.1</version> </dependency>

    解決方法參考自:https://www.cnblogs.com/newcaoguo/p/8831690.html

    另一種方法是使用@RepositoryDefinition注解,并從CrudRepository中copy你想暴露的CRUD方法
    src/main/java/com/example/springbootjpa/repository/UserRepository3

    package com.example.springbootjpa.repository;import com.example.springbootjpa.entity.User; import org.springframework.data.repository.RepositoryDefinition;import java.util.Optional;@RepositoryDefinition(domainClass = User.class, idClass = String.class) public interface UserRepository3 {Optional<User> findById(String id);User save(User user);}

    Repository方法的Null值處理

    從Spring Data2.0開始對于返回單個聚合實例的CRUD方法可以使用java8 Optional接口作為方法返回值來表明可能存在的缺省值,典型示例為CrudRepository的findById方法
    另外Spring也提供了幾個注解來處理Null值

    • @NonNullApi: 在包級別使用來聲明參數(shù)和返回值不能為Null

    • @NonNull: 在參數(shù)或返回值上使用,當(dāng)它們不能為Null時(如果在包級別上使用了@NonNullApi注解則沒有必要再使用@NonNull注解了)

    • @Nullable: 在參數(shù)或返回值上使用,當(dāng)它們可以為Null時

    查詢方法

    查詢創(chuàng)建Query Creation

    Spring Data Jpa通過解析方法名創(chuàng)建查詢,框架在進行方法名解析時,會先把方法名多余的前綴find…By, read…By, query…By, count…By以及get…By截取掉,然后對剩下部分進行解析,第一個By會被用作分隔符來指示實際查詢條件的開始。 我們可以在實體屬性上定義條件,并將它們與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片段如下表所示:

    KeywordSampleJPQL snippet
    AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
    OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
    Is,EqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = ?1
    BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2
    LessThanfindByAgeLessThan… where x.age < ?1
    LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1
    GreaterThanfindByAgeGreaterThan… where x.age > ?1
    GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
    AfterfindByStartDateAfter… where x.startDate > ?1
    BeforefindByStartDateBefore… where x.startDate < ?1
    IsNullfindByAgeIsNull… where x.age is null
    IsNotNull,NotNullfindByAge(Is)NotNull… where x.age not null
    LikefindByFirstnameLike… where x.firstname like ?1
    NotLikefindByFirstnameNotLike... findByFirstnameNotLike
    StartingWithfindByFirstnameStartingWith… where x.firstname like ?1 (parameter bound with appended %)
    EndingWithfindByFirstnameEndingWith… where x.firstname like ?1 (parameter bound with prepended %)
    ContainingfindByFirstnameContaining… where x.firstname like ?1 (parameter bound wrapped in %)
    OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
    NotfindByLastnameNot… where x.lastname <> ?1
    InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
    NotInfindByAgeNotIn(Collection<Age> ages)… where x.age not in ?1
    TruefindByActiveTrue()… where x.active = true
    FalsefindByActiveFalse()… where x.active = false
    IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstame) = UPPER(?1)

    具體Spring Data Jpa對方法名的解析規(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 注解的使用非常簡單,只需在聲明的方法上面標(biāo)注該注解,同時提供一個 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ù)位置時容易出錯。 要解決此問題,可以使用@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的解析方式如下:如果實體類在@Entity注解上設(shè)置了name屬性,則使用它。 否則,使用實體類的簡單類名。為避免在@Query注解使用實際的實體類名,就可以使用#{#entityName}進行代替。如以上示例中,@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目前不支持對原生查詢進行動態(tài)排序,但可以通過自己指定計數(shù)查詢countQuery來使用原生查詢進行分頁、排序,如:

    @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來完成動態(tài)分頁或排序,通常Pageable或Sort會是方法的最后一個參數(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ù)呢?
    對于Pageable參數(shù),在Spring Data 2.0之前我們可以new一個org.springframework.data.domain.PageRequest對象,現(xiàn)在這些構(gòu)造方法已經(jīng)廢棄,取而代之Spring推薦我們使用PageRequest的of方法

    new PageRequest(0, 5); new PageRequest(0, 5, Sort.Direction.ASC, "username"); new PageRequest(0, 5, new Sort(Sort.Direction.ASC, "username"));PageRequest.of(0, 5); PageRequest.of(0, 5, Sort.Direction.ASC, "username"); PageRequest.of(0, 5, Sort.by(Sort.Direction.ASC, "username"));

    注意:Spring Data PageRequest的page參數(shù)是從0開始的 zero-based page index

    對于Sort參數(shù),同樣可以new一個org.springframework.data.domain.Sort,但推薦使用Sort.by方法

    自定義修改、刪除 Modifying Queries

    單獨使用@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!

    多表查詢

    這里使用級聯(lián)查詢進行多表的關(guān)聯(lián)查詢

    多對多

    /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;}

    測試

    @Test public void findByIdTest() {Optional<User> optional = userRepository.findById("40289f0c65674a930165674d54940000");Set<Role> roles = optional.get().getRoles();System.out.println(optional.get()); }

    不出意外會報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ù)庫操作后,會關(guān)閉Session,當(dāng)我們想要用懶加載方式去獲得數(shù)據(jù)的時候,原來的Session已經(jīng)關(guān)閉,不能獲取數(shù)據(jù),所以會拋出這樣的異常。
    解決方法:
    在application.yml中做如下配置:

    @Test public void findByIdTest() {Optional<User> optional = userRepository.findById("40289f0c65674a930165674d54940000");Set<Role> roles = optional.get().getRoles();System.out.println(optional.get()); }

    一對多(多對一)

    /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;} 測試@Test public void findByIdTest() {Optional<Employee> optional = employeeRepository.findById("93fce66c1ef340fa866d5bd389de3d79");System.out.println(optional.get()); }

    ?

    結(jié)果報錯了...

    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這幾個注解。
    我們可以通過反編譯看一下Lombok生成的toString()方法

    // Employee public String toString() {return "Employee(id=" + getId() + ", empName=" + getEmpName() + ", empJob=" + getEmpJob() + ", deptId=" + getDeptId() + ", department=" + getDepartment() + ")"; } // Department public String toString() {return "Department(id=" + getId() + ", deptName=" + getDeptName() + ", employees=" + getEmployees() + ")"; }

    可以發(fā)現(xiàn)Lombok為我們生成的toString()方法覆蓋了整個類的所有屬性
    現(xiàn)在將@Data注解去掉,替換為@Setter、@Getter、@EqualsAndHashCode,重寫toString()方法

    // Department @Override public String toString() {return "Department{" +"id='" + id + '\'' +", deptName='" + deptName + '\'' +'}'; } // Employee @Override public String toString() {return "Employee{" +"id='" + id + '\'' +", empName='" + empName + '\'' +", empJob='" + empJob + '\'' +", deptId='" + deptId + '\'' +", department=" + department +'}'; }

    再次運行測試用例,測試通過,以上Employee toString()方法打印的department會觸發(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='人事部'}}

    再來測試查詢Department

    @Test public void findByIdTest() {Optional<Department> optional = departmentRepository.findById("0a4fe7234fff42afad34f6a06a8e1821");Set<Employee> employees = optional.get().getEmployees();Assert.assertNotEquals(0, employees.size()); }

    同樣還是報了堆棧溢出,錯誤定位在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()方法會使用所有屬性,注意,Department中employees是Set集合,當(dāng)我們調(diào)用department.getEmployees()時,Employee的hashCode()方法會被調(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; }

    自己動手重寫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); }

    再次運行測試用例,測試通過

    總結(jié):慎用@Data注解,使用@Getter、@Setter注解,需要時自己重寫toString()、equals()以及hashCode()方法

    審計Auditing

    參考自官方文檔5.9Auditing
    一般數(shù)據(jù)庫表在設(shè)計時都會添加4個審計字段,Spring Data Jpa同樣支持審計功能。Spring Data提供了@CreatedBy,@LastModifiedBy,@CreatedDate,@LastModifiedDate4個注解來記錄表中記錄的創(chuàng)建及修改信息。

    實體類

    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;}

    實體類上還添加了@EntityListeners(AuditingEntityListener.class),而AuditingEntityListener是由Spring Data Jpa提供的

    實現(xiàn)AuditorAware接口

    光添加了4個審計注解還不夠,得告訴程序到底是誰在創(chuàng)建和修改表記錄
    /src/main/java/com/example/springbootjpa/auditing/AuditorAwareImpl

    package com.example.springbootjpa.auditing;import org.springframework.data.domain.AuditorAware; import org.springframework.stereotype.Component;import java.util.Optional;@Component public class AuditorAwareImpl implements AuditorAware<String> {@Overridepublic Optional<String> getCurrentAuditor() {return Optional.of("admin");}}

    這里簡單的返回了一個"admin"字符串來代表當(dāng)前用戶名

    啟用Jpa審計功能

    在Spring Boot啟動類上添加@EnableJpaAuditing注解用于啟用Jpa的審計功能

    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查詢請查閱官方文檔

    參考資料:
    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的使用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。