Active Record z Google Guice i JPA cześć III
Wczoraj pracując nad kodem stwierdziłem, że należy przeprowadzić drobną refaktoryzację. W jej wyniku powstała klasa JPAToolkit, która zajmuje się komunikacją z bazą danych. Dzięki temu wyeliminowałem zależność od EntityManager w klasach encji. W kolejnych krokach zapewne uogólnię i tą zależność do interfejsu i tym samym nie będzie istotne czy implementacja opera się o JPA czy JDBC… pieśń przyszłości.
Dodatkowym motywatorem do takiej reaktoryzacji była chęć wykorzystania mechanizmów zarządzania transakcjami dostarczanymi wraz z Guice.
Klasa SelfManagedEntity
Swoisty „core” rozwiązania. Co prawda po refaktoryzacji tylko deleguje zadania do JPAToolkit, ale i tak jest to kluczowa klasa. Dostarcza ona interfejsu Active Record dla encji.
Listing 1. Klasa SelfManagedEntity
public abstract class SelfManagedEntity<X extends SelfManagedEntity<X>> {
public static interface EntityFactory<X, I> {
X makeById(@Assisted("id") I i);
}
@Inject
protected transient static EntityCache cache;
@Inject
transient static Injector injector;
@Inject
private transient static JPAToolkit jpaToolkit;
public static Long countInstances(
Class<? extends SelfManagedEntity<?>> entityClass) {
return JPAToolkit.countInstances(entityClass);
}
public static Collection<? extends SelfManagedEntity<?>> findBy(
Class<? extends SelfManagedEntity<?>> entityClass,
Object... namesAndValues) {
return JPAToolkit.findBy(entityClass, namesAndValues);
}
public static <T extends SelfManagedEntity<T>> T lookFor(
Class<T> entityClass, Object entityId) {
T find = null;
if ((find = cache.find(entityClass, entityId)) != null) {
return find;
}
if ((find = jpaToolkit.findById(entityClass, entityId)) != null) {
cache.manage(find);
return find;
}
return fromFactory(entityClass, entityId);
}
private static <T> T fromFactory(Class<T> entityClass, Object entityId) {
Class<?>[] declaredClasses = entityClass.getDeclaredClasses();
for (Class<?> ic : declaredClasses) {
final Class<?>[] iis = ic.getInterfaces();
for (Class<?> ii : iis) {
if (ii == EntityFactory.class) {
@SuppressWarnings({ "unchecked", "rawtypes" })
T find = (T) ((EntityFactory) injector.getInstance(ic))
.makeById(entityId);
cache.manage(find);
return find;
}
}
}
throw new RuntimeException("WTF??? No entity " + entityClass
+ " with ID " + entityId + ". RTFM and have nice day.");
}
public void delete() {
cache.detach(this);
jpaToolkit.delete(this);
}
@SuppressWarnings("unchecked")
public X save() {
return (X) jpaToolkit.save(this);
}
}
Najistotniejsze i najbardziej, nie bójmy się tego powiedzieć otwarcie pojebane, są metody lookFor i fromFactory. Pierwsza z nich najpierw przeszukuje wewnętrzny cache jeżeli znajdzie obiekt to go zwraca. Jeżeli nie to pyta się o niego DB. Jeżeli i tam nie ma nic interesującego to odpala tworzenie obiektu za pomocą AI.
fromFactory przeszukuje listę klas wewnętrznych w poszukiwaniu interfejsu rozszerzającego EntityFactory. Jeżeli znajdzie taki interfejs to pobiera instancję z injectora i tworzy obiekt o podanym identyfikatorze. Akurat ta metoda też powinna zostać poprawiona… Może w ostatecznym wydaniu 😉
Klasa JPAToolkit
Ostatni element układanki. Konfigurowany jako singleton!
Klasa JPAToolkit
public class JPAToolkit {
private static EntityManager em;
@Inject
private transient static Injector injector;
public static Long countInstances(
Class<? extends SelfManagedEntity<?>> entityClass) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Long> cq = cb.createQuery(Long.class);
cq.select(cb.count(cq.from(entityClass)));
return em.createQuery(cq).getSingleResult();
}
@SuppressWarnings("unchecked")
public static Collection<? extends SelfManagedEntity<?>> findBy(
Class<? extends SelfManagedEntity<?>> entityClass,
Object... namesAndValues) {
if (namesAndValues.length % 2 != 0) {
throw new RuntimeException("Names and values is not even.");
}
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<?> cq = cb.createQuery(entityClass);
Root<? extends SelfManagedEntity<?>> from = cq.from(entityClass);
List<Predicate> predicates = new LinkedList<Predicate>();
for (int i = 0; i >) em.createQuery(cq)
.getResultList();
}
public static void start() {
em = injector.getInstance(EntityManager.class);
em.setFlushMode(FlushModeType.COMMIT);
}
protected JPAToolkit() {
}
@Transactional
public void delete(Object entity) {
em.remove(entity);
flush();
}
public <T> T findById(Class<T> entityClass, Object id) {
return em.find(entityClass, id);
}
@Transactional
public Object save(Object entity) {
if (!em.contains(entity)) {
em.persist(entity);
} else {
em.merge(entity);
}
flush();
return entity;
}
@Transactional
private void flush() {
em.flush();
}
}
Tu jest trochę zabawy. Dziwne wymieszanie statycznych i niestatycznych metod. Wynika z tego, że mechanizm obsługujący transakcję za pomocą adnotacji @Transactional wymaga by metody były niestatyczne, a obiekt posiadał nieprywatny konstruktor. Tak! Aspekty w wykonaniu AOP Alliance są tu w użyciu.
Kod wymaga jeszcze trochę poprawek. Metoda findBy pozwala na wyszukanie tylko po polach „pierwszego rzędu” bez możliwości wnikania w głąb zależności. Na chwilę obecną wystarczy.
Podsumowanie
Celem było napisanie prostego mechanizmu AR w oparciu o JPA i Guice. tak by udowodnić, że można. Zresztą nie spotkałem się z niezależną implementacją AR opartą o JPA. Moja wymaga Guice, ale zapewne można by byłą ją uniezależnić i od tego elementu.