➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
Spring 項目使用 Redis 做 caching透過 CLI 學習 Kafka

JPA/Hibernate 使用方式(一)

Continued from previous post:
Java 開發筆記(四)

Table of contents

1 JPA/Hibernate 簡介

Back-end application 一般都需要連接 database 去查詢資料、更新資料,而如果選擇用 Java、TypeScript 呢啲屬於 strongly typed 既程序語言,為左方便 coding,都會寫啲 class 作為載體咁喺 memory 度緩存啲 table 紀錄數據,操作可以只係純查詢,但亦可以包括更新某個 column 既 value。簡單講,就係根據 class、fields 可以得到 SELECT SQL、UPDATE SQL,而 query result 可以用 typed objects 緩存等等。
而將 table 紀錄轉化成 typed objects 就需要一個過程,呢個過程就要用到 ORM(object-relational mapping),將程式既 class(人稱 entity/DO/DTO/VO/POJO)以及裡面既 instance fields 同 database table 以及佢既 columns 兩者做 mapping。
喺 Java 既世界裡面,最熱門既 ORM libraries 就係 JPA、Hibernate。佢地兩個並唔係二選一既關係——JPA 係一個 ORM 既標準,而 Hibernate 就係實現左 JPA 標準既一個 framework。事實上,Hibernate 係比 JPA 更早出現。
一般既 Spring projects 都會引入 JPA、Hibernate 既 libraries 去做 ORM,而國內或者中資公司可能會用 MyBatis,即係 prefer 寫 native SQL,但就唔算係一個 ORM framework。

2 動手寫

Project setup:
  • Spring Boot(web)
  • JPA/Hibernate
  • MySQL 或 MariaDB

2.1 Maven dependencies

1<parent> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-parent</artifactId> 4 <version>2.6.1</version> 5</parent> 6 7<dependencies> 8 <dependency> 9 <groupId>org.projectlombok</groupId> 10 <artifactId>lombok</artifactId> 11 </dependency> 12 13 <dependency> 14 <groupId>org.springframework.boot</groupId> 15 <artifactId>spring-boot-starter-web</artifactId> 16 </dependency> 17 18 <dependency> 19 <groupId>org.springframework.boot</groupId> 20 <artifactId>spring-boot-starter-data-jpa</artifactId> 21 </dependency> 22 23 <dependency> 24 <groupId>mysql</groupId> 25 <artifactId>mysql-connector-java</artifactId> 26 <version>8.0.26</version> 27 </dependency> 28</dependencies>

2.2 Application 配置

application.yml
1spring: 2 jpa: 3 show-sql: true 4 open-in-view: false 5 hibernate: 6 ddl-auto: update 7 properties: 8 hibernate: 9 dialect: org.hibernate.dialect.MySQL57InnoDBDialect 10 datasource: 11 url: jdbc:mysql://localhost:3306/mydb?useSSL=false 12 username: root 13 password: 14 driver-class-name: com.mysql.cj.jdbc.Driver

2.3 寫 Java code

2.3.1 Entity

1@Data 2@Accessors(chain = true) 3@FieldDefaults(level = PRIVATE) 4@Entity 5@Table(name = "person") 6public class PersonEntity { 7 @Id 8 @GeneratedValue(strategy = IDENTITY) 9 Long id; 10 11 String firstName; 12 String status; 13}
解釋:
  • @Data@Accessors 以及 @FieldDefaults 都係 Lombok annotations,用黎 generate methods、keyword 既 code,咁我地就冇需要寫 getters 同 setters。
  • @Entity 係用黎話畀 Hibernate 知呢個係一個 entity class。
  • @Table 係用黎話畀 Hibernate 知應該 map 去邊個 database table。

2.3.2 Repository interface:用 method 名砌 SQL

PersonRepo.java
1@Repository 2public interface PersonRepo extends JpaRepository<PersonEntity, Long> { 3 4 Optional<PersonEntity> findOneByFirstName(String firstName); 5 6 List<PersonEntity> findAllByStatusIn(List<String> statuses); 7}
解釋:
  • @Repository
    • 因為個 interface 已經 extends 左 Spring Boot JPA 既 JpaRepository,所以其實呢個 annotation 唔係必要。
  • findOneByFirstName
    • Return Optional<PersonEntity>
      • 因為 findOne,所以只會查詢到 1 個紀錄。
      • Return type 可以選擇性地用 Optional wrapper 包住,擁抱 Java 8 既新功能。
    • findOne 砌出黎果句 SQL 會做到 SELECT 以及 LIMIT 1 既效果。
    • ByFirstName 砌出黎果句 SQL 會做到 WHERE first_name = ? 既效果。
  • findAllByStatusIn
    • Return List<PersonEntity>
      • 因為 findAll,所以會查詢到多個紀錄。
      • 因為已經用左 List 做 wrapper,所以唔需要再用 Optional 包多層。
    • findAll 砌出黎果句 SQL 會做到 SELECT 既效果。
    • ByStatusIn 砌出黎果句 SQL 會做到 WHERE status IN (?) 既效果。

2.3.3 Repository interface:用 JPQL/HQL

