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值,则采用属性名作为数据库字段名;@JointColumn
用于指定表中外键的名称,此时功能类似@Column,但注意它修饰属性类型是另一个Entity类;@OneToOne
注解可以仅用于其中一个实体类,也可以同时应用于两个实体类(比如希望在查询一对一的两个实体之一时,可以加载另一个实体,两个方向都希望可以如此操作的时候),在没有应用该注解的mappedBy
属性时,Hibernate会在标注了@OneToOne的实体类对应的数据库表创建外键约束,即若两个实体类都应用了@OneToOne
注解的情况下,则两张表都会创建另一张表的外键约束,此时两张互相存在外键约束的表,在删除数据和删除表的时候,会相对麻烦,可以通过该注解的mappedBy
属性指定另一个实体的外键属性名称,表示当前实体对应的表不维护外检约束,由另一个实体对应的表维护外键约束;
外键约束3.1
@OneToOne
注解的cascade
属性表示在操作当前实体的时候,是否需要级联操作对应的外键记录,相关的操作属性值有:ALL
、PERSIST
、MERGE
、REMOVE
、REFRESH
、DETACH
;(查询默认就支持级联操作,没有相关属性值)
3.2@OneToOne
注解的fetch
属性表示是否启用懒加载,相关的属性值有:LAZY
、EAGER
;
3.3@OneToOne
注解的orphanRemoval
属性值为true
的时候,表示当被@OneToOne
注解修饰的外键实体被当前实体设置为null时,进行数据库操作时,JPA会级联删除对应的外键记录;
3.4@OneToOne
注解的optional
属性值表示当前实体是否能够将对应的外键实体设置为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 默认是懒加载 懒加载的优点( 提高查询性能)
@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会将对应的外键字段创建到多端的实体类对应的数据库表上,同时也会这张表创建对应的外键约束;@OneToMany
注解的fetch
属性默认是LAZY,即默认懒加载,这是因为一对多如果采用即时加载,在需要关联很多外键表但这些数据在业务上应用不到的情况下将会浪费很多性能在做外连接操作上。
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 默认是懒加载 懒加载的优点( 提高查询性能)
@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操作,维护多端记录的外键;
一端插入
多端插入- 根据查询条件场景的不同,可以通过多端或者一端查询多端的数据,以本场景为例,如果需要查询客户的所有记录,那么只需要查询出该客户,由于客户和消息存在一对多的关系,那么当查询出客户记录,通过客户访问消息的时候,Hibernate就会进一步查询该客户的所有消息;
@Test @Transactional(readOnly = true) public void testR() { // 懒加载过程: // 1.findById 只会查询Customer 和其他关联的立即加载 Optional<Customer> customer = repository.findById(1L); System.out.println("====================="); // 由于输出, 会自动调用customer.toString() System.out.println(customer); }
而若要查询是消息的某个状态,改状态不绑定某个客户,此时则可以通过多端的Message实体进行查询;
- 在多端通过命名接口的方式以外键作为查询条件的时候,接口的名称应该使用外键对应的实体名称,并且入参应该应用外捡实体类,但可用的查询条件仅为主键ID,如:
public interface MessageRepository extends PagingAndSortingRepository<Message, Long> { // 根据客户id查询所有信息 // 通过规定方法名来实现关联查询: 需要通过关联属性来进行匹配 // 但是只能通过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 设置关联表的外键名称
* */
@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
注解只能用于一对一、一对多、多对一映射关系中指定一个外键名称,而多对多映射关系会产生一张中间表,中间表会存在两张多对多映射关系表的外键约束,因此衍生出@JoinTable
注解,可以指定中间表的名称以及两个外键的名称;- 注意多对多不适用于级联删除,即如果应用
@ManyToMany
注解的cascade
属性,可能会导致Hibernate抛出ConstraintViolationException,/ * * 注意加上 * @Transactional @Commit 多对多其实不适合删除, 因为经常出现数据出现可能除了和当前这端关联还会关联另一端,此时删除就会: ConstraintViolationException。 * 要删除, 要保证没有额外其他另一端数据关联 * */ @Test @Transactional @Commit public void testD() { Optional<Customer> customer = repository.findById(14L); repository.delete(customer.get()); }
如下图所示,采用级联操作,当要删除c_id为14的值得时候,要级联删除r_id为9和10的记录,但这两条记录却被c_id为9的记录关联着,导致无法正常级联删除:
中间表映射关系
2.4 补充
不要试图直接在代码中创建一个ID存在于数据库的对象,然后寄希望于Hibernate会将该对象的修改应用到数据库中,这样创建出来的对象是游离态DETACH
的,Hibernate无法对其进行管理,如果需要令Hibernate托管一个对象,需要通过Repository从数据库中查询出该对象。
/**
* 错误例子
*/
@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. 如果希望保存已有的关联数据 ,就需要从数据库中查出来(持久状态)。否则 提示游离状态不能持久化;
* 2. 如果一个业务方法有多个持久化操作, 记得加上@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将会对当前实体类对应的表创建一个version字段,并应用乐观锁机制进行数据修改操作。
@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 {
/**
* 数据源
*/
@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 返回当前用户,泛型中的类型,根据实体类中的操作人类型决定
@Bean
public AuditorAware<String> auditorAware(){
return new AuditorAware() {
@Override
public Optional getCurrentAuditor() {
// 返回当前用户,可以从Session、redis等存储介质中获取当前用户
return Optional.of("xushu");
}
};
}
}
- 在实体类中新增添加人、添加时间、修改人、修改时间相关属性和注解,并在实体类上增加启用审计功能的注解
@EntityListeners(AuditingEntityListener.class)
,注意这里创建人和修改人的类型,需要和上面注册类中返回的AuditorAware<T>
泛型一致;
@Entity // 作为hibernate 实体类
@Table(name = "tb_customer") // 映射的表明
@Data
@EntityListeners(AuditingEntityListener.class)
public class Customer {
// ...
/**
* 实体创建人
*/
@CreatedBy
String createdBy;
/**
* 实体创建时间
*/
@Temporal(TemporalType.TIMESTAMP)
@CreatedDate
protected Date dateCreated = new Date();
/**
* 实体最后修改人
*/
@LastModifiedBy
String modifiedBy;
/**
* 实体修改时间
*/
@Temporal(TemporalType.TIMESTAMP)
@LastModifiedDate
protected Date dateModified = new Date();
}
-
经过上述步骤,每次新增、修改记录时,SpringData-JPA将会在数据库表内维护如下字段:
操作人审计数据库结构