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
- 冇
@Modifying
:Caused by: java.sql.SQLException: Can not issue data manipulation statements with executeQuery()
- 冇
@Transactional
:org.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))
- 我地需要用
AliasToBeanResultTransformer
既 ResultTransformer
黎話畀 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
左。
setParameter
、setParameterList
- 呢啲 methods 係用黎將 values 對應返 prepared statement 既 parameters。
2.3.5.2 將結果轉化成 Java Map
如果我地需要 dynamic 既 query result,即係 column 名係喺 compile time 既時候唔知既,咁我地可以用 AliasToEntityMapResultTransformer.INSTANCE
既 ResultTransformer
。
final List<Map<String, Object>> records = sessionFactory.getCurrentSession()
.createSQLQuery("SELECT * FROM person")
.setResultTransformer(AliasToEntityMapResultTransformer.INSTANCE)
.list();
3 參考資料