How to Maintain Bi-Directional Relationships with Spring Data Rest and JPA

How to maintain bi-directional relationships with Spring Data REST and JPA?

tl;dr

The key to that is not so much anything in Spring Data REST - as you can easily get it to work in your scenario - but making sure that your model keeps both ends of the association in sync.

The problem

The problem you see here arises from the fact that Spring Data REST basically modifies the books property of your AuthorEntity. That itself doesn't reflect this update in the authors property of the BookEntity. This has to be worked around manually, which is not a constraint that Spring Data REST makes up but the way that JPA works in general. You will be able to reproduce the erroneous behavior by simply invoking setters manually and trying to persist the result.

How to solve this?

If removing the bi-directional association is not an option (see below on why I'd recommend this) the only way to make this work is to make sure changes to the association are reflected on both sides. Usually people take care of this by manually adding the author to the BookEntity when a book is added:

class AuthorEntity {

void add(BookEntity book) {

this.books.add(book);

if (!book.getAuthors().contains(this)) {
book.add(this);
}
}
}

The additional if clause would've to be added on the BookEntity side as well if you want to make sure that changes from the other side are propagated, too. The if is basically required as otherwise the two methods would constantly call themselves.

Spring Data REST, by default uses field access so that theres actually no method that you can put this logic into. One option would be to switch to property access and put the logic into the setters. Another option is to use a method annotated with @PreUpdate/@PrePersist that iterates over the entities and makes sure the modifications are reflected on both sides.

Removing the root cause of the issue

As you can see, this adds quite a lot of complexity to the domain model. As I joked on Twitter yesterday:

#1 rule of bi-directional associations: don't use them… :)

It usually simplifies the matter if you try not to use bi-directional relationship whenever possible and rather fall back to a repository to obtain all the entities that make up the backside of the association.

A good heuristics to determine which side to cut is to think about which side of the association is really core and crucial to the domain you're modeling. In your case I'd argue that it's perfectly fine for an author to exist with no books written by her. On the flip side, a book without an author doesn't make too much sense at all. So I'd keep the authors property in BookEntity but introduce the following method on the BookRepository:

interface BookRepository extends Repository<Book, Long> {

List<Book> findByAuthor(Author author);
}

Yes, that requires all clients that previously could just have invoked author.getBooks() to now work with a repository. But on the positive side you've removed all the cruft from your domain objects and created a clear dependency direction from book to author along the way. Books depend on authors but not the other way round.

Spring Data REST Projection with bidirectional relationship resulting JSON infinite recursion

The best way we find would be with the Jackson annotation @JsonIgnoreProperties, that should be used in parent list to igore himself in the child. But, after a few tries, seems like this annotation is not working in projections, specifically for Spring Data REST.

Follow the example of what would be the correct way:

@Projection(name = "userProjection", types = { User.class })
public interface UserProjection {
@JsonIgnoreProperties({"user"})
List<CompanyProjection> getCompanyList();

// Other Getters
}

@Projection(name = "companyProjection", types = { Company.class })
public interface CompanyProjection {
UserProjection getUser();

// Other Getters
}

We send a ticket for this Spring Data REST issue and it has been accepted. We believe in a near future it will be corrected and we can use it.

For now, we adjust our projections so the list object can use a "derivation" of the original projection, ignoring the property that causes the infinite recursion.

Follow the example:

@Projection(name = "userProjection", types = { User.class })
public interface UserProjection {
List<CompanyProjectionWithoutUser> getCompanyList();

// Other Getters

// Projection without the User, that couses infinite recursion
public interface CompanyProjectionWithoutUser extends CompanyProjection {
@Override
@JsonIgnore
UserProjection getUser();
}
}

@Projection(name = "companyProjection", types = { Company.class })
public interface CompanyProjection {
UserProjection getUser();

// Other Getters
}

Spring Data JPA - bidirectional relation with infinite recursion

Here is how I handle this problem in my projects.

I used the concept of data transfer objects, implemented in two version: a full object and a light object.

I define a object containing the referenced entities as List as Dto (data transfer object that only holds serializable values) and I define a object without the referenced entities as Info.

A Info object only hold information about the very entity itself and not about relations.

Now when I deliver a Dto object over a REST API, I simply put Info objects for the references.

Let's assume I deliever a PlayerDto over GET /players/1:

public class PlayerDto{
private String playerName;
private String playercountry;
private TeamInfo;
}

Whereas the TeamInfo object looks like

public class TeamInfo {
private String teamName;
private String teamColor;
}

compared to a TeamDto

public class TeamDto{
private String teamName;
private String teamColor;
private List<PlayerInfo> players;
}

This avoids an endless serialization and also makes a logical end for your rest resources as other wise you should be able to GET /player/1/team/player/1/team

Additionally, the concept clearly separates the data layer from the client layer (in this case the REST API), as you don't pass the actually entity object to the interface. For this, you convert the actual entity inside your service layer to a Dto or Info. I use http://modelmapper.org/ for this, as it's super easy (one short method call).

Also I fetch all referenced entities lazily. My service method which gets the entity and converts it to the Dto there for runs inside of a transaction scope, which is good practice anyway.

Lazy fetching

To tell JPA to fetch a entity lazily, simply modify your relationship annotation by defining the fetch type. The default value for this is fetch = FetchType.EAGER which in your situation is problematic. That is why you should change it to fetch = FetchType.LAZY

public class TeamEntity {

@OneToMany(mappedBy = "team",fetch = FetchType.LAZY)
private List<PlayerEntity> members;
}

Likewise the Player

public class PlayerEntity {

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pla_fk_n_teamId")
private TeamEntity team;
}

When calling your repository method from your service layer, it is important, that this is happening within a @Transactional scope, otherwise, you won't be able to get the lazily referenced entity. Which would look like this:

 @Transactional(readOnly = true)
public TeamDto getTeamByName(String teamName){
TeamEntity entity= teamRepository.getTeamByName(teamName);
return modelMapper.map(entity,TeamDto.class);
}

Why Spring JPA Bidirectional OneToMany and ManyToOne is not updating the foreign key column?

You're missing the @JoinColumn on the child side:

@Entity
@Table(name = "ms_dealer")
public class Dealer {

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "account_account_id")
public Account account;

// other fields

}

