Denis Stephanov
Denis Stephanov

Reputation: 5241

Spring JPA Bi-Directional @ManyToOne doesn't work

we have linked 2 entities with bi-directional @ManyToOne relation like this:

@Entity
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Foo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "foo")
    private Set<Bar> bars;
}

@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Bar {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "foo_id")
    private Foo foo;
}

When I create Foo entity and Bar entity, and try to set created Foo into Bar, change is not reflected on other side.

Here is code from my integration test:

Foo foo = fooRepository.save(Foo.builder()...build());

Bar bar = new Bar();
bar.setFoo(foo);
bar.set...();
barRepository.save(bar);

Foo updatedFoo = fooRepository.findById(foo.getId()).get();
List<Bar> bars = updatedFoo.getBars(); // this is null

I have no idea why bars from fetched foo is null. When I try fetch given Bar from repository I can see saved Foo on this side.

Upvotes: 0

Views: 75

Answers (2)

Short Answer

  • JPA require you to set both sides of a relationship
  • As long as you set the owning side bar.setFoo() which you did, the database will be updated with the relationship changes. So your database is correct
  • The non-owning side foo will only reflect what is in the database if you manually set it, or you force it to be refreshed or reloaded eg entityManager.refresh(foo)

Notes

  • In a transactional session, every call to repository does not translate to database call. Since hibernate all ready has foo as a managed entity in line1, when you call, fooRepository.findById(foo.getId()).get(); JPA does not issue a select statement. If you search about Transactional write-behind and Repeatable Read, you can find out about this more

  • Foo foo = fooRepository.save(Foo.builder()...build()); - foo becomes managed entity, due to Repeatable read guarantee, JPA will return the same foo for all subsequent retrievals of foo.

  • It does not even go to database if you call fooRepository.findById(fooId) after the save.

  • barRepository.save(bar); - bar also becomes managed

  • Both foo and bar are managed entities, but foo says I don't have any bars and bar says I know a foo as you didn't set foo.getbars().add(bar). This is incorrect in memory as you have to explicitly set both side of relationships.

  • If you complete the current transactional method without adding the bar to foo, and then if you call fooRepository.findById(fooId) in another transactional, it will show you it get foo and bars.

More references

Upvotes: 0

Nayan
Nayan

Reputation: 311

have you try adding cascade to your mappings,

@OneToMany(fetch = FetchType.LAZY, mappedBy = "foo",cascade= CascadeType.ALL)
private Set<Bar> bars;

@ManyToOne(cascade= CascadeType.ALL)
@JoinColumn(name = "foo_id")
private Foo foo;

and your code:

Foo foo = fooRepository.save(Foo.builder()...build());

Bar bar = new Bar(); 
bar.setFoo(foo);
bar.set...();
barRepository.save(bar);

barRepository.save(bar) will not persist the foo.

when you add cascade it will persist both the entities or you need to manually save the foo entity with fooRepository.

Upvotes: 1

Related Questions