@ManyToMany i @ManyToOne w jednej tablicy asocjacyjnej

0

Witam, jestem w temacie Spring JPA i próbuje skonfigurować hibernate aby wygenerowanć jedną tablice asocjacyjną dla dwóch asocjacji @ManyToMany i @OneToMany:
Mam abstrakcyjny zespół:

@Entity
@NoArgsConstructor @Getter @Setter @EqualsAndHashCode @ToString
public abstract class Band {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
    }
}

Soliste dziedzidzącego po Zespole,
który posiada pole Musician musician. Chciałbym żeby hibernate zmapował mi to pole do tabeli asocjacyjnej "band_musician" w której trzymałbym też inne asocjacje między dziećmi Band a Muzykami

@Entity
@NoArgsConstructor @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true)
public class Soloist extends Band {
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinTable(
            name = "band_musician",
            joinColumns = { @JoinColumn(name = "band_id") },
            inverseJoinColumns = { @JoinColumn(name = "musician_id") })
    private Musician musician;
}

Kolejne dziecko ktore ma pole List<Musician> musicians. Jak wyżej chciałbym aby hibernate zmapował mi to na tabele "band_musician".

@Entity
@NoArgsConstructor @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true)
public class Chamber extends Band {
    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(
            name = "band_musician",
            joinColumns = { @JoinColumn(name = "band_id") },
            inverseJoinColumns = { @JoinColumn(name = "musician_id") })
    private List<Musician> musicians = new ArrayList<>();
}

Asocjacja ma być jednokierunkowa dlatego Musician wygląda tak:

@Entity
@NoArgsConstructor @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true)
public class Musician extends Person{
    private String roleName;
}

i ojciec Musician Person

@MappedSuperclass
@NoArgsConstructor @Getter @Setter @EqualsAndHashCode @ToString
public abstract class Person {
    @Id @GeneratedValue(strategy= GenerationType.AUTO)
    private Long id;
    private String imie;
    private String nazwisko;
}

Pytanie czy da się w jakiś sposób połączyć wszystkie asocjacje w jedenej tabeli "band_musician" ??
Taka konfiguracja rzuca takim błędem:

Caused by: org.h2.jdbc.JdbcSQLException: Naruszenie ograniczenia Klucza Głównego lub Indeksu Unikalnego: "PRIMARY KEY ON PUBLIC.BAND_MUSICIAN(BAND_ID)"
Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.BAND_MUSICIAN(BAND_ID)"; SQL statement:

Jak widać korzystam z bazy H2. Czy da się jakoś skonfigurować aby @ManyToOne generował klucz główny na dwóch kolumnach band_id i musician_id (intuicja mi podpowiada że tu jest problem ale poprawcie mnie jeśli się mylę)

3

Twój błąd wynika z tego, ze masz dwie tabele:
Soloist oraz Chamber
w obu masz jakieś rekordy - w obu masz kolejne idki od 1 w górę - w obu masz id = 1

Teraz masz tabelę Band_musician - tabela przeznaczona jest do trzymania relacji zarówno Soloist do Musician jak Chamber do Musician.

Teraz przykład, dlaczego to jest nieodpowiednie:
Soloist o id 1 ma Musician o id 1 -> w tabeli Band_musician mamy 1,1
Chamber o id 1 ma Musician o id 1 oraz 2 -> do tabeli Band_musician chcemy dodać 1,1 oraz 1,2 -> wywala się

Generalnie jest to błąd logiczny, bo w tabeli Band_musician nie rozróżniasz czy jest to id solisty czy chamber

0

Błąd od strony praktycznej opisała @szarotka, ale to nie wszystko. Masz błąd na poziomie koncepcyjnym. W przypadku klas Band, Soloist, Chamber masz dziedziczenie na poziomie Javy, ale nie uwzględniasz tego na poziomie bazy danych. Problem nie zaistniałby, jeżeli świadomie użył, byś mechanizmów pozwalających na mapowanie dziedziczenia, a nie zdawał się na domyślne konfiguracje.

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@SequenceGenerator(name="band_seq", initialValue=1, allocationSize=1)
@NoArgsConstructor @Getter @Setter @EqualsAndHashCode @ToString
public abstract class Band {
    @Id
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq")
    private Long id;
//...
}

