7

Given the following tables:

Car
int id PK
int modelId FK

CarDetails
int carId PK, FK to Car.id
varchar(50) description

How would I indicate that the @Id of CarDetails is also a foreign key to Car?

I've tried:

@Entity
public class Car {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @ManyToOne
    @JoinColumn(name = "modelId", nullable = false)
    private Model model;

    //setters & getters
}

@Entity
public class CarDetails {

    @Id
    @OneToOne
    @JoinColumn(name = "carId", nullable = false)
    private Car car;

    private String description;

    //setters & getters
}

However, I get the error

org.hibernate.MappingException: Composite-id class must implement Serializable: com.example.CarDetails

After implementing Serializable I get This class [class com.example.CarDetails] does not define an IdClass. But I still get the error after adding @IdClass(Car.class) to the CarDetails class.

UPDATE

The IdClass error originates from Spring: Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'carDetailsRepository': Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: This class [class com.example.CarDetails] does not define an IdClass

Here's the CarDetailsRepository:

public interface CarDetailsRepository extends JpaRepository<CarDetails, Car> {

}

Here are the relevant parts of my gradle build file:

plugins {
    id 'java'
    id 'eclipse'
    id 'maven-publish'
    id 'io.spring.dependency-management' version '1.0.3.RELEASE'
}

repositories {
    mavenCentral()
    mavenLocal()
}


