Spring Łączenie dwóch obiektów w relacji Many-To-Many

0

Cześć,

Tworzę aplikację back-end'ową w Spring do przechowywania informacji o muzykach i albumach muzycznych (baza to postgres). Mam klasę Album, która jest w relacji Many-To-Many z klasą Band. Mam problem z "połączeniem" dwóch obiektów tych klas w relacji Many-To-Many. Napisałem do tego klasę z endpoint'em post (funkcja associate) według przykładu z tej strony: https://www.rbsprogramming.com/articles/spring-boot-crud-web-api/ , ale bez skutku - endpoint zwraca pustą listę, relacja nie powstaje. Byłbym wdzięczny za pomoc w rozwiązaniu problemu. Poniżej zamieszam istotne klasy:

Kontroler AlbumBandController, zawierający funkcję associate, która powinna "łączyć" w relacji obiekty klas Band i Album poprzez podanie odpowiednich id obiektów znajdujących się już w bazie:

@RestController
public class AlbumBandController {
    @Autowired
    private AlbumRepository albumRepository;
    @Autowired
    private BandRepository bandRepository;

    @PostMapping("/album/{albumId}/band/{bandId}")
    public List<Band> associate(@PathVariable Long albumId, @PathVariable Long bandId) {
        Band band = this.bandRepository.findById(bandId).orElseThrow(() -> new MissingResourceException("Band", "Band"
                , bandId.toString()));

        return this.albumRepository.findById(albumId).map((album) -> {
            album.getBands().add(band);
            return this.albumRepository.save(album).getBands();
        }).orElseThrow(() -> new MissingResourceException("Album", "Album", albumId.toString()));
    }
}

Klasa Album:

@Entity
@Table(name="album")
public class Album {
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;
	@Column(name="title")
	private String title;
	@ManyToMany(targetEntity = Band.class, mappedBy = "albums")
	private List<Band> bands;
	@ManyToMany(targetEntity = Musician.class, mappedBy = "albums")
	private List<Musician> musicians;
	@Embedded
	@Column(name="duration")
	private Duration duration;
	@Column(name="dateofrelease")
	@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="dd/MM/yyyy", timezone="CET")
	private Date dateOfRelease;
	@Column(name="coverpath")
	private String coverPath;
	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public Duration getDuration() {
		return duration;
	}
	public void setDuration(Duration duration) {
		this.duration = duration;
	}
	public Date getDateOfRelease() {
		return dateOfRelease;
	}
	public void setDateOfRelease(Date dateOfRelease) {
		this.dateOfRelease = dateOfRelease;
	}
	public String getCoverPath() {
		return coverPath;
	}
	public void setCoverPath(String coverPath) {
		this.coverPath = coverPath;
	}
	public List<Band> getBands() {
		return bands;
	}
	public void setBands(List<Band> bands) {
		this.bands = bands;
	}
	public List<Musician> getMusicians() {
		return musicians;
	}
	public void setMusicians(List<Musician> musicians) {
		this.musicians = musicians;
	}
}

Klasa Band:

@Entity
@Table(name="band")
public class Band {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(name="name")
    private String name;
    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "album_band",
            joinColumns = @JoinColumn(name = "album_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "band_id",
                    referencedColumnName = "id"))
    private List<Album> albums;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public List<Album> getAlbums() {
        return albums;
    }
    public void setAlbums(List<Album> albums) {
        this.albums = albums;
    }
}
0

A w którym miejscu zakładana jest transakcja? Poza tym nie używaj many-to-many - to niedobrze jak obiekty wiedzą o sobie nawzajem.

0

A w którym miejscu zakładana jest transakcja?

Za tworzenie obiektów odpowiedzialne są ich kontrolery:
AlbumController:

@RestController
public class AlbumController {
	@Autowired
	AlbumService albumService;
	
