EDIT: I've added another answer which fits best in my opinion. I'm leaving this one as well to see the other steps I tried.
Though the solution is not elegant and not using JPA as requested. Just in case anything in here would be helpful
If I understand the main issue correctly - you want to bind Client entity with Role entity via User entity, by first setting User's Role and then transfer that "property" by using only UserId instead setting additionally RoleId while creating Client.
Basically after playing for a while with your model I think the main issue is to assign data to each other within a @Transactional methods. That seems to be caused ba Lazy fetch strategy.
My proposal for solution that binds all your Entities according expectations differs only from yours with ommiting the RoleId JoinColumn in Clients table. I have checked that when calling a service that would have @Transactional methods, you can assign a Role to the User and User to the Client with simple user.setRole(roleEntity) followed by client.setUser(userEntity).
All the data is then consistent. No need to call further like getters and setters as you mentioned in the second part of your question.
Question is if for any reason you need to have RoleId as well in your Clients Table, then this soultion would have to be enhanced by additional column?
UserRole.java
@Entity
@Table(name = "user_roles")
public class UserRole implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "role_id")
private Integer roleId;
//getters and setters and toString
}
User.java
@Entity
@Table(name = "users")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Integer userId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_role_id", referencedColumnName = "role_id")
private UserRole userRole;;
//getters and setters and toString;
}
Client.java
@Entity
@Table(name = "clients")
public class Client implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "client_id")
private Integer clientId;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumns({
@JoinColumn(name = "client_user_id", referencedColumnName = "user_id"),
})
private User user;
@Column(name = "client_role_id")
private Integer roleId;
@PrePersist
@PreUpdate
private void prePersist(){
try {
roleId = getUser().getUserRole().getRoleId();
} catch (NullPointerException e){
roleId = null;
}
}
//getters and setters and toString
}
UserService.java
@Service
public class UserService {
UserRepo userRepo;
public UserService(UserRepo userRepo) {
this.userRepo = userRepo;
}
@Transactional
public void save(User user) {
userRepo.save(user);
}
@Transactional
public User getReferenceById(int i) {
return userRepo.getReferenceById(i);
}
}
ClientService.java
@Service
public class ClientService {
private ClientRepo clientRepo;
private UserService userService;
public ClientService(ClientRepo clientRepo, UserService userService) {
this.clientRepo = clientRepo;
this.userService = userService;
}
@Transactional
public Client save(Client client){
return clientRepo.save(client);
}
@Transactional
public Client getReferenceById(int i) {
return clientRepo.getReferenceById(i);
}
@Transactional
public void printClient(Client client){
client = clientRepo.getReferenceById(client.getClientId());
System.out.println(client);
}
@Transactional
public void bindUserToClient(int userId, int clientId) {
Client entity = clientRepo.findById(clientId).orElseGet(Client::new);
entity.setUser(userService.getReferenceById(userId));
}
@Transactional
public void printClient(int i) {
clientRepo.findById(i).ifPresentOrElse(this::printClient, EntityNotFoundException::new);
}
}
This configuration after running this commandLineRunner:
@Configuration
public class Config {
@Bean
@Transactional
public CommandLineRunner commandLineRunner(
@Autowired UserRoleRepo roleRepo,
@Autowired UserService userService,
@Autowired ClientService clientService
) {
return args -> {
for (int i = 0; i < 5; i++) {
roleRepo.save(new UserRole());
}
for (int i = 5; i > 0; i--) {
User user = new User();
user.setUserRole(roleRepo.getReferenceById(i));
userService.save(user);
}
Client client = new Client();
client.setUser(userService.getReferenceById(2));
client = clientService.save(client);
clientService.printClient(client);
client = new Client();
client.setClientId(1);
clientService.printClient(client);
int userId = 5;
clientService.bindUserToClient(userId, 1);
clientService.printClient(1);
};
}
}
gave me correct output in the console:
Client{id=1, user=User{id=2, userRole=UserRole{id=4}}}
Client{id=1, user=User{id=2, userRole=UserRole{id=4}}}
Client{id=1, user=User{id=5, userRole=UserRole{id=1}}}
WORKAROUND
I tried to reach the goal by use of Spring JPA but could'nt.
The workaround that keeps the referential integrity was by creating a constrains through DB like below and add @PrePersist and @PreUpdate annotated method which is updating the client's roleId as intended.
create table clients
(
client_id integer not null,
client_user_id integer,
client_role_id integer,
primary key (client_id)
);
create table user_roles
(
role_id integer generated by default as identity,
primary key (role_id)
);
create table users
(
user_id integer generated by default as identity,
user_role_id integer,
primary key (user_id),
CONSTRAINT User_Role UNIQUE (user_id, user_role_id)
);
alter table users
add constraint FK_role_id foreign key (user_role_id) references user_roles (role_id);
alter table clients
add constraint FK_user_id foreign key (client_user_id, client_role_id) references users (user_id, user_role_id) on update cascade ;
Thanks to that I could for instance update userRole in user entity, and the change was reflected in the clients table as well without any further actions