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 < namesAndValues.length; i += 2) {
			predicates.add(cb.equal(from.get(namesAndValues[i].toString()),
					namesAndValues[i + 1]));
		}
		Predicate[] pa = new Predicate[predicates.size()];

		cq.where(predicates.toArray(pa));
		return (Collection<? extends SelfManagedEntity<?>>) 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.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax