poniedziałek, 14 kwietnia 2008

JPA - modelowanie dziedziczenia

Relacja dziedziczenia jest właściwa tylko dla modelu obiektowego i jako tako nie występuje w modelu relacyjnym. Z tego względu opracowane zostały podejścia do reprezentacji dziedziczenia w modelach relacyjnych:
  - jedna tabela na klasę konkretną (Single table per Concrete Class),
  - jedna tabela na hierarchię klas (Single table per Class Hierarchy),
  - złączenie podklasy (Joined Subclass).
Specyfikacja JPA, z użyciem adnotacji @Inheritance, umożliwia zastosowanie każdej z tych trzech strategii reprezentacji dziedziczenia.

Przykład - dziedzina problemu

Dla celów niniejszego artykułu zostanie wykorzystany poniższy model danych: Jest to prosta hierarchia klas, z których abstrakcyjna klasa Osoba jest klasą nadrzędną, a klasy OsobaPrawna i OsobaFizyczna z niej dziedziczą.

Jedna tabela na klasę konkretną (Single table per Concrete Class)

Wg. tej strategii model relacyjny prezentowałby się następująco: Te podejście przewiduje, iż na każdą klasę konkretną będzie przypadać jedna tabela w modelu relacyjnym. W naszym przypadku zostaną utworzone tabele dla klas OsobaPrawna i OsobaFizyczna. Przy czym tabele te będą zawierać również kolumny reprezentujące dziedziczone atrybuty. Tu trzeba zaznaczyć, że nie ma możliwości nadpisania mapowania dziedziczonych atrybutów - nie zadziała adnotacja @AttributeOverride(s).

Klasa Osoba nie będzie jawnie odwzorowana w modelu relacyjnym. Ta cecha sprawia, że polimorficzne asocjacje (tj. ogólne odwoływanie się do bytu Osoba, a nie do konkretnego rodzaju osoby) stają się nie możliwe, a polimorficzne zapytania bardzo trudne (w zapytaniach używa się unii, bądź preparuje się oddzielne zapytanie dla każdej podklasy).

Poniżej zamieszczam wycinki kodów definicji klas z mapowaniem:
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Osoba {
 ...
}
@Entity(name="osoba_prawna")
public class OsobaPrawna extends Osoba {
 ...
}
@Entity(name="osoba_fizyczna")
public class OsobaFizyczna extends Osoba {
 ...
}
Pełne źródła w postaci projektu maven'owego można pobrać tutaj.

Jedna tabela na hierarchię klas (Single table per Class Hierarchy)

Wg. tej strategii model relacyjny prezentowałby się następująco: W tym podejściu jednej hierarchii klas przypada tylko jedna tabela. W naszym przypadku oznacza to, że wszystkie klasy (Osoba, OsobaPrawna, OsobaFizyczna) będą reprezentowane tylko przez jedną tabelę. Tabela ta oprócz kolumn reprezentujących atrybuty tych klas, będzie posiadać kolumnę flagę (discriminator) - os_typ, która będzie określać jakiej klasy konkretnej jest dany rekord. Nie jest koniecznie definiowanie w klasie atrybutu reprezentującego discriminator'a, ale jak już jest to robione, to w mapowaniu tego atrybutu musi być wyłączona opcja zapisu i uaktualniania tej wartości, gdyż nie można jej zmieniać manualnie.

Te rozwiązanie nie posiada wad związanych z polimorfizmem, które posiadało poprzednie podejście. Jednak posiada inną wadę związaną z możliwościami definiowania ograniczeń kolumn (przedewszystkim not null) specyficznych dla klas konkretnych. Np. nie można wymusić żeby kolumna mapująca OsobaPrawna.regon nie mogła przyjmować wartości pustych, gdyż tego warunku nie sprosta żaden rekord reprezentujący obiekt OsobaFizyczna.

Poniżej zamieszczam wycinki kodów definicji klas z mapowaniem:
@Entity(name="osoba")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="os_typ")
public abstract class Osoba {
 ...
}
@Entity
@DiscriminatorValue(value="OP")
public class OsobaPrawna extends Osoba {
 ...
}
@Entity
@DiscriminatorValue(value="OF")
public class OsobaFizyczna extends Osoba {
 ...
}
Pełne źródła w postaci projektu maven'owego można pobrać tutaj.

