Relacja wiele-do-wielu i Criteria API w JPA 2.0

Jakiś czas temu zapowiadałem pojawienie się artykułu na temat relacji wiele-do-wielu zrealizowanej za pomocą RequestFactory w GWT. Od tamtej pory trochę wody w Wiśle upłynęło, a ja z braku czasu nie miałem kiedy napisać o swoich poczynaniach. Jako że miniony weekend znalazłem trochę czasu i spędziłem go pod znakiem GWT, postanowiłem nadrobić zaległości.

Zdecydowałem się jednak rozdzielić temat RequestFactory od tematu JPA i opisać te dwa zagadnienia osobno. Podyktowane jest to przede wszystkim chęcią rozpisania się trochę o tym pierwszym, a i w związku z JPA chciałbym przedstawić kilka ciekawostek. Materiału trochę jest, więc zebranie tego wszystkiego razem tylko wydłużyłoby czas oczekiwania na nowy wpis. Sam projekt, a raczej kontynuacja tego rozpoczętego przy pierwszych bojach z RequestFactory i rozwijanego dalej podczas opisu możliwości walidacji encji w JPA i GWT, jest już prawie na ukończeniu i można go pobrać z repozytorium.

Relacja wiele-do-wielu (many-to-many) jest chyba najtrudniejszą relacją do zamodelowania i zrozumienia dla początkujących programistów. Jako jedyna po stronie bazy danych wymaga utworzenia osobnej tabeli (tzw. intersekcji) łączącej rekordy dwóch głównych tabel. Z tego też powodu zazwyczaj sprawia najwięcej trudności przy mapowaniu tabel na encje za pomocą JPA (podobnie zresztą w czystym Hibernate). Upraszczając problem, relację wiele-do-wielu można rozłożyć na dwie relacje jeden-do-wielu (one-to-many) i dołożyć mapowanie osobnej encji pełniącej rolę intersekcji. Wtedy jednak w encjach głównych dostaniemy bezpośredni dostęp za pomocą kolekcji jedynie do encji intersekcji, a nie do siebie nawzajem. Jak więc poprawnie napisać mapowanie wiele-do-wielu używając adnotacji @ManyToMany?

Mapowanie relacji

Załóżmy, że mamy dwie klasy encji:

Car.java

@Entity
@Table(name = "T_CARS")
public class Car {

  @Id
  @GeneratedValue
  @Column(name = "ID")
  private Long id;

  @Version
  private Integer version;

  private String brand;

  private String model;

  private String color;

  @ManyToMany
  private Set<Meeting> meetings;

  public Car() {
  }

  // Gettery i Settery
}

oraz:

Meeting.java

@Entity
@Table(name = "T_MEETINGS")
public class Meeting {

  @Id
  @GeneratedValue
  @Column(name = "ID")
  private Long id;

  @Version
  private Integer version;

  private String name;

  private Date startDate;

  @ManyToMany
  private Set<Car> members;

  // Gettery i Settery
}

Jak widać, jeden samochód może uczestniczyć w wielu zjazdach, a w jednym zjeździe może uczestniczyć wiele samochodów. Adnotacje @ManyToMany nie są jednak kompletne i trzeba je uzupełnić informacjami o mapowaniu kluczy obcych w tabeli intersekcji.

Zasada jest taka, że jedna z encji musi pełnić rolę właściciela mapowania relacji i to w niej zawarte będą wszystkie niezbędne informacje. U nas tym właścicielem będzie klasa Car, a tak wygląda poprawione mapowanie atrybutu kolekcji:

  @ManyToMany
  @JoinTable(
      name = "T_CAR_MEETINGS",
      joinColumns = @JoinColumn(name = "T_CARS_ID", referencedColumnName = "ID"),
      inverseJoinColumns = @JoinColumn(name = "T_MEETINGS_ID", referencedColumnName = "ID")
  )
  private Set<Meeting> meetings;

