Search code examples
javahibernatejpamany-to-manyone-to-many

Java | JPA | Hibernate | AnnotationException: Use of @OneToMany or @ManyToMany targeting an unmapped class


I'm getting my feet wet with Java Spring Boot 2.7.15 and I'm having a bit of trouble. I've applied several solutions but they all lead back to the same error in the title.

Some background: I'm building a pet project learning management application where instructors have social media contact links. For this, I'm attempting to use a OneToMany relationship for Instructors to Socials.

Here's my code:

InstructorEntity.java

@Entity
@Table(name = "instructor", uniqueConstraints = @UniqueConstraint(columnNames = {"email"}))
public class InstructorEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String uuid;
    private String firstName;
    private String lastName;
    private String profession;
    @Column(columnDefinition = "longtext", length = 65555)
    private String bio;
    @Column(unique = true)
    private String email;
    private String phone;
    private String website;

    @OneToMany(
            cascade = CascadeType.ALL,
            orphanRemoval = true
    )
    @JoinTable(
            name = "instructor_social",
            joinColumns = {@JoinColumn(name = "instructor_id", referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name = "instructor_social_id", referencedColumnName = "id")}
    )
    private List<InstructorSocial> socials = new ArrayList<>();

    public InstructorEntity() {
    }

    public InstructorEntity(long id, String uuid, String firstName, String lastName, String profession, String bio, String email, String phone, String website) {
        this.id = id;
        this.uuid = uuid;
        this.firstName = firstName;
        this.lastName = lastName;
        this.profession = profession;
        this.bio = bio;
        this.email = email;
        this.phone = phone;
        this.website = website;
    }

    // Getters and Setters
}

InstructorSocialEntity.java

@Entity
@Table(name = "instructor_social")
public class InstructorSocialEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String uuid;
    private String name;
    private String image;

    public InstructorSocialEntity() {
    }

    public InstructorSocialEntity(long id, String uuid, String name, String image) {
        this.id = id;
        this.uuid = uuid;
        this.name = name;
        this.image = image;
    }

    // Getters and Setters
}

Updated Working Code

Instructor.java

@Entity
@Table(name = "instructor", uniqueConstraints = @UniqueConstraint(columnNames = {"email"}))
public class Instructor {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String uuid;
    private String firstName;
    private String lastName;
    private String profession;
    @Column(columnDefinition = "longtext", length = 65555)
    private String bio;
    @Column(unique = true)
    private String email;
    private String phone;
    private String website;

    @OneToMany(
            mappedBy = "instructor",
            cascade = CascadeType.ALL,
            orphanRemoval = true
    )
    private List<InstructorSocial> socials = new ArrayList<>();

    public Instructor() {
    }

    public Instructor(long id, String uuid, String firstName, String lastName, String profession, String bio, String email, String phone, String website) {
        this.id = id;
        this.uuid = uuid;
        this.firstName = firstName;
        this.lastName = lastName;
        this.profession = profession;
        this.bio = bio;
        this.email = email;
        this.phone = phone;
        this.website = website;
    }

InstructorSocial.java

@Entity
@Table(name = "instructor_social")
public class InstructorSocial {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String uuid;
    private String name;
    private String image;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "instructor_id")
    private Instructor instructor;

    public InstructorSocial() {
    }

    public InstructorSocial(long id, String uuid, String name, String image) {
        this.id = id;
        this.uuid = uuid;
        this.name = name;
        this.image = image;
    }

Edit Per one of the comments, I'm able to successfully run the app, but now I'm having a problem adding a Social to a list of the Instructor's Socials (i.e. Instructor.socials). The following code does, however, add the Social record into the instructor_social table in my database. Here's my code:

InstructorService.java

InstructorSocial social = new InstructorSocial();
social.setUuid();
social.setName(instructorSocial.getName());
social.setImage(instructorSocial.getImage());

foundInstructor.getSocials().add(social);
instructorRepository.save(foundInstructor);

Solution to the edit For full transparency, I figured out why my Instructor.socials lists weren't being populated... It was because of my Instructor.socials Model. Long story short, it had everything to do with me not setting up my Service without even defining what the InstructerService.getAllInstructors() method should return. Here is what I used:

service/InstructorServiceImpl.java

@Override
    public List<Instructor> getAllInstructors() {
        return instructorRepository
                .findAll().stream()
                .map(instructorEntity -> new Instructor(
                        instructorEntity.getId(),
                        instructorEntity.getUuid(),
                        instructorEntity.getFirstName(),
                        instructorEntity.getLastName(),
                        instructorEntity.getProfession(),
                        instructorEntity.getBio(),
                        instructorEntity.getEmail(),
                        instructorEntity.getPhone(),
                        instructorEntity.getWebsite(),
                        instructorEntity.getSocials()
                                .stream()
                                .map(instructorSocialEntity -> new InstructorSocial(
                                        instructorSocialEntity.getId(),
                                        instructorSocialEntity.getUuid(),
                                        instructorSocialEntity.getName(),
                                        instructorSocialEntity.getImage()
                                ))
                                .collect(Collectors.toList())
                ))
                .collect(Collectors.toList());
    }

Solution

  • Annotations like @JoinTable ,@JoinColumn are applied to the owning side of a relationship, i.e. the table that owns the foreign key to the other table.

    Here, have a look at the javadocs of the annotation.

    In the relationship between your tables, the owning side is the instructor_social table, so, it is the one that should have the @JoinXXX annotation on the field that represents the related table/Entity, i.e. instructor/InstructorEntity.

    Another thing that is missing in your code, that the Hibernate(JPA) complains of, is that the lack of mappedBy attribute in your @OneToMany annotation.

    Again, as per the docs,

    If the relationship is bidirectional, the mappedBy element must be used to specify the relationship field or property of the entity that is the owner of the relationship.

    Meaning, you have to update your @OneToMany annotation like so:

    @OneToMany(
            mappedBy = "instructor",
            cascade = CascadeType.ALL,
            orphanRemoval = true
    )
    private List<InstructorSocial> socials = new ArrayList<>();
    

    Here, mappedBy indicates how this entity (Instructor) is referenced from the owning entity (InstructorSocial).

    That means, you have to update your InstructorSocial entity to include the reference to the Instructor entity:

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "instructor_id")
    private InstructorEntity instructor;
    

    Notice the field name in the owning entity is the same as the value of the mappedBy attribute in the referenced entity, i.e "instructor". That specifies the bidirectional relation of these two entities.


    As a side note, you might consider refining the naming of your entities. Appending them with the suffix 'xxxEntity' can appear overly explanatory and may not enhance readability when sorted. Plus, Hibernate has the ability of generating the table and relation names from entities and their mapping configurations by the widely accepted RDBMS naming conventions. By allowing Hibernate to handle table organization, you can effectively address many trivial issues that may arise, especially if you are new to this field.