Spring JPA Unable To Find Composite Foreign Key Target Column (Non-PK)

Viewed 105

User.java

@Entity
@Table(name = "users")
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_role_id", referencedColumnName = "id")
    private UserRole userRole;

}

UserRole.java

@Data
@Entity
@Table(name = "user_roles")
public class UserRole implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

}

Client.java

@Data
@Entity
@Table(name = "clients")
public class Client implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumns({ @JoinColumn(name = "user_id", referencedColumnName = "id"),
        @JoinColumn(name = "user_role_id", referencedColumnName = "user_role_id") })
    private User user;

}

Error

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is org.hibernate.MappingException: Unable to find column with logical name: user_role_id in users

In RDBMS, users.(id, user_role_id) is unique so clients table can refer to that.

Last time, I was using insertable = false, updatable = false on user_role_id, but when I want to add records of new client, I always need to add user_role_id manually user.setUserRoleId(userRole.getId()) after user.setUserRole(userRole) and I think that is bad practice of ORM (it should be added automatically when I set user.setUserRole(userRole))

@Column(name = "user_role_id", insertable = false, updatable = false)
private Integer userRoleId;

What should I do so the relation can be mapped in Spring JPA? and what is the best practice?

In other words, this is also mean how to reference to foreign key generated logical name column?

2 Answers

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

OK! Please try following configuration: Below is a important code part and under this link you may find repository with working example

UserRole.java

@Data
@Entity
@Table(name = "user_roles")
public class UserRole implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "role_id")
    private Integer roleId;
}

User.java

@Data
@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, cascade = CascadeType.ALL)
    @JoinColumn(name = "user_role_id", referencedColumnName = "role_id")
    private UserRole userRole;
}

Client.java

@Data
@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(
            value = {
                    @JoinColumn(name = "client_role_id", referencedColumnName = "user_role_id"),
                    @JoinColumn(name = "client_user_id", referencedColumnName = "user_id"),
            }
            ,
            foreignKey = @ForeignKey(
                    name = "FK_user_with_role",
                    foreignKeyDefinition = "FOREIGN KEY (client_user_id, client_role_id)\n" +
            "   REFERENCES users \n" +
            "       (user_id, user_role_id) \n" +
            "   ON UPDATE CASCADE\n" +
            "   ON DELETE CASCADE")
    )
    private User user;
}

Please note that beside adding a foreignKey in the Client implementation, you MUST keep the sequence of @JoinColum annotations.. I don't know what is the reason behind, but if you flip those lines you'll still get your error as it was before :)

Related