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

Odpowiedz Nowy wątek
2019-08-15 14:46
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;
    }
}

Pozostało 580 znaków

2019-08-15 15:02
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.

edytowany 1x, ostatnio: Charles_Ray, 2019-08-15 15:02

Pozostało 580 znaków

2019-08-15 18:50
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).

Pozostało 580 znaków

2019-08-15 19:05
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.

Pozostało 580 znaków

2019-08-15 19:20
0

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

Pozostało 580 znaków

2019-08-15 19:25
0

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

Pozostało 580 znaków

2019-08-15 19:59
0

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

Pozostało 580 znaków

2019-08-16 09:50
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...

Masz problem? Pisz na forum, nie do mnie. Nie masz problemów? Kup komputer...
edytowany 3x, ostatnio: Shalom, 2019-08-16 09:54

Pozostało 580 znaków

2019-08-16 18:16
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.

edytowany 3x, ostatnio: Charles_Ray, 2019-08-16 19:23
Many-to-many nie potrzebne? Dany zespół chyba może mieć wiele albumów i dany album stworzony przez kilka zespołów. Jak tak można zapisać bez relacji many-tomany? - ewazdomu 2019-08-16 19:25
No to zależy jak zamodelujesz sobie domenę :) Ziemia nie jest płaska, natomiast mapy dobrze ją modelują w kontekście pewnych potrzeb. Możesz w ogóle nie potrzebować przejścia w drugą stronę, w większości przypadków one-to-many załatwia sprawę. - Charles_Ray 2019-08-16 19:38

Pozostało 580 znaków

2019-08-19 13:45
0

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

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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