JPA many-to-one relation - need to save only Id
Asked Answered
S

8

60

I have 2 classes: Driver and Car. Cars table updated in separate process. What I need is to have property in Driver that allows me to read full car description and write only Id pointing to existing Car. Here is example:

@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 object is just typical JPA object with no back reference to the Driver.

So what I was trying to achieve by this is:

  1. I can read full Car description using detailed JSON View
  2. or I can read only Id of the Car in Short JsonView
  3. and most important, when creating new Driver I just want to pass in JSON ID of the car. This way I dont need to do unnesessery reads for the Car during persist but just update Id.

Im getting following error:

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

I dont want to update instance of the Car in DB but rather just reference to it from Driver. Any idea how to achieve what I want?

Thank you.

UPDATE: Forgot to mention that the ID of the Car that I pass during creation of the Driver is valid Id of the existing Car in DB.

Slug answered 13/1, 2015 at 19:56 Comment(0)
O
11

That error message means that you have have a transient instance in your object graph that is not explicitly persisted. Short recap of the statuses an object can have in JPA:

  • Transient: A new object that has not yet been stored in the database (and is thus unknown to the entitymanager.) Does not have an id set.
  • Managed: An object that the entitymanager keeps track of. Managed objects are what you work with within the scope of a transaction, and all changes done to a managed object will automatically be stored once the transaction is commited.
  • Detached: A previously managed object that is still reachable after the transction commits. (A managed object outside a transaction.) Has an id set.

What the error message is telling you is that the (managed/detached) Driver-object you are working with holds a reference to a Car-object that is unknown to Hibernate (it is transient). In order to make Hibernate understand that any unsaved instances of Car being referenced from a Driver about be saved should also be saved you can call the persist-method of the EntityManager.

Alternatively, you can add a cascade on persist (I think, just from the top of my head, haven't tested it), which will execute a persist on the Car prior to persisting the Driver.

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

If you use the merge-method of the entitymanager to store the Driver, you should add CascadeType.MERGE instead, or both:

@ManyToOne(fetch=FetchType.LAZY, cascade={ CascadeType.PERSIST, CascadeType.MERGE })
@JoinColumn(name = "CAR_ID")
private Car car;
Obmutescence answered 13/1, 2015 at 20:28 Comment(9)
Thank you Tobb. So if Im creating Driver from following Json {"name":"Joe Doe", "carId":123} where Car with id 123 already exsits, then the object does exist in DB. Does it mean that I can't update Dirver::carId unless I will update referenced Car as well?Slug
First thing with JPA is that you have to stop thinking in terms of the database. JPA deals with Java objects, that is what you query, and that is what you get. Driver does not have an attribute carId, it has an attribute Car. So, if you recieve those data (I guess name is the name of a new Driver), you should first do an EntityManager#find on the carId to get the Car-object. Then create a new Driver, assign the fetched Car to it, and send it to EntityManager#persist. Since the Car is already persisted and managed I don't think you need any cascade for this to work..Obmutescence
I see. I guess there is no way for me to create Car object as I do and mark it as persisted (feeding it just an Id). Feels like awkward limitation for someone who comes from pure DB query world where updating ID column is simple task. I guess another solution would be to change the CAR_ID field from @ManyToOne to basic Long field and add Transient field Car. That will allow me to work only with Ids during get and set and when I need to do get detailed Car info I can set Car field from Resuorce Controller by reading that Car explicitly. Appreciate your help.Slug
That would be against the purpose of JPA, if you want to work like that you should check out Spring JDBC, there you can write all the queries yourself. Using a framework against it's purpose is rarely a good idea..Obmutescence
Hmm isnt it sounds clunky when if i need to create/update Driver i would have to do unnesessery read (to read Car from id) even tho i dont use all that data. Imagine having few properties like that and instead of single persist you endup do N reads before persist (where N is number of properties like Car) just for the sake of ideology. I would cringe at it as unoptimal.Slug
It is, but we have to remember that it's not 1983 no more. A lot of the principles we use are really old, and does not apply that well today, with the abundance of processor power, memory and disk space. JPA is in no way optimal, but in 95% of the cases it's good enough. That being said, I'm currently working on speeding up some JPA queries, and it can be a pain. It's really something that depends on the project, you have to weight speed against ease of programming. In most cases, the speed won't be an issue, so going for JPA will be the right choice, since it will save you time.Obmutescence
In the few cases where speed is the number one priority (weighs higher than the development cost), JPA should be avoided, instead one should use JDBC (Spring JDBC-template will be of great help in these cases, as it removes a lot of boilerplate code and results in prettier code.)Obmutescence
Thank you for the overview Tobb.Slug
Check @Rebeca answer below for how to use EntityManager.getReference() to bypass the un-needed read.Birdman
I
61

As an answer to okutane, please see snippet:

@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;

So what happens here is that when you want to do an insert/update, you only populate the carId field and perform the insert/update. Since the car field is non-insertable and non-updatable Hibernate will not complain about this and since in your database model you would only populate your car_id as a foreign key anyway this is enough at this point (and your foreign key relationship on the database will ensure your data integrity). Now when you fetch your entity the car field will be populated by Hibernate giving you the flexibility where only your parent gets fetched when it needs to.

Inositol answered 16/5, 2018 at 19:9 Comment(8)
> Now when you fetch your entity the car field will be populated by Hibernate giving you the flexibility where only your parent gets fetched when it needs to. But then should not fetch = FetchType.EAGER be LAZY?Columbus
@Columbus it doesn't really matter, it can be EAGER or LAZY depending on your use-case. The point here is that it's a write optimisation, so you don't need to fetch the parent entity before inserting your child entity, you can just fill in the id saving a round trip to the database. When you fetch your child you probably want the full graph to be fetched including the car field and (at least in my case) I wanted to fetch it using 1 query and not 2, hence the EAGER. If it's not clear yet let me know and I will work out a full working example demonstrating this.Inositol
Oh that's really helpful. I confused things before but now it's much clearer.Columbus
I think this is the simplest solutionEosin
I get this save the transient instance before flushingVarro
Is that also possible for a list of Ids?Asta
When the car changes, and you have the line setCarId(2), would the car property still point to the old Car (i.e. getCarId() would be 2, but getCar().getId() would be 1)? Could that cause problems?Literator
@ChrisA so you would indeed need to refresh the entity. It really depends on your use case if this is useful for you. This is a write optimisation, when you want to update/insert your child entity, you don't have to fetch the parent first, but you can just use the id of the parent. Obviously if you now want to have the new Car entity you would need to fetch it from the database which sort of defeats the purpose of this optimisation.Inositol
R
49

You can do this via getReference call in EntityManager:

EntityManager em = ...;
Car car = em.getReference(Car.class, carId);

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

This will not execute SELECT statement from the database.

Rebeca answered 12/9, 2015 at 18:37 Comment(3)
In Spring, use JpaRepository#getOne(), see Best Performance Practices for Hibernate 5 and Spring Boot 2 (Part 1), Item 11: Populating a Child-Side Parent Association Via ProxyLarondalarosa
I think this is what the OP was looking for, I was looking for this exact same thing, I want to save the childs without having to read the parent from the DB as I don't need it at this point, so doing a select to the DB would be inefficientKinna
Link of first comment is redirecting me to a different article for some reason, found the page in a web archive: web.archive.org/web/20200608212049/https://dzone.com/articles/…Orcus
E
19

You can work only with the car ID like this:

@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;
Ettie answered 6/3, 2017 at 18:52 Comment(3)
By setting insertable = false, updatable = false on the entity and not on the id I managed to insert a child with a valid parent id without having to fetch the parent. Thanks for the tip!Inositol
@JonckvanderKogel care to share the snippet? :)Fremantle
@Fremantle please see the snippet below. Hope it helps!Inositol
O
11

