Generyczne DAO dla Hibernate

12 lutego 2011, 16:58:32, Patryk Dobrowolski « Testy jednostkowe w Spring 3.0 | No Scope registered for scope 'session' »

Przeglądając kilka dni temu dokumentację J2EE i opisy wzorców projektowych zetknąłem się z sugestią, że DAO mogą być tworzone dynamicznie z wykorzystaniem wzorca Fabryka Abstrakcyjna. Ponieważ ostatnio zajmuję się tworzeniem warstwy DAO w swoim projekcie, pomyślałem że można spróbować takie coś zaimplementować. W zasadzie pewnie istnieje już cała tona tego typu rozwiązań, wątpię jednak, by cokolwiek z tego pasowało w 100% do moich wymagań.

Na Wikipedii napisano:

Data Access Object – jest to komponent dostarczający jednolity interfejs do komunikacji między aplikacją, a źródłem danych (np. bazą danych czy plikiem). Jest często łączony z wzorcami projektowymi. Dzięki DAO, aplikacja nie musi znać sposobu oraz ostatecznego miejsca składowania swoich danych, a ewentualne modyfikacje któregoś z czynników nie pociągają za sobą konieczności modyfikowania jej kodu źródłowego. Wzorzec ten jest często stosowany w modelu MVC (Model-View-Controller) do oddzielenia dostępu do danych od logiki biznesowej i warstwy prezentacji. Gotowe narzędzia do korzystania z DAO wchodzą w skład wielu popularnych języków programowania oraz platform (np. Java EE, Ruby on Rails).

Mamy więc takie założenia:

  • DAO obsługuje cztery podstawowe operacje CRUD
  • DAO skojarzone jest z jedną klasą domenową przechowującą konkretne informacje
  • DAO izoluje sposób wykonywania swoich operacji od wyższych warstw aplikacji, dzięki temu może zostać podmienione przez dowolne inne DAO korzystające z innych technologii

W moim przykładzie DAO będzie korzystać z Hibernate'a, co pociąga za sobą kilka kolejnych założeń. Ponieważ logika biznesowa musi zostać oddzielona od warstwy DAO, nie można wykorzystywać obiektów domenowych bezpośrednio do zapisu do bazy, w tym nie można ich mapować na tabele w bazie danych. Niezbyt fortunnym było by wstawianie adnotacji @Entity, czy @Column w klasach domenowych. W związku z tym dla każdej klasy domenowej musi istnieć właściwa klasa w warstwie DAO, której pola będą mapowane na kolumny w tabeli bazodanowej. Dla uproszczenia nazwijmy te typy klas odpowiednio DTO i DS. Tak więc DAO będzie przyjmować jako parametry i zwracać jedynie obiekty klas DTO, natomiast operować będzie na obiektach DS jako tych zmapowanych na rekordy w bazie danych.

Chciałbym, aby nasze DAO miało mniej więcej taki interfejs:

GenericDao.java

public interface GenericDao<T extends Identifiable> {

	public T create(T object);
	public T read(Integer id);
	public T update(T object);
	public boolean delete(T object);
}

gdzie T jest pewnym określonym typem rodzaju DTO. Identifiable to prosty interfejs zawierający jedną metodę, jego zastosowanie okaże się później całkiem oczywiste, nie będę więc wyjaśniał.

Identifiable.java

public interface Identifiable {

	public Integer getId();
}

Analogiczny interfejs utworzymy po stronie samego DAO. Teoretycznie można użyć tego samego, co było moim pomysłem na początku, ale po dalszej implementacji doszedłem do wniosku, że oddzielny interfejs pozwoli wykryć wiele błędów na etapie kompilacji.

HibernateIdentifiable.java

public interface HibernateIdentifiable {

	public Integer getId();
}

Do przenoszenia danych pomiędzy obiektami DTO i DS oraz w drugą stronę potrzebny nam będzie dodatkowy komponent:

Assembler.java

public interface Assembler<TDTO extends Identifiable, TDS extends HibernateIdentifiable> {
	
	public void copyDataDs2Dto(TDS ds, TDTO dto);	
	public void copyDataDto2Ds(TDTO dto, TDS ds);
	
	public TDTO ds2Dto(TDS ds);
	public TDS dto2Ds(TDTO dto);	
}

To proste cztery metody. Dwie z nich przepisują pola, a dwie tworzą nowe obiekty. Interfejs umożliwia przenoszenie informacji z obiektów DS do DTO i w drugą stronę. Jak widać tu również mamy parametryzowanie typem, a nawet dwoma. Implementacje tego interfejsu będą musiały niestety być tworzone dla każdej klasy DTO, ale dzięki temu interfejsowi kod za to odpowiedzialny zostanie wydzielony i nie będzie komplikował samego DAO. Oczywiście można pokusić się o napisanie w pełni konfigurowalnej, opartej o Reflection API implementacji, ja jednak uznałem, że to tylko skomplikuje cały proces zamiast go uprościć.

Sama klasa DAO powinna wyglądać mniej więcej tak:

HibernateGenericDao

