# 1. Spring Data 介紹 * 一個傘形項目,其中包含許多特定給數據庫的子項目 * 為數據訪問提供熟悉且一致的Spring編程摹本 * 目的:統一和簡化不同類型持久性存儲(關係型數據庫和NoSQL數據庫)的訪問 ## 1.1 Spring Data 主要模塊 * Spring Data common:支持每個Spring Data模塊的核心公共模塊 * Spring Data JDBC:對JDBC的Spring Data 數據庫支持 * Spring Data JPA:對JPA的Spring Data 數據庫支持 * Spring Data MongoDB:基於Spring的對象文件支持 和 MongoDB數據庫 * Spring Data Redis:從Spring應用程序輕鬆配置和訪問Redis * Spring Data REST:將Spring Data數據庫導出為超媒體驅動的RESTful資源 # 2. SpringDataJpa 配置 * 配置**EntityManagerFactory** * 基於**JPA**的應用程序要使用**EntityManagerFactory的實現類**來獲取**EntityManager實例**,通過 **EntityManager**來 **實現增刪改查**等數據庫訪問操作 * **實例類**:**LocalContainerEntityManagerFactoryBean** * 配置**相關屬性**: * **jpaVendorAdapter**:`JPA的實現方式`,需要一個**實現 JpaVendorAdapter接口的bean** * 配置 **HibernateJpaVendorAdapter** 的JPA實現,屬性: * **generateDdl**:`是否進行表的生成` * true:運行時創建(如果有表,就不會創建) * false:不會創建 * **showSql**:`顯示SQL語句` * true:顯示 * false:不顯示 * **packagesToScan**:`掃描實體類對象的路徑(包)` * 目的:哪些類需要**進行ORM的映射** * **dataSource**:`配置數據源`,需要一個dataSource的Bean * 配置**TransactionManager**(接口):`配置事務管理器` * **實現類**:**JpaTransactionManager** * 相關屬性: * **entityManagerFactory**:`指定要使用那一個EntityManagerFactory` * **使用補充**:設定多個DB連線 == 多個事務管理器,**@Transactional**需要`指定當前的事務管理器TransactionManager)` * **例**:**TransactionManager的**`BeanName(TestTransactionManager)` => **@Transactional**(`TestTransactionManager`) * TransactionManager接口的實現類 * JpaTransactionManager:JPA 使用 * DataSourceTransactionManager:Mybatis 使用 * HibernateTransactionManager:Hibernate 使用 * JtaTransactionManager:分布式事務 * jdbc、hibernate、Mybatis,都可以透過JtaTransactionManager來設定分布式事務 ## 2.1 xml 配置 * 整合JPA 需要指定 **@EnableJpaRepositories** * base-package:SQL的映射的包 * entity-manager-factory-ref:指定是那ㄧ個EntityManagerFactory * transaction-manager-ref:指定是那ㄧ個TransactionManager(事務管理器) * **底層**:**JpaRepositoriesAutoConfiguration.class** * **關閉JPA的話**,**忽略**此 **JpaRepositoriesAutoConfiguration.class** * 啟動註解方式的聲明式事務 的配置 **@EnableTransactionManagement** * 指定那一個**事務管理器** * 可以使用 `@Transactional` 註解作事務管理 * **Config類或啟動類添加此註解** ```xml <?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/data/jpa" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/data/jpa https://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> <!-- 整合JPA @EnableJpaRepositories --> <jpa:repositories base-package="com.nicolas.springDataJPA.repositories" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"/> <!-- id:Bean的名稱--> <!-- name:該Bean的方法名--> <!-- 配置 EntityManagerFactory--> <bean name="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <!-- 配置 EntityManagerFactory 中的屬性--> <!-- 1. JPA的實現方式 value是一個bean--> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="generateDdl" value="false"></property> <property name="showSql" value="true"></property> </bean> </property> <!-- 2. 是否顯示SQL--> <property name="packagesToScan" value="com.nicolas.springDataJPA.pojo"></property> <property name="dataSource"> <bean name="hikariDataSource" class="com.zaxxer.hikari.HikariDataSource"> <property name="jdbcUrl" value="jdbc:postgresql://localhost:5432/postgres"/> <property name="driverClassName" value="org.postgresql.Driver"/> <!-- <property name="username" value="yourUsername" />--> <!-- <property name="password" value="yourPassword" />--> <property name="maximumPoolSize" value="10"/> </bean> </property> <bean class="org.springframework.orm.jpa.JpaTransactionManager" name="transactionManager"> <!-- 指定該ManagerFactory為 entityManagerFactory這個bean--> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean> <!-- 啟動註解方式的聲明式事務 @EnableTransactionManagement --> <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven> </bean> </beans:beans> ``` ## 2.2 Config * 配置 ```java package com.nicolas.springDataJPA.config; import com.zaxxer.hikari.HikariDataSource; import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; 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.LocalContainerEntityManagerFactoryBean; 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; @Configuration //標記當前類為配置類 @EnableJpaRepositories(basePackages = "com.nicolas.springDataJPA.repositories", entityManagerFactoryRef="EntityManagerFactoryBean1", transactionManagerRef="TransactionManager1") //啟動jpa <jpa:repositories @EnableTransactionManagement //開啟事務 public class SpringDataConfig { @Bean public DataSource dataSource() { HikariDataSource hikariDataSource = new HikariDataSource(); hikariDataSource.setJdbcUrl("jdbc:postgresql://localhost:5432/postgres"); hikariDataSource.setDriverClassName("org.postgresql.Driver"); return hikariDataSource; } @Bean("EntityManagerFactoryBean1") //配置 JPA 實體管理器工廠 public LocalContainerEntityManagerFactoryBean entityManagerFactory( @Qualifier("CorePersistenceManagedTypes") PersistenceManagedTypes persistenceManagedTypes) { HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); vendorAdapter.setGenerateDdl(true); vendorAdapter.setShowSql(true); LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); factory.setJpaVendorAdapter(vendorAdapter); factory.setPackagesToScan("com.nicolas.springDataJPA.pojo"); factory.setDataSource(dataSource());// 方法注入Bean factory.setManagedTypes(persistenceManagedTypes); return factory; } @Bean("TransactionManager1")//配置事務管理器 public PlatformTransactionManager transactionManager(@Qualifier("EntityManagerFactoryBean1")EntityManagerFactory entityManagerFactory) { JpaTransactionManager txManager = new JpaTransactionManager(); txManager.setEntityManagerFactory(entityManagerFactory); return txManager; } // AOT下需要配置 @Bean("CorePersistenceManagedTypes") @Primary PersistenceManagedTypes managedTypes(ResourceLoader resourceLoader) { return new PersistenceManagedTypesScanner(resourceLoader) .scan("com.eunodata.eunoai.core.adapter.repository", "com.eunodata.eunoai.log.adapter.repository"); } } ``` * `LocalContainerEntityManagerFactoryBean` 是 Spring 的 FactoryBean,會產出 `EntityManagerFactory` * `@Bean` 回傳 **FactoryBean**,Spring 會**自動調用** `.getObject()`,注入 **「真實實例」** * **必須配置**: * setJdbcUrl * setDriverClassName * postgresql:org.postgresql.Driver * **如何查找**:該db驅動包下的 -> META-INF -> services -> java.sql.Driver -> **複製**`java.sql.Driver`中的內容 * setUserName * setPassword * **可選配置**: * setConnectionTimeout:連接超時 * 單位:秒 * setReadOnly:是否只讀數據庫 * false:不只讀數據庫 * setIdleTimeout:最小線程維持時間 * 單位:秒 * setMaxLifetime:最大線程維持時間 * 單位:秒 * setMaximumPoolSize:線程池最大維持數量 * setMinimumIdle:線程池最小維持數量 ### 2.2.1 EntityManagerFactoryBuilder 創建 EntityManagerFactory * **另一種創建EntityManagerFactory的方式** * **EntityManagerFactoryBuilder** 是用於構建 **EntityManagerFactory** 的一個工具類 * **dataSource**():設定`數據源` * **packages**():設置`實體類的包名`,告訴 JPA 實體類的位置 * 繼承**JpaRepository**接口的包 * **persistenceUnit**():設置`持久化單元`的名稱 * 多資料源 / 多 factory → 加上 .`persistenceUnit()` 可以**明確區分**,**不會造成 Spring 管理 bean 混淆** * **build**():構建並返回 LocalContainerEntityManagerFactoryBean 實例。 ```java= @Bean(name = "WorkspaceEntityManagerFactory") public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(EntityManagerFactoryBuilder builder) { Map<String, Object> properties = new HashMap<>(); properties.put("hibernate.hbm2ddl.auto", "none"); properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect"); return builder .dataSource(dataSource()) .properties(properties) .packages("com.nicolas.springDataJPA.pojo") .persistenceUnit("nicolas_db") .build(); } ``` ### 2.2.2 常用的 Hibernate 配置屬性及其作用 * **hibernate.dialect**:指定數據庫方言,告訴 Hibernate 生成與特定數據庫兼容的 SQL 查詢 * **mysql**:**org.hibernate.dialect.MySQLDialect** * **postgresql**:**org.hibernate.dialect.PostgreSQLDialect** * **hibernate.hbm2ddl.auto**:指定數據庫表結構的`生成和更新策略`。 * **validate**:在啟動時校驗數據庫表結構與映射配置的一致性,不進行任何修改。 * **update**:在啟動時更新數據庫表結構以與映射配置一致,但不刪除已經存在的列。 * **create**:在啟動時創建數據庫表結構,如果已經存在則先刪除再創建。 * **create-drop**:在啟動時創建數據庫表結構,但在關閉時刪除這些表。 * **none**:不對數據庫表結構進行任何操作。 * hibernate.show_sql:設置是否在控制台`打印生成的 SQL 查詢語句`。 * hibernate.format_sql:設置是否對`打印的 SQL 查詢進行格式化`,以便更容易閱讀。 * hibernate.generate_statistics:設置是否收集並展示 Hibernate 的性能統計信息。 # 3. 使用 Spring Data Repositories * 抽象的目標:顯著的減少為各種持久性儲存實現數據訪問層所需的樣本代碼 * 頂層接口為`CrudRepository<T, ID>` * **PagingAndSortingRepository<T, ID>**:為`排序、分頁`的接口,繼承**CrudRepository<T, ID>** ## 3.1 基本範例 * JavaBean * class上加 @Entity * 選擇一個屬性,做為key,再加上@Id、@GeneratedValue() ```java @Entity public class Customer { @Id @GeneratedValue() private Long id; private String address; private String name; } ``` * 繼承 **CrudRepository<T, ID>** 接口 ```java import com.nicolas.springDataJPA.pojo.Customer; import org.springframework.data.jpa.repository.JpaRepository; public interface CustomerRepositories extends JpaRepository<Customer, Long> { } ``` * 實際使用 ```java @SpringBootTest public class SpringDataJPAApplicationTests { @Autowired CustomerRepositories customerRepositories; @Test void contextLoads() { //Hibernate: select customer0_.id , customer0_.address , customer0_.name a from Customer customer0_ order by customer0_.id desc Sort sort = Sort.by("id").descending(); customerRepositories.findAll(sort); } } ``` # 4. 自定義操作 * jpql(原生SQL) * 規定方法名 * 動態查詢 * Query by Example(**只支持查詢**) * 通過 Specification * 通過 Querydsl (**需生成Q類**) ## 4.1 jpql(原生SQL) * 使用jpql、原生SQL這兩種方式 * jpql不能使用**增操作**,只能在hibernate下才支持,使用`(insert into ... select)`的方式 * 使用 **@Query** * **@Query** * 查詢如果返回單個實例 ,用**對象**接收,如果返回多個實例,通過**集合**接收 * **參數設置方式**: * **索引**:?數字 * **具名**::參數名,結合@Param(參數名) 指定參數名稱 * **增刪改操作**: * 要加上事務支持: **@Transactional** * 通常放在業務邏輯層聲明 * 通知JPA是增刪改操作:**@Modifying** * `回傳值為int` ### 4.1.1 jpql * Customer為一個JavaBean,有加上 @Entity註解 ```java public interface CustomerRepositories extends JpaRepository<Customer, Long> { //查詢 //1. 通過順序指定參數 @Query("select cus from Customer cus where cus.name=?1 ") Customer findCustomerByName(String name); //不能使用 * 號,當全部欄位 //2. 使用@Param註解進行綁定 @Query("select cus from Customer cus where cus.address=:address ") Customer findCustomerByAddress(@Param("address") String address); //修改,返回值只能指定int @Transactional // 增加事務管理,通常放在業務邏輯層聲明 @Modifying // 通知Jpa 這裡是增刪改 操作 @Query("Update Customer c set c.address=:address where c.id=:id ") int updateCustomerById(@Param("id") Long id, @Param("address") String address); //新增 JPQL 不支持插入操作 @Transactional @Modifying @Query("insert into Customer(name) select c.name from Customer c where c.id=?1 ") int insertCustomerBySelect(Long id); } ``` ### 4.1.2 原生SQL ```java public interface CustomerRepositories extends JpaRepository<Customer, Long> { //原生SQL @Query(value = "select * from Customer where name=:name", nativeQuery = true) Customer findCustomerByNameSQL(@Param("name") String name); @Transactional @Modifying @Query(value = "insert into Customer(id,address,name) values(:id,:address,:name ", nativeQuery = true) int insertCustomerBySQL(@Param("id") Long id, @Param("address") String address, @Param("name") String name); } ``` ## 4.2 規定方法名 * 兩個部分組成 * 主題關鍵字(前綴):決定`當前方法的作用`(查、刪) * 謂詞關鍵字和修飾符:決定`查詢條件` ### 4.2.1 規定方法名 | 關鍵詞 | 描述 | |----------------------------------------------------------------------|-----------------------------------------------------------------------------| | find...By,read..By,get..By,<br/>query..by,search..by,<br/>stream..by | 查詢方法,返回`Collection`或`Streamable`子類型或結果包裝器<br/>例如:`Page`、`GeoResult`或任何結果包裝器 | | exists...By | 返回`boolean`類型 | | count...By | 返回計數結果,`long`類型 | | delete...By、remoev...By | 返回 `void` 或 刪除計數(`int`) | | ...First\<number>... 、 ...Top\<number>... | 限制查詢結果只返回指定數量的記錄<br/>例:`First5`(前五條)、`Top5`(前五條) | | ...Distinct... | 確保查詢結果中不包含重複的數據行 | ### 4.2.2 謂詞關鍵字和修飾符 | 關鍵詞 | 範例 | JPQL片段 | |------------------------|-------------------------------------------------------------------------|------------------------------------------------------------------| | `Distinct` | `findDistinctByLastnameAndFirstname` | `select distinct …​ where x.lastname = ?1 and x.firstname = ?2` | | `And` | `findByLastnameAndFirstname` | `… where x.lastname = ?1 and x.firstname = ?2` | | `Or` | `findByLastnameOrFirstname` | `… where x.lastname = ?1 or x.firstname = ?2` | | `Is`, `Equals` | `findByFirstname`,<br/>`findByFirstnameIs`,<br/>`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`, `Null` | `findByAge(Is)Null` | `… where x.age is null` | | `IsNotNull`, `NotNull` | `findByAge(Is)NotNull ` | `… where x.age not null` | | `Like` | `findByFirstnameLike` | `… where x.firstname like ?1` | | `NotLike` | `findByFirstnameNotLike` | `… where x.firstname not like ?1` | | `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.firstname) = UPPER(?1)` | ### 4.2.3 範例 * **增**、**刪**、**改** 都要加 `@Transactional`、`@Modifying` ```java public interface CustomerNameRepositories extends JpaRepository<Customer, Long> { // find...By List<Customer> findByName(String name); //exists...By boolean existsByName(String name); //delete...By //增刪改需要加上事務、異動 @Transactional @Modifying int deleteByName(String name); } ``` ## 4.3 Query by Example(動態查詢) * **只支持查詢** * 不支持嵌套或分組的屬性約束 * 如 firstName = ?1 or ( firstName = ?2 and firstName = ?3 ) * **只支持字符串**(start/contains/ends/regex)匹配和其他屬性類型的精確匹配 * 繼承 **QueryByExampleExecutor\<T>** ```java public interface CustomerQBERRepositories extends JpaRepository<Customer,Long>,QueryByExampleExecutor<Customer> { } ``` ### 4.3.1 基本操作 ```java import org.springframework.data.domain.Example; @SpringBootTest public class QBERTest { @Autowired CustomerQBERRepositories customerQBERRepositories; @Test public void test1() { //查詢條件 Customer customer = new Customer(); customer.setName("MMMM"); customer.setAddress("ad"); //select id,address,name from Customer where name=? and address=? //通過Example 構建查詢條件 Example<Customer> of = Example.of(customer); List<Customer> all = customerQBERRepositories.findAll(of); System.out.println(all); } } ``` ### 4.3.2 使用 ExampleMatcher 匹配器 * 使用**ExampleMatcher** 建構匹配器,再放入**Exampler**建構查詢條件 * with....() 開頭 * **基本方法** * **withIgnorePaths**(string... ignoredPaths):忽略該欄位的條件 * **withIgnoreCase**(String... propertyPaths):該欄位的忽略大小寫 * **withStringMatcher**(StringMatcher defaultStringMatcher):所有字符串欄位套用該規則 * **withMatcher**(String propertyPath, MatcherConfigurer\<GenericPropertyMatcher> matcherConfigurer):`某個欄位套用該規則` ```java import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import java.util.List; @SpringBootTest public class QBERTest { @Autowired CustomerQBERRepositories customerQBERRepositories; @Test public void test2() { //查詢條件 Customer customer = new Customer(); customer.setId(2L); customer.setName("NN"); // customer.setAddress("ad"); //匹配器 ExampleMatcher exampleMatcher = ExampleMatcher.matching() .withIgnorePaths("id")//忽略ID屬性 .withIgnoreCase("name")//withIgnoreCase 忽略 name條件的大小寫 // .withStringMatcher(ExampleMatcher.StringMatcher.ENDING);//對所有條件的字符串進行結尾匹配 .withMatcher("name", ExampleMatcher.GenericPropertyMatcher::startsWith);//對單個條件進行限制 //通過Example 構建查詢條件 Example<Customer> of = Example.of(customer, exampleMatcher); List<Customer> all = customerQBERRepositories.findAll(of); System.out.println(all); } } ``` ## 4.4 Querydsl * 通用查詢框架,使用**通用API方式建構查詢** * 支持JPA、SQL、MongoDB等 * **代替**JPQL、Query by Example的方式 * `繼承` **QuerydslPredicateExecutor\<T>** * **需生成Q類** ### 4.4.1 前置 pom.xml配置 * 增加Querydsl函式庫 ```xml <!-- QueryDSL 函式庫 --> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>5.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>5.0.0</version> </dependency> ``` * 生成 Query Entity ```xml <build> <plugins> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin> </plugins> </build> ``` ### 4.4.2 範例 1. 繼承 **QuerydslPredicateExecutor\<T>** ```java public interface CustomerQuerydslRepositories extends JpaRepository<Customer, Long>, QuerydslPredicateExecutor<Customer> { } ``` 2. 測試 * 通過ID查找 * 查看name 是否包含"NN"、"nn" * 查看name 是否包含"NN"、"nn" 並且 id 大於3 ```java @SpringBootTest public class QuerydslTest { @Autowired CustomerQuerydslRepositories customerQuerydslRepositories; @Test public void test1() { QCustomer customer = QCustomer.customer; //通過ID查找 BooleanExpression eq = customer.id.eq(2L); //select * from Customer where id = ? Optional<Customer> one = customerQuerydslRepositories.findOne(eq); System.out.println(one.get()); } @Test public void test2() { QCustomer customer = QCustomer.customer; BooleanExpression eq = customer.name.in("NN", "nn"); //select * from Customer where name in (? , ?) Iterable<Customer> all = customerQuerydslRepositories.findAll(eq); System.out.println(all); } @Test public void test3() { QCustomer customer = QCustomer.customer; // name 在 ("NN", "nn") 並且 id 大於3 BooleanExpression eq = customer.name.in("NN", "nn") .and(customer.id.gt(3L));// gt 大於 // where (customer0_.name in (? , ?)) and id>? Iterable<Customer> all = customerQuerydslRepositories.findAll(eq); System.out.println(all); } } ``` ## 4.5 原生查詢 * **重點**:`通過JPA的 Repositories 進行查訊,查詢的列、表都是固定,無法調整` * **原生查詢**:可以**調整**要`顯示的欄位`,或要`查詢的表` ### 4.5.1 Specification * 繼承 **JpaSpecificationExecutor\<T>** * 不能分組 groupby * 透過 **Specification\<T>** 中的 **toPredicate(Root\<T> root, CriteriaQuery\<?> query, CriteriaBuilder criteriaBuilder)** {},來設定查詢細部的條件 * **root**:獲取`需要設定條件`的**欄位** * **criteriaBuilder**:設定條件,**並組合where的條件** * **query**:`組合`所有條件(where、orderby) * 當設定orderby 時,需要使用 * 透過**getRestriction()**` 返回Predicate類型` * 步驟 1. 獲取`需要設定條件`的**欄位** 2. 設定條件 3. 組合所有條件 #### 4.5.1.1 基本操作 * 繼承 **JpaSpecificationExecutor\<T>** ```java public interface CustomerSpecificationRepositories extends JpaRepository<Customer, Long>, JpaSpecificationExecutor<Customer> { } ``` * 測試 ```java @SpringBootTest public class SpecificationTest { @Autowired CustomerSpecificationRepositories customerSpecificationRepositories; @Test public void test2() { List<Customer> all = customerSpecificationRepositories.findAll(new Specification<Customer>() { @Override public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) { //Root:from 表,獲取到Column //CriteriaBuilder:設置各種條件 //CriteriaQuery:組和(order by , where) //1. 取的需要設定欄位的條件 Path<Long> id = root.get("id"); Path<String> address = root.get("address"); Path<String> name = root.get("name"); //2. 設定條件 //參數1:為哪個字段設定條件 //參數2:值 Predicate addressEqual = criteriaBuilder.equal(address, "28hm"); //address 等於 28hm Predicate idGreaterThan = criteriaBuilder.greaterThan(id, 3L);// id 要大於 3 CriteriaBuilder.In<String> in = criteriaBuilder.in(name); in.value("rr").value("dd").value("ka");// name 要在(rr,dd,ka) 之間 //2.1 拼接多條件,使用 and Predicate and = criteriaBuilder.and(addressEqual, idGreaterThan, in); //where address=? and id>3 and (name in (? , ? , ?)) return and; } }); System.out.println(all); } } ``` #### 4.5.1.2 增加排序,使用query 組合條件 ```java @SpringBootTest public class SpecificationTest { @Autowired CustomerSpecificationRepositories customerSpecificationRepositories; // 使用query @Test public void test3() { List<Customer> all = customerSpecificationRepositories.findAll((root, query, criteriaBuilder) -> { //1. 取的需要設定欄位的條件 Path<Long> id = root.get("id"); Path<String> address = root.get("address"); Path<String> name = root.get("name"); //2. 設定條件 //參數1:為哪個字段設定條件 //參數2:值 Predicate addressEqual = criteriaBuilder.equal(address, "28hm"); Predicate idGreaterThan = criteriaBuilder.greaterThan(id, 3L); CriteriaBuilder.In<String> in = criteriaBuilder.in(name); in.value("rr").value("dd").value("ka"); //2.1 拼接多條件,使用 and Predicate and = criteriaBuilder.and(addressEqual, idGreaterThan, in); //2.2 Order 排序 // Id 降序 Order idDesc = criteriaBuilder.desc(id); //3. 組合所有條件 return query.where(and).orderBy(idDesc).getRestriction(); }); } } ``` ### 4.5.2 基於querydsl * 初始化 **EntityManager** * 使用 **@PersistenceContext** 代替 **@Autowired** * 確保每個 Transaction 獨立綁定對應的 EntityManager 實例 * 如果用 `@Autowired`,Spring 無法自動處理「每個請求/交易一個實例」這件事 * 容易導致多個請求共用一個 EntityManager * 建立 **JPAQueryFactory** * 步驟 1. 構建基於 QueryDSL的查詢 2. **fetch()** 執行查詢 3. 處理、顯示數據 ```java @SpringBootTest public class QuerydslTest { //原生查詢 Specification //使用 @PersistenceContext,確保每個 Transaction 獨立綁定對應的 EntityManager 實例 @PersistenceContext EntityManager em; //自定義查詢 、分組 Specification @Test public void test5() { JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(em); QCustomer customer = QCustomer.customer; //顯示全部 JPAQuery<Customer> selectAll = jpaQueryFactory.select(customer).from(customer); List<Customer> customerList = selectAll.fetch(); //透過select中的類型,來判斷要返回類型 //顯示 id,name,是自定義類,所以是Tuple //1. 構建基於 QueryDSL的查詢 JPAQuery<Tuple> tupleJPAQuery = jpaQueryFactory.select(customer.id, customer.name) .from(customer) .where(customer.id.eq(4L)) .orderBy(customer.id.desc()); //2. fetch() 執行查詢 List<Tuple> fetch = tupleJPAQuery.fetch(); System.out.println(fetch); //3. 顯示返回數據 fetch.forEach(x -> { //取出對應的欄位的數據 System.out.println("id: " + x.get(customer.id) + " name: " + x.get(customer.name)); }); System.out.println("-----------"); for (Customer cus : customerList) { System.out.println(cus); } } } ``` ## 4.6 自製Repositroy(非JPA提供的Interface) * **擴充 Repository 的標準機制**,讓你: * 繼續享受 Spring Data JPA 提供的 CRUD 功能(透過 JpaRepository) * 可以自由實作自訂查詢邏輯(例如使用 Criteria API、QueryDSL、原生 SQL) * **實作規則**: | 元素 | 說明 | | -------------------------- | -------------------------------------------------------------------------------- | | Interface 命名 | `XxxRepositoryCustom` | | 實作類別命名 | `XxxRepositoryCustomImpl` | | 與主 Repository Interface 結合 | `public interface XxxRepository extends JpaRepository<...>, XxxRepositoryCustom` | | Spring 自動綁定 | 根據命名規則,Spring 會自動把 Custom Impl 綁進主 Repository | * **實作類別結尾**須為 `Impl` * **自定義實作後綴**:可用 `@EnableJpaRepositories` 手動調整規則 ```java @EnableJpaRepositories(repositoryImplementationPostfix = "CustomImpl") ``` * **默認**:`String repositoryImplementationPostfix() default "Impl";` * **組成圖**: ```txt MgmtApiLogEntityRepository (interface) │ ├── JpaRepository methods (findById, save, delete, etc.) │ └── MgmtApiLogEntityRepositoryCustom (interface) └── MgmtApiLogEntityRepositoryCustomImpl ← 你實作的邏輯 ``` * **注意**:**MgmtApiLogEntityRepositoryCustomImpl** 需要 **EntityManager** * **代碼**:**interface的多繼承** ```java // 使用JPA與自製Repositroy public MgmtApiLogEntityRepository extend JpaRepository, MgmtApiLogEntityRepositoryCustom{ ... } // 實作的邏輯 public MgmtApiLogEntityRepositoryCustomImpl implement MgmtApiLogEntityRepositoryCustom{ ... } ``` # 5. 多表關聯操作 * JPA並無作擴展,都是hibernate * 關聯操作配置 * ALL 所有持久化操作 * PERSIST 只有『插入』才會執行持久化操作 * MERGE 只有『修改』才會執行持久化操作 * REMOVE 只有『刪除』才會執行持久化操作 * **重點**:`如果沒有設定關聯操作,只會執行該對象的操作,不會一同執行關聯對象的操作` * 懶加載配置 ## 5.1 一對一 * Customer ```roomsql CREATE TABLE public.customer ( id serial4 NOT NULL, address varchar NULL, "name" varchar NULL, account_id int4 NULL, CONSTRAINT customer_pk PRIMARY KEY (id) ); ``` * Account ```roomsql CREATE TABLE public.account ( id serial4 NOT NULL, "name" varchar NULL, pwd varchar NULL, customer_id int8 NULL, CONSTRAINT account_pk PRIMARY KEY (id), ); ``` * 兩個table(Customer、Account) * 透過id連接 * 在Customer 設定Account類 => **單向關聯** * 在Account 設定Customer類 => **單向關聯** * 在Customer 設定Account類 並 在Account 設定Customer類 => **雙向關聯** * 設定關聯的關係 **@OneToOne** * **@OneToOne**中的**屬性** * **cascade**:關聯操作配置 * **CascadeType.ALL**:所有持久化操作 * **CascadeType.PERSIST**:只有`『插入』`才會執行持久化操作 * **CascadeType.MERGE**:只有`『修改』`才會執行持久化操作 * **CascadeType.REMOVE**:只有`『刪除』`才會執行持久化操作 * `如果沒有設定關聯操作,只會對該對象有效果,不會影響其關聯對象` * **fetch**:設置是否懶加載,默認是立即加載 * **FetchType.LAZY**:懶加載`(直到用到相應的對象才會查詢)` * **FetchType.EAGER**:立即加載`(默認)` * **重點**:`懶加載需要配置事務 @Transactional` * 當通過repository`調用完查詢方法時,session就會立即關閉`,一但session關閉就不能查詢 * 添加加了事務 **@Transactional** 後,`直到方法完成後,session才會關閉` * 懶加載**優點**:提高查詢性能 * **orphanRemoval**:`關聯移除 (修改的時候使用)` * **false**:**默認** * **ture**: * 一但把關聯的數據`設置為null`:刪除關聯數據 * `修改`為其關聯數據:刪除關聯數據,並產生新的關聯數據 * **optional**: `限制關聯的對像` 是能否為null * **true**:關聯的部分可以為 null **(默認)** * 當insert、update時,關聯對象可以為null * update時,關聯對象為null,會執行刪除關聯對象的操作 * **false**:不能為null * 當insert、update時,`如果沒有添加關聯對象的時`,會報錯 * **mappedBy**:`將外鍵約束指向另一方維護`**(通常在雙向關聯關係中,會放棄一方的外鍵約束)** * 值:另一方關聯的屬性名 * 設定`本表關聯的外鍵欄位` @JoinColumn ### 5.1.1 單向關係 在Customer 設定Account類 * 在Customer 設定Account類,使用 **@JoinColumn** 設定在Customer的欄位(`account_id`) * Customer ```java @Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor @Table(name = "customer") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "address") private String address; @Column(name = "name") private String name; //單向關聯 //一對一,cascade 配置關聯操作 @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true, optional = false) @JoinColumn(name = "account_id") //通過JoinColumn 設定 外鍵的字段名 private Account account; } ``` * Account ```java @Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor @Table(name = "account") public class Account { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "name") private String name; @Column(name = "pwd") private String pwd; } ``` #### 5.1.1.1 增 * Hibernate: insert into account (name, pwd) values (?, ?) * Hibernate: insert into customer (account_id, address, name) values (?, ?, ?) ```java @SpringBootTest public class OneToOneTest { @Autowired CustomerRepositories customerRepositories; //插入 @Test public void test1() { Account account = new Account(); account.setName("sswjhqkw"); account.setPwd("sswlaks"); Customer customer = new Customer(); customer.setName("ss1klas"); customer.setAddress("ssssl"); customer.setAccount(account); customerRepositories.save(customer); } } ``` #### 5.1.1.2 查 * 支持關聯操作 * 懶加載為什麼需要配置事務 * `當通過repository調用玩查詢方法時,session就會立即關閉,一但session就不能查詢, 加了事務後,直到方法完成後,session才會關閉` ```java @SpringBootTest public class OneToOneTest { @Autowired CustomerRepositories customerRepositories; // 查詢 原本就支持關聯操作 @Test @Transactional(readOnly = true) public void test2() { Optional<Customer> customer = customerRepositories.findById(2L); System.out.println("------"); System.out.println(customer.get()); } } ``` #### 5.1.1.3 刪 ```java @SpringBootTest public class OneToOneTest { @Autowired CustomerRepositories customerRepositories; @Test public void test3() { customerRepositories.deleteById(1L); } } ``` * SQL說明 * Hibernate: select customer0_.id as id1_1_0_, customer0_.account_id as account_4_1_0_, customer0_.address as address2_1_0_, customer0_.name as name3_1_0_ from customer customer0_ where customer0_.id=? * Hibernate: select account0_.id as id1_0_0_, account0_.name as name2_0_0_, account0_.pwd as pwd3_0_0_ from account account0_ where account0_.id=? * Hibernate: delete from customer where id=? * Hibernate: delete from account where id=? #### 5.1.1.4 改 ```java @SpringBootTest public class OneToOneTest { @Autowired CustomerRepositories customerRepositories; //修改 @Test public void test4() { Account account = new Account(); account.setName("9009"); account.setPwd("39392"); Customer customer = new Customer(); customer.setId(2L); customer.setName("2323"); customer.setAddress("83sass28398"); customer.setAccount(account); //設定 orphanRemoval = true > // customer.setAccount(null); 刪除該 account // customer.setAccount(xxx); 刪除該 account,並產生新的 account customerRepositories.save(customer); } } ``` ### 5.1.2 雙向綁定 * 在某一個對象上的 @OneToOne,設定mappedBy屬性,避免實質的DB相互設定FK,減少資源消耗 * 設定 mappedBy 後,將外鍵約束指向另一方維護 **(放棄一方的外鍵約束)** * Customer * 設定**mappedBy="customer"**:`表示Customer不設定跟Account的FK` ```java @Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor @Table(name = "customer") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "address") private String address; @Column(name = "name") private String name; @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true, optional = false, mappedBy = "customer") @JoinColumn(name = "account_id") //通過JoinColumn 設定 外鍵的字段名 private Account account; } ``` * Account ```java @Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor @Table(name = "account") public class Account { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "name") private String name; @Column(name = "pwd") private String pwd; @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "customer_id") private Customer customer; } ``` #### 5.1.2.1 插入 * **沒有設定**mappedBy * 多做一個update的操作,`對應相關欄位的處理` ``` Hibernate: insert into account (customer_id, name, pwd) values (?, ?, ?) Hibernate: insert into customer (account_id, address, name) values (?, ?, ?) Hibernate: update account set customer_id=?, name=?, pwd=? where id=? ``` * **有設定**mappedBy ``` Hibernate: insert into customer (address, name) values (?, ?) Hibernate: insert into account (customer_id, name, pwd) values (?, ?, ?) ``` #### 5.1.2.2 刪除 * **沒有設定**mappedBy * 多做一個update的操作,`對應相關欄位的處理` ``` Hibernate: select customer0_.id as id1_1_0_, customer0_.account_id as account_4_1_0_, customer0_.address as address2_1_0_, customer0_.name as name3_1_0_ from customer customer0_ where customer0_.id=? Hibernate: select account0_.id as id1_0_0_, account0_.customer_id as customer4_0_0_, account0_.name as name2_0_0_, account0_.pwd as pwd3_0_0_, customer1_.id as id1_1_1_, customer1_.account_id as account_4_1_1_, customer1_.address as address2_1_1_, customer1_.name as name3_1_1_ from account account0_ left outer join customer customer1_ on account0_.customer_id=customer1_.id where account0_.id=? Hibernate: update account set customer_id=?, name=?, pwd=? where id=? >> 將customer_id 設置為null,去除 雙向的關聯關係 Hibernate: delete from customer where id=? Hibernate: delete from account where id=? ``` * **有設定**mappedBy ``` Hibernate: select customer0_.id as id1_1_0_, customer0_.address as address2_1_0_, customer0_.name as name3_1_0_ from customer customer0_ where customer0_.id=? Hibernate: select account0_.id as id1_0_1_, account0_.customer_id as customer4_0_1_, account0_.name as name2_0_1_, account0_.pwd as pwd3_0_1_, customer1_.id as id1_1_0_, customer1_.address as address2_1_0_, customer1_.name as name3_1_0_ from account account0_ left outer join customer customer1_ on account0_.customer_id=customer1_.id where account0_.customer_id=? Hibernate: delete from account where id=? Hibernate: delete from customer where id=? ``` ### 5.1.3 虛擬欄位(推薦) * **目標**:Customer中獲取Account類,`但不儲存\不更新 account_id` * **作法**:調整 **@JoinColumn()** 設定 * **insertable**\\**updatable**:`設定為false` * **Customer** ```java @Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor @Table(name = "customer") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "address") private String address; @Column(name = "name") private String name; @OneToOne() @JoinColumn(name = "account_id", referencedColumnName = "account_id", insertable = false, updatable = false) private Account account; } ``` * @**JoinColumn**(name = "account_id", referencedColumnName = "account_id") * **name**:Customer的欄位(`指定外鍵列的名稱`) * **referencedColumnName**:Account的欄位(`指定目標表中的被引用列的名稱`) ## 5.2 一對多 * 概念:一個Customer`(一筆)`,對應多條信息Message`(多筆)` * **預設懶加載** * 使用的註解 **(設定在一的那一邊)** * @OneToMany() * @JoinColumn(name = "customer_id"):字段為多的那一端的字段名稱 ### 5.2.1 設定 * Customer(一) ```java @Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor @Table(name = "customer") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "address") private String address; @Column(name = "name") private String name; //單向關聯 //一對一,cascade 配置關聯操作 @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true, /*optional = false,*/mappedBy = "customer") @JoinColumn(name = "account_id") //通過JoinColumn 設定 本表關聯外鍵的字段名 private Account account; //一對多 @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "customer_id") //設定 多的那一端的字段名稱 private List<Message> messages; } ``` * Message(多) * 會建立一個FK customer.customer_id 對應 customer.id ```java @Entity @Data @Builder @Table(name = "message") public class Message { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "info") private String info; } ``` ## 5.3 多對一 * **預設懶加載** * 使用的註解 * **@ManyToOne** * `可不添加` **@JoinColumn**(name = "customer_id") * `因為『一對多』以設定對應的@JoinColumn` ### 5.3.1 設定 * Customer(一) ```java @Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor @Table(name = "customer") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "address") private String address; @Column(name = "name") private String name; //單向關聯 //一對一,cascade 配置關聯操作 @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true, /*optional = false,*/mappedBy = "customer") @JoinColumn(name = "account_id") //通過JoinColumn 設定 本表關聯外鍵的字段名 private Account account; //一對多 @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "customer_id") //設定 多的那一端的字段名稱 private List<Message> messages; } ``` * Message(多) ```java @Entity @Data @Builder @Table(name = "message") public class Message { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "info") private String info; //多對一 @ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "customer_id")//可不加,因為已在 一對多設定的@JoinColumn private Customer customer; } ``` ## 5.4 一對多 VS 多對一 * 插入: * 當插入 **\"多\"** 的數據時,使用`多對一關係`是更合理 * 查詢: * 當根據 **\"ㄧ\"** 來查詢 **\"多\"** 的數據時,使用`一對多關係`是更合理 ## 5.5 多對多 * 使用 **@ManyToMany**\ **@JoinTable()** * **刪除**: * **不適合刪除**, 因為會經常出現數據,可能除了和`當前這端`關聯還會`關聯另一端`,此時刪除就會出現異常 * 要刪除,要保證沒有額外其他另一端數據關聯 * 保存: * 如果保存的關聯數據,希望使用已有的,就需要從數據庫中查出(持久狀態)。否則,提示游離狀態不能持久化。 * 如果一個業務方法有多個持久化操作,要加上@Transactional,否則不能共用一個Session * 當單元測試中,使用到@Transactional,如果**有增刪改**`一定要加@Commit` * 單元測試會認為你的事務方法@Transactional,只是測試而已,不會提交事務 * **注意**:使用 **@JoinTable()** 關聯中間表後,每次新\刪\修,都會**刪除中間表的數據**(`依據該設定個關聯ID`) * **可能原因**:JPA 會檢驗數據是否一致,當中間表對應到的數據不同時,就會刪除 * 使用 **@JoinTable()** 的解法:將關聯的部分數據,也要一起放入新\刪\修的主要數據中,`無法只有讀的功能` * **其他解**:使用 **@JoinColumn**,**連接中間表**,就`不會觸發刪除\新增` * **問題**:獲取到更多的信息,包括當前的Entity和對應的Entity ### 5.5.1 單向關聯 多對多 #### 5.5.1.1 @JoinTable() * 映射中間表,和該表內對應的外鍵 * **作用**:中間表,需要@JoinTable()來維護外鍵 ` * 會自動產生table * 屬性 * name:中間表名,不設置會自動生成 * joinColumns:中間表 關聯`本table`的欄位名 * inverseJoinColumns:中間表 關聯`另一邊`的欄位名 #### 5.5.1.2 表 * Customer (關聯) ```java package com.nicolas.springDataJPA.pojo; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import javax.persistence.*; import java.util.List; @Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor @Table(name = "customer") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "address") private String address; @Column(name = "name") private String name; @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true, /*optional = false,*/mappedBy = "customer") @JoinColumn(name = "account_id") //通過JoinColumn 設定 本表關聯外鍵的字段名 private Account account; //一對多 @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "customer_id") //設定 多的那一端的字段名稱 private List<Message> messages; //單向多對多 @ManyToMany(cascade = CascadeType.ALL ) //中間表,需要@JoinTable()來維護外鍵 @JoinTable( name = "customer_role", //中間表名 joinColumns = {@JoinColumn(name = "c_id")}, //中間表 關聯本table的欄位名 inverseJoinColumns = {@JoinColumn(name = "r_id")} //中間表 關聯另一邊的欄位名 ) private List<Role> roles; } ``` * Role ```java @Entity @Data @Builder @Table(name = "role") public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "name") private String name; public Role(String name) { this.name = name; } public Role(Long id, String name) { this.id = id; this.name = name; } public Role() { } } ``` #### 5.5.1.3 儲存 ```java @SpringBootTest public class ManyToManyTest { @Autowired CustomerRepositories customerRepositories; @Autowired RoleRepositories repositories; @Test public void test1(){ List<Role> roles =new ArrayList<>(); roles.add(new Role("AA")); roles.add(new Role("BB")); Customer customer = new Customer(); customer.setRoles(roles); customerRepositories.save(customer); } //插入已保存关联数据 @Test @Transactional //保持session開啟,值到方法結束關閉 @Commit public void test2(){ List<Role> roles =new ArrayList<>(); roles.add(repositories.findById(1L).get()); roles.add(repositories.findById(2L).get()); //需要重數據庫拿出 >> 為持久狀態 Customer customer = new Customer(); customer.setName("TESEEE"); customer.setRoles(roles); customerRepositories.save(customer); } } ``` # 6. 樂觀鎖 * 屬於hibernate * 防止並發修改 * 作法: * 建立一個欄位來紀錄該筆資料是否有變動,`默認為0` * 當修改時,該欄位會變動 * 使用 **@Version** 標記該**屬性**(`會自動創建該欄位`) * private **@Version** Long version # 7. 審計功能 @EnableJpaAuditing ## 7.1 如何使用審計功能 1. 編寫 **AuditorAware\<T>** bean: 返回當前用戶 * **@CreatedBy**、**@LastModifiedBy**的類型是**String**,所以`返回AuditorAware的泛型`就設定**String** ```java @Configuration //標記當前類為配置類 public class SpringDataConfig { //配置審計 // AuditorAware 返回當前用戶, // @CreatedBy、@LastModifiedBy的類型是String,所以返回AuditorAware的泛型就設定String @Bean public AuditorAware<String> auditorAware(){ return new AuditorAware<String>() { @Override public Optional<String> getCurrentAuditor() { // 從 session、redis、... 取出 當前用戶 return Optional.of("AAA"); } }; } } ``` 2. 實體類 設定相應的註解 * 開發方式:`可以寫在一個基礎Entity,其他再去繼承這個基礎Entity` * 添加 **@EntityListeners(AuditingEntityListener.class)** * 設定`四個屬性`,加上`對應的註解` * **創建人**: **@CreatedBy** * **修改人**:**@LastModifiedBy** * **創建時間**: **@Temporal(TemporalType.TIMESTAMP)**、**@CreatedDate** * **修改時間**: **@Temporal(TemporalType.TIMESTAMP)**、**@LastModifiedDate** ```java @EntityListeners(AuditingEntityListener.class) @Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor @Table(name = "customer") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "address") private String address; @Column(name = "name") private String name; //樂觀鎖 private @Version Long version; //審計 //創建人 @CreatedBy private String createBy; //修改人 @LastModifiedBy private String modifiedBy; //實體創建時間 @Temporal(TemporalType.TIMESTAMP) @CreatedDate private Date dateCreated = new Date(); //實體修改時間 @Temporal(TemporalType.TIMESTAMP) @LastModifiedDate private Date dateModified = new Date(); } ``` 3. Application 中 **啟動 審計功能 ** * **啟動類**或**配置類** 添加 `@EnableJpaAuditing` ```java @EnableJpaAuditing //啟動 審計功能 @Configuration //標記當前類為配置類 public class SpringDataConfig { //配置審計 // AuditorAware 返回當前用戶, // @CreatedBy、@LastModifiedBy的類型是String,所以返回AuditorAware的泛型就設定String @Bean public AuditorAware<String> auditorAware(){ return new AuditorAware<String>() { @Override public Optional<String> getCurrentAuditor() { // 從 session、redis、... 取出 當前用戶 return Optional.of("AAA"); } }; } } ``` # 8. 查詢View * 使用**接口**來獲取資訊 * 獲取方法名:**get欄位名()** * **View** | uid | provider_name | | -------- | -------- | | wDcKeT+ARpGGxvg2luAwVA | Box-local | * 目標:獲取上述View中的數據 * 接口 ```java= public interface ProviderViewData { String getUid(); String getProvider_name(); } ``` * DAO ```java= import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.util.List; public interface CatalogViewRepositoryPeer extends JpaRepository<_Data, Long> { @Query(value = "SELECT * FROM provider_catalog_view ;",nativeQuery = true) List<ProviderViewData> findAllProviderViews(); } ``` # 9. log ## 顯示SQL * org.hibernate.SQL=DEBUG ## 顯示參數 * org.hibernate.orm.jdbc.bind=TRACE # X. 開發實際注意事項 ## X.1 注意一級緩存 * **一級緩存**:用於**儲存已查詢的數據**,以減少資料庫查詢的次數,提高查詢效率。 * 一個**Transactional**中`如果邏輯中間commit的話`,並再次查詢該物件時,要確**保再commit()前**沒有查詢該物件 * **不然會因為一級緩存,而獲取舊的物件** ## X.2 @Transactional的作用範圍:父子 * **@Transactional**`只能作用到父子`,無法作用到父子孫 * 如果為父子孫連接,該連接`至少要添加2個@Transactional`,分別為`[父子、父孫]` * 父層為**service層**所以一定要添加@Transactional * 前提:**service層**、**repositories層**、**DAO層** * `通常會在service層添加@Transactional` * 例1:@Transactional **無效** ```mermaid graph TB; A[DAO 層] --> B[repositories層]; B --> C[service層]; D("@Transactional")-.添加.- C ``` * 例2:@Transactional **有效** * **DAO 層**、**service層添加** `@Transactional` ```mermaid graph TB; A[DAO 層] --> B[repositories層]; B --> C[service層]; E("@Transactional")-.添加.- A D("@Transactional")-.添加.- C ``` * **repositories層**、 **service層添加** `@Transactional` ```mermaid graph TB; A[DAO 層] --> B[repositories層]; B --> C[service層]; E("@Transactional")-.添加.- B D("@Transactional")-.添加.- C ``` ## X.3 多個DB連線 (需要指定TransactionManager) * 一個**DB連線** == 一個**TransactionManager**(`事務管理器`) ### X.3.1 聲明式事務 * 使用 **@Transactional** 註解 * **設定對應的 TransactionManager** (`事務管理器`) * **例**:**TransactionManager** 的 BeanName(`TestTransactionManager`) => **@Transactional**(`TestTransactionManager`) ### X.3.2 編程式事務 * **TransactionManager** 的 BeanName(`TestTransactionManager`) ```java= import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; @Service public class TestUseCase { private final PlatformTransactionManager platformTransactionManager; public TestUseCase(@Qualifier("TestTransactionManager") PlatformTransactionManager platformTransactionManager) { this.platformTransactionManager = platformTransactionManager; } public void doTemplate() { TransactionTemplate transactionTemplate = new TransactionTemplate(); //設定當前的事務管理器 transactionTemplate.setTransactionManager(platformTransactionManager); transactionTemplate.execute(status -> { try { //執行事務操作 return "A"; } catch (Exception e) { //回滾 status.setRollbackOnly(); throw new RuntimeException(e.getMessage()); } }); } } ``` ## X.4 Service層捕獲SQL的Exception * **案例**:Service層的 save()**捕捉unique key的Exception** * **代碼**: ```java @Service public class ModelServiceImpl implements ModelService { public final ModelRepository modelRepository; public ModelServiceImpl(@Qualifier("modelRepositoryImpl") ModelRepository modelRepository) { this.modelRepository = modelRepository; } @Override @Transactional public String save(Model model) { try{ model.setModelId(UUID.randomUUID().toString().replace("-", "")); Model save = modelRepository.save(model); return save.getModelId(); }catch(Exception e){ throw new RuntimeException(e.getMessage()) } } } ``` * **當前問題**:**無法捕獲到unique key的Exception** * **原因**:該Exception的觸發,要在事務提交時,才會發生 => `添加 @Transactional 這個方法結束時,事務才會提交` * 因此會由呼叫該save()的方法,可以捕捉到此錯誤(`Controller層可以捕獲`) ### X.4.1 解決方式 * **法一**: 該 Service層的save() 方法不要添加@Transactional,由Repository層的save()開啟事務 * **法二**:`不去捕捉錯誤,自行檢測是否重複` ###### tags: `Spring` `SpringBoot` `SpringData JPA` `JPA`