简体   繁体   中英

Bidirectional OneToMany relationship with composite primary key

I'm trying to create bidirectional OneToMany relationship,
where one foreign key is part of composite primary index.

I have currently this schema:

CREATE TABLE users
(
    id INTEGER UNIQUE AUTO_INCREMENT,

    PRIMARY KEY (id)
);

CREATE TABLE user_roles
(
    name   VARCHAR(16) NOT NULL,
    userId INTEGER,

    PRIMARY KEY (userId, name),
    FOREIGN KEY (userId) REFERENCES users (id)
);

And I would like to represent it in hibernate.

My UserEntity is mapped by:

@Entity
@Table(name = "users")
public class UserEntity implements User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "id.userId")
    List<UserRoleEntity> roles;

    @Override
    public Integer getId() {
        return id;
    }

    @Override
    public List<String> getRoles() {
        return roles.stream()
                .map(UserRoleEntity::getName)
                .collect(Collectors.toList());
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setRoles(List<UserRoleEntity> roles) {
        this.roles = roles;
    }
}

And My UserRoleEntity:

@Embeddable // I haven't yet implemented hashcode etc
class UserAndRoleNameUniqueId implements Serializable { 
    protected Integer userId;
    protected String name;

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

@Entity
@Table(name = "user_roles")
public class UserRoleEntity {
    @EmbeddedId
    protected UserAndRoleNameUniqueId id = new UserAndRoleNameUniqueId();

    @ManyToOne
    @MapsId("userId")
    protected UserEntity user;

    public void setUserId(Integer userId) {
        this.id.userId = userId;
    }

    public Integer getUserId() {
        return user.getId();
    }

    public void setUser(UserEntity user) {
        this.user = user;
    }

    public String getName() {
        return id.name;
    }

    public void setName(String name) {
        this.id.name = name;
    }
}

I'm saving UserEntity object by:

            var userEntity = new UserEntity();

            userEntity.setRoles(
                    user.getRoles().stream().map(role -> {
                        var userRoleEntity = new UserRoleEntity();
                        userRoleEntity.setName(role);
                        return userRoleEntity;
                    }).collect(Collectors.toList())
            );

            sessionFactory.getCurrentSession().save(userEntity);
            return userEntity;

and I get Caused by: java.sql.SQLException: Unknown column 'userroleen_.user_id' in 'field list'

How can I fix it and achieve this mapping in a simple way?

To map the primary key of UserEntity with the composite primary key column in UserRoleEntity , you will have to map the userId field of your UserRoleEntityId .

First you would need to change the mapping in your UserEntity , right now you have

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "id.userId")
List<UserRoleEntity> roles;

You need to change it to this

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "user")
List<UserRoleEntity> roles;

Reason is that user class is parent here and we need to specify the field in child entity which will drive the relation among two.

Now, mapping in your child table UserRoleEntity is fine, it will map the userId field in your composite primary key to the field userId in UserEntity table, so in essence your user entity primary key will be shared.

Now, you have to be beware of few things here

  • You need to implement the equals and hashcode in your composite primary key, otherwise all new UserRoleEntity entities generated by your code, will be considered equals, because both fields would be null.
  • Also, you should not assign the composite primary key as you have done in above code, you must specify at least one field which will differ (here userId and name should be unique combination)
  • Since, you are sharing primary key with UserEntity table, you need to be aware that at the time of creating new user that will be available only after hibernate execute insert statement (unless it is manual generated key, which is not the case here), so name property in UserRoleEntityId must be unique at the time of adding new roles.

Last but not the least

You should synch both of your entities in one operation in bidirectional relationship.

1 So You are setting role to UserEntity using a setter, but you should have a add function or setter written in given manner

public void add(UserEntityRole role) {
    roles.add(role);
    role.setUser(this);
}

Otherwise, if client forgot to call setter on both entities and table constraint were not there, then it could save foreign key as null in table.

Footnotes


1 For more info on this check linked article.

Best way to map hibernate bidirectional relationship

    @ManyToOne
    @MapsId("id")
    protected UserEntity user;

With @MapsId you reference the id field of the other entity. So if you put @MapsId on UserRoleEntity then the id that you reference is the one from UserEntity

But the following does also seem bad

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "id.userId")
    List<UserRoleEntity> roles;

change that to

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "id")
    List<UserRoleEntity> roles;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM