JPA多对一关系-只需要保存ID

58

我有两个类: Driver和Car。汽车表在单独的进程中更新。我需要在Driver类中有一个属性,可以让我读取完整的汽车描述并只写入指向现有汽车的Id。以下是示例:

@Entity(name = "DRIVER")
public class Driver {
... ID and other properties for Driver goes here .....

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "CAR_ID")
    private Car car;

    @JsonView({Views.Full.class})
    public Car getCar() {
      return car;
    }
    @JsonView({Views.Short.class})
    public long getCarId() {
      return car.getId();
    }
    public void setCarId(long carId) {
      this.car = new Car (carId);
    }

}

Car对象只是一个典型的JPA对象,没有对Driver进行反向引用。

所以我试图实现的是:

  1. 我可以使用详细的JSON视图阅读完整的Car描述
  2. 或者我可以在短的JsonView中仅阅读Car的ID
  3. 最重要的是,在创建新的Driver时,我只想传递汽车的JSON ID。这样我就不需要在持久化期间进行不必要的Car读取,只需更新ID即可。

我得到以下错误:

object references an unsaved transient instance - save the transient instance before flushing : com.Driver.car -> com.Car

我不想在数据库中更新汽车实例,而只想从司机那里引用它。有什么办法可以实现我的愿望吗?

谢谢。

更新: 忘记提到我在创建司机时传递的汽车ID是数据库中现有汽车的有效ID。

8个回答

52
作为对okutane的回答,请查看代码片段:
@JoinColumn(name = "car_id", insertable = false, updatable = false)
@ManyToOne(targetEntity = Car.class, fetch = FetchType.EAGER)
private Car car;

@Column(name = "car_id")
private Long carId;

那么在这里发生的情况是,当您想要进行插入/更新时,只需填充carId字段并执行插入/更新即可。由于汽车字段是不可插入和不可更新的,因此Hibernate不会抱怨此事,而且在数据库模型中,您只需要将car_id填充为外键即可,此时已足够(并且您在数据库上的外键关系将确保数据完整性)。现在,当您获取实体时,Hibernate将填充汽车字段,从而使您能够灵活地在需要时仅获取父级。


现在当您获取实体时,Hibernate将填充汽车字段,使您只有在需要获取父级时才具有灵活性。但是,fetch = FetchType.EAGER 不应该是 LAZY 吗? - Niklas
2
@Niklas,这并不重要,它可以根据您的用例是EAGER还是LAZY。关键在于这是一种写优化,因此在插入子实体之前,您无需获取父实体,只需填写ID即可节省向数据库的一次往返。当您获取子实体时,可能希望获取完整的图形,包括汽车字段(至少在我的情况下),我想使用1个查询而不是2个来获取它,因此使用EAGER。如果还不清楚,请让我知道,我将制作一个完整的工作示例来演示这一点。 - Jonck van der Kogel
哦,这真的很有帮助。之前我有些混淆,但现在更清晰了。 - Niklas
我认为这是最简单的解决方案。 - Marco Sulla
我得到了这个 在刷新之前保存临时实例 - Charlo Poitras
一个ID列表也可以实现这个吗? - Just A Question

48
你可以通过EntityManager中的getReference方法来实现此操作:
EntityManager em = ...;
Car car = em.getReference(Car.class, carId);

Driver driver = ...;
driver.setCar(car);
em.persist(driver);

这不会执行来自数据库的SELECT语句。


14
在春季,使用JpaRepository#getOne(),参见Hibernate 5和Spring Boot 2的最佳性能实践(第1部分),项目11:通过代理填充子侧父关联。 - Adi Sutanto
我认为这就是 OP 所寻找的,我也在寻找完全相同的东西,我想要保存子项而不必从数据库中读取父项,因为此时我不需要它,所以从数据库进行选择会很低效。 - Osmar
1
第一条评论的链接似乎将我重定向到另一篇文章,但在Web存档中找到了该页面:https://web.archive.org/web/20200608212049/https://dzone.com/articles/50-best-performance-practices-for-hibernate-5-amp - PaulD