Pojawiła się nowa adnotacja @JoinTable, która dostarcza informacje o intersekcji. Adnotacja ta jest opcjonalna i jeśli jej nie zastosujemy, intersekcja zostanie utworzona na podstawie domyślnych reguł. My jednak chcemy mieć większą kontrolę nad tym co się dzieje, dlatego określimy kilka atrybutów adnotacji:

  • name – definiuje nazwę tabeli intersekcji
  • joinColumns- definiuje relację między intersekcją a encją będącą właścicielem relacji
    • name – definiuje nazwę klucza obcego w intersekcji
    • referencedColumnName – definiuje nazwę klucza głównego encji
  • inverseJoinColumns- definiuje relację między intersekcją a encją nie będącą właścicielem relacji
    • name – definiuje nazwę klucza obcego w intersekcji
    • referencedColumnName – definiuje nazwę klucza głównego encji

JPA zostało pomyślane tak, aby uniknąć powielania informacji o mapowaniu wiele-do-wielu w obu encjach. Dlatego mapowanie atrybutu kolekcji w encji Meeting (nie będąca właścicielem relacji) wygląda tak:

  @ManyToMany(mappedBy = "meetings")
  private Set members;

Poprzez parametr mappedBy został wskazany atrybut encji Car, który zawiera pełne mapowanie relacji. Należy pamiętać, by nie powielać adnotacji @JoinTable w obu encjach, gdyż może do skutkować pojawianiem się podwójnych rekordów w intersekcji. Wskazanie jednej encji jako właściciela relacji skutkuje też pewnym zachowaniem podczas modyfikacji kolekcji, o którym napiszę nieco niżej.

Modyfikacja kolekcji relacji

Jeśli relacja została poprawnie zmapowana, to modyfikacja kolekcji (czyli dodawanie i usuwanie powiązań) jest bardzo prosta. W tym celu wystarczy jedynie zmodyfikować kolekcję, a dostawca JPA wykona odpowiednią czynność na bazie danych. Należy pamiętać, że encja, której kolekcję modyfikujemy, musi być powiązana z aktualnym kontekstem JPA.

  public void join(long carId, long meetingId) {
    Car car = entityManager.find(Car.class, carId);
    Meeting meeting = entityManager.find(Meeting.class, meetingId);
    meeting.addMember(car);
  }

  public void disjoin(long carId, long meetingId) {
    Car car = entityManager.find(Car.class, carId);
    Meeting meeting = entityManager.find(Meeting.class, meetingId);
    meeting.removeMember(car);
  }

Mała uwaga: powiązanie w bazie danych zostanie zmodyfikowane tylko wtedy, gdy zmodyfikujemy kolekcję encji, która jest właścicielem mapowania. Jeśli tą samą operację wykonamy na kolekcji drugiej encji – nie zostanie to zapisane w bazie. Początkowo myślałem, że to błąd w mapowaniu, jednak po doczytaniu dokumentacji dowiedziałem się, dlaczego tak jest. W powyższym kodzie powiązania modyfikuje z obiektu encji Member (która nie jest właścicielem mapowania relacji), więc w jaki sposób to działa?

Bardzo ważną rzeczą podczas użytkowania JPA jest zapewnienie synchronizacji między obiektami encji. Podczas gdy JPA zajmuje się warstwą bazodanową, to my musimy aktualizować powiązania w warstwie obiektowej. Oznacza to mniej więcej tyle, że usuwając powiązanie z kolekcji jednej encji, musimy te samo powiązanie usunąć z encji drugiej.

Zobaczmy więc kod dla metod addMember(Car) i removeMember(Car) w encji Meeting:

  public void addMember(Car member) {
    members.add(member);
    member.getMeetings().add(this);
  }

  public void removeMember(Car member) {
    members.remove(member);
    member.getMeetings().remove(this);
  }

