Hibernate HHH000179警告和narrow proxy

原文:https://marcin-chwedczuk.github.io/HHH000179-narrowing-proxy-to-class-this-operation-breaks-equality

译者:无意中流逝

本文将会解释为何hibernate会出现HHH000179警告,且无视这个警告会产生什么bug。

为了理解”窄化代理“(Narrowing proxy),首先要理解hibernate的代理原理。当我们读取一个懒加载的属性或者调用EntityManager::getReference方法时,hibernate会返回一个代理。这个代理运行时生成的类的实例。(如javassit)

例如:

@Entity
@Table(name = "person")
public class Person extends BaseEntity {
    @Column(name = "person_name", nullable = false)
    private String name;

    @ManyToOne(optional = false, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "house_id")
    private House house;

    @OneToMany(mappedBy = "owner", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Pet> pets = new HashSet<>(0);

    // ...
}

生成的类看起来像这样:

public class Person_$$_jvst5ed_2 
        extends Person 
        implements HibernateProxy, ProxyObject {
 
    private MethodHandler handler;
    private static Method[] _methods_;
 
    // plenty of other stuff here
 
    public final UUID _d7getId() {
        return super.getId();
    }
 
    public final UUID getId() {
        Method[] var1 = _methods_;
        return (UUID)this.handler.invoke(this, var1[14], var1[15], new Object[0]);
    }
}

这个类是继承实体类的

如果把代理类和类继承机制混合在一起会发生什么情况?

下面是一个简单继承关系

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "animal_type")
public abstract class Pet extends BaseEntity {
    @Column
    private String name;

    @JoinColumn(name = "owner_id", nullable = false)
    @OneToOne(optional = false, fetch = FetchType.LAZY)
    private Person owner;

    public abstract String makeNoise();
    // ...
}

@Entity
@DiscriminatorValue("cat")
public class Cat extends Pet { /* ... */ }

@Entity
@DiscriminatorValue("dog")
public class Dog extends Pet { /* ... */ }

当我们用EntityManager::getReference去加载一个Pet,我们会得到继承Pet的代理类。hibernate还不知道这个是Cat还是Dog

// 在先前的事务中:
Cat gerard = new Cat("gerard");
entityManager.persist(gerard);

gerardId = gerard.getId();

// 在当前事务中:
Pet pet = entityManager.getReference(Pet.class, gerardId);

assertThat(pet)
        .is(hibernateProxy()) //pet是个代理
        .is(uninitialized()) //pet未初始化
        .isInstanceOf(Pet.class)// pet是Pet的实例
        .isNotInstanceOf(Cat.class); // pet不是Cat的实例 

我们或许会强制让hibernate去查询数据库以加载实体,但不改变代理的类型

// makeNoise() 访问成员getter方法,导致代理初始化
logger.info("Pet is a cat: " + pet.makeNoise()); // “喵~喵~”

assertThat(pet)
        .isNot(uninitialized())//代理已初始化
        .isNotInstanceOf(Cat.class);//代理不是Cat的实例

即使现在hibernate已经知道pet其实是Cat,但也改变不了代理的类型,代理依旧是Pet。这会导致一些问题,比如pet instanceof Cat会返回false,尽管pet真真切切是一个Cat

还有另一个问题,如果makeNoise()直接访问成员变量,代理将不会触发初始化,也就不会从数据库获取数据,导致方法读取了一个未初始化的值。在处理实体状态的时,我们应该始终使用getter和setter

你也许会想,如果重新load一次Pet(在代理已经初始化之后),hibernate会返回Cat实体的实例。实际上hibernate更倾向于返回一级缓存中已有的实例而不是创建一个新的

Pet pet2 = entityManager.getReference(Pet.class, gerardId);

assertThat(pet2)
        .isNotInstanceOf(Cat.class)//pet2不是Cat的实例
        .isSameAs(pet);//pet2是pet本身

当我们明确表示要load一个Cat

// HHH000179: Narrowing proxy to class Cat - this operation breaks ==
Pet pet3 = entityManager.getReference(Cat.class, gerardId);
assertThat(pet3)
        .isInstanceOf(Cat.class)//pet3是Cat实例
        .isNot(hibernateProxy());//pet3不是hibernateProxy类

这就是HHH000179警告,hibernate解除了Cat实例的代理。现在Session里有两个不同的类(代理和Cat实例),且都指向同一个实体

因为pet指向的是Cat实例,Cat实例的修改也会反映到代理中

assertThat(pet.getName()).isEqualTo("gerard");//pet.getName()返回"gerard"

assertThat(pet).isNotSameAs(pet3);//pet != pet3

pet3.setName("proton");