That error message means that you have have a transient instance in your object graph that is not explicitly persisted. Short recap of the statuses an object can have in JPA:

  • Transient: A new object that has not yet been stored in the database (and is thus unknown to the entitymanager.) Does not have an id set.
  • Managed: An object that the entitymanager keeps track of. Managed objects are what you work with within the scope of a transaction, and all changes done to a managed object will automatically be stored once the transaction is commited.
  • Detached: A previously managed object that is still reachable after the transction commits. (A managed object outside a transaction.) Has an id set.

What the error message is telling you is that the (managed/detached) Driver-object you are working with holds a reference to a Car-object that is unknown to Hibernate (it is transient). In order to make Hibernate understand that any unsaved instances of Car being referenced from a Driver about be saved should also be saved you can call the persist-method of the EntityManager.

Alternatively, you can add a cascade on persist (I think, just from the top of my head, haven't tested it), which will execute a persist on the Car prior to persisting the Driver.

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

If you use the merge-method of the entitymanager to store the Driver, you should add CascadeType.MERGE instead, or both:

@ManyToOne(fetch=FetchType.LAZY, cascade={ CascadeType.PERSIST, CascadeType.MERGE })
@JoinColumn(name = "CAR_ID")
private Car car;
Obmutescence answered 13/1, 2015 at 20:28 Comment(9)
Thank you Tobb. So if Im creating Driver from following Json {"name":"Joe Doe", "carId":123} where Car with id 123 already exsits, then the object does exist in DB. Does it mean that I can't update Dirver::carId unless I will update referenced Car as well?Slug
First thing with JPA is that you have to stop thinking in terms of the database. JPA deals with Java objects, that is what you query, and that is what you get. Driver does not have an attribute carId, it has an attribute Car. So, if you recieve those data (I guess name is the name of a new Driver), you should first do an EntityManager#find on the carId to get the Car-object. Then create a new Driver, assign the fetched Car to it, and send it to EntityManager#persist. Since the Car is already persisted and managed I don't think you need any cascade for this to work..Obmutescence
I see. I guess there is no way for me to create Car object as I do and mark it as persisted (feeding it just an Id). Feels like awkward limitation for someone who comes from pure DB query world where updating ID column is simple task. I guess another solution would be to change the CAR_ID field from @ManyToOne to basic Long field and add Transient field Car. That will allow me to work only with Ids during get and set and when I need to do get detailed Car info I can set Car field from Resuorce Controller by reading that Car explicitly. Appreciate your help.Slug
That would be against the purpose of JPA, if you want to work like that you should check out Spring JDBC, there you can write all the queries yourself. Using a framework against it's purpose is rarely a good idea..Obmutescence
Hmm isnt it sounds clunky when if i need to create/update Driver i would have to do unnesessery read (to read Car from id) even tho i dont use all that data. Imagine having few properties like that and instead of single persist you endup do N reads before persist (where N is number of properties like Car) just for the sake of ideology. I would cringe at it as unoptimal.Slug
It is, but we have to remember that it's not 1983 no more. A lot of the principles we use are really old, and does not apply that well today, with the abundance of processor power, memory and disk space. JPA is in no way optimal, but in 95% of the cases it's good enough. That being said, I'm currently working on speeding up some JPA queries, and it can be a pain. It's really something that depends on the project, you have to weight speed against ease of programming. In most cases, the speed won't be an issue, so going for JPA will be the right choice, since it will save you time.Obmutescence
In the few cases where speed is the number one priority (weighs higher than the development cost), JPA should be avoided, instead one should use JDBC (Spring JDBC-template will be of great help in these cases, as it removes a lot of boilerplate code and results in prettier code.)Obmutescence
Thank you for the overview Tobb.Slug
Check @Rebeca answer below for how to use EntityManager.getReference() to bypass the un-needed read.Birdman
B
3
public void setCarId(long carId) {
    this.car = new Car (carId);
}

It is actually not saved version of a car. So it is a transient object because it hasn't id. JPA demands that you should take care about relations. If entity is new (doesn't managed by context) it should be saved before it can relate with other managed/detached objects (actually the MASTER entity can maintain it's children by using cascades).

Two ways: cascades or save&retrieval from db.

Also you should avoid set entity ID by hand. If you do not want to update/persist car by it's MASTER entity, you should get the CAR from database and maintain your driver with it's instance. So, if you do that, Car will be detached from persistence context, BUT still it will have and ID and can be related with any Entity without affects.

Bourne answered 13/1, 2015 at 20:24 Comment(2)
Thank you Enthusiast. Does it mean that its transient because Id does not exist or I didnt initialized it somehow by reading? Cause Id of the car that Im passing is validSlug
Yes, it is, because it doesn't registered in persistence context. JPA cannot see it as a saved object at all! :) Even it doesn't exists in DB, so how can you relate it with something who doesn't exists?:)Bourne
B
1

Add optional field equal false like following

@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; 

That way you can set just car's id as

driver.setCar(new Car(1));

and then persist driver normal

driverRepo.save(driver);

You will see that car with id 1 is assigned perfectly to driver in database

Description:

So what make this tiny optional=false makes may be this would help more https://stackoverflow.com/a/17987718

Bookseller answered 30/3, 2019 at 2:44 Comment(1)
This does also change semantics of relation. When optional=false is set, it changes default value (true) and from that point, there is no possibility to save Driver instance, which have (driver.getCar() == null) == true. If this is OK, perhaps this constrain should be there before any question asked.Mcglothlin
T
1

Here's the missing article that Adi Sutanto linked.

Item 11: Populating a Child-Side Parent Association Via Proxy Executing more SQL statements than needed is always a performance penalty. It is important to strive to reduce their number as much as possible, and relying on references is one of the easy to use optimization.

Description: A Hibernate proxy can be useful when a child entity can be persisted with a reference to its parent ( @ManyToOne or @OneToOne lazy association). In such cases, fetching the parent entity from the database (execute the SELECT statement) is a performance penalty and a pointless action. Hibernate can set the underlying foreign key value for an uninitialized proxy.

Key points:

  • Rely on EntityManager#getReference() In Spring

  • use JpaRepository#getOne() Used in this example,

  • in Hibernate, use load()

  • Assume two entities, Author and Book, involved in a unidirectional @ManyToOne association (Author is the parent-side) We fetch the author via a proxy (this will not trigger a SELECT), we create a new book

  • we set the proxy as the author for this book and we save the book (this will trigger an INSERT in the book table)

Output sample:

  • The console output will reveal that only an INSERT is triggered, and no SELECT

Source code can be found here.

If you want to see the whole article put https://dzone.com/articles/50-best-performance-practices-for-hibernate-5-amp into the wayback machine. I'm not finding a live version of the article.

PS. I'm currently on a way to handle this well when using Jackson object mapper to deserialize Entities from the frontend. If you're interested in how that plays into all this leave a comment.

Telethon answered 25/1, 2023 at 17:59 Comment(0)
M
-8

Use cascade in manytoone annotation @manytoone(cascade=CascadeType.Remove)

Metaplasm answered 13/1, 2015 at 20:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.