5

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?

0

2 Answers 2

1
+50

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);
    }
}
Sign up to request clarification or add additional context in comments.

Comments

0

The @MapsId("century") maps only part of the composite key but Hibernate expects @MapsId to map the entire primary key from the parent (id, century), not just one column.

You can fix it by removing @MapsId and explicitly defining the join columns:

@Entity
@Table(name = "\"user\"") // 'user' is reserved keyword in SQL
public class User {

    @EmbeddedId
    private UserPK id;

    private String name;

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

1 Comment

The part about @MapsId is correct, but this configuration does not work. If you try to insert a state and then a user, you get org.hibernate.exception.ConstraintViolationException: could not execute statement [NULL not allowed for column "STATE_ID"

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.