assertThat(pet.getName()).isEqualTo("proton");//pet.getName()返回"proton"

你或许会认为,内存中有两个地方指向同一条数据库数没什么不妥,但如果没有重写equals()hashCode()时会导致一些问题。

// 假设Alice是猫主人
Person alice = entityManager.find(Person.class, aliceId);

// 虽然Alice拥有这只猫,但两种判断结果截然不同
assertThat(alice.getPets().contains(pet)) 
        .isFalse();

assertThat(alice.getPets().contains(pet3))
        .isTrue();


//使用默认equals()/hashCode()会出现这种情况

重写equals()方法可以修复此bug,比如通过比较主键是否相等

@MappedSuperclass
public abstract class BaseEntity {
    @Id
    @Type(type="binary(16)")
    private UUID id;

    protected BaseEntity() {
        this.id = UUID.randomUUID();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || !(o instanceof BaseEntity)) return false;

        BaseEntity that = (BaseEntity) o;

        // 记得使用getter
        return getId().equals(that.getId());
    }

    @Override
    public int hashCode() {
        return getId().hashCode();
    }
}

我们还可以通过延迟加载来重现上述行为,您可以在文末附带的源代码中找到如何执行此操作的示例。

实际应用的意义

最近我在开发一个基于内部框架的模块,我姑且称这个框架为X。框架包含了一些我无法修改的实体。为了给实体增加属性,我只能使用继承(幸运的是大部分类都只对应单个数据库表)。最后我们手上有一堆父类和一堆简单继承的子类。而且实体里也有大量对这些父类或子类的引用。正如你所预料的,到处都是HHH000179警告。所以我花了几个小时的时间来弄清楚这个警告是什么意思。在我们的例子中,只需要提供适当的equals()hashCode()。但总而言之,我想展示最后一个更真实的例子。

框架X的实体

@Entity
@Table(name = "extensible_user")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "discriminator")
@DiscriminatorValue("NOT_USED")
public class LegacyUser {
    @Id
    @GeneratedValue
    private Long id;

    @Column
    private String userPreference1;

    @Column
    private String userPreference2;
    // ...
}

@Entity
@Table(name = "document")
public class LegacyDocument {
    @Id
    @GeneratedValue
    private Long id;

    @Column
    private String contents;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "owner_id")
    // !!! Entity referes to super class !!!
    // 实体引用的是父类
    private LegacyUser owner;

    // ...
}

我的模块中的实体

@Entity
@DiscriminatorValue("EXTENDED")
public class ExtendedUser extends LegacyUser {
    @Column
    private String userPreference3;
    // ...
}

@Entity
@Table(name = "comment")
public class Comment {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(/*...*/)
    @JoinColumn(name = "document_id")
    private LegacyDocument document;

    @ManyToOne(/*...*/)
    @JoinColumn(name = "author_id")
    // !!! Entity refers to subclass !!!
    //实体引用的是子类
    private ExtendedUser author;

    @Column
    private String contents;
    // ...
}

原来的类LegacyDocument使用父类LegacyUser记录用户,新的类Comment使用子类ExtendedUser记录用户

缺少正确的equals()会导致如下问题:

LegacyDocument document = 
    entityManager.find(LegacyDocument.class, documentId);

// we load some data from document owner
LegacyUser documentOwner = document.getOwner();
doSomethingWithOwner(documentOwner);

// HHH000179: Narrowing proxy to class ExtendedUser 
//  - this operation breaks ==
// 当hibernate加载comment时,
// 相同id的ExtendedUser在其他地方以ExtendedUser形式存在现。
// hibernate已经意识到这documentOwner其实是ExtendedUser,那个代理的类型是错的
List<Comment> comments = entityManager.createQuery(
            "select c from Comment c where c.document.id = :docId",
            Comment.class)
        .setParameter("docId", document.getId())
        .getResultList();

// Now the most interesting part
ExtendedUser commentAuthor = comments.get(0).getAuthor();

// comment author and doc author is the same user
// 评论的作者id和文档作者id相同
assertThat(commentAuthor.getId())
        .isEqualTo(documentOwner.getId());

// 但commentAuthor != documentOwner
assertThat(commentAuthor)
        .isNotSameAs(documentOwner);

// 不重写hashCode()/equals()会出现如下问题
Set<LegacyUser> users = new HashSet<>();
users.add(commentAuthor);
users.add(documentOwner);

assertThat(users).hasSize(2);

这就是我关于HHH000179警告的讨论,最重要的是正确重写hashCode()/equals(),这样就可以安心无视这个警告。

源码: https://github.com/marcin-chwedczuk/hibernate_narrowing_proxy_warning_demo

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值