Search code examples
hibernatejpaconstructorannotationslombok

Generic one-to-many relationship with constructor


I have an entity that uses a generic one-to-many relationship. Unfortunately, the lobmok @AllArgsConstructor, fails to map/cast a List<? extends Translatable> to a List< EducationTranslation>. The properties of EducationTranslation and Translatable are exactly the same and EducationTranslation extends Translatable. If I remove my custom constructor I get the following error:

Can not set java.lang.Long field EducationTranslation.id to Translatable

Is there an annotation or a design pattern that would allow me to get rid of the self written constructor and keep the code cleaner and shorter?

@OneToMany(
    targetEntity = EducationTranslation.class,
    cascade = CascadeType.ALL,
    fetch = FetchType.LAZY)
private List<? extends Translatable> translationList = new ArrayList<>();


public Education(Long id, Profile profile, Collection<? extends Translatable> translationList) {
    this.id = id;
    this.profile = profile;
    this.translationList = translationList
        .stream()
        .map(x -> new EducationTranslation(x.getId(), x.getCode(), x.getTranslation()))
        .collect(Collectors.toList());
}

Solution

  • Collections constrained using wildcards are... somewhat inconvenient with JPA entities.

    Note that after the initial creation, you will never be able to insert anything into a list declared as List<? extends Translatable> without casting, meaning the following code will not work:

    Education education = entityManager.find(Education.class, id);
    education.getTranslationList().add(new EducationTranslation(...)); // compilation error
    

    The reason it behaves like that is that List<? extends Translatable> is interpreted as 'list of some unknown element type that extends Translatable', and the compiler cannot allow your code to add anything to such a list, not knowing what exact type ? stands for. I imagine you would hope for .add(new EducationTranslation()) to be legal, because EducationTranslation does extend Translatable but here is a better illustration of why it can't possibly be allowed:

    List<Dog> dogs = new ArrayList<>(List.of(new Dog())); //just a mutable collection with a Dog inside
    List<? extends Animal> someAnimals = dogs; //a list of Dogs is a list of 'a type that extends Animal', so - valid assignment
    someAnimals.add(new Cat()); //hey, a Cat does extend Animal, so this should be legal, right?
    Dog shapeshifter = dogs.get(1); //...right???
    

    To sum up, it will be much more convenient to simply declare translationList as List<EducationTranslation>.

    why I am allowed to initially add elements and save it the date store, but then get a compile error when trying to add an element using the same object signature

    You're not using 'the same signature'. Your code is not calling .add() at any point. It simply constructs a List whose effective type is List<EducationTranslation>, and then assigns it to a field declared List<? extends Translatable>. The problem is, you cannot call .add(new EducationTranslation()) using the reference stored in that field, because of the way the field is declared, not what you assign to it. It's pretty much the same scenario as in the following snippet:

    Object obj = "Hello";
    obj.length(); // you know obj points to a String, but you assign it to a reference with a more general type, so this call is illegal
    

    Note that if you want to hide the concrete type of the list element from a client of the Education class, then the following code is still legal:

    interface TranslationHolder {
    
        List<? extends Translatable> getTranslationList();
    }
    
    class Education implements TranslationHolder {
    
        private List<EducationTranslation> translationList = new ArrayList<>();
    
        //this getter is a valid implementation for TranslationHolder.getTranslationList()
        public List<EducationTranslation> getTranslationList() {
            return translationList;
        }
    }
    
    class ClassThatUsesEducationButOnlyKnowsItAsTranslationHolder  {
    
       public void execute() {
            TranslationHolder holder = getEducationAsTranslationHolderInSomeIndirectWay();
            holder.getTranslationList(); // this still return a reference to List<? extends Translatable>, NOT List<EducationTranslation>
       }
    }
    

    So, if the point was to abstract away EducationTranslation from a client of the Education class, you don't need to declare the field itself as a wildcard list.