1. EntityManager
EntityManager不是線程安全的,而EntityManagerFactory是線程安全的,如果需要向Spring容器注入EntityManager對象喷兼,可使用@PersistenceContext
注解园匹,它保證了每個線程使用的EntityManager是獨立的霉旗。
@PersistenceContext
private EntityManager em;
2. 映射關系注解
2.1 一對一關系
/**
* 客戶表澄步,每個客戶擁有一個賬戶,為一對一關系
*/
@Entity // 作為hibernate 實體類
@Table(name = "tb_customer") // 映射的表明
@Data
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long custId; //客戶的主鍵
@Column(name = "cust_name")
private String custName; //客戶名稱
@Column(name = "cust_address")
private String custAddress;//客戶地址
@OneToOne(mappedBy = "customer", cascade = CascadeType.ALL,
fetch = FetchType.LAZY, orphanRemoval = true, optional=false)
@JoinColumn(name = "account_id") // 設置外鍵的字段名
private Account account;
}
/**
* 賬戶表目锭,每個賬戶對應一個客戶评汰,一對一關系
*/
@Entity
@Table(name = "tb_account")
@Data
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username; // 未采用@Column注解指定字段名
private String password;
@OneToOne // 一對一映射
@JoinColumn(name = "customer_id") // 外鍵ID
private Customer customer;
}
@Column
注解不是必須的纷捞,若未指定該注解的name值,則采用屬性名作為數(shù)據(jù)庫字段名被去;@JointColumn
用于指定表中外鍵的名稱兰绣,此時功能類似@Column,但注意它修飾屬性類型是另一個Entity類编振;@OneToOne
注解可以僅用于其中一個實體類缀辩,也可以同時應用于兩個實體類(比如希望在查詢一對一的兩個實體之一時,可以加載另一個實體踪央,兩個方向都希望可以如此操作的時候)臀玄,在沒有應用該注解的mappedBy
屬性時,Hibernate會在標注了@OneToOne的實體類對應的數(shù)據(jù)庫表創(chuàng)建外鍵約束畅蹂,即若兩個實體類都應用了@OneToOne
注解的情況下健无,則兩張表都會創(chuàng)建另一張表的外鍵約束,此時兩張互相存在外鍵約束的表液斜,在刪除數(shù)據(jù)和刪除表的時候累贤,會相對麻煩,可以通過該注解的mappedBy
屬性指定另一個實體的外鍵屬性名稱少漆,表示當前實體對應的表不維護外檢約束臼膏,由另一個實體對應的表維護外鍵約束;
外鍵約束3.1
@OneToOne
注解的cascade
屬性表示在操作當前實體的時候示损,是否需要級聯(lián)操作對應的外鍵記錄渗磅,相關的操作屬性值有:ALL
、PERSIST
检访、MERGE
始鱼、REMOVE
、REFRESH
脆贵、DETACH
医清;(查詢默認就支持級聯(lián)操作,沒有相關屬性值)
3.2@OneToOne
注解的fetch
屬性表示是否啟用懶加載卖氨,相關的屬性值有:LAZY
会烙、EAGER
;
3.3@OneToOne
注解的orphanRemoval
屬性值為true
的時候,表示當被@OneToOne
注解修飾的外鍵實體被當前實體設置為null時双泪,進行數(shù)據(jù)庫操作時持搜,JPA會級聯(lián)刪除對應的外鍵記錄;
3.4@OneToOne
注解的optional
屬性值表示當前實體是否能夠?qū)耐怄I實體設置為null與否焙矛。
2.2 一對多關系
場景:一個客戶可以有多條消息
@Entity // 作為hibernate 實體類
@Table(name = "tb_customer") // 映射的表明
@Data
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long custId; //客戶的主鍵
@Column(name = "cust_name")
private String custName;//客戶名稱
@Column(name = "cust_address")
private String custAddress;//客戶地址
// 一對多
// fetch 默認是懶加載 懶加載的優(yōu)點( 提高查詢性能)
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private List<Message> messages;
}
@Entity
@Table(name = "tb_message")
@Data
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String info;
public Message(String info) {
this.info = info;
}
// 一定要有、否則查詢就會有問題
public Message() {
}
}
@OneToMany
注解表示一對多的外鍵關系残腌,由于是一對多村斟,它應該放置在表示一端的實體類中贫导,修飾的屬性應該是集合類型,比如本例的List<Message>蟆盹,而Hibernate會將對應的外鍵字段創(chuàng)建到多端的實體類對應的數(shù)據(jù)庫表上孩灯,同時也會這張表創(chuàng)建對應的外鍵約束;@OneToMany
注解的fetch
屬性默認是LAZY逾滥,即默認懶加載峰档,這是因為一對多如果采用即時加載,在需要關聯(lián)很多外鍵表但這些數(shù)據(jù)在業(yè)務上應用不到的情況下將會浪費很多性能在做外連接操作上寨昙。
2.3 多對一關系
場景:一個客戶可以有多條消息
@Entity // 作為hibernate 實體類
@Table(name = "tb_customer") // 映射的表明
@Data
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long custId; //客戶的主鍵
@Column(name = "cust_name")
private String custName;//客戶名稱
@Column(name = "cust_address")
private String custAddress;//客戶地址
// 一對多
// fetch 默認是懶加載 懶加載的優(yōu)點( 提高查詢性能)
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private List<Message> messages;
}
@Entity
@Table(name = "tb_message")
@Data
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String info;
public Message(String info) {
this.info = info;
}
public Message(String info, Customer customer) {
this.info = info;
this.customer = customer;
}
// 一定要有讥巡、否則查詢就會有問題
public Message() {
}
// 多對一
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
@JoinColumn(name = "customer_id")
private Customer customer;
}
如果需要持久化多端的實體集合,建議應用多端實體的Repository進行持久化操作舔哪,如果使用一端的實體類進行持久化欢顷,Hibernate會先插入一端的實體,再插入多端的實體集合捉蚤,最后再對每一條多端的記錄進行update操作抬驴,維護多端記錄的外鍵;
一端插入
多端插入- 根據(jù)查詢條件場景的不同缆巧,可以通過多端或者一端查詢多端的數(shù)據(jù)布持,以本場景為例,如果需要查詢客戶的所有記錄陕悬,那么只需要查詢出該客戶鳖链,由于客戶和消息存在一對多的關系,那么當查詢出客戶記錄墩莫,通過客戶訪問消息的時候芙委,Hibernate就會進一步查詢該客戶的所有消息;
@Test @Transactional(readOnly = true) public void testR() { // 懶加載過程: // 1.findById 只會查詢Customer 和其他關聯(lián)的立即加載 Optional<Customer> customer = repository.findById(1L); System.out.println("====================="); // 由于輸出狂秦, 會自動調(diào)用customer.toString() System.out.println(customer); }
而若要查詢是消息的某個狀態(tài)灌侣,改狀態(tài)不綁定某個客戶,此時則可以通過多端的Message實體進行查詢裂问;
- 在多端通過命名接口的方式以外鍵作為查詢條件的時候侧啼,接口的名稱應該使用外鍵對應的實體名稱,并且入?yún)搼猛鈸鞂嶓w類堪簿,但可用的查詢條件僅為主鍵ID痊乾,如:
public interface MessageRepository extends PagingAndSortingRepository<Message, Long> { // 根據(jù)客戶id查詢所有信息 // 通過規(guī)定方法名來實現(xiàn)關聯(lián)查詢: 需要通過關聯(lián)屬性來進行匹配 // 但是只能通過id來進行匹配 List<Message> findByCustomer(Customer customer); }
2.3 多對多關系
場景:一個客戶可以有多個角色,一個角色可以賦給多個客戶
@Entity // 作為hibernate 實體類
@Table(name = "tb_customer") // 映射的表明
@Data
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long custId; //客戶的主鍵
@Column(name = "cust_name")
private String custName;//客戶名稱
@Column(name = "cust_address")
private String custAddress;//客戶地址
// 單向多對多
@ManyToMany(cascade = CascadeType.ALL)
/*中間表需要通過@JoinTable來維護外鍵:(不設置也會自動生成)
* name 指定中間表的名稱
* joinColumns 設置本表的外鍵名稱
* inverseJoinColumns 設置關聯(lián)表的外鍵名稱
* */
@JoinTable(
name = "tb_customer_role",
joinColumns = {@JoinColumn(name = "c_id")},
inverseJoinColumns = {@JoinColumn(name = "r_id")}
)
private List<Role> roles;
}
@Entity
@Table(name="tb_role")
@Data
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name="role_name")
private String rName;
public Role(String rName) {
this.rName = rName;
}
public Role(Long id, String rName) {
this.id = id;
this.rName = rName;
}
public Role() {
}
@ManyToMany(cascade = CascadeType.ALL)
private List<Role> roles;
}
- 多對多同樣存在單向多對多椭更,以及雙向多對多的配置哪审;
@JoinTable
注解,由于@JoinColumn
注解只能用于一對一虑瀑、一對多湿滓、多對一映射關系中指定一個外鍵名稱滴须,而多對多映射關系會產(chǎn)生一張中間表,中間表會存在兩張多對多映射關系表的外鍵約束叽奥,因此衍生出@JoinTable
注解扔水,可以指定中間表的名稱以及兩個外鍵的名稱;- 注意多對多不適用于級聯(lián)刪除朝氓,即如果應用
@ManyToMany
注解的cascade
屬性魔市,可能會導致Hibernate拋出ConstraintViolationException,/ * * 注意加上 * @Transactional @Commit 多對多其實不適合刪除赵哲, 因為經(jīng)常出現(xiàn)數(shù)據(jù)出現(xiàn)可能除了和當前這端關聯(lián)還會關聯(lián)另一端待德,此時刪除就會: ConstraintViolationException。 * 要刪除誓竿, 要保證沒有額外其他另一端數(shù)據(jù)關聯(lián) * */ @Test @Transactional @Commit public void testD() { Optional<Customer> customer = repository.findById(14L); repository.delete(customer.get()); }
如下圖所示磅网,采用級聯(lián)操作,當要刪除c_id為14的值得時候筷屡,要級聯(lián)刪除r_id為9和10的記錄涧偷,但這兩條記錄卻被c_id為9的記錄關聯(lián)著,導致無法正常級聯(lián)刪除:
中間表映射關系
2.4 補充
不要試圖直接在代碼中創(chuàng)建一個ID存在于數(shù)據(jù)庫的對象毙死,然后寄希望于Hibernate會將該對象的修改應用到數(shù)據(jù)庫中燎潮,這樣創(chuàng)建出來的對象是游離態(tài)DETACH
的,Hibernate無法對其進行管理扼倘,如果需要令Hibernate托管一個對象确封,需要通過Repository從數(shù)據(jù)庫中查詢出該對象。
/**
* 錯誤例子
*/
@Test
@Transactional
@Commit
public void testC() {
List<Role> roles = new ArrayList<>();
roles.add(new Role(9L, "超級管理員"));
roles.add(new Role(10L, "商品管理員"));
Customer customer = new Customer();
customer.setCustName("諸葛");
customer.setRoles(roles);
repository.save(customer);
}
/**
* 正確例子
* 1. 如果希望保存已有的關聯(lián)數(shù)據(jù) 再菊,就需要從數(shù)據(jù)庫中查出來(持久狀態(tài))爪喘。否則 提示游離狀態(tài)不能持久化;
* 2. 如果一個業(yè)務方法有多個持久化操作纠拔, 記得加上@Transactional秉剑,否則不能共用一個session;
* 3. 在單元測試中用到了@Transactional , 如果有增刪改的操作一定要加@Commit
* 4. 單元測試會認為你的事務方法@Transactional稠诲,只用于測試侦鹏,不會提交事務,從而需要單獨加上@Commit
*/
@Test
@Transactional
@Commit
public void testC() {
List<Role> roles = new ArrayList<>();
roles.add(roleRepository.findById(9L).get());
roles.add(roleRepository.findById(10L).get());
Customer customer = new Customer();
customer.setCustName("諸葛");
customer.setRoles(roles);
repository.save(customer);
}
3. 樂觀鎖
可以在實體類的屬性上使用@Version標注臀叙,Hibernate將會對當前實體類對應的表創(chuàng)建一個version字段略水,并應用樂觀鎖機制進行數(shù)據(jù)修改操作。
@Entity // 作為hibernate 實體類
@Table(name = "tb_customer") // 映射的表明
@Data
public class Customer {
@Version
private Long version;
}
4. 操作人審計
- 添加如下依賴
<!--spring-test -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.10</version>
<scope>test</scope>
</dependency>
- 添加配置類劝萤,啟用審計功能
@EnableJpaAuditing
渊涝,注冊返回當前操作用戶的Bean對象AuditorAware<T>
@Configuration // 標記當前類為配置類 =xml配文件
// 啟動JPA的Repository掃描,就是XML配置中的<jpa:repositories ...>標簽
@EnableJpaRepositories(basePackages="com.tuling.repositories")
@EnableTransactionManagement // 開啟事務
@EnableJpaAuditing // 開啟JPA審計功能
public class SpringDataJPAConfig {
/**
* 數(shù)據(jù)源
*/
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/springdata_jpa?characterEncoding=UTF-8");
return dataSource;
}
/**
* EntityManagerFactory
*/
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
vendorAdapter.setShowSql(true);
LocalContainerEntityManagerFactoryBean factory
= new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(vendorAdapter);
factory.setPackagesToScan("com.tuling.pojo");
factory.setDataSource(dataSource());
return factory;
}
/**
* 事務管理器
*/
@Bean
public PlatformTransactionManager transactionManager(
EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
// AuditorAware 返回當前用戶,泛型中的類型驶赏,根據(jù)實體類中的操作人類型決定
@Bean
public AuditorAware<String> auditorAware(){
return new AuditorAware() {
@Override
public Optional getCurrentAuditor() {
// 返回當前用戶炸卑,可以從Session既鞠、redis等存儲介質(zhì)中獲取當前用戶
return Optional.of("xushu");
}
};
}
}
- 在實體類中新增添加人煤傍、添加時間、修改人嘱蛋、修改時間相關屬性和注解蚯姆,并在實體類上增加啟用審計功能的注解
@EntityListeners(AuditingEntityListener.class)
,注意這里創(chuàng)建人和修改人的類型洒敏,需要和上面注冊類中返回的AuditorAware<T>
泛型一致龄恋;
@Entity // 作為hibernate 實體類
@Table(name = "tb_customer") // 映射的表明
@Data
@EntityListeners(AuditingEntityListener.class)
public class Customer {
// ...
/**
* 實體創(chuàng)建人
*/
@CreatedBy
String createdBy;
/**
* 實體創(chuàng)建時間
*/
@Temporal(TemporalType.TIMESTAMP)
@CreatedDate
protected Date dateCreated = new Date();
/**
* 實體最后修改人
*/
@LastModifiedBy
String modifiedBy;
/**
* 實體修改時間
*/
@Temporal(TemporalType.TIMESTAMP)
@LastModifiedDate
protected Date dateModified = new Date();
}
-
經(jīng)過上述步驟,每次新增凶伙、修改記錄時郭毕,SpringData-JPA將會在數(shù)據(jù)庫表內(nèi)維護如下字段:
操作人審計數(shù)據(jù)庫結(jié)構(gòu)