Analogiczne metody znalazły się również w encji Car, każde więc ich wywołanie zapewni spójność danych w obu obiektach. W kodzie należy jednak zadbać o to, aby kolekcje nie były modyfikowane w żaden inny sposób (np. uczyniając gettery i settery kolekcji metodami o dostępie pakietowym, bądź w getterze zwracać nową instancję kolekcji).

Przeszukiwanie za pomocą Criteria API

Jeśli śledzisz na bieżąco moje wpisy związane z narzędziami ORM, zapewne zauważyłeś, że jestem zwolennikiem korzystania z API w pełni obiektowego, zamiast pisania zapytań SQL/HQL/JPQL. Criteria API – bo o nim mowa, jest początkowo nieco trudniejsze w użyciu, jednak w łatwiejszy sposób pozwala na budowanie dynamicznych zapytań. Przykładów użycia kryteriów w JPA znajdziesz wiele, ja pokaże tu jeden, dotyczący opisywanej relacji wiele-do-wielu.

public Collection<Meeting> list(long carId) {
  CriteriaBuilder builder = entityManager.getCriteriaBuilder();
  CriteriaQuery<Meeting> crit = builder.createQuery(Meeting.class);
  Root<Meeting> root = crit.from(Meeting.class);
  SetJoin<Meeting, Car> join = root.join(Meeting_.members);
  Path<Long> idPath = join.get(Car_.id);
  crit = crit.select(root);
  crit.where(builder.equal(idPath, carId));
  return entityManager.createQuery(crit).getResultList();
}

Powyższy przykład pokazuje, jak wyszukać wszystkie obiekty encji Meeting, które zostały powiązane z encją Car o podanym w argumencie metody identyfikatorze. Kluczowe są tu linie:

  • 5 – pobierany jest tu obiekt reprezentujący kolekcję members encji Meeting
  • 6 – tworzona jest ścieżka do identyfikatora obiektów kolekcji
  • 8 – budowane jest kryterium równości przekazanego w argumencie metody identyfikatora z zawartością ścieżki utworzonej w linii 6

Całość wygląda może nieco skomplikowanie, jednak po dokładnej analizie wszystko wydaje się na miejscu. Criteria API w JPA cechuje się dość dużym rozbiciem na pojedyncze zagadnienia i wszystko robi się tu w kilku linijkach więcej, niż w analogicznym mechanizmie Hibernate’a.

Tym przykładem chciałem jednak dojść do kolejnej ciekawostki, z którą nie spotkałem się jeszcze pisząc w „czystym” Hibernate. Jeśli zauważyłeś konstrukcje Meeting_.members oraz  Car_.id, pewnie zacząłeś się zastanawiać, co to jest?

Generowanie metamodeli

Metamodele są specyficznymi klasami, mającymi za zadanie opisanie atrybutów encji. Dzięki nim pisanie zapytań nie wymaga już operowania na ciągach znaków przechowujących nazwy atrybutów, gdyż te mamy zapisane w metamodelu. Koniec z błędami wynikającymi z literówek, koniec z doszukiwaniem się wszystkich zależności po zmianie nazwy atrybutu. Metamodele zawierają też szereg dodatkowych informacji na temat atrybutów encji, takich jak typ atrybutu czy jego opcjonalność. Dla programisty najważniejsze jest jednak to, że takie metamodele mogą być generowane automatycznie.

Przykładowy metamodel dla encji Car:

@StaticMetamodel(Car.class)
public abstract class Car_ {

  public static volatile SingularAttribute<Car, Long> id;
  public static volatile SingularAttribute<Car, String> model;
  public static volatile SingularAttribute<Car, String> color;
  public static volatile SingularAttribute<Car, String> brand;
  public static volatile SetAttribute<Car, Meeting> meetings;
  public static volatile SingularAttribute<Car, Integer> version;

}