JPQL(Java Persistence query language)/HQL(Hibernate query language)既基本 syntax 同 native SQL 差唔多。因為係有 ORM 既元素喺裡面,所以會用 class 名、field 名黎代替 table 名、column 名。
最簡單既 syntax 係咁:
1@Repository 2public interface PersonRepo extends JpaRepository<PersonEntity, Long> { 3 4 @Query("SELECT p FROM PersonEntity p WHERE p.firstName = :firstName") 5 Optional<PersonEntity> getByFirstName(@Param("firstName") String firstName); 6 7 @Query("SELECT p FROM PersonEntity p WHERE p.status IN :statuses") 8 List<PersonEntity> getAllByStatusIn(@Param("statuses") List<String> statuses); 9}
解釋:
  • getByFirstName
    • :firstName 會成為 prepared statement 既 parameter(i.e. = ?
    • @Param("firstName") 既 variable value 會對應返 :firstName
  • getAllByStatusIn
    • :statuses 會成為 prepared statement 既 parameter(i.e. IN (?)
    • @Param("statuses") 既 variable values 會對應返 :statuses

2.3.4 Repository interface:用 native SQL

1import org.springframework.transaction.annotation.Transactional; 2 3@Repository 4public interface PersonRepo extends JpaRepository<PersonEntity, Long> { 5 6 @Query(nativeQuery = true, value = "SELECT * FROM person WHERE first_name = :firstName") 7 Optional<PersonEntity> getByFirstName(@Param("firstName") String firstName); 8 9 @Query(nativeQuery = true, value = "SELECT * FROM person WHERE status IN :statuses") 10 List<PersonEntity> getAllByStatusIn(@Param("statuses") List<String> statuses); 11 12 @Transactional 13 @Modifying 14 @Query(nativeQuery = true, value = "UPDATE person SET status = ?2 WHERE id = ?1") 15 void updateStatus(Long id, String status); 16}
解釋:
  • updateStatus
    • 如果句 SQL 會修改數據,就需要加上 @Modifying 以及 @Transactional,否則會導致 exception
      • @ModifyingCaused by: java.sql.SQLException: Can not issue data manipulation statements with executeQuery()
      • @Transactionalorg.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query
    • ?1?2 會成為 prepared statements 既 parameters(i.e. = ?
    • Method parameters 既次序會對應返 ?1?2(index 係 1-based)

2.3.5 DAO class:用 native SQL(Spring Boot 1.5 + Hibernate 5.0)

2.3.5.1 將結果轉化成 POJO class

1import org.springframework.transaction.annotation.Transactional; 2 3@Component 4@Transactional 5public class PersonDao { 6 7 @Autowired EntityManagerFactory emf; 8 9 SessionFactory sessionFactory; 10 11 @PostConstruct 12 public void init() { 13 sessionFactory = emf.unwrap(SessionFactory.class); 14 } 15 16 @SuppressWarnings("unchecked") 17 public PersonEntity getByFirstName(String firstName) { 18 19 final List<PersonEntity> persons = sessionFactory 20 .getCurrentSession() 21 .createSQLQuery("SELECT id, first_name firstName, status FROM person WHERE first_name = :firstName") 22 .addScalar("id", StandardBasicTypes.LONG) 23 .addScalar("firstName", StandardBasicTypes.STRING) 24 .addScalar("status", StandardBasicTypes.STRING) 25 .setResultTransformer(Transformers.aliasToBean(PersonEntity.class)) 26 .setParameter("firstName", firstName) 27 .list(); 28 29 return persons.isEmpty() ? null : persons.get(0); 30 } 31 32 @SuppressWarnings("unchecked") 33 public List<PersonEntity> getAllByStatus(List<String> statuses) { 34 return sessionFactory 35 .getCurrentSession() 36 .createSQLQuery("SELECT id, first_name firstName, status FROM person WHERE status IN :statuses") 37 .addScalar("id", StandardBasicTypes.LONG) 38 .addScalar("firstName", StandardBasicTypes.STRING) 39 .addScalar("status", StandardBasicTypes.STRING) 40 .setResultTransformer(Transformers.aliasToBean(PersonEntity.class)) 41 .setParameterList("statuses", statuses) 42 .list(); 43 } 44}
解釋:
  • SessionFactory
    • 呢個例子將 EntityManagerFactory unwrap 成 SessionFactory,然後 create session,得到 Session object 再 create native SQL 既 SQLQuery object。
  • setResultTransformer(Transformers.aliasToBean(PersonEntity.class))
    • 我地需要用 AliasToBeanResultTransformerResultTransformer 黎話畀 Hibernate 知我地想將 query result 轉化成某個特定 class 既 objects。
      • 否則,得到既結果就會係一個 List<Object[]> 既 object。
  • addScalar("firstName", StandardBasicTypes.STRING)
    • 我地需要自己 map query result 既 column,仲要話畀 Hibernate 知 data type。
    • 如果 column 有 _,需要畀一個冇 _ 既 alias(first_name firstName 或者 first_name AS firstName)。
    • addScalar 既第 1 個 argument 係用 SQL 裡面個 column 既 alias。
    • 如果 column 有 _ 但係句 SQL 裡面冇畀 column alias 或者冇 call 岩 addScalar,咁就會令最後轉化出黎既 PersonEntity object 對應既 field value null 左。
  • setParametersetParameterList
    • 呢啲 methods 係用黎將 values 對應返 prepared statement 既 parameters。

2.3.5.2 將結果轉化成 Java Map

如果我地需要 dynamic 既 query result,即係 column 名係喺 compile time 既時候唔知既,咁我地可以用 AliasToEntityMapResultTransformer.INSTANCEResultTransformer
final List<Map<String, Object>> records = sessionFactory.getCurrentSession() .createSQLQuery("SELECT * FROM person") .setResultTransformer(AliasToEntityMapResultTransformer.INSTANCE) .list();

3 參考資料