Reputation: 3000
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
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
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