➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
Spring Security OAuth2Istio 入門

JPA/Hibernate 使用方式(二)

Continued from previous post:
JPA/Hibernate 使用方式(一)

Table of contents

1 Maven dependencies

1<parent> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-parent</artifactId> 4 <version>3.4.0</version> 5</parent> 6 7<dependencies> 8 <dependency> 9 <groupId>org.springframework.boot</groupId> 10 <artifactId>spring-boot-starter-web</artifactId> 11 </dependency> 12 <dependency> 13 <groupId>org.springframework.boot</groupId> 14 <artifactId>spring-boot-starter-security</artifactId> 15 </dependency> 16 17 <dependency> 18 <groupId>org.springframework.boot</groupId> 19 <artifactId>spring-boot-starter-data-jpa</artifactId> 20 </dependency> 21 22 <!-- for multiple Spring profiles --> 23 <dependency> 24 <groupId>com.h2database</groupId> 25 <artifactId>h2</artifactId> 26 </dependency> 27 <dependency> 28 <groupId>com.microsoft.sqlserver</groupId> 29 <artifactId>mssql-jdbc</artifactId> 30 </dependency> 31 <dependency> 32 <groupId>com.oracle.database.jdbc</groupId> 33 <artifactId>ojdbc11</artifactId> 34 </dependency> 35 <dependency> 36 <groupId>org.postgresql</groupId> 37 <artifactId>postgresql</artifactId> 38 </dependency> 39 40 <dependency> 41 <groupId>org.projectlombok</groupId> 42 <artifactId>lombok</artifactId> 43 <scope>provided</scope> 44 </dependency> 45</dependencies>

2 Java code

2.1 Controller

OrderController.java
1@Slf4j 2@RestController 3@RequestMapping("/orders") 4public class OrderController { 5 6 OrderService service; 7 8 public OrderController(OrderService service) { 9 this.service = service; 10 } 11 12 @GetMapping 13 public List<OrderEntity> getAllWithSearch( 14 @RequestParam(name = "remarks" , required = false) String remarks) { 15 log.info("GET /orders{}", remarks == null ? "" : ("?remarks=" + remarks)); 16 return service.getAllWithSearch(remarks); 17 } 18 19 @GetMapping("/{id}") 20 public OrderEntity getById(@PathVariable(name = "id", required = true) Long id) { 21 log.info("GET /orders/{}", id); 22 return service.getById(id); 23 } 24 25 @PostMapping 26 public OrderEntity create(@RequestBody OrderEntity order) { 27 log.info("POST /orders"); 28 return service.create(order); 29 } 30 31 @PutMapping 32 public OrderEntity update(@RequestBody OrderEntity order) { 33 log.info("PUT /orders"); 34 return service.update(order); 35 } 36 37 @DeleteMapping("/{id}") 38 public OrderEntity delete(@PathVariable(name = "id", required = true) Long id) { 39 log.info("DELETE /orders/{}", id); 40 return service.delete(id); 41 } 42}

2.2 Service