17

你可以像这样仅使用car ID 进行工作:

@JoinColumn(name = "car")
@ManyToOne(targetEntity = Car.class, fetch = FetchType.LAZY)
@NotNull(message = "Car not set")
@JsonIgnore
private Car car;

@Column(name = "car", insertable = false, updatable = false)
private Long carId;

10
通过在实体上设置insertable = false、updatable = false,而不是在id上设置,我成功地插入了一个有效父id的子对象,而无需获取父对象。感谢这个提示! - Jonck van der Kogel
@JonckvanderKogel 能分享一下代码片段吗? :) - okutane
@okutane请查看下面的代码片段。希望能有所帮助! - Jonck van der Kogel

11
这个错误信息意味着您的对象图中存在一个瞬态实例,该实例未得到明确的持久化。JPA中对象可能存在的状态有:

  • 瞬态:尚未存储在数据库中的新对象(因此对实体管理器未知)。没有设置id。
  • 托管:实体管理器跟踪的对象。托管对象是您在事务范围内使用的对象,对托管对象所做的所有更改都将自动保存一次事务提交。
  • 游离:以前管理过的对象,在事务提交后仍然可访问。(事务外的托管对象)已设置id。

错误消息告诉您的是正在使用的(托管/游离)Driver对象持有对一个Hibernate未知的Car对象的引用(它是瞬态的)。为了使Hibernate理解应该保存从Driver引用到的任何未保存的Car实例,可以调用EntityManager的persist方法。

或者,您可以添加级联持久化(我认为,只是凭记忆,没有测试),这将在持久化Driver之前执行对Car的持久化。

@ManyToOne(fetch=FetchType.LAZY, cascade=CascadeType.PERSIST)
@JoinColumn(name = "CAR_ID")
private Car car;

如果您使用EntityManager的merge方法来存储Driver,应该加入CascadeType.MERGE,或者两者都要加入:
@ManyToOne(fetch=FetchType.LAZY, cascade={ CascadeType.PERSIST, CascadeType.MERGE })
@JoinColumn(name = "CAR_ID")
private Car car;

4
使用JPA时,首先需要停止以数据库为中心的思维方式。JPA处理Java对象,这是您查询和获取的内容。Driver没有carId属性,它有一个Car属性。因此,如果您收到这些数据(我猜name是新Driver的名称),您应该首先在carId上执行EntityManager#find以获取Car对象。然后创建一个新的Driver,将获取的Car分配给它,并发送到EntityManager#persist。由于Car已经被持久化和管理,我认为您不需要任何级联即可使其工作。 - Tobb
2
我明白了。我猜我没有办法像我这样创建Car对象并将其标记为持久化(只提供一个Id)。对于那些来自纯数据库查询世界的人来说,更新ID列似乎是一个简单的任务,这种限制感觉很尴尬。 我想另一个解决方案是将CAR_ID字段从@ManyToOne更改为基本的Long字段,并添加Transient字段Car。这将允许我在获取和设置时仅使用Ids,并且当我需要获取详细的Car信息时,我可以通过显式读取该Car来从Resuorce Controller设置Car字段。 感谢您的帮助。 - Andrei V
2
这将违背JPA的目的,如果您想这样工作,应该查看Spring JDBC,在那里您可以自己编写所有查询。使用框架违背其目的很少是一个好主意。 - Tobb
6
如果我需要创建/更新驱动程序,但我不使用所有与车辆有关的数据,那么我必须进行不必要的读取(通过id读取汽车),这听起来很笨拙。如果有几个类似的属性,而且在持久化之前必须进行N次读取(其中N是像车辆这样的属性数量),那么这种做法只是为了追求理念而让人感到不舒服,显然不是最优解。 - Andrei V
2
这是事实,但我们必须记住现在已经不是1983年了。我们使用的许多原则非常古老,不适用于今天拥有丰富处理器能力、内存和磁盘空间的情况。JPA并不是最优选择,但在95%的情况下它已经足够好了。话虽如此,我目前正在努力加速一些JPA查询,这可能会很麻烦。这确实取决于项目,您必须权衡速度与编程的易用性。在大多数情况下,速度不会成为问题,因此选择JPA将是正确的选择,因为它将节省您的时间。 - Tobb
显示剩余4条评论

3
public void setCarId(long carId) {
    this.car = new Car (carId);
}

这实际上不是一个 car 的保存版本。因此,它是一个瞬态对象,因为它没有 id。JPA 要求您应该注意关系。如果实体是新的(未被上下文管理),则在与其他已管理/分离对象相关联之前,应将其保存(实际上 MASTER 实体可以使用级联来维护其子项)。 两种方式:从数据库中进行级联保存和检索
此外,您应该避免手动设置实体的 ID如果您不想通过其 MASTER 实体更新/持久化 car,则应从数据库获取 CAR 并使用其实例来维护您的driver。因此,如果您这样做,则 Car 将脱离持久性上下文,但仍将具有 ID,并且可以与任何实体相关联而不受影响。

谢谢您的热情回复。这是否意味着它是瞬态的,因为ID不存在或者我没有通过读取某种方式初始化它?因为我传递的汽车ID是有效的。 - Andrei V
是的,因为它没有在持久化上下文中注册。JPA根本看不到它作为一个已保存的对象! :) 即使它不存在于数据库中,那么你如何将它与不存在的东西相关联呢?:) - 00Enthusiast

1
在HTML中添加可选字段,将其设置为false,如下所示:

添加可选字段equal false

@ManyToOne(optional = false) // Telling hibernate trust me (As a trusted developer in this project) when building the query that the id provided to this entity is exists in database thus build the insert/update query right away without pre-checks
private Car car; 

那么您可以仅将汽车的ID设置为:
driver.setCar(new Car(1));

然后正常持久化驱动程序

driverRepo.save(driver);

你会看到,id为1的汽车已经在数据库中完美地分配给了司机。
描述:
所以这个小的optional=false可能会有所帮助https://dev59.com/22Ml5IYBdhLWcg3w9q3u#17987718

这也改变了关联的语意。当设置 optional=false 时,它会更改默认值(true),从那时起,就没有可能保存具有 (driver.getCar() == null) == true 的 Driver 实例。 如果这是可以接受的,也许这个约束应该在提出任何问题之前就存在。 - Lubo

1
这里是 Adi Sutanto 链接的缺失文章。
项 11:通过代理填充子级父级关联 执行超出需要的 SQL 语句总是会导致性能下降。努力减少其数量非常重要,而依赖于引用是易于使用的优化方法之一。 描述:当子实体可以使用对其父实体的引用进行持久化(@ManyToOne@OneToOne 懒惰关联)时,Hibernate 代理可能非常有用。在这种情况下,从数据库中获取父实体(执行 SELECT 语句)会导致性能下降和无意义的操作。Hibernate 可以为未初始化的代理设置底层外键值。 关键点:
  • 依赖于 Spring 中的 EntityManager#getReference()

  • 在此示例中使用 JpaRepository#getOne(),

  • 在 Hibernate 中,使用 load()

  • 假设涉及到一个单向 @ManyToOne 关联的两个实体,即 AuthorBook(Author 是父端)。我们通过代理获取作者(这不会触发 SELECT),并创建一个新书,

  • 我们将代理设置为该书籍的作者,并保存书籍(这将在 book 表中触发一个 INSERT)。

  • 输出示例:
    • 控制台输出将显示仅触发了一个 INSERT ,没有 SELECT
    源代码可以在此处找到。
    如果您想查看整篇文章,请将https://dzone.com/articles/50-best-performance-practices-for-hibernate-5-amp放入 wayback 机器中。我没有找到现有版本的文章。
    PS:我目前正在处理使用 Jackson 对象映射器从前端反序列化实体的方式。如果您对这些内容感兴趣,请留言。

    -8

    在manytoone注释中使用级联

    @manytoone(cascade=CascadeType.Remove)

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接