Tu jest trochę magii, ale bardzo prostej. Adnotacja @Inheritance mówi hibciowi, aby wygenerował osobne tabele dla każdej z klas, które dziedziczą po tej klasie. Następnie para @SequenceGenerator i @GeneratedValue konfiguruje nam sekwencję, która będzie użyta do generowania ID. I to jest właśnie clou programu. Wszystkie tabele reprezentujące klasy dziedziczące po Band będą korzystać z tej samej sekwencji, dzięki czemu identyfikatory będą unikalne i nie będzie problemu z łączeniem tych tabel w ramach jednej tabeli pośredniej.

Jeżeli zrobisz podobną sztuczkę dla Person, to efekt będzie jeszcze lepszy.

Dlaczego nie korzystać z InheritanceType.JOINED, ponieważ jeżeli dobrze rozumiem chcesz mieć pełne dane Soloist i Chamber w osobnych tabelach.

0

Jak dla mnie to dziedziczenie solisty po zespole to duży zonk. Zespół to grupa muzyków, w której część to soliści, albo cały zespół to jeden solista. Jeśli już koniecznie chcesz dziedziczyć to solistę po muzyku. Wydzielanie osobnej klasy dla chamber to też coś nie tak, bo robisz rozszerzanie klasy przez wartość atrybutu zamiast poprzez dodanie atrybutu. Zespół powinien mieć pole type, i tam możesz wpisywać wartości: chamber, orchestra, solist, choć i to może być zbędne, bo określenie rodzaju zespołu możesz zrobić po zliczeniu członków zespołu. Unikasz wtedy robienia triggerów, które powinny zmienić ci rodzaj zespołu, gdy zmienia się liczba członków. Wtedy masz tylko związek wiele do wielu między tabelą band i musican. Z osobną klasą i tabelą dla solisty też właściwie trzeba się zastanowić, bo co się stanie jak ten sam muzyk, grający w jednym zespole, będzie grał też jako solista? Będziesz musiał umieszczać tego samego muzyka w dwu różnych tabelach.

0

@cs projekt jest studencki, musze zrealizowć w nim kilka przypadków użyć lecz wpierw chciałem podszkolić się w samym spring data więc sama "sensowność" klas nie ma znaczenia. W tym wypadku muzyk jest członkiem różnych rodzajów zespołów np solisty, zespołu kameralnego. Wprowadziłem poprawki zgodnie z zaleceniami @Koziołek dodałem do obu rodziców (person i band) id po sekwencji oraz określiłem @Inheritance lecz nadal mam problem z integralnością kluczy głównych. Przeanalizowałem to i chyba mam problem ktory nie wiem jak rozwiązać wykorzystując hibernate. Chce dla każdego ze złączeń:

solist -> musiain (liczność 1)
chamber -> musician (liczność wiele)

stworzyć jedną tabele asocjacyjną w ktorej będą przechowywane dane na przykład w taki sposób:

ID    BAND_ID  	MUSICIAN_ID  
1     1         1
2     2         2
3     2         3
4     3         2

I tu mam problem, jak poprosić hipcia(zapamiętam to :) ) aby wygenerował mi w tej tabeli swoje własne ID !!. szukałem czy da się jakoś to zrobić @JoinColumn niestety nie mam pojęcia jak przez to przebrnąć.

Poniżej zamieszczam jak wygląda przykładowa baza do powyższej tabeli i kod javy przedstawiam poniżej

wszystkie dzieci band to jedna tabela(SINGLE_TABLE):

