欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JPA referencedColumnName 非主键列时FetchType.LAZY失效处理

程序员文章站 2022-05-21 22:11:30
...

在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方法后才可以使用

参考

  1. 使用@ManyToOne(fetch=FetchType.LAZY),懒加载无效,这是怎么回事

  2. Hibernate 5 & JPA 2.1 延迟加载大字段属性