You have used mappedBy on the parent side, but there is no mapping on the child side. You need to indicate, that the Dealer is the relationship owner - it has the foreign key.

Edit: if you're persisting (not merging) the Account entity, together with its children, you should not pass ids of child entities. (Actually passing any ids upon persist is a code smell and most probably a performance killer.) The json used should look like:

{
"accountName": "string",
"accountEmail": "string",
"accountAddress": "string",
"town": "string",
"npwp": "string",
"phoneNumber": "string",
"fax": "string",
"remarks": "string",
"entryTime": "2020-04-07T15:01:29.404Z",
"active": true,
"dealer": [
{
"dealerName": "string",
"dealerEmail": "string",
"dealerAddress": "string"
}
]
}

Before saving both-side synchronization might also be needed:

account.getDealer().forEach(d -> d.setAccount(account));

Edit:

From Author edits must cascade to child:

@OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true)
public List<Dealer> dealer;

You might also add @JsonIgnore over Action or List<Dealer> to avoid stackoverflow on serialization to json.

Spring boot JPA bidirectional infinite cycle issue

Use

PlayerEntity:

@JsonManagedReference
@OneToOne(mappedBy = "player", cascade = CascadeType.ALL, fetch =
FetchType.LAZY, orphanRemoval = true)
private GameEntity game;

GameEntity:

  @JsonBackReference
@JoinColumn(name = "player_id")
@OneToOne(fetch = FetchType.LAZY)
private PlayerEntity player;

Or vice versa.

how to Fix spring boot one to many bidirectional infinity loop?

with the JSON its a problem with bi-directional mapping. Use the below properties.

@JsonIgnoreProperties("employer")
@JsonIgnoreProperties("employees")

please keep fetching type as eager.

hope this will work.

Spring Data Rest and entities' IDs in relationships

You can create a projection and use it by default with an excerpt.

To define the projection :

@Projection(
name = "customBook",
types = { Book.class })
public interface CustomBook {

@Value("#{target.id}")
long getId();

String getTitle();

@Value("#{target.getLibrary().getId()}")
int getLibraryId();
}

And call :

http://192.168.33.20:8080/books/22?projection=customBook

To use this projection by default configure your repo :

@RepositoryRestResource(excerptProjection = CustomBook.class)
public interface BookRepository extends CrudRepository<Book, Long> {}


Related Topics



Leave a reply



Submit