OrderService.java
1import org.springframework.transaction.annotation.Transactional; 2 3@Slf4j 4@Service 5@Transactional(rollbackFor = Throwable.class) 6public class OrderService { 7 8 OrderRepo repo; 9 EntityManager em; 10 11 public OrderService(OrderRepo repo, EntityManager em) { 12 this.repo = repo; 13 this.em = em; 14 } 15 16 17 18// public List<OrderEntity> getAll() { 19// log.info("getAll"); 20// return repo.findAll(); 21// } 22 23 public List<OrderEntity> getAllWithSearch(String remarks) { 24 log.info("getAllWithSearch (remarks={})", remarks); 25 26 final Specification<OrderEntity> spec = StringUtils.isBlank(remarks) ? null 27 : (root, query, criteriaBuilder) -> { 28 return criteriaBuilder.like(root.get("remarks"), "%" + remarks + "%"); 29 }; 30 31 return repo.findAll(spec); 32 } 33 34 public OrderEntity getById(Long id) { 35 log.info("getById (id={})", id); 36 return repo.findById(id).orElseThrow(); 37 } 38 39 public OrderEntity create(OrderEntity order) { 40 log.info("create (order={})", order); 41 return repo.save(order); 42 } 43 44 public OrderEntity update(OrderEntity order) { 45 46 // 而家 order 係 detached entity 47 48 log.info("update (order={})", order); 49 order = repo.saveAndFlush(order); 50 51 // repo.saveAndFlush 會即時更新 DB 紀錄,然後返回一個 managed entity 52 // 所以而家 order 係 managed entity 53 54 em.refresh(order); // 用 EntityManager 喺 DB 重新拎過呢個 order 紀錄,放落 first level cache 55 56 return order; 57 } 58 59 public OrderEntity delete(Long id) { 60 log.info("delete (id={})", id); 61 final OrderEntity order = repo.findById(id).orElseThrow(); 62 repo.deleteById(id); 63 return order; 64 } 65}
DatabaseUserDetailsService.java
1@Slf4j 2@Service 3public class DatabaseUserDetailsService implements UserDetailsService { 4 5 UserRepo userRepo; 6 7 public DatabaseUserDetailsService(UserRepo userRepo) { 8 this.userRepo = userRepo; 9 } 10 11 @Override 12 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 13 14 log.info("loadUserByUsername - username: {}", username); 15 16 final UserEntity userEntity = userRepo.findByUsername(username); 17 18 if (userEntity == null) { 19 log.warn("User [{}] cannot be found.", username); 20 throw new UsernameNotFoundException("User [" + username + "] cannot be found."); 21 } 22 23 log.info("User [{}] is found.", username); 24 25 return User.builder() 26 .username(userEntity.getUsername()) 27 .password(userEntity.getPassword()) 28 .disabled(!userEntity.isEnabled()) 29 .roles("USER") 30 .build(); 31 } 32}

2.3 Repository

OrderRepo.java
@Repository public interface OrderRepo extends JpaRepository<OrderEntity, Long>, JpaSpecificationExecutor<OrderEntity> { }
UserRepo.java
1@Repository 2public interface UserRepo extends JpaRepository<UserEntity, Long>, 3 JpaSpecificationExecutor<UserEntity> { 4 5 UserEntity findByUsername(@Param(value = "username") String username); 6}

2.4 Entity

BaseEntity.java
1@Data 2@FieldDefaults(level = PROTECTED) 3@MappedSuperclass 4@EntityListeners(AuditingEntityListener.class) 5public abstract class BaseEntity { 6 7 @Id 8 @GeneratedValue(strategy = GenerationType.IDENTITY) 9 Long id; 10 11 @CreatedBy 12 @Column(name = "created_user", updatable = false) 13 String createdUser; 14 15 @CreatedDate 16 @Column(name = "created_datetime", updatable = false) 17 LocalDateTime createdDatetime; 18 19 @LastModifiedBy 20 @Column(name = "last_modified_user") 21 String lastModifiedUser; 22 23 @LastModifiedDate 24 @Column(name = "last_modified_datetime") 25 LocalDateTime lastModifiedDatetime; 26 27 @Version 28 @Column(name = "version") 29 Long version; 30}
OrderEntity.java
1@Data 2@ToString(callSuper = true) 3@EqualsAndHashCode(callSuper = true) 4@FieldDefaults(level = PRIVATE) 5@Entity 6@Table(name = "tbl_order") 7public class OrderEntity extends BaseEntity { 8 9 @Column(name = "remarks") 10 String remarks; 11 12 @Column(name = "order_datetime") 13 LocalDateTime orderDatetime; 14}
UserEntity.java
1@Data 2@ToString(callSuper = true) 3@EqualsAndHashCode(callSuper = true) 4@FieldDefaults(level = PRIVATE) 5@Entity 6@Table(name = "tbl_user") 7public class UserEntity extends BaseEntity { 8 9 @Column(name = "username") 10 String username; 11 12 @Column(name = "password") 13 String password; 14 15 @Column(name = "enabled") 16 boolean enabled; 17}

2.5 Spring Security config