Złączenie podklasy (Joined Subclass)

Wg. tej strategii model relacyjny prezentowałby się następująco: W tym podejściu każda klasa (nawet abstrakcyjna) ma w relacyjnym modelu swoją reprezentacje w postaci tabeli. Oznacza to, że dane konkretnego obiektu będą zapisane po części w tabeli klasy abstrakcyjnej oraz po części w tabeli klasy konkretnej. U nas np. atrybuty obiektu OsobaPrawna będą przechowywane w tabeli OsobaPrawna jak oraz w zakresie dziedziczonych atrybutów w tabeli Osoba.
Byt dziedziczący musi mieć odwołanie do bytu z którego dziedziczy. W roli tego odwołania może być wspólny klucz główny. I tak wiersz w tabeli klast OsobaPrawna będzie miał klucz główny równy co do wartości do klucza wiersza w tabeli klasy Osoba.

Te podejście nie ma wad poprzednich rozwiązań. Ponadto taki model relacyjny charakteryzuje się wyższym stopniem normalizacji w stosunku do konkurencyjnych rozwiązań. Co też może również być wadą tej strategii, gdyż wyższy stopień normalizacji oznacza, że dane są rozproszone w większej ilości tabelach, co zwiększa skomplikowanie zapytań i w pewnym stopniu czas ich wykonania.

Poniżej zamieszczam wycinki kodów definicji klas z mapowaniem:
@Entity(name="osoba")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Osoba {
 ...
}
@Entity(name="osoba_prawna")
@PrimaryKeyJoinColumn(name="op_id")
public class OsobaPrawna extends Osoba {
 ...
}
@Entity(name="osoba_fizyczna")
@PrimaryKeyJoinColumn(name="of_id")
public class OsobaFizyczna extends Osoba {
 ...
}
Pełne źródła w postaci projektu maven'owego można pobrać tutaj.

Dziedziczenie z klasy nie będącą encją

Specyfikacja JPA przewiduje dwa warianty zachowania w przypadku dziedziczenia z klasy nie będącej encją.
Wariant pierwszy - klasa bazowa nie jest oznaczona adnotacją @MappedSuperclass. W tym przypadku atrybuty odziedziczone są ulotne i nie będą zapisane w bazie danych po operacji utrwalenia obiektu.
Wariant drugi - klasa bazowa jest oznaczona adnotacją @MappedSuperclass. Wówczas dziedziczone atrybuty są trwałe wg. mapowania zdefiniowanego w tej bazowej klasie. Przy czym istnieje możliwość nadpisania tych mapowań poprzez użycie adnotacji @AttributeOverride(s).
Moim zdaniem mapowane klasy nadrzędne są ciekawym rozwiązaniem i można go użyć chociażby do definicji klasy bazowej wszystkich encji, która będzie zawierać atrybuty zawierające informacje o dokonywanych na encji zmianach.
Poniżej zamieszczam przykład użycia mapowanej klasy:
@MappedSuperclass
public class EncjaAudytowalna {
 private String uzytkownikModyfikujacy;
 private Date dataModyfikacji;
 
 @Column(name="audyt_uzytkownik")
 public String getUzytkownikModyfikujacy() {
  return uzytkownikModyfikujacy;
 }
 public void setUzytkownikModyfikujacy(String uzytkownikModyfikujacy) {
  this.uzytkownikModyfikujacy = uzytkownikModyfikujacy;
 }
 
 @Column(name="audyt_data")
 @Temporal(value=TemporalType.TIMESTAMP)
 public Date getDataModyfikacji() {
  return dataModyfikacji;
 }
 public void setDataModyfikacji(Date dataModyfikacji) {
  this.dataModyfikacji = dataModyfikacji;
 }
}
@Entity
@AttributeOverride(name="uzytkownikModyfikujacy", column=@Column(name="uz_au_um"))
public class Uzytkownik extends EncjaAudytowalna {
 private Long id;
 private String name;

 @Id
 @GeneratedValue(strategy=GenerationType.AUTO)
 public Long getId() {
  return id;
 }
 public void setId(Long id) {
  this.id = id;
 }

 @Column(name="nazwa")
 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
}

Zasoby

Hibernate Annotations - 2.2.4. Mapping inheritance
Specyfikacja JPA

Brak komentarzy: