---
# System prepended metadata

title: SpringData JPA 學習
tags: [SpringDataJPA, SpringBoot, Java]

---





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