Issue
Good morning, nice to greet you.
I am new to Spring Boot recently, and I am working with a REST API, which is basically a playlist with songs, basically the REST API should have the following structure. A playlist can have many songs:
{
"name": "Lista 1",
"description": "Lista de reproduccion 2020 spotify",
"songs": [
{
"title": "Tan Enamorados",
"artist": "CNCO",
"album": "Tan Enamorados",
"year": 2020,
"playList": 1
},
{
"title": "Hawai",
"artist": "Maluma",
"album": "PAPI JUANCHO",
"year": 2020,
"playList": 1
}
]
}
Currently this is how I have my entities configured
Entity SONGS
@Entity
@Table(name = "SONGS")
public class Songs{
@JsonIgnore
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "ID")
private Long id;
@Column(name = "title")
private String title;
@Column(name = "artist")
private String artist;
@Column(name = "album")
private String album;
@Column(name = "year")
private int year;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PLAY_LIST_ID")
private PlayList playList;
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
public String getArtist() {
return this.artist;
}
public void setArtist(String artist) {
this.artist = artist;
}
public String getAlbum() {
return this.album;
}
public void setAlbum(String album) {
this.album = album;
}
public int getYear() {
return this.year;
}
public void setYear(int year) {
this.year = year;
}
public PlayList getPlayList() {
return this.playList;
}
public void setPlayList(PlayList playList) {
this.playList = playList;
}
}
Entity PLAYLIST
@Entity
@Table(name = "PLAY_LIST")
public class PlayList {
@JsonIgnore
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "ID")
private Long id;
@Column(name="name")
private String name;
@Column(name="description")
private String description;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "playList")
private List<Songs> songs = new ArrayList<>();
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
public List<Songs> getSongs() {
return this.songs;
}
public void setSongs(List<Songs> songs) {
this.songs = songs;
}
}
Controller
@RestController
@RequestMapping("playlist")
public class PlayListController {
@Autowired
private PlayListService playListService;
//Get playlist by id with songs belongs that playlist
@GetMapping("/{id}")
public Optional<PlayList> getPlayListByID(@PathVariable(value = "id") Long id) {
Optional<PlayList> playList = playListService.getById(id);
return playList;
}
@PostMapping("/create")
public PlayList createPlayList(@RequestBody PlayList playList) {
return playListService.savePlayList(playList);
}
}
My class PlayListServiceImpl
@Service
public class PlayListServiceImpl implements PlayListService {
@Autowired
private PlayListRepository playListRepository;
public Optional <PlayList> getById(Long Id) {
Optional <PlayList> playList= playListRepository.findById(Id);
return playList;
}
@Override
public PlayList savePlayList(PlayList playList) {
return playListRepository.save(playList);
}
}
My Repository
@Repository
public interface PlayListRepository extends JpaRepository<PlayList, Long> {
Optional<PlayList> findById(Long Id);
}
However, I have infinite recursion problem when I try to get a playlist with the get method:
org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: com.example.api.songs.entity.PlayList[\"songs\"]->org.hibernate.collection.internal.PersistentBag[0]->com.example.api.songs.entity.Songs[\"playList\"]->com.example.api.songs.entity.PlayList[\"songs\"]->org.hibernate.
This is due to the playList field found in the Songs entity, when trying to get the playlist with the songs, I get an array that becomes recursive and not the id of the playList, which is what I want to get, the reality is that I get something like this:
I can solve by applying a JsonIgnore in the field, however this affects me that when I go to save a playlist, I cannot pass the idPlayList field to each song to make sure that each song is saved with its respective idPlayList
I think the best thing would be to build a DTO here that helps me to have the relationship of the 2 tables, however, I don't know how I would do it in a way that allows me, when executing a get from the playList, to obtain only the idPlayList, and when saving power pass the idPlayList in each song that is going to be saved with a playlist, or if there is another way in which I could store a playlist with its songs and have them stored directly, since currently I have to assign by BD to each song the idPlayList
Solution
Add
spring.jpa.open-in-view=false
to theapplication.properties
to haveLazyInitializationException
. It will help to build good queries. What is this spring.jpa.open-in-view=true property in Spring Boot?Load
PlayList
withsongs
using JPQL query withLEFT JOIN FETCH
to fetch songs with the list. Sosongs
will be already loaded andplayList
in the song will be lazy.@EntityGraph
can be used also.select list from PlayList list left join fetch list.songs where list.id = :listId
There is
Hibernate5Module
in Jackson. It allow to omit lazy associations. So Jackson will not touchplayList
in the song. Configure Jackson to omit lazy-loading attributes in Spring BootTo build something like a real system. You can add
PlayListResponse
,SongResponse
classes and map entities to those classes on the service level. Probably you will not needHibernate5Module
in this case. But better to experiment with this module too.Enable database logging in
application.properties
with
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
logging.level.org.hibernate.SQL=DEBUG
spring.jpa.properties.hibernate.use_sql_comments=true
Check transactions and SQL in the logs carefully. Don't allow Hibernate to produce unnecessary queries.
How to save the songs with a list
Add this method to the PlayList
@Transient
public void addSong(Song song) {
song.setPlayList(this);
songs.add(song);
}
Add songs to the list using addSong()
method.
Save PlayList
. All the songs of a list will be saved by Hibernate with the correct list id, because there is cascade = CascadeType.ALL
in the @OneToMany
.
Answered By - v.ladynev