SecurityConfig.java
1@Configuration 2public class SecurityConfig { 3 4 @Bean 5 public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 6 return http 7 .authorizeHttpRequests(authz -> authz 8 .requestMatchers("/orders/**").hasRole("USER") 9 .anyRequest().permitAll()) 10 .httpBasic(Customizer.withDefaults()) 11 .formLogin(e -> e.disable()) 12 .csrf(e -> e.disable()) 13 .headers(e -> e.frameOptions(o -> o.disable())) // for H2 console 14 .build(); 15 } 16 17 @Bean 18 public BCryptPasswordEncoder passwordEncoder() { 19 return new BCryptPasswordEncoder(); 20 } 21}

2.6 Auditing implementation

AuditorAwareImpl.java
1@Component 2public class AuditorAwareImpl implements AuditorAware<String> { 3 4 @Override 5 public Optional<String> getCurrentAuditor() { 6 7 final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 8 9 if (authentication == null || !authentication.isAuthenticated()) { 10 return Optional.empty(); 11 } 12 13 return Optional.of(authentication.getName()); 14 } 15}

2.7 Jackson config

JacksonConfig.java
1@Configuration 2public class JacksonConfig { 3 4 @Bean 5 public ObjectMapper objectMapper() { 6 7 final SimpleModule module = new SimpleModule(); 8 module.addSerializer(LocalDateTime.class, new LocalDateTimeToEpochSerializer()); 9 module.addDeserializer(LocalDateTime.class, new LocalDateTimeToEpochDeserializer()); 10 11 final ObjectMapper mapper = new ObjectMapper(); 12 mapper.registerModule(module); 13 14 return mapper; 15 } 16} 17 18class LocalDateTimeToEpochSerializer extends JsonSerializer<LocalDateTime> { 19 20 @Override 21 public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) 22 throws IOException { 23 gen.writeNumber(value.toInstant(ZoneOffset.UTC).toEpochMilli()); 24 } 25} 26 27class LocalDateTimeToEpochDeserializer extends JsonDeserializer<LocalDateTime> { 28 29 @Override 30 public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) 31 throws IOException, JacksonException { 32 return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getLongValue()), ZoneOffset.UTC); 33 } 34}

3 Application 配置

application.yml
1spring: 2 jpa: 3 show-sql: true 4 hibernate: 5 ddl-auto: update # 自動創建 tables 6 7--- 8 9logging.level: 10 org.springframework.security: DEBUG 11 org.hibernate.SQL: DEBUG # show SQL statements 12 org.hibernate.orm.jdbc.bind: TRACE # show parameters
application-h2.yml(Spring profile h2):
1# H2 (SQL Server mode) 2spring: 3 datasource: 4 url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;TRACE_LEVEL_FILE=4;MODE=MSSQLServer;DATABASE_TO_UPPER=FALSE;CASE_INSENSITIVE_IDENTIFIERS=TRUE 5 username: sa 6 password: sa 7 h2: 8 console: 9 enabled: true 10 path: /h2-console
application-mssql.yml(Spring profile mssql):
spring: datasource: url: jdbc:sqlserver://localhost:1433;databaseName=dev;encrypt=false username: sa password: StrongPassword123
application-oracle.yml(Spring profile oracle):
spring: datasource: url: jdbc:oracle:thin:@localhost:1521:xe username: system password: oracle
application-postgres.yml(Spring profile postgres):
spring: datasource: url: jdbc:postgresql://localhost:5432/postgres username: postgres password: postgres

4 初始化 DB

4.1 初始化 tables

如果 spring.jpa.hibernate.ddl-auto 既配置係 none,我地就要自行初始化 DB 裡面既 table schema。而以下既 CREATE SQL 都係由 JPA/Hibernate 生成。
SQL Server、H2(SQL Server mode):
1create table tbl_order ( 2 id bigint identity not null, 3 remarks varchar(255), 4 order_datetime datetime2(6), 5 6 created_user varchar(255), 7 created_datetime datetime2(6), 8 last_modified_user varchar(255), 9 last_modified_datetime datetime2(6), 10 version bigint, 11 12 primary key (id) 13); 14 15create table tbl_user ( 16 id bigint identity not null, 17 username varchar(255), 18 password varchar(255), 19 enabled bit, 20 21 created_user varchar(255), 22 created_datetime datetime2(6), 23 last_modified_user varchar(255), 24 last_modified_datetime datetime2(6), 25 version bigint, 26 27 primary key (id) 28);
Oracle:
1create table tbl_order ( 2 id number(19,0) generated by default as identity, 3 remarks varchar2(255 char), 4 order_datetime timestamp(6), 5 6 created_user varchar2(255 char), 7 created_datetime timestamp(6), 8 last_modified_user varchar2(255 char), 9 last_modified_datetime timestamp(6), 10 version number(19,0), 11 primary key (id) 12); 13 14create table tbl_user ( 15 id number(19,0) generated by default as identity, 16 username varchar2(255 char), 17 password varchar2(255 char), 18 enabled number(1,0) check (enabled in (0,1)), 19 20 created_user varchar2(255 char), 21 created_datetime timestamp(6), 22 last_modified_user varchar2(255 char), 23 last_modified_datetime timestamp(6), 24 version number(19,0), 25 26 primary key (id) 27);
PostgreSQL:
1create table tbl_order ( 2 id bigint generated by default as identity, 3 remarks varchar(255), 4 order_datetime timestamp(6), 5 6 created_user varchar(255), 7 created_datetime timestamp(6), 8 last_modified_user varchar(255), 9 last_modified_datetime timestamp(6), 10 version bigint, 11 12 primary key (id) 13); 14 15create table tbl_user ( 16 id bigint generated by default as identity, 17 username varchar(255), 18 password varchar(255), 19 enabled boolean, 20 21 created_user varchar(255), 22 created_datetime timestamp(6), 23 last_modified_user varchar(255), 24 last_modified_datetime timestamp(6), 25 version bigint, 26 27 primary key (id) 28);

