一些表模型没有通过外键约束来执行他们的外键引用。这是一种不好的做法,经常导致外键引用指向不存在的记录。当Hibernate试图解决这样一个破碎的引用时,它会抛出一个EntityNotFoundException。因此,你应该为每个外键引用定义一个外键约束。
但是,在有人决定不使用外键约束并将应用程序部署到生产中后,这个决定往往很难逆转。而这就使你陷入了这样的境地:你需要建立一个持久化层来处理引用不存在的记录的关联。
默认情况下,如果Hibernate试图解决一个破碎的外键引用,它会抛出一个异常。当然,解决这个问题的最好方法是清理你的数据库并修复你的外键引用。但如果这不是一个选项,你需要决定你是否。
- 想在每次调用可能被破坏的关联的getter方法时处理一个EntityNotFoundException ,还是
- 使用Hibernate的*@NotFound注解来告诉Hibernate获取一个可能被破坏的关联并忽略它,或者在实例化实体对象时抛出一个FetchNotFoundException* 。
内容
- 1Hibernate的@NotFound注解
- 2不使用@NotFound的懒惰抓取
- 3一个通常更好的选择
- 4结论
用Hibernate专有的*@NotFound*注解对关联进行注解有3个效果。
- Hibernate假设表模型没有为该关联定义外键约束,如果它生成了表模型,就不会生成外键约束。
- 你可以定义Hibernate是否应该忽略破损的外键引用或者抛出一个异常。
- Hibernate会急切地获取关联,即使你将其FetchType设置为LAZY。
我将在下一节中详细介绍Hibernate的强制急切获取。首先,让我们仔细看看*@NotFound注解和两个支持的NotFoundAction*。
NotFoundAction.EXCEPTION
你可以定义NotFoundAction.EXCEPTION,方法是用*@NotFound注解来映射你的关联,并将动作* 属性设置为EXCEPTION或保持空。这告诉Hibernate,如果它不能解决外键引用,就抛出一个FetchNotFoundException。
@Entity
public class ChessGame {
@ManyToOne(fetch = FetchType.LAZY)
@NotFound(action = NotFoundAction.EXCEPTION)
private ChessPlayer playerBlack;
...
}
这种行为可能看起来与你没有用*@NotFound*注释关联的行为非常相似。但是有两个区别。
- Hibernate抛出一个FetchNotFoundException,而不是EntityNotFoundException。
- Hibernate忽略了配置的FetchType,并试图急切地获取关联以验证外键引用。由于这个原因,Hibernate在实例化实体对象时抛出FetchNotFoundException,而不是在你第一次使用关联时抛出。这使得FetchNotFoundException 更容易处理。
当我在一个测试案例中使用映射时,你可以在日志输出中看到这一切,该案例获取了一个外键引用被破坏的ChessGame 实体。
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
ChessGame game = em.find(ChessGame.class, 10L);
log.info(game.getPlayerWhite() + " - " + game.getPlayerBlack());
em.getTransaction().commit();
em.close();
Hibernate在获取ChessGame实体的查询中连接并选择了playerBlack关联,并抛出了FetchNotFoundException。
17:04:20,702 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.chessTournament_id,c1_0.date,c1_0.playerBlack_id,p1_0.id,p1_0.birthDate,p1_0.firstName,p1_0.lastName,p1_0.version,c1_0.playerWhite_id,c1_0.round,c1_0.version from ChessGame c1_0 left join ChessPlayer p1_0 on p1_0.id=c1_0.playerBlack_id where c1_0.id=?
17:04:20,712 ERROR [com.thorben.janssen.sample.TestSample] - org.hibernate.FetchNotFoundException: Entity `com.thorben.janssen.sample.model.ChessPlayer` with identifier value `100` does not exist
NotFoundAction.IGNORE
将NotFoundAction 设置为IGNORE 使你能够在你的业务代码中处理破碎的外键引用。如果不能解决外键引用,Hibernate不会抛出一个异常,而是将关联属性设置为空。由于这个原因,你无法再区分一个关联是否没有被设置,或者它引用的是一个不再存在的记录。你需要为你的应用程序决定,是否要以不同的方式处理这两种情况。如果是这样的话,你就不能使用NotFoundAction.IGNORE。
像前面的例子一样,你需要用Hibernate的*@NotFound注解来注解映射关联的属性。但是这一次,你还需要将动作设置为NotFoundAction.IGNORE*。
@Entity
public class ChessGame {
@ManyToOne(fetch = FetchType.LAZY)
@NotFound(action = NotFoundAction.IGNORE)
private ChessPlayer playerBlack;
...
}
然后,当你执行与上一节相同的测试案例时,Hibernate不再抛出一个异常,而是将playerBlack 属性初始化为null。
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
ChessGame game = em.find(ChessGame.class, 10L);
log.info(game.getPlayerWhite() + " - " + game.getPlayerBlack());
em.getTransaction().commit();
em.close();
17:23:24,203 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.chessTournament_id,c1_0.date,c1_0.playerBlack_id,p1_0.id,p1_0.birthDate,p1_0.firstName,p1_0.lastName,p1_0.version,c1_0.playerWhite_id,c1_0.round,c1_0.version from ChessGame c1_0 left join ChessPlayer p1_0 on p1_0.id=c1_0.playerBlack_id where c1_0.id=?
17:23:24,223 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.birthDate,c1_0.firstName,c1_0.lastName,c1_0.version from ChessPlayer c1_0 where c1_0.id=?
17:23:24,237 INFO [com.thorben.janssen.sample.TestSample] - ChessPlayer [id=4, firstName=Fabiano, lastName=Caruana, birthDate=1992-07-30, version=0] - null
不使用*@NotFound*进行懒惰的获取
我在前面提到,用*@NotFound注解一个关联会将获取行为改为FetchType.EAGER*。即使你在关联映射中明确设置了FetchType.LAZY,也是如此,就像我在前面的例子中做的那样。
@Entity
public class ChessGame {
@ManyToOne(fetch = FetchType.LAZY)
@NotFound(action = NotFoundAction.IGNORE)
private ChessPlayer playerBlack;
...
}
原因很简单。Hibernate需要使用FetchType.EAGER来确保它只在引用现有实体对象时初始化关联属性。
如果你没有用*@NotFound*来注释你的关联属性,Hibernate就会期望一个外键约束来验证外键引用。 由于这个原因,它只需要检查外键引用是否被设置。如果是这样的话,它知道它将能够解析该引用,并使用一个代理对象初始化实体属性。当你第一次使用该代理时,Hibernate将执行一个SQL语句来解析外键引用。
如果你用*@NotFound*来注释关联属性,Hibernate就不能再相信外键引用。如果没有外键约束,该引用可能会被破坏。因此,Hibernate不能简单地使用外键值来实例化一个代理对象。它首先需要检查该引用是否有效。否则,它需要将关联属性设置为空。
执行这个额外的查询会产生性能问题。但在检查外键引用和试图获取关联实体之间,只有很小的性能差异。由于这个原因,Hibernate团队决定对所有用*@NotFound*注释的关联使用急切的获取方式。
一个通常更好的选择
Hibernate的*@NotFound映射的强制急切获取会导致性能问题。尽管实现可能更复杂,但通常最好不要用@NotFound*来注释你的关联,并在你的业务代码中处理破碎的外键引用。
如果外键引用被设置,Hibernate会实例化一个代理对象,并在第一次使用代理对象时尝试解决它。
17:35:52,212 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.chessTournament_id,c1_0.date,c1_0.playerBlack_id,c1_0.playerWhite_id,c1_0.round,c1_0.version from ChessGame c1_0 where c1_0.id=?
17:35:52,241 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.birthDate,c1_0.firstName,c1_0.lastName,c1_0.version from ChessPlayer c1_0 where c1_0.id=?
17:35:52,255 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.birthDate,c1_0.firstName,c1_0.lastName,c1_0.version from ChessPlayer c1_0 where c1_0.id=?
17:35:52,260 ERROR [com.thorben.janssen.sample.TestSample] - jakarta.persistence.EntityNotFoundException: Unable to find com.thorben.janssen.sample.model.ChessPlayer with id 100
如果外键引用被破坏,Hibernate会抛出一个EntityNotFoundException,你需要在业务代码中处理它。这种方法的明显缺点是,你需要在业务代码的不同地方处理这个异常。
你需要决定你是否愿意这样做以获得FetchType.LAZY的性能优势,或者你是否喜欢Hibernate的*@NotFound*映射提供的易用性。
停用外键约束
如果你决定在你的业务代码中处理破碎的外键引用,并使用Hibernate来生成你的表模型,你需要告诉Hibernate不要生成外键约束。
注意:只有当你在一个不使用外键约束的传统应用程序上工作时,你才应该使用这个方法。如果你还有选择的话,你应该总是使用外键约束来强制执行你的外键引用
你可以通过用*@JoinColumn注解来停用外键约束的生成,并将foreignKey属性设置为@ForeignKey(ConstraintMode.NO_CONSTRAINT)*。这个注解只影响Hibernate对表模型的生成,在运行时没有影响。
@Entity
public class ChessGame {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private ChessPlayer playerBlack;
...
}
结论
你的表模型应该通过一个外键约束来验证所有的外键引用。这可以确保新的外键引用只能引用现有的记录,而且你不能删除仍然被引用的记录。
不幸的是,一些架构师和开发团队决定避免外键约束。迟早,这些数据库会包含破碎的外键引用,你需要在你的实体映射或业务代码中处理。
如果你想在实体映射中处理它们,你可以用*@NotFound来注解关联。这告诉Hibernate不要期待或产生任何外键约束。然后,Hibernate会急切地获取关联以检查外键引用的有效性。对破损引用的处理取决于你的NotFoundAction*。Hibernate可以忽略它,用null 初始化属性,或者抛出一个EntityFetchException。
如果你喜欢在你的业务代码中处理破碎的外键引用,你可以用*@JoinColumn注释你的关联属性,并定义ConstraintMode.NO_CONSTRAINT*。这样,Hibernate在生成表模型时就不会生成外键约束了。在运行时,它不会检查外键引用,直到生成的代理对象试图解决它。