dependencies {

    pmd group: 'org.hibernate', name: 'hibernate-tools', version: '5.2.3.Final'
    pmd group: 'org.hibernate', name: 'hibernate-core', version: '5.2.10.Final'
    pmd group: 'org.hibernate.common', name: 'hibernate-commons-annotations', version: '5.0.1.Final'

    compile('org.springframework.boot:spring-boot-starter')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('org.hibernate:hibernate-validator:5.2.4.Final')
    compile('org.hibernate:hibernate-envers:5.2.10.Final')
    compile('org.hibernate:hibernate-core:5.2.10.Final')
    compile('org.hibernate.common:hibernate-commons-annotations:5.0.1.Final')
    runtime('net.sourceforge.jtds:jtds:1.3.1')
    runtime('com.microsoft.sqlserver:sqljdbc:4.2')
    runtime('javax.el:javax.el-api:2.2.4')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

dependencyManagement {
    imports { mavenBom('org.springframework.boot:spring-boot-dependencies:1.5.4.RELEASE') }
}
James
  • 2,876
  • 18
  • 72
  • 116
  • 1
    Your mapping seems OK. Are you sure you're using JPA 2? – crizzis Jun 27 '17 at 17:57
  • Implement Serializable? – Rana_S Jun 27 '17 at 18:18
  • @crizzis - I'm using hibernate 5 which I believe is a JPA 2 implementation. – James Jun 27 '17 at 18:34
  • @Rossi - not sure I understand the question. I'm receiving an error that states that I must implement `Serializable` if I don't do such. – James Jun 27 '17 at 18:36
  • Take a look: https://stackoverflow.com/questions/9271835/why-composite-id-class-must-implement-serializable – Rana_S Jun 27 '17 at 18:41
  • @RossiRobinsion, how is that thread related? `CarDetails` is **not** a composite-id entity – crizzis Jun 27 '17 at 18:48
  • @James, I've tried your code and got the same error, but it went away after making `CarDetails` implement `Serializable`... – crizzis Jun 27 '17 at 19:02
  • @crizzis - Thanks. The exception `This class [class com.example.CarDetails] does not define an IdClass` is originating from Spring during bean creating `Error creating bean with name 'carDetailsRepository'`. So it appears that Spring Data JPA is requiring the IdClass. But even if you add the annotation, Spring still complains. After more research, I just found it is a bug https://jira.spring.io/browse/DATAJPA-866. – James Jun 27 '17 at 19:17
  • I have a question. So what is correct? `extends JpaRepository` or `extends JpaRepository`? – H Athukorala Nov 08 '20 at 20:42

2 Answers2

5

Probably the best way is to reference carId twice in your CarDetails entity--once for the @Id and once for the foreign key reference. You must declare one of these references with insertable=false and updatable=false so that JPA doesn't get confused trying to manage the same column in two spots:

@Entity
public class CarDetails {

    @Id
    @Column(name = "carId", insertable = false, updatable = false)
    private int carId; // don't bother with getter/setter since the `car` reference handles everything

    @OneToOne
    @JoinColumn(name = "carId", nullable = false)
    private Car car;

    private String description;

    //setters & getters
}

It looks odd, but it'll work and it's actually the (most) preferred method.

Alvin Thompson
  • 5,388
  • 3
  • 26
  • 39
  • I should mention that the reason this is the preferred method is because it's the form you'll need to use anyway if your second entity's ID is a composite key made up of the IDs of two (or more) different entities, and you want foreign key references to those entities. So to keep things consistent just always use this form. – Alvin Thompson Jun 27 '17 at 20:03
  • Thanks. This works well in my example where `carId` is an `int`. However, if `carId` was changed to `String`, I would have to manually set a non-null value prior to saving the entity. Otherwise, an exception will be thrown. Don't have to set it with `int` because it has a default value (i.e. 0). – James Jun 27 '17 at 21:11
  • If `carId` is a String, then `car.id` must also be a String. It still works. Have faith! :) – Alvin Thompson Jun 27 '17 at 21:13
  • In other words, you don't have to set `carId` to anything at all (hence no getters or setters). You ignore `carId` in your code and just use the `car` reference. If you want to get the car ID you use `getCar().getId()` just like you would have if `carId` weren't there. – Alvin Thompson Jun 27 '17 at 21:18
  • Agreed; `car.id` must also be a String in that case. I tried it with both `carId` and `car.id` being `String` and I get `Caused by: org.hibernate.id.IdentifierGenerationException: ids for this class must be manually assigned before calling save()`. – James Jun 27 '17 at 21:18
  • @James that's a separate issue. You can't use `@GeneratedValue(strategy = GenerationType.IDENTITY)` on a String. You can only use it on numbers. – Alvin Thompson Jun 27 '17 at 21:20
  • In other words, JPA only knows how to automatically assign an ID to a new entity if the ID is a number type (int, long, Long, etc). If the ID is a String you must manually set the new entity's ID before attempting to save it (and get rid of the line I mentioned above). – Alvin Thompson Jun 27 '17 at 21:24
  • Interesting... I removed that annotation. Not sure why it is complaining... The originating exception is `org.springframework.orm.jpa.JpaSystemException: ids for this class must be manually assigned before calling save(): com.example.CarDetails; nested exception is org.hibernate.id.IdentifierGenerationException: ids for this class must be manually assigned before calling save()` – James Jun 27 '17 at 21:25
  • @James: Yeah, before you can save an entity whose ID isn't automatically generated you must assign an ID to it. For example, `car.setId("foo");` – Alvin Thompson Jun 27 '17 at 21:28
  • Ah ok. Thanks. So, in the `String` scenario a bit of manual duplication to maintain. – James Jun 27 '17 at 21:32
  • Sorry... I misread your comment. I get an exception if only call `carDetails.set(car)` where car already has its id set. I have to call `carDetails.setId("foo");` in order for this to work. – James Jun 27 '17 at 21:37
  • @James you should never have to call `CarDetails.setId`. When you create a new Car, and if `car.id` is a String, you will need to call `Car.setId` on it. But when you create a new CarDetails, you just need to call `CarDetails.setCar` on it, regardless of whether `car.id` is a String. – Alvin Thompson Jun 27 '17 at 21:51
  • Makes sense. I would do exactly that but I get the exception unless I explicitly set the id on CarDetails. Do you get the exception when you try? Have you tried doing this with a Spring Repository? I did find a Spring JPA Data bug that appears to be related https://jira.spring.io/browse/DATAJPA-866. – James Jun 27 '17 at 21:58
  • If you’re getting an exception in that scenario, make sure you have the `insertable = false...` line mentioned above. – Alvin Thompson Jun 27 '17 at 21:59
  • I have `insertable = false` – James Jun 27 '17 at 22:00
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/147762/discussion-between-james-and-alvin-thompson). – James Jun 27 '17 at 22:04
  • 1
    Actually, in that scenario you’re right–you’ll need to set it in both places. I forgot to add, in the definition of `CarDetails.setCar(Car newCar)` you may want to optionally add `carId = newCar.getId();` (with null checks of course) to avoid having to manually do this. – Alvin Thompson Jun 27 '17 at 22:09
  • That applies regardless of the type of `car.id`. Good catch. – Alvin Thompson Jun 27 '17 at 22:15
  • Thanks. That will work. Another option is to change my database schema such that `CarDetails` has two separate cols for FK and PK. So one col for FK back to the `Car` and another col that is a PK auto generating `id`. – James Jun 27 '17 at 22:19
  • i strongly recommend against doing that. you’re breaking normalization on your database. what do you do if the columns wind up with two different values? – Alvin Thompson Jun 27 '17 at 22:22
  • I would put a unique constraint on the FK (`CarDetails.carId`) so that two rows in `CarDetails` cannot refer to the same carId. – James Jun 27 '17 at 22:25
  • Thanks for covering the additional scenario of a String being the PK and FK. I plan to upvote & accept your answer. I do think another viable option is to add an auto gen `id` to `CarDetails` and put the unique constraint on the FK (`CarDetails.carId`). Would appreciate your thoughts of doing that with the unique constraint vs adding the extra code in the setter. – James Jun 28 '17 at 15:15
  • 1
    @James That would work, but I would argue that you're not reducing complexity; you're just moving it from JPA to the DB (since you have to add a column). What's worse, insertions will be much slower (now there are **2** indexes and the DB has to produce a new autoincrement ID for each insert). – Alvin Thompson Jun 28 '17 at 16:58
  • It did not wrok for me without @MapsId – Steve Aug 25 '20 at 18:13
1

You might try mapping CarDetails like this:

@Entity
public class CarDetails {

    @Id
    private int id;

    @MapsId
    @OneToOne
    @JoinColumn(name = "carId", nullable = false)
    private Car car;

    private String description;

    //setters & getters
}

Note the @MapsId annotation.

Brian Vosburgh
  • 3,146
  • 1
  • 18
  • 18
  • `@MapsId` applies to embeddables and not entities. – Alvin Thompson Jun 27 '17 at 19:52
  • @AlvinThompson This should be valid, according to the JPA 2.1 spec, Section 2.4.1.3, Example 4, Case (b): "The dependent entity has a single primary key attribute corresponding to the relationship attribute. The primary key attribute is of the same basic type as the primary key of the parent entity. The MapsId annotation applied to the relationship attribute indicates that the primary key is mapped by the relationship attribute. " – Brian Vosburgh Jun 28 '17 at 02:33
  • If you edit your answer and give a quick working example I'll remove the down vote (I hate down voting). – Alvin Thompson Jun 29 '17 at 13:07