Jest kilka ważnych zasad odnośnie tworzenia metamodeli. Jeśli zabierzemy się do tego ręcznie, należy pamiętać, że klasa metamodelu musi znajdować się w tym samym pakiecie co klasa encji, a nazwa klasy metamodelu to nazwa klasy encji kończąca się przyrostkiem „_”. Więcej informacji o tych zasadach znajdziesz w dokumentacji Hibernate Metamodel Generator.

Leniwego programistę zapewne bardziej interesuje automatyczne generowanie metamodeli. Nic prostszego, jeśli do zarządzania projektem używasz maven’a, wystarczy dopisać mały fragment konfiguracji do pliku pom.xml. Jeśli chcesz włączyć generowanie metamodeli w swoim IDE, wystarczy kilka kliknięć w oknie konfiguracji. Nie będę opisywał, co dokładnie trzeba zrobić, bo jest to świetnie opisane w dokumentacji przytoczonej powyżej.

Metamodele sprawdzają się o wiele lepiej od niepewnych ciągów znaków, w końcu właśnie po to zostały stworzone. Szkoda tylko, że nie da się ich użyć po stronie klienckiej GWT, gdyż nie ma ich implementacji javascript’owej.

To tyle w ramach części poświęconej JPA. Już wkrótce pojawi się kolejny wpis uzupełniający informacje o RequestFactory, w ramach którego postaram się coś więcej napisać o samej zasadzie działania tego mechanizmu. Jeśli czekasz z niecierpliwością, już dziś możesz sięgnąć do Repozytorium, gdzie znajdziesz już gotowy kod projektu.

Skomentuj

