@ElementCollection on HashMap with generics returns duplicates

Problem Statement:

When fetching a Marklar from the database, it is returned with duplicated Foos. Specifically, there are as many Foos 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 Foos 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 Foos. 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;
    }
}

Please also post the other entity definitions and show how you fetch the data.

Thank you, I’ve added the other classes.

And here is a test example of creating, saving and retrieving a Marklar:

@Test
@DisplayName("Test fetching a Foo")
@SuppressWarnings("unchecked cast")
void test() throws Exception {

  final DefaultMarklar<String> marklar =
      new DefaultMarklar<>(new URI("https://google.com"), String.class, getContainer());
  System.out.println("Created Marklar: " + marklar);

  final DefaultMarklar<String> savedMarklar = repository.save(marklar);
  System.out.println("Saved Marklar: " + savedMarklar);

  System.out.println("Fetching all Marklars");
  final List<Marklar<?>> marklars = repository.findAll();
  assertThat(marklars.size()).isNotZero();

  final DefaultMarklar<String> fetchedMarklar = 
      (DefaultMarklar<String>) marklars.get(0);
  System.out.printf("Fetched Marklar: %s%n", fetchedMarklar);
  assertThat(fetchedMarklar.getFooContainer().getFoos().size())
      .isEqualTo(marklar.getFooContainer().getFoos().size())
      .isEqualTo(1);
}

private @NotNull FooContainer getContainer() {
  final Foo foo = new Foo();
  foo.addBar(0, "Mar");
  foo.addBar(1, "Klar");
  foo.addBar(2, "MarKlar");

  final FooContainer fooContainer = new FooContainer();
  fooContainer.addFoo(foo);
  return fooContainer;
}

Please also post the SQL query that is executed. I think the use of EAGER might be what is causing this issue since FooContainer#foos is a bag, any EAGER fetching after a fetch for a BAG should always use SELECT fetching to avoid these cardinality issues.

You are right on the money, sir! I removed eager fetching for Bars, and added the @Transactional annotation to the method retrieving the data, replaced direct references to lazily loaded fields in toString() methods with calls to their respective getters, and presto! everything is working as desired.

Thank you very much.