javahibernatejpadata-modelinghibernate-onetomany

How to use generic types into a super class using a ManyToOne relationship with hibernate


I have one generic class using single table inheritance and other sub classes :

File, the main class

Folder extends File

Card extends File

Abstract FileLink extends File : a FileLink abstract class extending File

FolderLink extends FileLink and is composed with a Folder.

CardLink extends FileLink and is composed with a Card.

In my File class I have a collection of File with a oneToMany relationship, that can contain any type of File, so folders or folder links or card links.

@DiscriminatorColumn(name = "file_type", discriminatorType = DiscriminatorType.INTEGER)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class File{

    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    List<File> subFiles = new Linkedlist()

    @ManyToOne(fetch = FetchType.LAZY)
    protected File parent;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(length= 40)
    private String name;

    @Lob
    private String description;
}

A folder can have several links.

@Entity
@DiscriminatorValue(File.FOLDER + "")
public class Folder extends File {

    @OneToMany(mappedBy = "linkedFolder", fetch = FetchType.LAZY, targetEntity = FolderLink.class)
    protected Set<FolderLink> folderLinks = new HashSet<>();

    private Object folderAttribute;
}

same for a card

@Entity
@DiscriminatorValue(File.CARD+ "")
public class Card extends File {

    @OneToMany(mappedBy = "linkedCard", fetch = FetchType.LAZY, targetEntity = CardLink.class)
    protected Set<CardLink> cardLinks = new HashSet<>();

    private Object cardAttribute;
}

The Filelink class represent a link to a file and do not have much specific attributes used, just the parent file and the file linked.

In fact when I serialize my subfiles collection I want my links (FolderLink or Cardlinks) to return the linked files attributes values.

So the Folderlink class is supposed to return the Folder attributes values, CardLink the Card attributes values, and FileLink the File attributes values.

My actual modelisation is made this way :

@NoArgsConstructor
public abstract class FileLink extends File {
    
    @Override
    public String getName() {
        return "Link to "+this.getLinkedFile().getName();
    }

    @Override
    public String getDescription() {
        return this.getLinkedFile().getDescription();
    }
    ...

    @JsonIgnore
    public abstract File getLinkedFile();
}

@Entity
@DiscriminatorValue(File.FOLDER_LINK + "")
public class FolderLink extends FileLink {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "folder_linked_id", insertable = false, updatable = false)
    protected Folder linkedFolder;

    @JsonIgnore
    public File getLinkedFolder() {
        return this.linkedFolder;
    }

    @Override
    public String getFolderAttribute() {
        return "Link to "+this.getLinkedFile().getFolderAttribute();
    }

    @Override
    public File getLinkedFile() {
        return this.getLinkedFolder();
    }
}


@Entity
@DiscriminatorValue(File.CARD_LINK + "")
public class CardLink extends FileLink {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "card_linked_id", insertable = false, updatable = false)
    protected Card linkedCard;

    @JsonIgnore
    public File getLinkedCard() {
        return this.linkedCard;
    }

    @Override
    public String getCardAttribute() {
        return "Link to "+this.getLinkedFile().getCardAttribute();
    }

    @Override
    public File getLinkedFile() {
        return this.getLinkedCard();
    }
}

So when a FolderLink is serialized I can retrieve the linked folder attributes in addition with the attributes declared in its parent class (File) through the FileLink class, same for a card link.

But what I'd like to do is to declare the OneToMany relationship into the File and the FileLink classes only so :

@DiscriminatorColumn(name = "file_type", discriminatorType = DiscriminatorType.INTEGER)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class File{ 

    @OneToMany(mappedBy = "linkedFile", fetch = FetchType.LAZY, targetEntity = FileLink.class)
    protected Set<FileLink> fileLinks = new HashSet<>();

}

The Filelink class would not be abstract anymore.

@NoArgsConstructor
@Entity
@DiscriminatorValue(File.FILE_LINK + "")
public class FileLink extends File {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "file_linked_id", insertable = false, updatable = false)
    protected File linkedFile;

    @Override
    public String getName() {
        return "Link to "+this.getLinkedFile().getName();
    }

    ...

    @JsonIgnore
    public File getLinkedFile(){
       return this.linkedfile;
    }
}

@Entity
@DiscriminatorValue(File.FOLDER_LINK + "")
public class FolderLink extends FileLink {

    @Override
    public String getFolderAttribute() {
        return "Link to "+((Folder)this.getLinkedFile()).getFolderAttribute();
    }

}

@Entity
@DiscriminatorValue(File.CARD_LINK + "")
public class CardLink extends FileLink {

    @Override
    public String getCardrAttribute() {
        return "Link to "+((Card)this.getLinkedFile()).getCardAttribute();
    }

}

But it doesn't work. When I create a FolderLink I know that the linked file is a Folder but if I retrieve it from the super class I can't cast it into a Folder, hibernate tell me he cannot cast a File into a Folder, and it's normal because a File is not a Folder.

Is there a way to achieve that goal ? My current implementation is convenient enough to me but if I could do more it would be great.


Solution

  • what you are looking for is Polymorphic Association Mapping, as you mentioned

    hibernate tell me he cannot cast a File into a Folder, and it's normal because a File is not a Folder.

    you can define the type you expect by adding another column as type control.

    package com.example;
    
    import javax.persistence.Entity;
    import javax.persistence.JoinColumn;
    import javax.persistence.Column;
        
    import org.hibernate.annotations.Any;
    import org.hibernate.annotations.AnyMetaDef;
    import org.hibernate.annotations.Cascade;
    import org.hibernate.annotations.MetaValue;
    
    @Entity
    @DiscriminatorValue(File.FILE_LINK + "")
    public class FileLink extends File {
    
        @Any (
             metaColumn = @Column(name = "linkedFileType"),
             fetch=FetchType.LAZY
        )
        @AnyMetaDef(idType = "long", metaType = "string", metaValues = {
             @MetaValue(targetEntity = Folder.class, value = "folder"),
             @MetaValue(targetEntity = Card.class, value = "card")
        })
        @Cascade({org.hibernate.annotations.CascadeType.ALL})
        @JoinColumn(name = "file_linked_id")
        protected File linkedFile;
    
        @Override
        public String getName() {
            return "Link to "+this.getLinkedFile().getName();
        }
    
        ...
    
        @JsonIgnore
        public File getLinkedFile(){
           return this.linkedfile;
        }
    }