10 Komentarze.

  1. Ciekawa sprawa z tym metamodelem. Dzięki za info, nie natrafiłem na to wcześniej. Spróbuję wykorzystać w projekcie, który akurat tworzę „po godzinach” ;), wykorzystuję w nim właśnie między innymi JPA (w implementacji Hibernate). Pzdr.

  2. Tu możesz zobaczyć pom’a:
    http://websvn.avd.pl/wsvn/yuppy/projects/trunk/sample-gwt-requestfactory/pom.xml

    Użyłem pluginu maven-processor-plugin i build-helper-maven-plugin.

    Jeśli używasz Hibernate, to jako dependency trzeba dopisać:

    <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-jpamodelgen</artifactId>
    <version>1.1.1.Final</version>
    </dependency>

    Nie wiem tylko, czy da się jakoś połączyć konfigurację maven’a z eclipse’m, żeby sam załapał, że trzeba dołączyć nowy katalog ze źródłami metamodeli. Musiałem ręcznie włączyć w eclipse przetwarzanie adnotacji i wskazać odpowiedni katalog, a chciałbym jakąś ten krok pominąć :)

  3. Dzięki. Ja od kilku miesięcy używam IntelliJ IDEA, więc będą kombinował z nią, ale jak na razie jestem pozytywnie zaskoczony jej użytecznością, szybkością i mnogością automagicznych ficzerów :) (typu np. znalazłam konfigurację połączenia do bazy w projekcie, to połączę się i będę podpowiadać Ci strukturę tabel itp.). Polecam

  4. Jak wygram licencję na IDEE na którymś WJUG’u, to też się chętnie przesiądę :) Eclipse niestety momentami zdycha…

    W każdym bądź razie z tego co wynika z dokumentacji Hibernate Metamodel Generator, zarówno Eclipse jak i IDEA wspierają automatyczne przetwarzanie adnotacji. NetBeans’a trzeba by było sprawdzić, ponoć od wersji 7 też już takie wsparcie zawiera.

  5. To ja polecam wam zapoznać się jeszcze z Hadesem.

    A po co? Ano po to, że stworzenie prostego dao z przykładową metodą listowania meetings z wpisu plus przykładowe pobranie meetingów o danej nazwie sprowadza się do:

    public interface MeetingDao extends GenericDao {
    @Query(„from Meeting m where m.members..id = :carId”)
    public List listByCarId(@Param(„carId”) Long carId);

    public List findByName(String name);
    }

    I nic więcej. Żadnych metamodeli, żadnych wielkich Dao, żadnych criteria do prymitywnych prostych zapytań. A to jeszcze nie wszystko. Spokojnie wystarczy do wielu zastosowań a pełni funkcjonalne dao (z paginacją i specyfikacją) dla encji to powyższe kilka linijek.

  6. Hades, to tylko pewna gotowa implementacja GenericDao, korzystająca pod spodem z JPA – nic więcej. Co najgorsze, wymaga Springa, a nie wszyscy muszą chcieć go używać :)

    W artykule raczej nie chodziło mi o to, żeby wszystko pokazać w najprostszy możliwy sposób. Owszem, definiowanie zapytań w Hadesie, JPQL’u (z którego Hades pewnie też korzysta), HQL’u, czy nawet czystym SQL’u będzie często łatwiejsze, niż posługiwanie się Criteria API. Jednak nie wyobrażam sobie np. implementacji dynamicznego filtrowania poprzez sklejanie stringów, tu po prostu Criteria są wygodniejsze. Pomijając fakt, że ja po prostu bardziej lubię Criteria, niż wszelakie *QL’e.

    Przeglądając sieć na temat utworzenia zapytania w Criteria API do przeszukiwania obiektów połączonych intersekcją na prawdę nie znalazłem wielu przykładów, ani tym samym w 100% trafnych odpowiedzi. Dlatego zdecydowałem się na umieszczenie tutaj takiego zapytania. Człowiek przede wszystkim uczy się na prostych rzeczach.

  7. Wiem czym jest Hades. A dla jasności, to nie musisz w nim polegać tylko na JPQL. Masz też api do dynamicznego filtrowania.

    A tak z innej beczki to szkoda, że ludzie odchodzą od SQL do API…Nie wiedziałem też, że nie można w komentarzu wskazać na inne (mym skromnym zdaniem – ciekawe) rozwiązanie bo nie do końca pokrywa się z ideą autora postu :) Tobie może się nie przyda, ale być może ktoś nie będzie chciał pisać criterii, metamodeli… i wystarczy mu zwykłe dao.

  8. Oczywiście, że można komentować w taki sposób i można wskazywać alternatywne rozwiązania – nigdzie nie napisałem, że nie jest to na miejscu :) Czytelnicy jak najbardziej powinni wiedzieć, że wiele problemów można rozwiązać na wiele różnych sposobów, a ja tylko wybrałem jeden z nich i właśnie ten zaprezentowałem. Moja wcześniejsza wypowiedź miała raczej za zadanie pokazać, że można to zrobić za pomocą Criteria API, bo ja w zasadzie myślę trochę przeciwnie: zbyt mało ludzi używa Criteria w porównaniu do *QL’i i nie są w stanie docenić wielu zalet, jakie niesie te API.

  9. > Metamodele sprawdzają się o wiele lepiej od niepewnych ciągów znaków, w końcu właśnie po to zostały stworzone. Szkoda tylko, że nie da się ich użyć po stronie klienckiej GWT, gdyż nie ma ich implementacji javascript’owej.

    Podobnym rozwiązaniem są w gxt3.0 interfejsy ValueProvider

    • Ogólnie w czystym GWT AutoBean’y opierają się na takich ValueProviderach (nazywa się to trochę inaczej, ale o to samo chodzi), a GXT korzysta właśnie z tych rozwiązań GWT’owych. Choć i tak nie da się w niektórych miejscach uniknąć zaszywania nazw kolumn w stringach (chociażby przy jakimś generycznym mechanizmie sortowania i wyszukiwania na gridach), a przynajmniej tak jest w GWT, bo nie wiem jak zostało to rozwiązane w nowym GXT.

Skomentuj


UWAGA - Możesz używać HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

CommentLuv badge