Search code examples
javaspring-boothibernatejpaspring-data-jpa

Reversed JPA results for @OneToMany relationship


I will split my problem into 3 sections:

  1. JPA Entities
  2. Current database setup
  3. The problem

1. JPA Entities

Foo

@Entity
@Table(name = "foo")
public class Foo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false, unique = true)
    private Long id;
    
    // ... other fields

    @OneToMany(mappedBy = "fooer", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Bar> barInfo = new ArrayList<>();

    @OneToMany(mappedBy = "fooing", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Bar> baringInfo = new ArrayList<>();
}

Bar


@Entity
@Table(name = "bar")
public class Bar {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false, unique = true)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "fooer_id", referencedColumnName = "id", nullable = false,
                foreignKey = @ForeignKey(name = "bar_fooer_id_fkey"))
    private Foo fooer;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "fooing_id", referencedColumnName = "id", nullable = false,
                foreignKey = @ForeignKey(name = "bar_fooing_id_fkey"))
    private Foo fooing;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "baz_id", referencedColumnName = "id", nullable = false,
                foreignKey = @ForeignKey(name = "bar_baz_id_fkey"))
    private Baz baz;
}

Baz

@Entity
@Table(name = "baz")
public class Baz {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false, unique = true)
    private Long id;
    
    // ... other fields
}

2. Current database setup

DDL

CREATE TABLE some_schema.bar
(
    id        int8 GENERATED BY DEFAULT AS IDENTITY ( INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1 NO CYCLE) NOT NULL,
    fooer_id  int8 NOT NULL,
    fooing_id int8 NOT NULL,
    baz_id    int8 NOT NULL,
    CONSTRAINT bar_id_key UNIQUE (id),
    CONSTRAINT bar_fooer_id_fkey FOREIGN KEY (fooer_id) REFERENCES some_schema.foo (id),
    CONSTRAINT bar_fooing_id_fkey FOREIGN KEY (fooing_id) REFERENCES some_schema.foo (id),
    CONSTRAINT bar_baz_id_fkey FOREIGN KEY (baz_id) REFERENCES some_schema.baz (id)
);

3. The problem

Let's say we have two entities of type Foo (John[id=111] and Alice[id=222]) and one entity of type Baz (Paperwork[id=999]). John wants to request help from Alice that relates to Paperwork. My current implementation of adding request explained above looks like this:

final var john = getById(contextId); // fooing
final var alice = getById(id);       // fooer
final var paperwork = bazService.getBazById(bazId);

john.getBarInfo()
    .add(Bar.builder()
            .fooer(alice)
            .fooing(john)
            .baz(paperwork)
            .build());

fooRepository.save(john); // <-- Here john have barInfo with values set above and baringInfo empty which is what we want

Data in table bar looks like this:

id fooer_id fooing_id baz_id
1 222 111 999

Which from the perspective of current implementation and requirement looks okay. But the problem lies in retrieving entity and entities of type Foo.

Expected result by retrieving either by ID or whole list:

{
  "John": {
    "barInfo": {
      "fooers": [
        {
          "foo_id": 222,
          "baz_id": 999
        }
      ],
      "fooing": []
    }
  },
  "Alice": {
    "barInfo": {
      "fooers": [],
      "fooing": [
        {
          "foo_id": 111,
          "baz_id": 999
        }
      ]
    }
  }
}

Actual result:

{
  "John": {
    "barInfo": {
      "fooers": [],
      "fooing": [
        {
          "foo_id": 111,
          "baz_id": 999
        }
      ]
    }
  },
  "Alice": {
    "barInfo": {
      "fooers": [
        {
          "foo_id": 222,
          "baz_id": 999
        }
      ],
      "fooing": []
    }
  }
}

The data displayed above is pseudo-representation of entity Foo retrieved from Spring repository (not wrapped to other object)

The fooing/fooers seems to be reversed (I assume there is something wrong with my associations between entities) and I'm wondering if there is some flaw in my approach. Any suggestions will be much appreciated.


Versions used in project:

  • Spring Boot 3.2.2
  • Java 17

Solution

  • Figured it out!

    All I had to do was reverse mappedBy parameters for Foo:

    @Entity
    @Table(name = "foo")
    public class Foo {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id", nullable = false, unique = true)
        private Long id;
        
        // ... other fields
    
        @OneToMany(mappedBy = "fooing", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
                              ~~~~~~~~
        private List<Bar> barInfo = new ArrayList<>();
    
        @OneToMany(mappedBy = "fooer", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
                              ~~~~~~~
        private List<Bar> baringInfo = new ArrayList<>();
    }
    

    And in service related to adding request instead of adding to list of barInfo or baringInfo, we should have just saved new Bar and it will be associated with respective entities:

    final var john = getById(contextId); // fooing
    final var alice = getById(id);       // fooer
    final var paperwork = bazService.getBazById(bazId);
    final var bar = Bar.builder()
                       .fooer(alice)
                       .fooing(john)
                       .baz(paperwork)
                       .build();
    barRepository.save(bar);
    

    Retrieving information about Foo gives expected result mentioned in my question.