javaspringspring-bootjpa

@EmbeddedId and @MapsId used with foreign key partially appearing in primary key


I'm running a Spring Boot app.

I have these two tables in my database. I fill an example of data that contains these tables.

Table user

Here columns id and century are PKs, and column state_id is a FK from table state with a reference of column id and century is also a FK from table state. So the century column is a FK and a PK at the same time.

id century name state_id
33 21 John Doe 1

Table state

Here, columns id and century are both primary keys of the table

id century label
1 21 cold

I want to model these two tables as JPA entities, so I did it like this:

StatePK.java:

@Embeddable
public class StatePK implements Serializable {
     private Long id;
     private Integer century;
}

State.java:

@Entity
@Table(name = "state")
public class State {

       @EmbeddedId
       private StatePK id;

       private String label;
}

UserPK.java:

@Embeddable
public class UserPK implements Serializable {
     private Long id;
     private Integer century;
}

User.java:

@Entity
@Table(name = "user")
public class User implements Serializable {

       @EmbeddedId
       private UserPK id;

       private String name;

       @MapsId("century")
       @ManyToOne(optional = false)
       @JoinColumns(value = {
           @JoinColumn(name = "century", referencedColumnName = "century"),
           @JoinColumn(name = "state_id", referencedColumnName = "id")})
       private State state;
}

When I launch the application, I have the following error:

Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is java.lang.IllegalStateException: PostInitCallback queue could not be processed...\r\n - PostInitCallbackEntry - EmbeddableMappingType(fr.xxx.User#{id})#finishInitialization

How can I resolve this error?


Solution

  • Your current configuration cannot work with @MapsId. This is because the primary key of the dependent entity (user) contains only part of the primary key of the parent entity (state).

    According to the docs of @MapsId

    Designates a ManyToOne or OneToOne relationship attribute that provides the mapping for an EmbeddedId primary key, an attribute within an EmbeddedId primary key, or a simple primary key of the parent entity.

    The attributes in the @EmbeddedId field of type UserPK are not sufficient to uniquely identify the related state. A state is identified by the pair (id;century), while a user contains only century in its @EmbeddedId. This is not enough for @MapsId to map the parent entity's primary key into the dependent entity's primary key.

    To fix your problem, you should refactor UserPK, and replace century with a field of type StatePK. Then, annotate the field state in User with @MapsId, and supply the name of the new StatePK attribute.

    For further reference, here is a link to section 2.4.1. Primary Keys Corresponding to Derived Identities of Jakarta Specification. This paragraph covers how the identity of an entity can be derived from another entity. Below (section 2.4.1.3. Examples of Derived Identities) are also showcased all possible derivation scenarios. Your situation falls under example 3 - case (b). Notice how in all examples, the dependent entity's primary key includes entirely the parent entity's primary key.

    StatePK.java

    @Embeddable
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class StatePK implements Serializable {
        private Long id;
        private Integer century;
    }
    

    State.java

    @Entity
    @Table(name = "state")
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class State {
    
        @EmbeddedId
        private StatePK id;
        private String label;
    }
    

    UserPK.java

    @Embeddable
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class UserPK implements Serializable {
        private Long id;
        private StatePK stateKey;
    }
    

    User.java

    @Entity
    @Table(name = "users")
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public class User implements Serializable {
    
        @EmbeddedId
        private UserPK id;
        private String name;
    
        @MapsId("stateKey")
        @ManyToOne(optional = false)
        @JoinColumns({
                @JoinColumn(name = "state_id", referencedColumnName = "id"),
                @JoinColumn(name = "state_century", referencedColumnName = "century")
        })
        private State state;
    }
    

    Test Application

    @SpringBootApplication
    public class DemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    
        @Bean
        public CommandLineRunner demo(EntityDemoService service) {
            return args -> service.demonstrateEntities();
        }
    }
    
    @Service
    class EntityDemoService {
    
        @PersistenceContext
        private EntityManager entityManager;
    
        @Transactional
        public void demonstrateEntities() {
            State state = new State();
            state.setId(new StatePK(1L, 21));
            state.setLabel("California");
    
            entityManager.persist(state);
            System.out.println("Inserted State: " + state);
    
            User user = new User();
            user.setId(new UserPK(100L, new StatePK(1L, 21)));
            user.setName("John Doe");
            user.setState(state);
    
            entityManager.persist(user);
            System.out.println("Inserted User: " + user.getName());
    
            entityManager.flush();
            entityManager.clear();
    
            System.out.println("\n--- Reading back from database ---\n");
    
            UserPK searchKey = new UserPK();
            searchKey.setId(100L);
            searchKey.setStateKey(new StatePK(1L, 21));
    
            User foundUser = entityManager.find(User.class, searchKey);
            System.out.println("Found User: " + foundUser);
        }
    }