# 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`