<h1>Spring Boot</h1> <h2>DB</h2> <h3>JDBC & JPA</h3> <h4>JDBC</h4> DBC (Java Database Connectivity) 顧名思義,就是透過 Java 來連接資料庫的一種技術,它可以透過每個資料庫所對應的 JDBC Driver 來與資料庫進行連接,並透過 JDBC 的 API 來向所指定的資料庫發送想要執行的 SQL 命令。 這裡我們以 SQL Server 為例,在 SQL Server ─ 使用 Java 連線到 SQL 的範例中,我們可以看到光是要透過 JDBC 與資料庫進行連線的方法就已經有點繁瑣了,再加上執行查詢或是加上資料的部分,也必須組合出依照對應資料庫所合法的 SQL 語句,這樣的方式非常容易造成程式碼的不易撰寫,也容易在組合 SQL 命令的時候容易出錯,更沒辦法在想要切換不同資料庫時有較好的體驗。 基於種種不方便的原因,因此在後期就開始漸漸捨棄直接以 JDBC 的方式進行開發,而開始有了一種新的實現理念 ─ ORM。 <h4>ORM</h4> ORM (Object-Relational Mapping) ORM,若是照著字義上來翻譯,那就是「物件關係的映射」,實際上它其實只是一種概念。 在程式語言中,通常在表示資料都是以「物件的方式」來撰寫,例如在 Java 中表示物件的方法: ``` public class Product { private Long id; private String name; private Integer remain; public Product() { } public Product(Long id, String name, Integer remain) { this.id = id; this.name = name; this.remain = remain; } /* getter and setter ... */ } ``` 而對應在「關聯式資料庫 (RDBMS)」中,表示資料的形式則會是: ``` +---------------------+ | Product | +====+=======+========+ | ID | NAME | REMAIN | +----+-------+--------+ | 1 | apple | 10 | +----+-------+--------+ | 2 | book | 15 | +----+-------+--------+ ``` 在兩者之間,存在著一種對應的映射關係,這種映射關係的概念,就是所謂的 ORM。而 ORM 概念的誕生,其實也是因為物件導向程式開發的崛起。 當物件與表資料之間存在映射關係的時候,就可以讓我們在開發程式時,無須操作許多繁瑣的 SQL 語句,同時在資料的安全面上,也可以避免 SQL 語句的一些惡意注入攻擊。因此在 ORM 的概念上,是可以減少那些對資料庫進行基本操作的程式開發。 不過既然是具有映射的關係,就代表著在有些時候效能可能不如原本來的好。另外雖然 ORM 的概念可以減輕那些 SQL 的學習成本,但其實也就只是學個半套,若對於資料庫本身的基本設計不清楚,那麼在資料庫的設計與維護上,會更容易有力不從心的感覺。 所以 ORM 的概念是可以將資料提取並應射程物件的形式,但如果對於需要做複雜的操作,像是多個表之間的 join 或查詢,比起直接下 SQL 語句是相較來的稍微綁手綁腳些。 <h4>JPA</h4> JPA (Java Persistence API) 以字義上來翻譯,JPA 就是「Java 持久化的 API」,但甚麼叫做持久化? 對於 ORM 的概念來說,其最主要的想法就是可以簡易的將要儲存的資料從物件轉換為資料庫可以讀懂的格式,也可以簡易的將要從資料庫取出的資料映射成程式語言可以讀懂的物件型態。 而在將資料「儲存」與「讀取」的過程,就稱之為「持久化」,也就是將資料從瞬時狀態改為持久狀態的一個過程。或者更白話一點的說,持久化就是將資料儲存到資料庫的一種過程。 從上面的說明來理解,簡單的說「透過 Java 將資料儲存到資料庫的 API」就叫做 JPA。不過實際上 JPA 只是一種規範 (或者叫他 Interface) 而已,它是由 Sun (官方)所統一的規範,規範了「映射物件與持久層的 API 的實作方式」,像是你可以透過 JPA 來映射「物件的欄位」與「表的欄位」之間的對應關係: ``` import javax.persistence.*; @Entity @Table(name = "PRODUCT") public class Product { @Id @Column(name = "ID") private Long id @Column(name = "NAME") private String name; @Column(name = "REMAIN") private Integer remain; /* constructors ... */ /* getter and setter ... */ } ``` 也因為統一了不同的 ORM 框架的實現技術,因此在面對一致的 JPA 規範時,開發者也就可以隨時切換想要使用的 ORM 框架了。 另外,使用 JPA 還有一個好處,那就是可以使用 JPQL (Java Persistence Query Language) 的命令語句向資料庫下命令語句,但 JPQL 與 SQL 最大的差異在於: * SQL 在不同的資料庫中,有不同的 SQL 命令語句 * JPQL 操作的對象不是著重在資料庫,而是著重在 JPA 的 Entity Object 下類似 SQL 的命令語句 也就是說當使用 JPQL 的時候,並不會隨著不同的資料庫而需要做對應 SQL 語句修改。 <h4>Spring JDBC v.s Spring Data JPA </h4> 資料庫的工具中,可以分成兩類: 1. 在 Spring Boot 中執行 sql 語法,去操作資料庫 * 這一類的工具,就是直接在 Spring Boot 中去執行原始的 sql 語法,然後透過這些 sql 語法去存取資料庫的數據 * Spring JDBC 和 MyBatis 都屬於這一類 2. 使用 ORM 的概念,去操作資料庫 * 這一類的工具,則是會透過 ORM (Object Relational Mapping) 的概念,去操作資料庫 * 所以只要使用這類的工具,基本上就很少寫 sql 語法了,而是會套用另一種新的概念(即是 ORM),去存取資料庫的數據 * Spring Data JPA 和 Hibernate 都屬於這一類 所以回到最一開始的問題「Spring JDBC 和 Spring Data JPA 的差別在哪裡?」的話,就是 Spring JDBC 是透過執行 sql 語法去操作資料庫,而 Spring Data JPA 則是透過 ORM 的概念去操作資料庫 也因為這兩種概念差比較多,因此此系列文只會介紹 Spring JDBC 的部分,也就是介紹要如何在 Spring Boot 中去執行 sql 語法,進而去操作資料庫 ![image](https://hackmd.io/_uploads/ryvWuWqeC.png) <h4>CRUD</h4> CRUD 所代表的,是「資料庫」中的「Create (新增)、Read (查詢)、Update (修改)、Delete (刪除) 操作」的統稱,用以表示資料庫中最基礎的那些操作 * Create(新增) * Read(查詢) * Update(修改) * Delete(刪除) ![image](https://hackmd.io/_uploads/rkIQYWqgR.png) 參考網站 : https://medium.com/learning-from-jhipster/13-%E7%94%9A%E9%BA%BC%E6%98%AF-jdbc-orm-jpa-orm%E6%A1%86%E6%9E%B6-hibernate-c762a8c5e112 https://ithelp.ithome.com.tw/m/articles/10336253 <hr> <h3>資料庫連線設定</h3> <h4>步驟一:<br> 在 pom.xml 載入 Spring JDBC、資料庫 Driver ( 以MySQL為例 )</h4> 想要在 Spring Boot 中使用 Spring JDBC 的功能的話,首先會需要在 pom.xml 這個檔案裡面新增以下的程式,將 Spring JDBC 的功能給載入進來 ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> ``` 同時也得加入對應的資料庫 driver (驅動程式),這樣 Spring Boot 後續才能去操作該資料庫,需要在 pom.xml 檔案裡面,再新增以下的程式,將 MySQL 的 driver 給載入進來 ``` <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> ``` <h4>步驟二:<br> 在 Spring Boot 中設定資料庫連線資訊</h4> 要在 Spring Boot 中設定資料庫的連線資訊的話,需要打開 resources 資料夾底下的 application.properties 檔案 ( application.properties 檔案是 Spring Boot 的設定檔,用來存放 Spring Boot 中的設定值) 在這個檔案裡面,添加以下的程式,這樣就完成了 MySQL 資料庫的連線設定了 ``` spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/myjdbc?serverTimezone=Asia/Taipei&characterEncoding=utf-8 spring.datasource.username=root spring.datasource.password=springboot ``` <h5>1. spring.datasource.driver-class-name</h5> 表示要使用的是哪種資料庫的 driver,此處我們填寫的是 MySQL 的 driver <h5>2. spring.datasource.url</h5> 表示要連接到哪台資料庫上,此處的 jdbc:mysql://localhost:3306 是表示要連接到我們自己電腦上的 MySQL 資料庫裡面,而後面跟著的 /myjdbc,是指定要連線到 MySQL 資料庫中的 myjdbc 這個 database。 並且在 myjdbc 後面的 ? 處,有加上兩個參數: serverTimezone=Asia/Taipei 是表示我們指定所使用的時區是台北時區,而 characterEncoding=utf-8 則表示,我們所使用的編碼是 utf-8(這樣在處理中文的時候才不會出現亂碼) <h5>3. spring.datasource.username</h5> 要填入 MySQL 資料庫中的帳號,此處填上預設的帳號 root <h5>4. spring.datasource.password</h5> 要填入上面那個帳號的密碼,此處填上 springboot 參考網站 : https://ithelp.ithome.com.tw/articles/10337137 <hr> <h3>JDBC 的用法</h3> ![image](https://hackmd.io/_uploads/rJnRlfcgR.png) <h4>update()</h4> * INSERT * UPDATE * DELETE <h5>基本用法</h5> **步驟一: 注入一個 NamedParameterJdbcTemplate** 可以先使用 *@Autowired*,將NamedParameterJdbcTemplate 注入進來 ``` @Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; ``` ![image](https://hackmd.io/_uploads/S1Ow7f5eC.png) **步驟二:撰寫 sql 語法** 可以去創建一個 String 類型的變數 sql,並且在裡面寫上到時候想要執行的 sql 語法 ![image](https://hackmd.io/_uploads/ByRzEM9eR.png) **步驟三:新增一個 Map<String, object> 的 map 變數** 去新增一個類型為 Map<String, object> 的 map 變數出來 ``` Map<String, Object> map = new HashMap<>(); ``` ![image](https://hackmd.io/_uploads/H1SCrGcgA.png) **步驟四:使用 update() 方法** 最後一步,就是去使用 namedParameterJdbcTemplate 的 update() 方法,並且把上面所新增的 sql 和 map 這兩個變數,依照順序的給傳進去 ``` namedParameterJdbcTemplate.update(sql, map); ``` ![image](https://hackmd.io/_uploads/HkhP8M9l0.png) <h4>update() 中的 map 參數用法</h4> **例子:根據前端的參數,動態的決定 sql 中的值** 1. 可以先創建一個 Student class 出來,並且在裡面創建兩個變數 id 和 name(和其對應的 getter 和 setter),程式如下: ``` public class Student { private Integer id; private String name; // getter 和 setter public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } ``` 2. 接著在 StudentController 裡,在 insert() 方法的參數部分,使用 @RequestBody,去接住前端傳過來的參數 ![image](https://hackmd.io/_uploads/ryoivz5lA.png) 3. 接住前端所傳遞的變數之後,接著我們就可以來運用 map 變數,將前端所傳過來的 id 和 name 的值,插入一筆數據到資料庫中 * **sql 語法**:將「3 和 John」的地方,改成「:studentId 和 :studentName」 * **map 變數**:在 map 變數中 put 兩組 key-value 的值進去 ![image](https://hackmd.io/_uploads/ryRv_GcxR.png) <h4>query()</h4> * SELECT <h5>query() 的用法</h5> query() 方法的前兩個參數和 update() 方法一樣,都是先放入「要執行的 sql 語法」,接著是放入「動態決定 sql 變數的 map」 而 query() 方法特別的地方,就在於他的第三個參數 RowMapper ![image](https://ithelp.ithome.com.tw/upload/images/20231012/20151036m57twtjdOE.png) <h5>RowMapper 的用途</h5> 在 query() 方法中的第三個參數 RowMapper,他的用途,就是「將資料庫查詢出來的數據,轉換成是 Java object」 像是我們可以創建一個新的 StudentRowMapper class,然後讓他去 implements RowMapper 這個 interface,實作如下的程式: ``` public class StudentRowMapper implements RowMapper<Student> { @Override public Student mapRow(ResultSet rs, int rowNum) throws SQLException { Student student = new Student(); student.setId(rs.getInt("id")); student.setName(rs.getString("name")); return student; } } ``` 參考網站 : https://ithelp.ithome.com.tw/articles/10337891 https://ithelp.ithome.com.tw/articles/10338405 <hr> <h3>JPA 的用法</h3> ![image](https://hackmd.io/_uploads/rynNDwvzC.png) **一:添加相依性( MySQL範例 )** 添加Spring Data JPA的相關依賴。在`pom.xml`文件中,確保以下依賴被包含: ``` <dependencies> <!-- 其他相關依賴... --> <!-- Spring Data JPA --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- 資料庫驅動依賴 --> <dependency> <groupId>org.mysql</groupId> <artifactId>mysql</artifactId> </dependency> </dependencies> ``` <hr> **二: 新增 `application.property` 檔案內容( MySQL範例 )** ``` spring.application.name = 資料庫名稱 spring.datasource.url = jdbc:mysql://localhost:3306/資料庫名稱?serverTimezone=UTC&characterEncoding=utf8 spring.datasource.username = root spring.datasource.password = 密碼 spring.jpa.database-platform = org.hibernate.dialect.MySQL8Dialect spring.jpa.hibernate.ddl-auto = update spring.jpa.show-sql = true ``` <hr> **三:創建 Entity** * 定義類: 創建一個普通的 Java 類來表示資料庫表。這個類可以包含屬性,這些屬性將對應於表中的列。 * 添加註釋: 使用 JPA 註釋來標識實體類,以告訴 Spring Data JPA 如何映射它到資料庫表。一些常見的註釋包括: * `@Entity`: 標識這個類是一個 JPA 實體。 * `@Table`: 指定與實體關聯的資料庫表的名稱。 * 定義主鍵: 使用 `@Id` 註釋來指定實體的主鍵屬性。主鍵屬性的值必須是唯一的,它將用於在資料庫中標識每個記錄。 * 映射屬性: 使用不同的註釋來映射屬性,如 `@Column` 用於指定屬性與表中列的映射關係。 * 關聯關係: 如果實體類之間存在關聯關係(例如,`@OneToMany` `@OneToOne` `@ManyToMany`),可以使用註釋來定義這些關係。 ``` import ... @Entity @Table(name = "reader") public class Reader { @Id // 主鍵 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "reader_seq_generator") // @GeneratedValue 標記了該欄位的值是自動生成的。 // strategy = GenerationType.SEQUENCE 指定了使用序列產生策略,表示資料庫中會使用一個序列來生成唯一識別符。 // generator = "reader_seq_generator" 指定了使用哪個名稱為 "reader_seq_generator" 的序列生成器。這個名稱必須與 @SequenceGenerator 中指定的名稱一致。 @SequenceGenerator(name = "reader_seq_generator", sequenceName = "reader_seq", allocationSize = 1) // @SequenceGenerator 是用來定義序列生成器的設定。 // name = "reader_seq_generator" 指定了序列生成器的名稱,這個名稱會被 @GeneratedValue 中的 generator 屬性所引用。 // sequenceName = "reader_seq" 指定了在資料庫中使用的序列的名稱。 // allocationSize = 1 指定了每次分配給序列的增量大小。在這裡設置為 1 表示每次生成的值都是不同的且唯一的。 private Integer readerId; @Column(nullable = true, name = "readerName") private String readerName; @Column(nullable = true, name = "readerPassword") private String readerPassword; @OneToMany(mappedBy = "reader") // 代表此讀者借閱的書籍列表。 // 由 Book 實體類別中的 "reader" 屬性映射而來。 private List<Book> readerBorrowList; public Reader() {} // 一些框架(如Spring)在創建實體類的實例時,通常會使用預設建構子。 // 如果沒有預設建構子,框架可能無法正常地創建該類別的實例。 public Reader(Integer readerId, String readerName, String readerPassword, List<Book> readerBorrowList) { this.readerId = readerId; this.readerName = readerName; this.readerPassword = readerPassword; this.readerBorrowList = readerBorrowList; } ``` * 補充(GeneratedValue strategy): 1. IDENTITY: 使用資料庫的自動增量策略來生成唯一識別符。通常是透過資料庫的特殊關鍵字(如 MySQL 的 AUTO_INCREMENT)來實現。這意味著資料庫會在插入新紀錄時自動生成識別符。 2. SEQUENCE: 使用資料庫中的序列來生成唯一識別符。這通常用於支援不支援自動增量的資料庫,如 Oracle。@SequenceGenerator 註釋用於定義序列生成器的屬性。 3. TABLE: 使用資料庫中的特殊表來存儲和生成唯一識別符。這個策略的性能可能比其他策略稍差,因為它需要在表中保留狀態。 4. AUTO: 這是一個由 JPA 提供者自動選擇的策略,它會根據資料庫的特性來決定使用哪種生成方式,通常會優先選擇 IDENTITY 或 SEQUENCE。 <hr> **四:創建 Repository** Spring Data JPA Repository 是用於執行資料庫操作的介面。Spring Data JPA 將自動為我們生成實現這些操作的程式碼,從而減少了重複性工作。 ``` public interface ReaderRepository extends JpaRepository<Reader, Integer>{ Reader findByReaderId(int readerId); } ``` <hr> **五:創建 Controller -- 資料庫基本CRUD操作** * **前置動作** ``` @RequestMapping(path = "library/service", produces = MediaType.APPLICATION_JSON_VALUE) @RestController("testConV2") public class Api { @Autowired private ReaderRepository readerRepository; ... } ``` * **新增記錄 (Create)** 要向資料庫中插入新的記錄,可以使用 Spring Data JPA 提供的 ***save*** 方法 ![image](https://hackmd.io/_uploads/rkyvQHIW0.png) ``` @PostMapping("addReader") public ResponseEntity<String> addReader(@RequestBody Reader readerInfo) { readerRepository.save(readerInfo); return ResponseEntity.ok("Ok, 已新增讀者"); } ``` * **查詢記錄 (Read)** Spring Data JPA 提供了多種方式來查詢記錄。一種常見的方式是**使用方法命名來約定** ![image](https://hackmd.io/_uploads/SJKXmHUbA.png) ``` @GetMapping("loginReader") public ResponseEntity<Reader> loginReader(@RequestParam int readerId, @RequestParam String readerPassword) { Reader reader = readerRepository.findByReaderId(readerId); if (reader != null && reader.getReaderPassword().equals(readerPassword)) { return ResponseEntity.ok(reader); } else { return ResponseEntity.notFound().build(); } } ``` * **更新記錄 (Update)** 首先需要**先查詢出要更新的記錄**,然後**修改**其屬性並**保存回資料庫**。 ![image](https://hackmd.io/_uploads/BkHM8SL-R.png) 1. 首先使用 **findById** 查詢 Reader內的 readerId 記錄 2. 然後使用 **set** 更新紀錄 3. 最後使用 **save** 保存回資料庫 ``` @PutMapping("editReader") public ResponseEntity<Reader> editReader(@RequestParam Long readerId, @RequestBody Reader readerInfo) { Optional<Reader> readerOptional = readerRepository.findById(readerId); if (readerOptional.isPresent()) { Reader existingReader = readerOptional.get(); existingReader.setReaderName(readerInfo.getReaderName()); existingReader.setReaderPassword(readerInfo.getReaderPassword()); existingReader.setReaderClassification(readerInfo.getReaderClassification()); existingReader.setReaderIsValid(readerInfo.isReaderIsValid()); readerRepository.save(existingReader); return ResponseEntity.ok(existingReader); } else { return ResponseEntity.notFound().build(); } } ``` * **刪除記錄 (Delete)** 首先需要**先查詢出要刪除的記錄**,然後再**刪除**其屬性 ![image](https://hackmd.io/_uploads/HkCIABL-R.png) 1. 首先使用 **findById** 查詢 Reader內的 readerId 記錄 2. 然後使用 **deleteById** 刪除紀錄 <hr> **六:自訂查詢方法** * **方法命名約定** 在 Spring Data JPA 中,方法命名約定是一種快速建立查詢方法的方式。例如,如果要根據姓氏和名字查詢 Employee 記錄,可以按照以下方式命名方法: ``` List<Employee> findByLastNameAndFirstName(String lastName, String firstName); ``` Spring Data JPA 將根據方法名自動生成 SQL 查詢,查詢姓氏和名字同時匹配的 Employee 記錄。 除了使用 `And` 連接多個條件,還可以使用 `Or`、`Between`、`GreaterThan` 等關鍵詞來建立方法名。這樣可以快速生成各種複雜的查詢方法。 * **使用 `@Query` 註解** 除了方法命名約定,Spring Data JPA 還允許使用`@Query`註解來定義自訂 JPQL(Java Persistence Query Language) 或 SQL 查詢。 ``` @Query("SELECT e FROM Employee e WHERE e.lastName = :lastName") List<Employee> findCustomByLastName(@Param("lastName") String lastName); ```` 上述程式碼使用 `@Query` 註解定義了一個自訂查詢,查詢姓氏為指定值的 Employee 記錄。`@Param` 註解用於指定參數名稱。 * **Native SQL 查詢** 如果需要執行原生 SQL 查詢,可以使用 `nativeQuery` 屬性將查詢指定為原生 SQL。 ``` @Query(value = "SELECT * FROM employees WHERE last_name = :lastName", nativeQuery = true) List<Employee> findCustomByLastName(@Param("lastName") String lastName); ``` 上述程式碼中,`nativeQuery = true`表示這是一個原生 SQL 查詢。 * **方法名與`@Query`混合使用** 有時候,我們需要將方法名約定和`@Query`註釋混合使用以滿足更複雜的查詢需求。 ``` @Query("SELECT e FROM Employee e WHERE e.lastName = :lastName AND e.age >= :minAge") List<Employee> findCustomByLastNameAndMinAge(@Param("lastName") String lastName, @Param("minAge") int minAge); ``` 上述程式碼中,我們同時使用了方法名約定和 `@Query` 註釋來創建查詢方法,查詢姓氏為指定值且年齡大於等於指定值的 Employee 記錄。 通過自定義查詢方法,你可以靈活地執行各種查詢操作,無論是簡單的條件查詢還是複雜的連接查詢。 <hr> **七:數據關聯與映射** 在實際應用程式中,數據之間的關係通常比單個實體更複雜。Spring Data JPA 提供了豐富的功能來管理實體之間的關聯關係,包括一對一、一對多、多對一和多對多。本節將介紹如何在實體類中定義關聯關係以及如何使用 Spring Data JPA 註解來映射這些關係。 * **什麼是關聯映射?** 關聯映射指的是實體類之間的關係如何映射到資料庫表之間的關係。常見的資料庫關聯包括一對一、一對多、多對一和多對多關聯。 在 Spring Data JPA 中,我們使用 JPA 註解來定義實體類之間的關聯關係,然後 JPA 將自動處理這些關聯的映射。 * **一對一關聯** 一對一關聯映射是指兩個實體類之間存在唯一的關聯,例如,一個人只能有一個護照號碼,而一個護照號碼也只能對應一個人。下面是一個示例,展示如何在 Spring Data JPA 中實現一對一關聯。 ``` @Entity public class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToOne(mappedBy = "person", cascade = CascadeType.ALL) private Passport passport; } ``` ``` @Entity public class Passport { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String number; @OneToOne @JoinColumn(name = "person_id") private Person person; } ``` 上述示例中,`Person` 實體類和 `Passport` 實體類之間建立了一對一關聯。`Person` 類中的 `passport` 屬性使用 `@OneToOne` 註解來定義關聯,並使用 `mappedBy` 屬性指定關聯關係由 `Passport` 類的 `person` 屬性來維護。這樣,當保存或刪除 `Person` 實例時,相應的 `Passport` 實例也會被保存或刪除。 * **一對多關聯** 讓我們以一對多關聯關係為例,假設我們有兩個實體類:Author 作者和 Book 書籍。一個作者可以寫多本書,而一本書只有一個作者。以下是示例程式碼: ``` @Entity public class Author { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "author") private List<Book> books; } ``` ``` @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; @ManyToOne @JoinColumn(name = "author_id") private Author author; } ``` 在上述程式碼中,`Author` 實體類使用 `@OneToMany` 註解來定義了一對多關聯關係,`mappedBy` 屬性指定了在 `Author` 實體中與之關聯的屬性是 `books`。而 `Book` 實體類使用 `@ManyToOne` 註解來定義多對一關聯關係,並通過 `@JoinColumn` 註解指定了外鍵列的名稱。 這個關係描述了一個作者可以寫多本書,而一本書也只能有一個作者。 * **多對多關聯** 讓我們再看一個多對多的關聯關係的示例,假設我們有兩個實體類:Student 學生和 Course 課程。一個學生可以選修多門課程,一門課程也可以被多個學生選修。以下是示例程式碼: ``` @Entity public class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @ManyToMany @JoinTable( name = "student_course", joinColumns = @JoinColumn(name = "student_id"), inverseJoinColumns = @JoinColumn(name = "course_id") ) private List<Course> courses; } ``` ``` @Entity public class Course { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; @ManyToMany(mappedBy = "courses") private List<Student> students; } ``` 在上述程式碼中,`Student` 實體類和 `Course` 實體類都使用 `@ManyToMany` 註解來定義了多對多關聯關係。通過 `@JoinTable` 註解,我們定義了一個名為 `student_course` 的中間表來維護這個關係。`joinColumns` 屬性指定了學生表的外鍵列,`inverseJoinColumns` 屬性指定了課程表的外鍵列。 這個關係描述了一個學生可以選修多門課程,而一門課程也可以被多個學生選修。 <hr> <h3>參考網站 :</h3> https://ithelp.ithome.com.tw/articles/10325963 https://ithelp.ithome.com.tw/articles/10326749 <hr> <h2>Jasypt</h2> <h3>Jasypt 介紹</h3> 在系統開發過程中,常常需要處理敏感資訊,例如資料庫帳號密碼、API 金鑰等。 如果這些資訊直接以明碼存放在程式或設定檔中,將會造成極大的安全風險。 **Jasypt**(Java Simplified Encryption)是一個簡單易用的 Java 加密工具庫,透過直覺的 API 與配置方式,開發者不需要深入研究加密演算法,就能快速在程式中實現加密與解密,保護敏感資訊不被外洩。 <h3>如何使用 jasypt 加密</h3> - **加密敏感資訊**:利用 Jasypt 提供的工具或程式 API,將帳號、密碼等明碼轉換成加密字串。 - **引入依賴**:在 Spring Boot 專案中加入 Jasypt 依賴,讓應用程式能自動辨識並處理加密字串。 - **應用設定**:將生成的 `ENC(...)` 加密字串放入 `application.properties` 或 `application.yml`,在系統啟動時自動解密供程式使用。 <h4>Step1. 透過 jasypt 加密,取得加密後字串</h4> 1. 寫個小程式加密 ``` private static void EncryptAndDecrypt(String plainText) { StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor(); EnvironmentPBEConfig config = new EnvironmentPBEConfig(); config.setAlgorithm("PBEWithMD5AndDES"); config.setPassword("EbfYkitulv73I2p0mXI50JMXoaxZTKJ7"); // 加密密鑰 encryptor.setConfig(config); System.out.println("加密前: " + plainText); String encrypted = encryptor.encrypt(plainText); System.out.println("加密後: " + encrypted); String decrypted = encryptor.decrypt(encrypted); System.out.println("解密後: " + decrypted); } ``` 2. 透過 Command Line 執行 jar 加密 1) 下載 jasypt jar ``` curl -L -o jasypt-1.9.3.jar https://repo1.maven.org/maven2/org/jasypt/jasypt/1.9.3/jasypt-1.9.3.jar ``` 2) 加密你要的明文(例:把 sqladmin 加密) ``` java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input=sqladmin password=wpgxxxx algorithm=PBEWithMD5AndDES keyObtentionIterations=1000 providerName=SunJCE saltGeneratorClassName=org.jasypt.salt.RandomSaltGenerator stringOutputType=base64 ``` 執行後會印出一串 Base64 密文,把它包起來: ``` ENC(3CmtiX0umHVl5hccouTFNaaNN9tNE6FR) ``` 貼回 application.yml/.properties 就完成了。 3) 解密驗證一下 ``` java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringDecryptionCLI input=3CmtiX0umHVl5hccouTFNaaNN9tNE6FR password=wpgxxxx algorithm=PBEWithMD5AndDES keyObtentionIterations=1000 providerName=SunJCE saltGeneratorClassName=org.jasypt.salt.RandomSaltGenerator stringOutputType=base64 ``` 加密: ![image](https://hackmd.io/_uploads/S1tdaiqFee.png) 解密: ![image](https://hackmd.io/_uploads/By6g0iqYxx.png) 3. 使用線上小工具加密 沒有執行環境,可以直接使用線上網站來加密,也可以達到一樣的效果,不過這個網站目前只能使用default的 PBEWithMD5AndDES 演算法 。 https://www.devglan.com/online-tools/jasypt-online-encryption-decryption <h4>Step2. 引入依賴與配置加密字串,將加密字串後應用在 Spring Boot</h4> 如果要應用在資料庫密碼加密上的話,在取得加密字串後,需要做以下動作: 1. 引入 Maven 依賴,在 POM.xml 中新增 dependency ``` <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> ``` 2. properties 設定檔使用 ENC(…) 格式 ``` spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver spring.datasource.jdbc-url=${JDBC-URL} # spring.datasource.username=${BPAAS-FCST-RDB-USERNAME} # spring.datasource.password=${BPAAS-FCST-RDB-PASSWORD} spring.datasource.username=ENC(+DaOHMwe2HhIAAe6NJfcX/ZU+VYA9d4q) spring.datasource.password=ENC(CZ61+PIktNXBCoZscZGtfmMV4yxj17/V1irlH+k3vi8=) spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring.datasource.poolName=hikariPool-Bpaas ``` 3. 指定使用自訂的 Encryptor Bean application.properties: ``` jasypt.encryptor.bean=encryptorBean ``` ![image](https://hackmd.io/_uploads/By0ngh9Fxg.png) ``` package com.wpgholdings.bpaas.config; import org.jasypt.encryption.StringEncryptor; import org.jasypt.encryption.pbe.PooledPBEStringEncryptor; import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties; @EnableEncryptableProperties @Configuration public class EncryptorConfiguration { @Bean(name = "encryptorBean") public StringEncryptor stringEncryptor() { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); SimpleStringPBEConfig config = new SimpleStringPBEConfig(); config.setPassword("wpgxxxx"); config.setAlgorithm("PBEWithMD5AndDES"); config.setKeyObtentionIterations("1000"); config.setPoolSize("1"); config.setProviderName("SunJCE"); config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator"); config.setStringOutputType("base64"); encryptor.setConfig(config); return encryptor; } } ``` * password:加密金鑰,解密必須相同 * algorithm:使用的加密演算法(例:PBEWithMD5AndDES、PBEWITHHMACSHA512ANDAES_256) * keyObtentionIterations:金鑰衍生迭代次數,增加破解難度(這裡是 1000 次) * poolSize:加密器物件池大小(同時有多少執行緒可以共用加密器),通常設 1 就好 * providerName:指定使用哪個 JCE 提供者,SunJCE 是 Java 內建 * saltGeneratorClassName:Salt 生成器,這裡用 RandomSaltGenerator,每次加密會隨機產生新 Salt * stringOutputType:輸出字串的格式,常見 base64 或 hexadecimal <h3>原理補充</h3> <h4>Jasypt 原理</h4> Jasypt (Java Simplified Encryption) 是一個封裝好的 Java 加密工具,背後是基於 JCE(Java Cryptography Extension)的加密演算法。 它的原理簡單來說就是 「用一把金鑰 + 演算法,把明文轉成密文,並能再用同樣金鑰還原」。 核心要素有三個: 1. 演算法 (Algorithm) * 例如:PBEWithMD5AndDES(舊的)、PBEWITHHMACSHA512ANDAES_256(較新的 AES 演算法)。 * PBE = Password-Based Encryption,代表是透過密碼衍生金鑰。 2. 金鑰 (Password / Secret Key) * 一個由開發者設定的字串,例如 wpgxxxx。 * 這是加密與解密的唯一依據。 3. 隨機 Salt + Iterations * Salt:每次加密時會產生不同的隨機鹽值,避免相同明文總是得到相同密文。 * Iterations:重複運算多次,增加破解難度。 <h4>加密流程</h4> 以 sqladmin 為例,Jasypt 在加密時會做這些步驟: 1. 輸入明文:sqladmin 2. 讀取金鑰:wpgxxxx 3. 產生隨機 salt(例如 16 bytes) 4. 透過演算法 (PBEWithMD5AndDES): * 把「明文 + salt + 金鑰」送進加密演算法 * 重複計算 iterations 次(例如 1000 次) 5. 輸出密文 (Base64 編碼):例如 33nf9xwICSj5VRRatT5sEkIeC4GcgK2O 6. 包裝成 ENC(...) → ENC(33nf9xwICSj5VRRatT5sEkIeC4GcgK2O) 因為 salt 每次不同,所以「同一個明文」會得到「不同的密文」,但解密後結果一樣。 <h4>解密流程</h4> 解密過程就是反過來: 1. 輸入密文:例如 33nf9xwICSj5VRRatT5sEkIeC4GcgK2O 2. 讀取金鑰:wpgxxxx 3. 從密文中取出當初的 salt 4. 用相同演算法與 iterations,把加密過程「倒回去」 5. 得到明文:還原出 sqladmin <h4>為什麼每次加密結果不同?</h4> 因為 Jasypt 預設使用 隨機 Salt。 * 例如你連續兩次加密 sqladmin,得到的可能是: ``` ENC(33nf9xwICSj5VRRatT5sEkIeC4GcgK2O) ENC(0Kcp5XSPHnv6+ehq80kTtXcJTyjSGfPm) ``` * 雖然長得不一樣,但用相同的金鑰解密,都會還原成同一個明文 sqladmin。 這就是 非決定性加密,可以避免攻擊者透過比對密文推測出明文。 <h4>總結</h4> * 加密 : 明文 + 金鑰 + Salt + 演算法 → 密文 (ENC(…)) * 解密 : 密文 + 金鑰 + 演算法 → 明文 * Salt 的作用:確保即使明文一樣,每次加密結果也不同。 * Iterations 的作用:增加暴力破解難度。 * 金鑰的重要性:金鑰不對,即使演算法正確也無法解出。 ``` java -cp jasypt-1.9.3.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input=sqladmin password=wpgxxxx algorithm=PBEWithMD5AndDES keyObtentionIterations=1000 providerName=SunJCE saltGeneratorClassName=org.jasypt.salt.RandomSaltGenerator stringOutputType=base64 ``` * 金鑰:wpgxxxx * 演算法:PBEWithMD5AndDES * Salt 機制:RandomSaltGenerator(每次不同) * 運算次數:1000 次 <h3>參考網站</h3> https://sinyilin.github.io/spring-boot/20240823/2374258038/