使用泛型的 HashMap 上的 JPA @ElementCollection 返回重复项
这让我很不舒服。
问题陈述:
从数据库获取 Marklar
时,返回时会出现重复的 Foo
。具体来说,bars
HashMap 中有多少个 Foo
就有多少个元素。例如,如果我在 FooCollection
中保存一个带有 one Foo
的 Marklar
,并且如果 bars
是:
{0, "data1", 1, "data2", 2, "data3"}
当我读回它时,我将在 FooCollection
中得到三个 Foo
。到底是怎么回事?
定义:
我有以下类结构:
Foo<T>
^
|
| 1:M
FooContainer
^
|
| 1:1
<<Marklar>>
Marklar
有一个FooCollection
,其中有几个Foo
。 Foo
是一个泛型类,定义为:
@Getter
@Setter
@ToString
@Entity
public class Foo<T> {
@Id @GeneratedValue private Long id;
@ElementCollection(fetch = FetchType.EAGER)
private final Map<Integer, String> bars = new HashMap<>();
@Type(type = "java.lang.Class")
private final Class<T> clazz;
@Column(columnDefinition = "LONGTEXT")
@Convert(converter = PatternConverter.class)
private Pattern pattern;
public void addBar(Integer key, String bar) {
bars.put(key, bar);
}
public Foo(Class<T> clazz) {
this.clazz = clazz;
}
public Foo() {
this.clazz = null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Foo)) return false;
Foo<?> foo = (Foo<?>) o;
return Objects.equals(getId(), foo.getId())
&& Objects.equals(getBars(), foo.getBars())
&& Objects.equals(getClazz(), foo.getClazz())
&& Objects.equals(getPattern(), foo.getPattern());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getBars(), getClazz(), getPattern());
}
}
问题陈述:
从数据库获取 Marklar
时,返回时会出现重复的 Foo
。具体来说,bars
HashMap 中有多少个 Foo
就有多少个元素。例如,如果我在 FooCollection
中保存一个带有 one Foo
的 Marklar
,并且如果 bars
是:
{0, "data1", 1, "data2", 2, "data3"}
当我读回它时,我会在 FooContainer
中得到三个 Foo
。到底是怎么回事?
定义:
我有以下类结构:
Foo<T>
^
|
| 1:M
FooCollection
^
|
| 1:1
<<Marklar>>
Marklar
有一个FooContainer
,其中有几个Foo
。 Foo
是一个泛型类,定义为:
@Getter
@Setter
@ToString
@Entity
public class Foo<T> {
@Id @GeneratedValue private Long id;
@ElementCollection(fetch = FetchType.EAGER)
private final Map<Integer, String> bars = new HashMap<>();
@Type(type = "java.lang.Class")
private final Class<T> clazz;
@Column(columnDefinition = "LONGTEXT")
@Convert(converter = PatternConverter.class)
private Pattern pattern;
public void addBar(Integer key, String bar) {
bars.put(key, bar);
}
public Foo(Class<T> clazz) {
this.clazz = clazz;
}
public Foo() {
this.clazz = null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Foo)) return false;
Foo<?> foo = (Foo<?>) o;
return Objects.equals(getId(), foo.getId())
&& Objects.equals(getBars(), foo.getBars())
&& Objects.equals(getClazz(), foo.getClazz())
&& Objects.equals(getPattern(), foo.getPattern());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getBars(), getClazz(), getPattern());
}
}
FooContainer
:
@Getter
@Setter
@ToString
@Entity
public class FooContainer {
@Id @GeneratedValue private Long id;
@OneToMany(targetEntity = Foo.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private final List<Foo> foos = new ArrayList<>();
public void addFoo(Foo foo) {
foos.add(foo);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FooContainer)) return false;
FooContainer that = (FooContainer) o;
return Objects.equals(getId(), that.getId()) && Objects.equals(getFoos(), that.getFoos());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getFoos());
}
}
Marklars
从 MarklarRepository
中获取:
@Repository
public interface MarklarRepository extends JpaRepository<Marklar<?>, UUID> {}
A Marklar
是一个抽象类:
@Getter
@Setter
@Entity
@ToString
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Marklar<T> {
@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
private UUID id;
@Convert(converter = UriConverter.class)
private final URI baseUri;
@Type(type = "java.lang.Class")
private final Class<T> clazz;
private boolean enabled = true;
public Marklar() {
this.baseUri = null;
this.clazz = null;
}
public Marklar(@NotNull URI baseUri, @NotNull Class<T> clazz) {
this.baseUri = baseUri;
this.clazz = clazz;
}
}
作为 DefaultMarklar
实现:
@Getter
@Setter
@ToString(callSuper = true)
@Entity
@RequiredArgsConstructor
public final class DefaultMarklar<T> extends Marklar<T> {
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private final FooContainer fooContainer;
public DefaultMarklar() {
super();
this.fooContainer = null;
}
public DefaultMarklar(
@NotNull URI baseUri, @NotNull Class<T> clazz, @NotNull FooContainer fooContainer) {
super(baseUri, clazz);
this.fooContainer = fooContainer;
}
}
This one is giving me fits.
Problem Statement:
When fetching a Marklar
from the database, it is returned with duplicated Foo
s. Specifically, there are as many Foo
s as there are elements in the bars
HashMap. For example, if I am saving a Marklar
with one Foo
in its FooCollection
, and if bars
is:
{0, "data1", 1, "data2", 2, "data3"}
I will get three Foo
s in the FooCollection
when I read it back. What is going on?
Definitions:
I have the following class structure:
Foo<T>
^
|
| 1:M
FooContainer
^
|
| 1:1
<<Marklar>>
A Marklar
has a FooCollection
, which has several Foo
s. A Foo
is a generic class defined as:
@Getter
@Setter
@ToString
@Entity
public class Foo<T> {
@Id @GeneratedValue private Long id;
@ElementCollection(fetch = FetchType.EAGER)
private final Map<Integer, String> bars = new HashMap<>();
@Type(type = "java.lang.Class")
private final Class<T> clazz;
@Column(columnDefinition = "LONGTEXT")
@Convert(converter = PatternConverter.class)
private Pattern pattern;
public void addBar(Integer key, String bar) {
bars.put(key, bar);
}
public Foo(Class<T> clazz) {
this.clazz = clazz;
}
public Foo() {
this.clazz = null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Foo)) return false;
Foo<?> foo = (Foo<?>) o;
return Objects.equals(getId(), foo.getId())
&& Objects.equals(getBars(), foo.getBars())
&& Objects.equals(getClazz(), foo.getClazz())
&& Objects.equals(getPattern(), foo.getPattern());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getBars(), getClazz(), getPattern());
}
}
Problem Statement:
When fetching a Marklar
from the database, it is returned with duplicated Foo
s. Specifically, there are as many Foo
s as there are elements in the bars
HashMap. For example, if I am saving a Marklar
with one Foo
in its FooCollection
, and if bars
is:
{0, "data1", 1, "data2", 2, "data3"}
I will get three Foo
s in the FooContainer
when I read it back. What is going on?
Definitions:
I have the following class structure:
Foo<T>
^
|
| 1:M
FooCollection
^
|
| 1:1
<<Marklar>>
A Marklar
has a FooContainer
, which has several Foo
s. A Foo
is a generic class defined as:
@Getter
@Setter
@ToString
@Entity
public class Foo<T> {
@Id @GeneratedValue private Long id;
@ElementCollection(fetch = FetchType.EAGER)
private final Map<Integer, String> bars = new HashMap<>();
@Type(type = "java.lang.Class")
private final Class<T> clazz;
@Column(columnDefinition = "LONGTEXT")
@Convert(converter = PatternConverter.class)
private Pattern pattern;
public void addBar(Integer key, String bar) {
bars.put(key, bar);
}
public Foo(Class<T> clazz) {
this.clazz = clazz;
}
public Foo() {
this.clazz = null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Foo)) return false;
Foo<?> foo = (Foo<?>) o;
return Objects.equals(getId(), foo.getId())
&& Objects.equals(getBars(), foo.getBars())
&& Objects.equals(getClazz(), foo.getClazz())
&& Objects.equals(getPattern(), foo.getPattern());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getBars(), getClazz(), getPattern());
}
}
FooContainer
:
@Getter
@Setter
@ToString
@Entity
public class FooContainer {
@Id @GeneratedValue private Long id;
@OneToMany(targetEntity = Foo.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private final List<Foo> foos = new ArrayList<>();
public void addFoo(Foo foo) {
foos.add(foo);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FooContainer)) return false;
FooContainer that = (FooContainer) o;
return Objects.equals(getId(), that.getId()) && Objects.equals(getFoos(), that.getFoos());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getFoos());
}
}
Marklars
are fetched from the MarklarRepository
:
@Repository
public interface MarklarRepository extends JpaRepository<Marklar<?>, UUID> {}
A Marklar
is an abstract class:
@Getter
@Setter
@Entity
@ToString
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Marklar<T> {
@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
private UUID id;
@Convert(converter = UriConverter.class)
private final URI baseUri;
@Type(type = "java.lang.Class")
private final Class<T> clazz;
private boolean enabled = true;
public Marklar() {
this.baseUri = null;
this.clazz = null;
}
public Marklar(@NotNull URI baseUri, @NotNull Class<T> clazz) {
this.baseUri = baseUri;
this.clazz = clazz;
}
}
Implemented as a DefaultMarklar
:
@Getter
@Setter
@ToString(callSuper = true)
@Entity
@RequiredArgsConstructor
public final class DefaultMarklar<T> extends Marklar<T> {
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private final FooContainer fooContainer;
public DefaultMarklar() {
super();
this.fooContainer = null;
}
public DefaultMarklar(
@NotNull URI baseUri, @NotNull Class<T> clazz, @NotNull FooContainer fooContainer) {
super(baseUri, clazz);
this.fooContainer = fooContainer;
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
按照 @Tomas 的建议,将获取类型从 Lazy 更改为 Easy 可以解决重复问题,但也会影响结果。
如果您想将 EagerType 保留为 EAGER,只需指定 FechMode 为 Select:
Changing the Fetching type from Lazy to Easy as @Tomas recommended solves the duplication, however it also impact the results.
If you want to keep the EagerType to EAGER, just specify the FechMode to Select:
问题在于 Hibernate 将
bars
视为一个单独的包,同时急切地获取多个包可能会导致诸如此类的基数问题。我从
Bars
地图中删除了fetch = FetchType.EAGER
,并添加了@Transactional
对调用方法进行注释,一切都很好。此外,我还将对延迟加载字段(例如在 toString() 方法中)的任何直接引用替换为对其各自 getter 的调用。The problem was that
bars
is considered a separate bag by Hibernate, and eager fetching of multiple bags simultaneously can result in cardinality problems such as this.I removed
fetch = FetchType.EAGER
from theBars
map, and added the@Transactional
annotation to the calling method, and all is well. Also, I replaced any direct references to lazily loaded fields (such as intoString()
methods) with calls to their respective getters.