public class HibernateGenericDao<TDTO extends Identifiable, TDS extends HibernateIdentifiable>
		implements GenericDao<TDTO> {
	
	private SessionFactory sessionFactory;
	private Assembler<TDTO, TDS> assembler;
	private Class<TDS> dsType;
	
	public HibernateGenericDao(Assembler<TDTO, TDS> assembler, Class<TDS> dsType) {
		this.assembler = assembler;
		this.dsType = dsType;
	}
	
	@Override
	public TDTO create(TDTO object) {
		
		checkParameterNotNull(object);
		if (object.getId() != null) {
			throw new IllegalArgumentException(
				"Cannot create object with id already set. Use update() instead.");
		}
		try {
			TDS ds = assembler.dto2Ds(object);			
			getSession().save(ds);
			return assembler.ds2Dto(ds);
		} catch (HibernateException e) {
			return null;
		}
	}

	@Override
	public TDTO read(Integer id) {
		
		checkParameterNotNull(id, "id");
		try {
			TDS ds = readInternal(dsType, id);
			TDTO result = ds == null ? null : assembler.ds2Dto(ds);
			return result;
		} catch (HibernateException e) {
			return null;
		}
	}

	// .. metody update i delete

	protected Session getSession() {		
		return sessionFactory.getCurrentSession();
	}
	
	protected void checkParameterNotNull(Object parameter) {
		checkParameterNotNull(parameter, "");
	}
	protected void checkParameterNotNull(Object parameter, String parameterName) {
		if (parameter == null) {
			throw new IllegalArgumentException(
				"parameter " + parameterName + " cannot be null");
		}
	}
	
	protected <T> T readInternal(Class<T> type, Integer id) {
		Criteria c = getSession().createCriteria(type);
		c.add(Restrictions.idEq(id));
		@SuppressWarnings("unchecked")
		T ds = (T) c.uniqueResult();
		return ds;	
	}
	
	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}
}

To w zasadzie wystarczy, aby tworzyć masowo DAO przystosowane do różnego rodzaju klas. Na przykład tak jak poniżej:

Uzytkownik.java

public class Uzytkownik implements Identifiable {

	private Integer id;
	private String email;
	private String haslo;

	// gettery i settery
}

UzytkownikDs.java

@Entity
@Table(name = "uzytkownicy")
public class UzytkownikDs implements HibernateIdentifiable {
	
	@Id @GeneratedValue(strategy = GenerationType.AUTO)	
	private Integer id;	
	
	@Column
	private String email;
	@Column
	private String haslo;

	// gettery i settery
}

UzytkownikAssembler.java

public class UzytkownikAssembler implements Assembler<Uzytkownik, UzytkownikDs> {

	@Override
	public void copyDataDs2Dto(UzytkownikDs ds, Uzytkownik dto) {
		dto.setEmail(ds.getEmail());
		dto.setHasloHash(ds.getHasloHash());
		dto.setId(ds.getId());		
	}
	@Override
	public void copyDataDto2Ds(Uzytkownik dto, UzytkownikDs ds) {	
		ds.setEmail(dto.getEmail());
		ds.setHasloHash(dto.getHasloHash());
		ds.setId(dto.getId());	
	}
	@Override
	public Uzytkownik ds2Dto(UzytkownikDs ds) {			
		Uzytkownik result = new Uzytkownik();
		copyDataDs2Dto(ds, result);
		return result;
	}
	@Override
	public UzytkownikDs dto2Ds(Uzytkownik dto) {			
		UzytkownikDs result = new UzytkownikDs();
		copyDataDto2Ds(dto, result);		
		return result;
	}
}

Main.java

public class Main {

	public static void main(String[] args) {
		
		UzytkownikAssembler assembler = new UzytkownikAssembler();
		HibernateGenericDao<Uzytkownik, UzytkownikDs> hdao = 
			new HibernateGenericDao<Uzytkownik, UzytkownikDs>(
					assembler, UzytkownikDs.class);
		dao.setSessionFactory(sessionFactory);
		GenericDao<Uzytkownik> dao = hdao;
		
		dao.create(jakisUzytkownik);

	}
}

Ja utworzyłem jeszcze zestaw klas i interfejsów wykorzystujący wzorzec projektowy Fabryka Abstrakcyjna, co pozwoliło mi wydzielić generyczne DAO do oddzielnej biblioteki, a Springowa konfiguracja w XML przykładowego DAO wygląda tak:

hibernate-daos.xml


	
		
			
				
					pl.jedenpies.blog.domain.Uzytkownik
					pl.jedenpies.blog.db.hibernate.dao.factory.HibernateUzytkownikDaoFactory
				
			
		
	

 	
 		pl.jedenpies.blog.domain.Uzytkownik	  
	

Rozwiązanie nie jest w 100% elastyczne. Nie rozwiązuje też wielu problemów, które mogłyby pojawić się w przyszłości w bardzo złożonych, jednak wydaje mi się dla podstawowych rozwiązań jest w pełni wystarczające.

Zostaw komentarz

Treść:
Podpis:
Strona WWW:
Kod:

Weryfikacja antyspamowa