DTYPE(dyskryminator)  	ID  	NAME  
solist                  1       "marcus miller"
chamber                 2       "marcus miller band"
solist                  1       "snoopy"

muzyk ma swoją indywidualną tabele (TABLE_PER_CLASS):

ID  	IMIE  	    NAZWISKO  	ROLE_NAME  
1	     Marcus	    Miller	    Bass
2	     Trombone   Shorty	    Trąbka
3	     Calvin     Broadus	    Stoner               //SNOOP DOG

W tym momencie model w javie wyglada tak:

Band

@Entity
@NoArgsConstructor @Getter @Setter @EqualsAndHashCode @ToString
@SequenceGenerator(name="band_seq", initialValue=1, allocationSize=1)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Band {
    @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "band_seq")
    private Long id;
    private String name;
}

Chamber extends Band

@Entity
@NoArgsConstructor @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true)
@DiscriminatorValue("chamber")
public class Chamber extends Band {
    @ManyToMany(cascade = CascadeType.ALL)
// TU CHCE ZŁĄCZYĆ Z TĄ SAMĄ TABELĄ CO U SOLISTY
    @JoinTable(
            name = "musician_band",
            joinColumns = { @JoinColumn(name = "band_id") },
            inverseJoinColumns = { @JoinColumn(name = "musician_id") })
            private List<Musician> musicians = new ArrayList<>();
}

Soloist extends Band

@Entity
@NoArgsConstructor @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true)
@DiscriminatorValue("soloist")
public class Soloist extends Band {
    @ManyToOne(cascade = CascadeType.ALL)
// TU CHCE ZŁĄCZYĆ Z TĄ SAMĄ TABELĄ CO U SOLISTY
    @JoinTable(
            name = "musician_band",
            joinColumns = { @JoinColumn(name = "band_id") },
            inverseJoinColumns = { @JoinColumn(name = "musician_id") })
    private Musician musician;
}

Person

@MappedSuperclass
@NoArgsConstructor @Getter @Setter @EqualsAndHashCode @ToString
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@SequenceGenerator(name="person_seq", initialValue=1, allocationSize=1)
public abstract class Person {
    @Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="person_seq")
    private Long id;
    private String imie;
    private String nazwisko;
}

Musician extends Person

@Entity
@NoArgsConstructor @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true)
public class Musician extends Person{
    private String roleName;
}

Po odpaleniu tego dostaje

Caused by: org.h2.jdbc.JdbcSQLException: Naruszenie ograniczenia Klucza Głównego lub Indeksu Unikalnego: "PRIMARY KEY ON PUBLIC.MUSICIAN_BAND(BAND_ID)"
Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.MUSICIAN_BAND(BAND_ID)"; SQL statement:
insert into musician_band (band_id, musician_id) values (?, ?) [23505-197]

czyli można się tego spodziewać bo nigdzie nie tworze dla tej tabeli musician_band odziellny primary key

Czyli jeszcze raz podsumowując jak zmusić by te dwa @JoinTable w chamber i solist miały własny primary key i pokazywały na tą samą tabelę

0
solist -> musiain (liczność 1)
chamber -> musician (liczność wiele)

stworzyć jedną tabele asocjacyjną w ktorej będą przechowywane dane na przykład w taki sposób:

ID    BAND_ID  	MUSICIAN_ID  
1     1         1
2     2         2
3     2         3
4     3         2

Możesz tworzyć wiele związków między tymi samymi tabelami, ale każdy związek to para (klucz główny -> klucz obcy), a Ty chcesz dla tej samej pary kluczy musician_id i band_id tworzyć raz związek typu wiele do wielu (klasa Chamber) a raz wiele do jednego (klasa Soloist). Nie da się tego pogodzić i jeśli został przyjęty związek wiele do jednego, to przy dodaniu kolejnego członka do bandu wyrzuca Ci naruszenie ograniczenia klucza głównego.

Dlatego uważam, że tworzenie osobnych klas dla zespołów o różnej liczbie członków niewiele da.

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