	@CrossOrigin(origins = "http://localhost:4200")
	@PostMapping(path="/album")
	public ResponseEntity<String> addAlbum(@RequestBody Album album) {
		albumService.saveAlbum(album);
		return new ResponseEntity<String>(HttpStatus.OK);
	}
	
	@CrossOrigin(origins = "http://localhost:4200")
	@GetMapping(path="/album")
	Iterable<Album> getAlbums() {
		return albumService.getAllAlbums();
	}
	
	@CrossOrigin(origins = "http://localhost:4200")
	@PutMapping(path="/album/{id}")
	ResponseEntity<String> updateAlbum(@RequestBody Album album, @PathVariable Long id) {
		albumService.updateAlbum(album, id);
		return new ResponseEntity<String>(HttpStatus.OK);
	}
}

BandController:

@RestController
public class BandController {
    @Autowired
    BandService bandService;

    @CrossOrigin(origins = "http://localhost:4200")
    @PostMapping(path="/band")
    public ResponseEntity<String> addBand(@RequestBody Band band) {
        bandService.saveBand(band);
        return new ResponseEntity<String>(HttpStatus.OK);
    }

    @CrossOrigin(origins = "http://localhost:4200")
    @GetMapping(path="/band")
    Iterable<Band> getBands() {
        return bandService.getAllBands();
    }
}

Funkcja associate powinna łączyć obiekty, żadnej transakcji tutaj nie ma (przynajmniej w takim sensie w jakim ja to rozumiem).

0

Nie ma transakcji -> nie commita -> zmiany z persistence context nie zostaną utrwalone w bazie.

Dodaj @Transactional na metodzie tego kontrolera. Jeśli zadziała, to zrefaktoryzuj, wydziel serwis.

0

Dodałem @Transactional do metody associate - bez zmian, wciąż nie działa.

0

Wrzuć kod na gita, sklonuję i chętnie pomogę. Przy okazji mam kilka innych uwag ;)

0

Cześć, dzięki za pomoc, link do git'a: https://github.com/johnfractal0/musesite-back-end

1
  1. mappedBy = "albums" czyli właścicielem jest albums a ty robisz odwrotnie. Musisz do listy albums w Band dodawać coś jeśli już
  2. Masz tam na oko transakcje niżej niż kontroler (i dobrze) więc te zmiany w obiektach po wyciągnięciu z bazy NIE SĄ persystowane bo encje są już detached. Albo musisz explicite zrobić save()/update() albo musisz mieć cały czas aktywną sesje/transakcje. Pomyśl trochę co by się działo jakby to zawsze "automagicznie" by się zapisywało w bazie, tak jak ty teraz sobie wyobrażasz że powinno. Bierzesz listę bandów z bazy i chcesz z nią coś zrobić, powiedzmy coś odfiltrować i bum wywaliło ci odfiltrowane elementy z bazy :D Przyznasz sam że byłoby to dość słabe...
1

Jak zrobisz tak jak @Shalom poradził, dane uda się zapisać do tabelki łączącej (dodatkowo adnotację @JoinTable możesz skasować, ona nic nie wnosi do relacji poza nadpisaniem defaultowych nazw tabel i kolumn). Natomiast potem i tak dostaniesz java.lang.StackOverflowError przy próbie serializacji List<Band> do JSON, ponieważ masz relacje w obie strony. Pozostałe kontrolery również nie będą działać. Moja rada:

  1. Nie zwracaj z kontrolerów encji bazodanowych.
  2. Zastąp many-to-many przez one-to-many, naprawdę nie potrzebujesz relacji N-M.

EDIT: proszę: https://github.com/johnfractal0/musesite-back-end/pull/1/files Oczywiście da się to zrobić o wiele lepiej, dałem kilka komentarzy FIXME.

0

@tabularasa1234: akurat napisałem dzisiaj na ten temat wpis na blogu, może pomoże ;)
https://sztukakodu.pl/jak-zmapowac-relacje-many-to-many-i-nie-zwariowac/

1 użytkowników online, w tym zalogowanych: 0, gości: 1