在JPA中使用Embeddable Id保持OneToMany

时间:2017-06-26 21:36:13

标签: java database spring hibernate jpa

授权服务基于http://www.svlada.com/jwt-token-authentication-with-spring-boot/(遗憾的是它没有提供注册示例)

我有以下实体和服务:

User.java

package com.test.entity;

import javax.persistence.*;
import java.io.Serializable;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "user")
public class User implements Serializable {
  private static final long serialVersionUID = 1322120000551624359L;

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

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

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

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

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

  @Column(name = "activated")
  private Boolean activated;

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

  @Column(name = "activation_token_exp")
  private Timestamp activationTokenExpirationDate;

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

  @Column(name = "reset_token_exp")
  private Timestamp resetTokenExpirationDate;

  @Column(name = "created")
  private LocalDateTime created;

  @Column(name = "updated")
  private LocalDateTime updated;

  @OneToMany(cascade = CascadeType.ALL)
  @JoinColumn(name = "user_id", referencedColumnName = "id")
  private List<UserRole> roles = new ArrayList<>(0);

  public User() { }

  // getters and setters
}

UserRole.java

@Entity
@Table(name = "user_role")
public class UserRole implements Serializable {
  @Embeddable
  public static class Id implements Serializable {
    private static final long serialVersionUID = 1322120000551624359L;

    @Column(name = "user_id")
    protected Long userId;

    @Enumerated(EnumType.STRING)
    @Column(name = "role")
    protected Role role;

    public Id() { }

    public Id(Long userId, Role role) {
      this.userId = userId;
      this.role = role;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o)
        return true;
      if (o == null || getClass() != o.getClass())
        return false;

      Id id = (Id) o;

      if (! userId.equals(id.userId))
        return false;
      return role == id.role;
    }

    @Override
    public int hashCode() {
      int result = userId.hashCode();
      result = 31 * result + role.hashCode();
      return result;
    }
  }

  @EmbeddedId
  Id id = new Id();

  @Enumerated(EnumType.STRING)
  @Column(name = "role", insertable = false, updatable = false)
  protected Role role;

  public UserRole() {
  }

  public UserRole(Role role) {
    this.role = role;
  }

  public Role getRole() {
    return role;
  }

  public void setRole(Role role) {
    this.role = role;
  }
}

UserService.java

@Override
  public User registerUser(UserDTO userDto) {
    Optional<User> existingUser = this.getByUsername(userDto.getUsername());
    if (existingUser.isPresent()) {
      throw new RegistrationException("User is already taken");
    }

    User newUser = new User();
    newUser.setUsername(userDto.getUsername());
    newUser.setPassword(encoder.encode(userDto.getPassword()));
    newUser.setFirstName(userDto.getFirstName());
    newUser.setLastName(userDto.getLastName());
    newUser.setActivated(Boolean.FALSE);
    newUser.setActivationToken(RandomUtil.generateActivationKey());
    newUser.setActivationTokenExpirationDate(Timestamp.valueOf(LocalDateTime.now().plusSeconds(ACTIVATION_TOKEN_TTL)));
    newUser.setCreated(LocalDateTime.now());

    newUser.addRole(new UserRole(Role.MEMBER));

    return userRepository.save(newUser);
  }

但是这行return userRepository.save(newUser);会抛出异常,因为无法保持关系。我无法手动设置UserRole ID(userId + role),因为我还没有(用户将被保留)

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'role' cannot be null
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:404)
    at com.mysql.jdbc.Util.getInstance(Util.java:387)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:934)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3966)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3902)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2526)
    ...
    ...

这是将复合主键作为Embeddable来保持这种关系的正确方法吗?

如果我避免使用UserRole设置realation,则用户会被正确保留(没有角色)

DB

CREATE TABLE `user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `password` varchar(64) NOT NULL,
  `first_name` varchar(20) DEFAULT NULL,
  `last_name` varchar(20) DEFAULT NULL,
  `activated` tinyint(1) NOT NULL DEFAULT '0',
  `activation_token` varchar(50) DEFAULT NULL,
  `activation_token_exp` timestamp NULL DEFAULT NULL,
  `reset_token` varchar(50) DEFAULT NULL,
  `reset_token_exp` timestamp NULL DEFAULT NULL,
  `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8;

CREATE TABLE `user_role` (
  `user_id` bigint(20) unsigned NOT NULL,
  `role` varchar(50) NOT NULL DEFAULT '',
  PRIMARY KEY (`user_id`,`role`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2 个答案:

答案 0 :(得分:5)

UserRole中,角色被映射两次:一次作为简单属性

@Enumerated(EnumType.STRING)
@Column(name = "role", insertable = false, updatable = false)
protected Role role;

再一次在嵌入式ID中:

@Enumerated(EnumType.STRING)
@Column(name = "role")
protected Role role;

当您致电userRepository.save(newUser)时,您只需将简单属性UserRole.role设置为指向非空角色。但是,由于简单属性标记为insertable=false,因此在INSERT语句中忽略它。反过来,UserRole.id.role设置为null,这是INSERT语句正在考虑的值。由于您为role列创建了非空约束,因此INSERT语句失败。

(请注意DEFAULT ''只有在INSERT子句的字段列表中不存在列时才会受到尊重,这不是这里的情况。

解决方案是,只要设置UserRole.id.role,就更新User.role的值。

答案 1 :(得分:2)

您的UserRole班级映射不正确。您有Role两次通过EmbeddedId映射两次,一次直接映射到UserRole。您必须删除第二个UserRole,该类将如下所示。

@Entity
@Table(name = "user_role")
public class UserRole implements Serializable {
    @Embeddable
    public static class Id implements Serializable {
        private static final long serialVersionUID = 1322120000551624359L;

        @Column(name = "user_id")
        protected Long userId;

        @Enumerated(EnumType.STRING)
        @Column(name = "role")
        protected Role role;

        public Id() {
        }

        public Id(Long userId, Role role) {
            this.userId = userId;
            this.role = role;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            Id id = (Id) o;

            if (!userId.equals(id.userId))
                return false;
            return role == id.role;
        }

        @Override
        public int hashCode() {
            int result = userId.hashCode();
            result = 31 * result + role.hashCode();
            return result;
        }
    }

    @EmbeddedId
    Id id;

    public UserRole(Id id) {
        super();
        this.id = id;
    }

    public Id getId() {
        return id;
    }

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

另外,注释Id不再初始化。将registerUser更改为:

public User registerUser(String userName) {

    User newUser = new User();
    newUser.setUsername(userName);
    newUser.setPassword("password");
    newUser.setFirstName("name");
    //set other fields.

    userRepository.save(newUser);
    newUser.addRole(new UserRole(new UserRole.Id(newUser.getId(), Role.MEMBER)))
    userRepository.save(newUser);

    return newUser; 
}

当您为user_role创建复合主键时,您只提供ROLE而不是user_id,JPA会将user_id称为null。我们需要提供两者。