4.2 初始化數據

SQL Server、Oracle、H2:
insert into tbl_user (username, password, enabled) values ('mick', '$2a$12$589/AIvI/dCgNo/JBs3kSe1nxT29oXmxmYvU7NMcMH1P1B2cNe2Xa', 1); insert into tbl_user (username, password, enabled) values ('jack', '$2a$12$vxae.8GVw3GhoGUXaLSA3uDyHvm6X246zzuS3Z4TxhbhTKI6tKthW', 1);
PostgreSQL:
insert into tbl_user (username, password, enabled) values ('mick', '$2a$12$589/AIvI/dCgNo/JBs3kSe1nxT29oXmxmYvU7NMcMH1P1B2cNe2Xa', true); insert into tbl_user (username, password, enabled) values ('jack', '$2a$12$vxae.8GVw3GhoGUXaLSA3uDyHvm6X246zzuS3Z4TxhbhTKI6tKthW', true);
用戶資料:
用戶名密碼(加密前)
mickmickpw
jackjackpw

5 測試

5.1 Create API

新增 order 紀錄(ID = 1):
curl -X POST -u mick:mickpw localhost:8080/orders -H "Content-Type: application/json" -d "{ \"remarks\": \"foo\", \"orderDatetime\": 1730000000000 }"
結果:
1{ 2 "id": 1, 3 "createdUser": "mick", 4 "createdDatetime": 1742149186385, 5 "lastModifiedUser": "mick", 6 "lastModifiedDatetime": 1742149186385, 7 "version": 0, 8 "remarks": "foo", 9 "orderDatetime": 1730000000000 10}
新增 order 紀錄(ID = 2):
curl -X POST -u mick:mickpw localhost:8080/orders -H "Content-Type: application/json" -d "{ \"remarks\": \"foobar\", \"orderDatetime\": 1740000000000 }"
結果:
1{ 2 "id": 2, 3 "createdUser": "mick", 4 "createdDatetime": 1742149186702, 5 "lastModifiedUser": "mick", 6 "lastModifiedDatetime": 1742149186702, 7 "version": 0, 8 "remarks": "foobar", 9 "orderDatetime": 1740000000000 10}

5.2 Read API

查詢所有 order 紀錄:
curl -X GET -u mick:mickpw localhost:8080/orders
結果:
1[ 2 { 3 "id": 1, 4 "createdUser": "mick", 5 "createdDatetime": 1742149186385, 6 "lastModifiedUser": "mick", 7 "lastModifiedDatetime": 1742149186385, 8 "version": 0, 9 "remarks": "foo", 10 "orderDatetime": 1730000000000 11 }, 12 { 13 "id": 2, 14 "createdUser": "mick", 15 "createdDatetime": 1742149186702, 16 "lastModifiedUser": "mick", 17 "lastModifiedDatetime": 1742149186702, 18 "version": 0, 19 "remarks": "foobar", 20 "orderDatetime": 1740000000000 21 } 22]
搜尋所有 order 紀錄(remarks contains bar):
curl -X GET -u mick:mickpw localhost:8080/orders?remarks=bar
結果:
1[ 2 { 3 "id": 2, 4 "createdUser": "mick", 5 "createdDatetime": 1742149186702, 6 "lastModifiedUser": "mick", 7 "lastModifiedDatetime": 1742149186702, 8 "version": 0, 9 "remarks": "foobar", 10 "orderDatetime": 1740000000000 11 } 12]

5.3 Update API

修改 order 紀錄(ID = 2):
curl -X PUT -u jack:jackpw localhost:8080/orders -H "Content-Type: application/json" -d "{\"id\": 2, \"createdUser\": \"fake\", \"createdDatetime\": 111111, \"lastModifiedUser\": \"fake\", \"lastModifiedDatetime\": 111111, \"remarks\": \"bar\", \"orderDatetime\": 1740000000005, \"version\": 0}"
結果:
1{ 2 "id": 2, 3 "createdUser": "mick", 4 "createdDatetime": 1742149186702, 5 "lastModifiedUser": "jack", 6 "lastModifiedDatetime": 1742149191350, 7 "version": 1, 8 "remarks": "bar", 9 "orderDatetime": 1740000000005 10}
查詢所有 order 紀錄:
curl -X GET -u mick:mickpw localhost:8080/orders
結果:
1[ 2 { 3 "id": 1, 4 "createdUser": "mick", 5 "createdDatetime": 1742149186385, 6 "lastModifiedUser": "mick", 7 "lastModifiedDatetime": 1742149186385, 8 "version": 0, 9 "remarks": "foo", 10 "orderDatetime": 1730000000000 11 }, 12 { 13 "id": 2, 14 "createdUser": "mick", 15 "createdDatetime": 1742149186702, 16 "lastModifiedUser": "jack", 17 "lastModifiedDatetime": 1742149191350, 18 "version": 1, 19 "remarks": "bar", 20 "orderDatetime": 1740000000005 21 } 22]
用同之前一樣既 version value(即係 0)去修改 order 紀錄(ID = 2):
curl -X PUT -u jack:jackpw localhost:8080/orders -H "Content-Type: application/json" -d "{\"id\": 2, \"createdUser\": \"fake\", \"createdDatetime\": 111111, \"lastModifiedUser\": \"fake\", \"lastModifiedDatetime\": 111111, \"remarks\": \"bar2\", \"orderDatetime\": 1740000000007, \"version\": 0}"
結果:
1{ 2 "timestamp": 1742114659243, 3 "status": 500, 4 "error": "Internal Server Error", 5 "path": "/orders" 6}
Console log:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [code.entity.OrderEntity#2]] with root cause org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [code.entity.OrderEntity#2]
查詢所有 order 紀錄:
curl -X GET -u mick:mickpw localhost:8080/orders
結果:
1[ 2 { 3 "id": 1, 4 "createdUser": "mick", 5 "createdDatetime": 1742149186385, 6 "lastModifiedUser": "mick", 7 "lastModifiedDatetime": 1742149186385, 8 "version": 0, 9 "remarks": "foo", 10 "orderDatetime": 1730000000000 11 }, 12 { 13 "id": 2, 14 "createdUser": "mick", 15 "createdDatetime": 1742149186702, 16 "lastModifiedUser": "jack", 17 "lastModifiedDatetime": 1742149191350, 18 "version": 1, 19 "remarks": "bar", 20 "orderDatetime": 1740000000005 21 } 22]

5.4 Delete API

刪除 order 紀錄(ID = 2):
curl -X DELETE -u mick:mickpw localhost:8080/orders/2
結果:
1{ 2 "id": 2, 3 "createdUser": "mick", 4 "createdDatetime": 1742149186702, 5 "lastModifiedUser": "jack", 6 "lastModifiedDatetime": 1742149191350, 7 "version": 1, 8 "remarks": "bar", 9 "orderDatetime": 1740000000005 10}
查詢所有 order 紀錄:
curl -X GET -u mick:mickpw localhost:8080/orders
結果:
1[ 2 { 3 "id": 1, 4 "createdUser": "mick", 5 "createdDatetime": 1742149186385, 6 "lastModifiedUser": "mick", 7 "lastModifiedDatetime": 1742149186385, 8 "version": 0, 9 "remarks": "foo", 10 "orderDatetime": 1730000000000 11 } 12]

6 筆記

6.1 JPA/Hibernate 既 first level cache

  • 當我地執行 JpaRepositorysave()delete() methods 既時候,JPA/Hibernate 會執行 SELECT SQL 用 ID 獲取 DB 紀錄,然後放喺 JPA/Hibernate 既 session 既 first level cache。
  • 如果 JPA/Hibernate 既 session 既 first level cache 裡面有紀錄,JPA/Hibernate 就唔會再訪問 DB 獲取紀錄。
  • JPA/Hibernate 既 session 會隨住 @Transactional 創建既 DB transaction 而完結。
    • 通常一個 HTTP request 係一個 single unit of work,負責執行一個完整既業務操作,所以通常只會用一個 DB transaction 做曬所有 DB 改動。
    • Spring 既 @Transactional annotation 既 propagation attribute 既默認值係 Propagation.REQUIRED
      • 無論一個 HTTP request 經過幾多個 @Service classes 以及 @Repository interfaces,就算佢地都有 annotate 到 @Transactional,佢地都只會 share 同一個 DB transaction。

6.2 用 @Version 實現 optimistic locking

OrderEntity 裡面既 version field annotate 左 @Version,JPA/Hibernate 會幫我地管理呢個 field/column 既 value。

6.2.1 新增紀錄

  • 喺用 repo.save(order); 新增紀錄既時候:
    • Annotate 左 @Versionversion field 既 column value 會係 0

6.2.2 更新紀錄

  • JPA/Hibernate 會用到 optimistic locking 既方式去更新紀錄。
  • repo.saveAndFlush(order); 既時候:
    • JPA/Hibernate 生成既 UPDATE SQL 既 WHERE clause 會多左 version = ?
    • 如果 prepared statement 既 version parameter 同 DB 紀錄既 version column 一樣,就會更新紀錄,而更新後既紀錄既 version column 會係本來既 +1
    • 如果 prepared statement 既 version parameter 同 DB 紀錄既 version column 唔一樣,就會更新唔到紀錄,JPA/Hibernate 會 throw StaleObjectStateException
    • 如果 DB 紀錄既 version column 係 null,JPA/Hibernate 就唔會執行 UPDATE SQL,但係會 throw StaleObjectStateException
    • 如果 HTTP request body 既 version field 係 null,JPA/Hibernate 就會 throw Detached entity with generated id '1' has an uninitialized version value 'null': code.entity.OrderEntity.version
  • 必需要喺 transaction 裡面進行。
JPA/Hibernate 生成既 UPDATE SQL:
update tbl_order set last_modified_datetime=?,last_modified_user=?,order_datetime=?,remarks=?,version=? where id=? and version=?

6.2.3 刪除紀錄

  • repo.delete(id); 既時候:
    • JPA/Hibernate 生成既 DELETE SQL 既 WHERE clause 會多左 version = ?
    • 如果 prepared statement 既 version parameter 同 DB 紀錄既 version column 唔一樣,就會更新唔到紀錄,JPA/Hibernate 會 throw StaleObjectStateException
    • 如果 DB 紀錄既 version column 係 null,JPA/Hibernate 就唔會執行 DELETE SQL,所以就唔會刪除到呢個紀錄,亦唔會 throw 任何 exception。
JPA/Hibernate 生成既 DELETE SQL:
delete from tbl_order where id=? and version=?

6.3 Auditing

  • 因為我地有 AuditorAwareImpl,所以 annotate 左 @CreatedBy@LastModifiedBy 既 fields 會用左個 HTTP request 所屬既 thread 裡面既 ThreadLocalSecurityContext 既資料。
    • 即係通過左 Spring Security authentication 既 Authentication object 既 name field。
  • 我地可以用 @Column 既 attribute updatable = false 黎避免 @CreatedBy 以及 @CreatedDate 既 fields 喺日後被更新。
  • 依測試所見,就算 HTTP request body 裡面有 lastModifiedUser 以及 lastModifiedDatetime fields,喺保存紀錄之前,Spring 都會覆寫佢地既 values。

6.4 Criteria API

criteriaBuilder.like(root.get("remarks"), "%" + remarks + "%") 會生成以下既 SELECT SQL:
select oe1_0.id,oe1_0.created_datetime,oe1_0.created_user,oe1_0.last_modified_datetime,oe1_0.last_modified_user,oe1_0.order_datetime,oe1_0.remarks,oe1_0.version from tbl_order oe1_0 where oe1_0.remarks like ? escape ''
JPA/Hibernate 生成左 like ? escape '' 出黎:
  • 因為係 prepared statement,所以唔會有 SQL injection 既問題。
  • 就算我地冇提供到 escape character,佢都會自動加上 escape ''

7 參考資料