1

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.

kaizokun
  • 926
  • 3
  • 9
  • 31
  • When designing an inheritance hierarchy, whenever you type `B extends A`, you should be asking yourself the question: 'can `B` be used wherever `A` is expected?'. From this perspective, `FolderLink` is redundant, because you can assign a `Folder` to `FileLink.linkedFile` (due to the fact that `Folder` extends `File`). Also, `Folder` inherits the `fileLinks` property from `File` – crizzis Jul 24 '20 at 09:05
  • @crizzis yes but linkedFile link a File so if I serialize a FileLink I can only get the File attributes not the Folder attributes, that is why I need a FolderLink class to do so. I want to serialize a FolderLink or a CardLink (Card extending File too with its own attributes) like it is a Folder or a Card. – kaizokun Jul 24 '20 at 11:50
  • In that case, perhaps you simply need a lightweight interface to express the *capabilities* of both `FolderLink` and `Folder` (i.e. `PhysicalFolder implements Folder` and `FolderLink implements Folder`; not sure about the naming), there is no need for inheritance. Note that in your setup, `FolderLink` inherits physical attributes from `Folder` that it never uses, since the getters delegate to `linkedFolder` – crizzis Jul 24 '20 at 12:54
  • `FolderLink` and `FileLink` could then both inherit from `File` or, better still, from a parent superclass (e.g. `AbstractFilesystemItem`) that does not declare the physical properties they don't need – crizzis Jul 24 '20 at 12:54
  • FolderLink doesn't inherit attributes from Folder, it is not a Folder but just a File, it is composed with a Folder. Yes Folder and FolderLink should share the same interface but it is not the problem here (Actually Jackson just need the same attributes names ...) The ***Link classes inherit from File because I use single table Inheritance Type. – kaizokun Jul 24 '20 at 13:57

1 Answers1

2

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;
    }
}
Saeed.Gh
  • 1,285
  • 10
  • 22
  • it works fine thanks for your help. Something I don't understand is that I didn't have to create any 'linkedFileType' column but it seems to work without. – kaizokun Jul 29 '20 at 18:08
  • what is hibernate.hbm2ddl mode in your configuration? I guess it updated your table automatically. – Saeed.Gh Jul 29 '20 at 18:13
  • well ddl-auto is on update mode, but no column has been created and it works fine without, quite strange. – kaizokun Jul 29 '20 at 18:28
  • After a few test it seems that these annotations @Any etc are not even necessary, because in my File class I already use a discriminator value for the different subclasses of File. So if declare the linkedFile as a File, hibernate can populate it with the right subClass. I don't know why it didn't work before actually, big mystery ! – kaizokun Jul 31 '20 at 09:53