OneToMany and Composite Primary Key

Hi,

I am trying to understand why my implementation works. As a basic setup, I have a OneToMany mapping with each user having multiple authorities:

CREATE TABLE users
(
    id         BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    name       VARCHAR(255) NOT NULL UNIQUE,
    password   TEXT         NOT NULL,
    is_enabled BOOLEAN      NOT NULL
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

CREATE TABLE user_authorities
(
    user_id   BIGINT UNSIGNED NOT NULL,
    authority VARCHAR(255)    NOT NULL,
    CONSTRAINT uc_user_id_authority UNIQUE (user_id, authority),
    CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users (id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

As an ORM represenation, I have a UserEntity class

package kbh.qata.storage;

import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
@Table(name = "users")
public class UserEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Long id;

  @Column(name = "name")
  private String name;

  @Column(name = "password")
  private String password;

  @Column(name = "is_enabled")
  private boolean enabled;

  @OneToMany(
      cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE},
      mappedBy = "user",
      orphanRemoval = true)
  private Set<UserAuthorityEntity> userAuthorities = new HashSet<>();

  public void addUserAuthority(UserAuthorityEntity userAuthority) {
    this.userAuthorities.add(userAuthority);
    userAuthority.setUser(this);
  }
}

together with a UserAuthorityEntityclass

package kbh.qata.storage;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
// @IdClass(UserAuthorityId.class)
@Table(name = "user_authorities")
public class UserAuthorityEntity {

  @Id
  @Column(name = "user_id")
  private Long userId;

  // @Id
  @Column(name = "authority")
  private String authority;

  @ManyToOne
  @MapsId
  @JoinColumn(name = "user_id")
  private UserEntity user;
}

Now, what bothers me: Why is it enough to have a simply @Id annotation in the UserAuthorityEntity class? As can be seen from the SQL statements, this does not suffice as a primary key, since only the user_id and authority describe a unique tuple.

As you can see in the commented out section, I tried to setup a UserAuthorityId class to be able to to set both userId and authority as a primary key tuple.

However, this approach does not work. When starting the application, calling the AdminInitializer class, Hibernate complains that the userId of the UserAuthorityEntity is null. Which is correct, but why doesn’t Hibernate simply set it as the id it references to from the users table? Is Hibernate “confused” about the two @Id annotations? Do I do something wrong?

Thanky in advance for an support.
pd

Hi @patient-developer, when using @MapsId with a composite identifier you should specify which property should correspond to the associated entity’s id: in your case, I believe it should be @MapsId("userId"). If that doesn’t work, you might have found a bug when using @IdClass.

You could also try using an @EmbeddedId mapping for the UserAuthorityEntity instead and see if that works as well. Something like:

@Embeddable
public class UserAuthorityId {
  @Column(name = "user_id")
  private Long userId;

  @Column(name = "authority")
  private String authority;
}

@Entity
@Table(name = "user_authorities")
public class UserAuthorityEntity {
  @EmbeddedId
  private UserAuthorityId id;

  @ManyToOne
  @MapsId("userId")
  @JoinColumn(name = "user_id")
  private UserEntity user;
}

Hi @mbladel,
thank you very much for your reponse.

The case with @IdClass is not working, i.e., see the branch hibernate-id-class. As said, I just want to know if my understanding is correct and thus my expectations accurate: JPA needs a primary key in order to uniquely identify database rows. Since the table user_authorities does not provide such a key but both columns combined describe a unique entry, JPA expects something similar, i.e., providing (annotating) the columns with @Id which serve as uniquely identify a row.

So, somehow file a bug report? :thinking:

In contrast to that, thank you very much for your example with the approach using @Embeddable. I tested it on another branch hibernate-embedded-id and it seems to work.

So, somehow file a bug report? :thinking:

Yes please, you can open a new ticket in our issue tracker since I believe the @IdClass mapping should also work correctly. It would be great if you could attach a simplified reproducer that uses only Hibernate, to ensure the bug is on our side, you can start from our test case templates.

Hi @mbladel,

I am struggeling to understand how to use your shared Test Case Template. :frowning:

How do I apply my database schema?

From my side, I already provide a fully working example on the public hibernate-id-class branch.

Simply

  • checkout the repository
  • run a local MariaDB database via docker-compose with the provided docker-compose.ymlfile
  • initialize the tables via ./gradlew update
  • start the Spring Boot app and the AdminInitializer.class will crash

Would that also suffice as a test-case? :slight_smile:

How do I apply my database schema?

The test case templates are configured to run by default on H2 database and with hibernate.hbm2ddl.auto enabled. You should just add your entity mappings to the classpath (e.g. in the same package as JPAUnitTestCase.java), and the schema will be generated for you.

Would that also suffice as a test-case?

It would be great if you could isolate the problem to a simple case that uses only Hibernate, to make sure the bug is indeed caused by the library and not some third party framework or component that happens to use it. If you really can’t come up with a simplified reproducer you can still open a Jira and attach what you already have, but it probably take more time to troubleshoot the bug so less chances it will be solved soon.

Hi @mbladel,
I did setup a Bug report with the jira issue HHH-17755 - let’s see what happens. :slight_smile:

Thanks for your support
– pd

For the sake of completeness, issue HHH-17755 has been resolved. :slight_smile: