luso
luso

Reputation: 3000

Persisting OneToMany with Embeddable Id in JPA

An authorisation service is based upon http://www.svlada.com/jwt-token-authentication-with-spring-boot/ (sadly it doesn't provide a registration example)

I have the following Entities and Service:

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);
  }

This line return userRepository.save(newUser); however throws an exception as is not able to persist the relation. I can't set the UserRole ID (userId + role) manually as I don't yet have them (the user is to be persisted)

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)
    ...
    ...

Is this the correct way to persist this kind of relation while having an composite primary key as Embeddable?

If I avoid setting the realation with UserRole, the user gets persisted correctly (with no roles)

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;

Upvotes: 2

Views: 2908

Answers (2)

Rohit
Rohit

Reputation: 2152

Your UserRole class mapping is not correct. You have Role mapped twice once via the EmbeddedId and once directly in UserRole. You have to remove the second UserRole, the class will look like this.

@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;
    }
}

Also, note Id is no longer initialised.Change the registerUser to :

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; 
}

When you are creating the composite primary key for user_role you provide only ROLE and not the user_id, JPA complains user_id as null. We need to provide both.

Upvotes: 3

crizzis
crizzis

Reputation: 10716

In UserRole, the role is mapped twice: once as a simple property

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

and once more inside the embedded id:

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

At the time you call userRepository.save(newUser), you've only set the simple property UserRole.role to point to a non-null role. However, since the simple property is marked as insertable=false, it is being ignored in the INSERT statement. UserRole.id.role, in turn, is set to null and that is the value that is being considered for the INSERT statement. Since you've created a non-null constraint for the role column, the INSERT statement fails.

(note that DEFAULT '' is only honored when the column is not present in the INSERT clause's field list, which is not the case here)

The solution is, simply, to update the value of UserRole.id.role whenever User.role is being set.

Upvotes: 5

Related Questions