強(qiáng)大!Spring Boot + JPA 實(shí)體類設(shè)計(jì)五大實(shí)戰(zhàn)技巧
環(huán)境:SpringBoot3.4.2
1. 簡介
項(xiàng)目開發(fā)中,實(shí)體類的有效設(shè)計(jì)和實(shí)現(xiàn)對于構(gòu)建健壯且易于維護(hù)的應(yīng)用程序至關(guān)重要。JPA與Spring Boot的強(qiáng)大功能相結(jié)合,使開發(fā)人員能夠簡化數(shù)據(jù)庫操作并創(chuàng)建高度功能化的應(yīng)用程序。
本篇文章我們將深入探討基于Spring Boot項(xiàng)目開發(fā)時,如何利用JPA設(shè)計(jì)實(shí)體類的重要最佳實(shí)踐。通過遵循這些最佳實(shí)踐,開發(fā)人員可以確保應(yīng)用程序的完整性、性能和可擴(kuò)展性。
2. 最佳實(shí)踐
2.1 實(shí)體類與繼承
- 使用@Entity注解來標(biāo)記您的實(shí)體類,以表明它們是JPA實(shí)體
- 如果表名與類名不同,請使用@Table注解來指定表名
- 對于應(yīng)由多個實(shí)體繼承的公共屬性,可以考慮使用@MappedSuperclass
實(shí)體類基類設(shè)計(jì)
@MappedSuperclass
public abstract class BaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 其它公共屬性
@Temporal(TemporalType.TIMESTAMP)
private Date createTime ;
//getters, setters
}
BaseEntity類被注解為@MappedSuperclass。它包含了你希望在多個實(shí)體類之間共享的公共字段。Employee類繼承自BaseEntity,也就從超類繼承了id, createTime字段。通過基類的設(shè)計(jì)實(shí)現(xiàn)了代碼的復(fù)用性,并保持了實(shí)體層次的清晰和結(jié)構(gòu)化。
具體實(shí)體類
@Entity
@Table(name = "t_employees")
public class Employee extends BaseEntity {
private String name ;
private String address ;
// getters, setters
}
主鍵說明
- 主鍵字段使用@Id注解
- 主鍵生成策略使用@GeneratedValue注解(該注解有多種生成策略,如:GenerationType.IDENTITY、GenerationType.SEQUENCE等)
關(guān)聯(lián)關(guān)系
- 使用@OneToOne、@OneToMany、@ManyToOne和@ManyToMany來定義實(shí)體之間的關(guān)系
- 使用fetch屬性來控制加載行為(例如,LAZY(延遲加載)或EAGER(急切加載))
- 利用mappedBy來定義雙向關(guān)聯(lián)中的擁有方
@Entity
@Table(name = "t_department")
public class Department extends BaseEntity {
private String name;
private String code ;
@OneToMany(mappedBy = "department")
private List<Employee> employees = new ArrayList<>() ;
// getters, setters
}
@Entity
@Table(name = "t_employee")
public class Employee extends BaseEntity {
// ...
@ManyToOne
@JoinColumn(name = "department_id")
private Department department;
// getters, setters
}
級聯(lián)操作
- 使用cascade屬性來指定級聯(lián)操作(例如,CascadeType.ALL、CascadeType.PERSIST)
- 在使用級聯(lián)刪除(CascadeType.DELETE)時要特別小心,以避免意外丟失數(shù)據(jù)
public class Department extends BaseEntity {
// ...
@OneToMany(mappedBy = "department",
cascade = {
CascadeType.REFRESH,
CascadeType.PERSIST,
CascadeType.REMOVE
},
orphanRemoval = true
)
private List<Employee> employees = new ArrayList<>() ;
}
說明:
- 如果你將級聯(lián)操作設(shè)置為CascadeType.ALL,所有操作(例如,持久化、合并、刪除)都應(yīng)該從父實(shí)體(Department)級聯(lián)到子實(shí)體(Employee)
- orphanRemoval = true:此選項(xiàng)指定當(dāng)從Department集合中移除對某個Employee實(shí)體的引用時,這個孤立的Employee實(shí)體也應(yīng)該從數(shù)據(jù)庫中刪除
如下示例,保存Department時會級聯(lián)保存所有的Employee。
@Resource
private DepartmentRepository departmentRepository ;
@Test
public void testSave() {
Department department = new Department() ;
department.setCode("S0001") ;
department.setName("研發(fā)部") ;
Employee e1 = new Employee("張三", "SC") ;
e1.setDepartment(department) ;
Employee e2 = new Employee("Pack", "XJ") ;
e2.setDepartment(department) ;
department.setEmployees(List.of(e1, e2)) ;
this.departmentRepository.saveAndFlush(department) ;
}
控制臺輸出
同樣也適用于其它的級聯(lián)操作。
2.2 有效性驗(yàn)證
- 使用驗(yàn)證注解(如@NotNull、@Size等)直接在實(shí)體類中強(qiáng)制實(shí)施數(shù)據(jù)完整性約束。
- 將JPA驗(yàn)證與Spring的@Valid注解結(jié)合使用,以自動驗(yàn)證傳入的數(shù)據(jù)。
@Entity
@Table(name = "t_department")
public class Department extends BaseEntity {
@NotEmpty(message = "部門名稱不能為空")
@Length(min = 2)
private String name;
@NotEmpty(message = "部門代碼不能為空")
private String code ;
}
當(dāng)我們執(zhí)行如下代碼時程序?qū)伋霎惓?/p>
@Test
public void testSave() {
Department department = new Department() ;
department.setCode("S0001") ;
// 沒有設(shè)置name屬性
this.departmentRepository.saveAndFlush(department) ;
}
注意,你需要開啟如下配置:
spring:
jpa:
properties:
hibernate:
'[javax.persistence.validation.mode]': auto
2.3 審計(jì)
- 通過添加如@CreatedBy、@CreatedDate、@LastModifiedBy和@LastModifiedDate等字段來實(shí)現(xiàn)實(shí)體審計(jì),以跟蹤是哪個用戶何時創(chuàng)建或修改了實(shí)體
- 利用Spring的@EntityListeners來管理審計(jì)行為
如下比較完善的一個實(shí)體基類
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id ;
@Temporal(TemporalType.TIMESTAMP)
private Date createTime = new Date() ;
@CreatedBy
protected String createdBy;
@CreatedDate
@Column(nullable = false, updatable = false)
protected LocalDateTime createdDate;
@LastModifiedBy
protected String lastModifiedBy;
@LastModifiedDate
protected LocalDateTime lastModifiedDate;
}
如上我們又定義了一個可以審計(jì)的實(shí)體基類;如果哪個實(shí)體需要被審計(jì)那么就繼承該類即可。
@Entity
@Table(name = "t_product")
public class Product extends AuditableEntity {
private String name;
private Double price;
// getters, setters
}
注意,要使得審計(jì)功能生效,我們還需要做如下配置:
開啟審計(jì)功能。
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
當(dāng)前審計(jì)人Bean。
我們需要提供一個你當(dāng)前操作的人是誰的bean。
@Component
public class SystemAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of("Pack") ;
}
}
這里你需要根據(jù)自己的實(shí)際情況來編寫。
2.4 DTO映射
- 當(dāng)查詢數(shù)據(jù)時,考慮使用DTO投影來僅查詢需要的字段,從而提高性能
- 使用Spring Data JPA的@Query注解或查詢方法來創(chuàng)建自定義投影
如下實(shí)體類
@Entity
@Table(name = "t_author")
public class Author extends BaseEntity {
private String name ;
private Integer age ;
private String address ;
private String sex ;
private String email ;
}
該實(shí)體字段表多,我們可能只需要部分字段,如:name,sex,那么我們可以定義如下的投影:
public interface AuthorProjection {
String getName();
String getSex();
}
使用@Query查詢
public interface AuthorRepository extends JpaRepository<Author, Long> {
@Query("select e.name as name, e.sex as sex from Author e")
List<AuthorProjection> queryAuthors() ;
}
注意,你需要在sql中使用 as 別名。
2.5 索引
在實(shí)體類上我們可以通過@Table注解的indexes屬性指定列來創(chuàng)建索引。
@Entity
@Table(name = "t_author", indexes = {
@Index(columnList = "name, sex")
})
public class Author extends BaseEntity {}
當(dāng)服務(wù)啟動時,會自動創(chuàng)建基于 "name", "sex" 2個字段的聯(lián)合索引。
索引對于提高查詢性能至關(guān)重要,尤其是在處理大型數(shù)據(jù)集時。根據(jù)你應(yīng)用程序的查詢模式,仔細(xì)選擇哪些列要創(chuàng)建索引是非常關(guān)鍵的。