JPA referencedColumnName 非主键列时FetchType.LAZY失效处理
在Spring JPA 的级联操作中,当配置referencedColumnName为非主键列,FetchType.LAZY就会失效。
下面我们通过一个例子来看一看这个问题,以及 通过 PersistentAttributeInterceptable 接口来解决这个问题。
referencedColumnName为主键列时
下面看一个 People 和 Address 的referencedColumnName为主键列的例子。
People
package com.johnfnash.learn.domain;
import java.io.Serializable;
import java.sql.Timestamp;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
@Entity
public class People implements Serializable {
private static final long serialVersionUID = -4580187061659309868L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "name", nullable = true, length = 20)
private String name;
@Column(name = "sex", nullable = true, length = 1)
private String sex;
@Column(name = "birthday", nullable = true)
private Timestamp birthday;
// People 是关系的维护段,当删除 people时,会级联删除 address
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
// people 中的 address_id 字段参考 address 表的id字段
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;
public People() {
super();
}
public People(String name, String sex, Timestamp birthday, Address address) {
super();
this.name = name;
this.sex = sex;
this.birthday = birthday;
this.address = address;
}
// getter, setter
@Override
public String toString() {
return "People [id=" + id + ", name=" + name + ", sex=" + sex + ", birthday=" + birthday + "]";
}
}
由于 @JoinColumn 的配置,数据库中 people 表中会有一个 address_id 字段,通过外键引用 address 表的主键列(id)。
Address
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@Entity
@JsonIgnoreProperties(value={"hibernateLazyInitializer","handler","fieldHandler"})
public class Address implements Serializable {
private static final long serialVersionUID = -4960690362029661737L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;
@Column(name = "phone", nullable = false, length = 11)
private String phone;
@Column(name = "zipcode", nullable = true, length = 6)
private String zipcode;
@Column(name = "address", nullable = true, length = 100)
private String address;
//如果不需要根据Address级联查询People,可以注释掉
// @OneToOne(mappedBy = "address", cascade = {CascadeType.MERGE, CascadeType.REFRESH}, optional = false)
// private People people;
public Address() {
super();
}
public Address(String phone, String zipcode, String address) {
this.phone = phone;
this.zipcode = zipcode;
this.address = address;
}
// getter, setter
// ......
@Override
public String toString() {
return "Address [id=" + id + ", phone=" + phone + ", zipcode=" + zipcode + ", address=" + address + "]";
}
}
测试
创建 PeopleRepository,如下:
public interface PeopleRepository extends JpaRepository<People, Long> {
}
测试类:
import java.sql.Timestamp;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.johnfnash.learn.domain.Address;
import com.johnfnash.learn.domain.People;
import com.johnfnash.learn.repository.PeopleRepository;
@RunWith(SpringRunner.class)
@SpringBootTest
public class PeopleAddressTests {
@Autowired
private PeopleRepository peopleRepository;
@Test
public void savePeople() {
Address address = new Address("12345678901", "54002", "xxxx road");
People people = new People("ZS", "1", new Timestamp(System.currentTimeMillis()), address);
peopleRepository.save(people);
System.out.println(people.getId());
}
@Test
public void findPeople() {
People people = peopleRepository.getOne(1L);
System.out.println(people);
//System.out.println(people.getAddress());
}
}
先执行 savePeople 方法,再执行 findPeople 方法。通过下面的log,我们可以看到 people 对象中的address确实没有加载。
Hibernate: select people0_.id as id1_4_0_, people0_.address_id as address_5_4_0_, people0_.birthday as birthday2_4_0_, people0_.name as name3_4_0_, people0_.sex as sex4_4_0_ from people people0_ where people0_.id=?
People [id=1, name=ZS, sex=1, birthday=2019-01-19 18:09:57.0]
打开 findPeople 中的注释,如下
@Test
public void findPeople() {
People people = peopleRepository.getOne(1L);
System.out.println(people);
System.out.println(people.getAddress());
}
再次执行 findPeople 方法,输出的日志如下
Hibernate: select people0_.id as id1_4_0_, people0_.address_id as address_5_4_0_, people0_.birthday as birthday2_4_0_, people0_.name as name3_4_0_, people0_.sex as sex4_4_0_ from people people0_ where people0_.id=?
People [id=1, name=ZS, sex=1, birthday=2019-01-19 18:09:57.0]
Hibernate: select address0_.id as id1_0_0_, address0_.address as address2_0_0_, address0_.phone as phone3_0_0_, address0_.zipcode as zipcode4_0_0_ from address address0_ where address0_.id=?
Address [id=1, phone=12345678901, zipcode=54002, address=xxxx road]
这次因为用到了 address 信息,所以进行了查询。
referencedColumnName为非主键列时
依旧使用上面的例子,只是 People 类的 address 属性中 referencedColumnName 引用的字段改成非主键列 address。
// People 是关系的维护段,当删除 people时,会级联删除 address
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
// people 中的 address_id 字段参考 address 表的id字段
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;
通过下面的测试方法进行测试
@Test
public void findPeople() {
People people = peopleRepository.getOne(1L);
System.out.println(people);
//System.out.println(people.getAddress());
}
输出日志如下,我们可以看到address对象立即就加载了,并没有延迟加载。
Hibernate: select people0_.id as id1_4_0_, people0_.address_id as address_5_4_0_, people0_.birthday as birthday2_4_0_, people0_.name as name3_4_0_, people0_.sex as sex4_4_0_ from people people0_ where people0_.id=?
Hibernate: select address0_.id as id1_0_0_, address0_.address as address2_0_0_, address0_.phone as phone3_0_0_, address0_.zipcode as zipcode4_0_0_ from address address0_ where address0_.address=?
People [id=1, name=ZS, sex=1, birthday=2019-01-19 18:47:31.0]
查询资料,网上说 ,当referencedColumnName指向的列不是主键列时,hibernate去查询address时,理论上懒加载是需要使用proxy的,需要proxy的情况是,知道每个people都对应了一个address对象,但是这个时候,hibernate不知道是不是每一个people都对应于一个address,那么这个时候,他就回去查询一次,确定是否有address,那么他都去查询一次了,所以这个时候懒加载也就失效了。
那么这种情况下有没有什么办法对address进行懒加载呢?在hibernate中,可以通过继承 PersistentAttributeInterceptable 接口来实现延迟加载字段。下面将进行介绍。
PersistentAttributeInterceptable 实现字段延迟加载
首先,修改一下 People 类,继承 PersistentAttributeInterceptable 接口并实现相应方法,修改注解的设置以及修改一下相关字段的set方法和get方法。
import java.io.Serializable;
import java.sql.Timestamp;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import org.hibernate.annotations.LazyToOne;
import org.hibernate.annotations.LazyToOneOption;
import org.hibernate.engine.spi.PersistentAttributeInterceptable;
import org.hibernate.engine.spi.PersistentAttributeInterceptor;
@Entity
public class People implements PersistentAttributeInterceptable, Serializable {
private static final long serialVersionUID = -4580187061659309868L;
private PersistentAttributeInterceptor interceptor;
private Long id;
private String name;
private String sex;
private Timestamp birthday;
private Address address;
public People() {
super();
}
public People(String name, String sex, Timestamp birthday, Address address) {
super();
this.name = name;
this.sex = sex;
this.birthday = birthday;
this.address = address;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column(name = "name", nullable = true, length = 20)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Column(name = "sex", nullable = true, length = 1)
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Column(name = "birthday", nullable = true)
public Timestamp getBirthday() {
return birthday;
}
public void setBirthday(Timestamp birthday) {
this.birthday = birthday;
}
// People 是关系的维护段,当删除 people时,会级联删除 address
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
// people 中的 address_id 字段参考 address 表的id字段
@JoinColumn(name = "address_id", referencedColumnName = "address")
@LazyToOne(LazyToOneOption.NO_PROXY)
public Address getAddress() {
if (interceptor != null) {
return (Address) interceptor.readObject(this, "address", address);
}
return address;
}
public void setAddress(Address address) {
if (interceptor != null) {
this.address = (Address) interceptor.writeObject(this, "owner", this.address, address);
return;
}
this.address = address;
}
@Override
public String toString() {
return "People [id=" + id + ", name=" + name + ", sex=" + sex + ", birthday=" + birthday + "]";
}
@Override
public PersistentAttributeInterceptor $$_hibernate_getInterceptor() {
return interceptor;
}
@Override
public void $$_hibernate_setInterceptor(PersistentAttributeInterceptor interceptor) {
this.interceptor = interceptor;
}
}
再次执行测试方法 findPeople,发现address又可以延迟加载了。
Hibernate: select people0_.id as id1_4_0_, people0_.birthday as birthday2_4_0_, people0_.name as name3_4_0_, people0_.sex as sex4_4_0_ from people people0_ where people0_.id=?
People [id=1, name=ZS, sex=1, birthday=2019-01-19 18:47:31.0]
注意事项:实现PersistentAttributeInterceptor的实体,重写PersistentAttributeInterceptor的get,set方法后才可以使用