06 kwietnia 2007

Stadia rozwojowe encji - powtórka

O cyklu rozwojowym (ang. lifecycle) encji pisałem już w poprzednich wpisach, np. Java Persistence - Rozdział 3.2 Cykl rozwojowy instancji encji, ale jako, że jest to podstawowa wiedza i nigdy jej za wiele (nawet jeśli miałaby być przypominana co parę dni), więc ten wpis poświęcę na przypomnienie tematu. Zrozumienie cykli rozwojowych encji to według mnie zrozumienie połowy specyfikacji (druga połowa to poznanie adnotacji mapujących encje do tabel w bazie danych), więc tym bardziej jest to uzasadnione. Tym razem nie będzie czytania, ale analiza metody testującej zagadnienie, którą przygotowałem na zaprezentowanie publicznego interfejsu zarządcy trwałości - klasa javax.persistence.EntityManager, który odpowiada za zarządzanie cyklem rozwojowym encji.

Wyróżniamy 4 stadia rozwojowe encji:
  • NOWY (ang. new) - identyfikator trwały encji nie istnieje (przypominam, że wiele z pól nie może być przez nas zarządzanych - poza wyjątkowymi sytuacjami - i do nich należy pole udekorowane adnotacją @Id, będące wskazaniem identyfikatora trwałego)
  • ZARZĄDZANY (ang. managed) - encja związana jest z kontekstem utrwalania (zarządcą utrwalania) oraz ma przypisany trwały identyfikator
  • ODŁĄCZONY (ang. detached) - encja z trwałym identyfikatorem, ale odłączona od kontekstu utrwalania (zarządcą utrwalania)
  • USUNIĘTY (ang. removed) - encja z trwałym identyfikatorem, związana z kontekstem utrwalania (zarządcą utrwalania), która została usunięta
Nie ma ich wiele, ale jak widać z pytania Removing entities from persistence context (EJB3), które pojawiło się na grupie Enterprise Java magazynu JavaWorld:

I have a problem with removing entities:
When I call em.remove(object), the object is removed from database successfully but it remains in persistence context. The strange behavior is if I call em.remove(object) again, EntityManager inserts the entity in database!!!
So, my questions are
1. How I can remove an entity from both database and persistence context?
2. How can I obtain state of entity? (e.g managed, removed or ... )
3. Why calling remove for removed entity, inserts the entity in database again?

temat mimo tego nie jest trywialny (niestety często wynika to z ludzkiej natury, która proste rzeczy próbuje komplikować, aby prostymi nie były, bo byłoby za prosto).

Przyjrzyjmy się, w jaki sposób można pozyskać informacje o aktualnym stadium, w jakim znajduje się encja. Nie można otrzymać tej informacji wprost (bo i po co), ale wystarczy skorzystać z funkcji EntityManagera, aby wyznaczyć stadium encji.

Oto zapowiadana metoda testu jednostkowego. Miłej lektury!

// Wyczyść kontekst trwały (zarządcę trwałości)
em.clear();

// Utwórz encję - encja w stadium NOWY
Osoba encja = new Osoba("Jacek", "Laskowski");

assert em.contains(encja) == false : "Encja powinna być w stadium NOWY";

try {
em.refresh(encja);
assert false : "Oczekiwano wyjątku java.lang.IllegalArgumentException, gdyż encja jest w stanie NOWY";
} catch (IllegalArgumentException oczekiwano) {
}

// Encja przechodzi do stadium ZARZĄDZANY
encja = em.merge(encja);

assert em.contains(encja) : "Encja powinna być w stadium ZARZĄDZANY - związana z kontekstem";
assert encja.getNumer() > 0 : "Encja powinna już mieć przypisany trwały identyfikator";

final long numer = encja.getNumer();

// Sprawdźmy, czy encja jest w bazie danych - wersja mniej profesjonalna
{
Query query = em.createQuery("SELECT o FROM Osoba o WHERE o.numer = :numer");
query.setParameter("numer", numer);
try {
query.getSingleResult();
assert false : "Oczekiwano wyjątku, gdyż encja nie powinna być jeszcze w bazie danych";
} catch (Exception oczekiwano) {
}
}

// zapiszmy encję w bazie danych
// Faktyczny zapis będzie dopiero podczas zatwierdzenia transkacji bądź operacji flush
EntityTransaction tx = em.getTransaction();
tx.begin();
em.persist(encja);
tx.commit();

try {
em.refresh(encja);
} catch (Exception e) {
e.printStackTrace();
assert false : "Encja w stadium ZARZĄDZANY, a mimo to złapano wyjątek " + e;
}

// Sprawdźmy, czy encja jest w bazie danych - wersja profesjonalna
try {
Osoba encjaZBazy = (Osoba) em.find(Osoba.class, numer);
assert encjaZBazy != null : "Encja musi istnieć w bazie danych";
assert encja == encjaZBazy : "Encje stworzona przez new oraz zwrócona z bazy są identyczne";
} catch (Exception e) {
e.printStackTrace();
assert false : "Nieoczekiwany wyjątek " + e
+ ". Encja powinna być już w bazie danych (może nie fizycznie, ale prawie)";
}

// Wszystkie encje stają się odłączone, zmiany lokalne niezapisane do bazy danych są tracone
em.clear();

assert em.contains(encja) == false : "Encja nie jest związana z kontekstem trwałym (ale ma identyfikator trwały)";

try {
em.remove(encja);
assert false : "Oczekiwano wyjątku java.lang.IllegalArgumentException, gdyż encja jest w stanie ODŁĄCZONY";
} catch (IllegalArgumentException oczekiwany) {
}

// Encja przechodzi do stadium ZARZĄDZANY
encja = em.merge(encja);

try {
em.refresh(encja);
} catch (IllegalArgumentException oczekiwano) {
assert false : "Złapano wyjątek java.lang.IllegalArgumentException mimo, że encja jest w stanie ZARZĄDZANY";
}

try {
// Encja przechodzi do stadium USUNIĘTY
em.remove(encja);
} catch (IllegalArgumentException e) {
e.printStackTrace();
assert false : "Złapano wyjątek java.lang.IllegalArgumentException mimo, że encja jest w stanie ZARZĄDZANY";
}

try {
em.merge(encja);
assert false : "Oczekiwano wyjątku java.lang.IllegalArgumentException, gdyż encja jest w stanie USUNIĘTY";
} catch (IllegalArgumentException oczekiwany) {
}