【问题标题】:Generic one-to-many relationship with constructor与构造函数的通用一对多关系
【发布时间】:2021-01-11 20:39:30
【问题描述】:

我有一个使用通用一对多关系的实体。不幸的是,lobmok @AllArgsConstructor 无法映射/投射 List 扩展为 List。 EducationTranslation 和 Translatable 的属性完全相同,EducationTranslation extends Translatable。 如果我删除我的自定义构造函数,我会收到以下错误:

无法将 java.lang.Long 字段 EducationTranslation.id 设置为 可翻译

是否有注释或设计模式可以让我摆脱自己编写的构造函数并保持代码更简洁更短?

@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());
}

【问题讨论】:

  • (1) “无法设置字段”错误是 JPA 抱怨它无法使用反射读取/填充 EducationTranslation 的 id 属性。我非常怀疑这与Education 的构造函数有什么关系,(2) 你确实意识到你永远无法将任何东西插入声明为List&lt;? extends Translatable&gt; 的列表中,对吧?
  • 关于(2)这是由于对象的未知内存大小造成的吗?我认为设置 targetEntity 会设置内存大小
  • 不,那是因为泛型是如何工作的。声明指出:“这是扩展 Translatable 的某种类型的对象列表”。您不能向这样的列表插入任何内容,因为编译器不知道该列表接受哪种特定类型的对象。如果没有强制转换,列表一旦被分配就会变成有效的只读
  • 好吧,我没有插入“任何东西”。我的 EducationTranslation 扩展了 Translatable。我有点通过我的流迭代做一个演员。
  • 是的,您的流管道会生成一个List&lt;EducationTranslation&gt;。然后将其分配给List&lt;? extends Translatable&gt; 类型的属性。在此之后,translationList.add(&lt;anything&gt;) 将产生一个编译错误,不管你用什么代替 &lt;anything&gt;。这很好,因为您的管道无论如何都会生成一个不可变的列表,但是如果您保存实体,从数据存储中加载它并尝试添加一个元素,您会仍然得到一个编译错误。只是想确保您知道这一点

标签: hibernate jpa constructor annotations lombok


【解决方案1】:

使用通配符限制的集合...对于 JPA 实体有点不方便。

请注意,在初始创建之后,您将永远无法在声明为List&lt;? extends Translatable&gt; 的列表中插入任何内容而不进行强制转换,这意味着以下代码将不起作用:

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

其行为的原因是List&lt;? extends Translatable&gt; 被解释为“扩展Translatable 的某些未知元素类型的列表”,并且编译器不允许您的代码向这样的列表添加任何内容,不知道是什么exact 类型 ? 代表。我想你会希望.add(new EducationTranslation()) 是合法的,因为EducationTranslation 确实 扩展了Translatable 但这里更好地说明了为什么它不可能被允许:

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???

综上所述,将translationList 声明为List&lt;EducationTranslation&gt; 会方便得多。

为什么允许我最初添加元素并将其保存在日期存储中,但在尝试使用相同的对象签名添加元素时出现编译错误

您没有使用“相同的签名”。您的代码在任何时候都不调用.add()。它只是构造一个有效类型为List&lt;EducationTranslation&gt; 的List,然后将其分配给声明为List&lt;? extends Translatable&gt; 的字段。问题是,您不能使用存储在该字段中的引用调用.add(new EducationTranslation()),因为该字段的声明方式,而不是您分配给它的内容。这与以下 sn-p 中的场景几乎相同:

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

注意,如果你想对Education类的客户端隐藏列表元素的具体类型,那么下面的代码仍然是合法的:

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>
   }
}

因此,如果要从Education 类的客户端 中抽象出EducationTranslation,则无需将字段本身声明为通配符列表。

【讨论】:

  • 非常感谢!特别是狗-猫变形器的例子选择得非常好,让我笑了,意识到我的逻辑错误:D
猜你喜欢
  • 2014-02-13
  • 2019-02-16
  • 1970-01-01
  • 1970-01-01
  • 2021-12-29
  • 1970-01-01
  • 2019-11-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多