Не повторяйте DAO!Источник: IBM developerWorks Россия Пэр Мелквист
Для большинства разработчиков написание одного и того же кода для каждого DAO в системе стало привычкой. Хотя можно было бы назвать повторение "загрязнением кода", большинство из нас научились жить с этим. Кроме того, существуют обходные пути. Вы можете использовать многочисленные ORM-средства для удаления повторений кода. Например, при помощи Hibernate вы может просто использовать операции сессии непосредственно для всех ваших персистентных доменных объектов. Обратной стороной такого подхода является потеря типизации. Зачем вам нужен типизированный интерфейс для вашего кода доступа к данным? Я бы ответил так: он уменьшает ошибки программирования и увеличивает производительность при использовании с современными IDE-средствами. Прежде всего, типизированный интерфейс четко указывает, какие персистентные доменные объекты доступны. Во-вторых, он устраняет необходимость использования подверженных ошибкам приведений типа (проблема более типична для операций запроса, чем для CRUD). Наконец, он поддерживает функцию автозавершения, имеющуюся в большинстве современных IDE. Использование автозавершения является быстрым способом вспомнить, какие запросы доступны для определенного класса домена. В данной статье я покажу вам, как избежать повторения DAO-кода, одновременно пользуясь преимуществами типизированного интерфейса. Фактически, все, что вам нужно написать для каждого нового DAO - это Hibernate-файл отображения, традиционный Java-интерфейс и 10 строк в вашем конфигурационном файле Spring. Реализация DAOШаблон DAO должен быть хорошо известен любому разработчику корпоративных Java-приложений. Реализации шаблона значительно отличаются между собой, однако давайте проясним положения, лежащие в основе реализации DAO, представленной в данной статье:
|
public interface GenericDao <T, PK extends Serializable> { /** Сохранить объект newInstance в базе данных */ PK create(T newInstance); /** Извлечь объект, предварительно сохраненный в базе данных, используя * указанный id в качестве первичного ключа */ T read(PK id); /** Сохранить изменения, сделанные в объекте. */ void update(T transientObject); /** Удалить объект из базы данных */ void delete(T persistentObject); } |
Реализация приведенного в листинге 1 интерфейса с Hibernate тривиальна (листинг 2). Просто вызываются соответствующие Hibernate-методы и добавляется приведение типов. Spring занимается управлением сессиями и транзакциями. Конечно, я предполагаю, что эти функции корректно установлены. Эта тема хорошо рассмотрена в справочных руководствах по системам Hibernate и Spring.
Листинг 2. Первая реализация обобщенного DAO
public class GenericDaoHibernateImpl <T, PK extends Serializable> implements GenericDao<T, PK>, FinderExecutor { private Class<T> type; public GenericDaoHibernateImpl(Class<T> type) { this.type = type; } public PK create(T o) { return (PK) getSession().save(o); } public T read(PK id) { return (T) getSession().get(type, id); } public void update(T o) { getSession().update(o); } public void delete(T o) { getSession().delete(o); } // Не показаны реализации getSession() и setSessionFactory() } |
Наконец, в конфигурации Spring я создаю экземпляр GenericDaoHibernateImpl
. Конструктору GenericDaoHibernateImpl
нужно указать, за какой доменный класс будет отвечать экземпляр DAO. Это необходимо для того, чтобы Hibernate знал во время исполнения, каким типом объекта управляет DAO. В листинге 3 я передаю доменный класс Person
из примера приложения в конструктор и устанавливаю предварительно настроенную фабрику Hibernate-сессий в качестве параметра для созданного экземпляра DAO:
Листинг 3. Конфигурирование DAO
<bean id="personDao" class="genericdao.impl.GenericDaoHibernateImpl"> <constructor-arg> <value>genericdaotest.domain.Person</value> </constructor-arg> <property name="sessionFactory"> <ref bean="sessionFactory"/> </property> </bean> |
Я еще не закончил, но то, что имеется, уже определенно можно использовать. В листинге 4 вы можете увидеть пример использования обобщенного DAO в том виде, какой он имеет на данный момент времени:
public void someMethodCreatingAPerson() { ... GenericDao dao = (GenericDao) beanFactory.getBean("personDao"); // Это обычно нужно внедрять Person p = new Person("Per", 90); dao.create(p); } |
На данный момент времени у меня есть обобщенный DAO, способный выполнять типизированный CRUD-операции. Абсолютно обоснованно было бы создать подкласс GenericDaoHibernateImpl
для добавления возможности выполнять запросы для каждого доменного объекта. Однако, поскольку целью данной статьи является демонстрация того, как можно это сделать без явного Java-кода для каждого запроса, я буду использовать два дополнительных инструмента для ввода запросов к DAO, а именно, именованные запросы Spring AOP и Hibernate.
Вы можете использовать внедрения (introductions) в Spring AOP для добавления функциональности в существующий объект, заключив объект в прокси-объект, определив новые интерфейсы, которые он должен реализовать, и передав все ранее неподдерживаемые методы одному обработчику. В моей реализации DAO я использую внедрения для добавления нескольких методов finder в существующий обобщенный DAO-класс. Поскольку методы finder специфичны для каждого доменного объекта, они применяются к типизированным интерфейсам обобщенного DAO.
В листинге 5 приведена используемая конфигурация Spring:
Листинг 5. Конфигурация Spring для FinderIntroductionAdvisor
<bean id="finderIntroductionAdvisor" class="genericdao.impl.FinderIntroductionAdvisor"/> <bean id="abstractDaoTarget" class="genericdao.impl.GenericDaoHibernateImpl" abstract="true"> <property name="sessionFactory"> <ref bean="sessionFactory"/> </property> </bean> <bean id="abstractDao" class="org.springframework.aop.framework.ProxyFactoryBean" abstract="true"> <property name="interceptorNames"> <list> <value>finderIntroductionAdvisor</value> </list> </property> </bean> |
В конфигурационном файле, приведенном в листинге 5, я определил три Spring-компонента. Первый компонент, FinderIntroductionAdvisor, обрабатывает все методы, включенные в DAO и недоступные в классе GenericDaoHibernateImpl
. Через некоторое время я рассмотрю компонент Advisor подробно.
Второй компонент является "абстрактным". В Spring это означает, что компонент можно использовать повторно в определениях других компонентов, но нельзя создать его экземпляр. В отличие от абстрактного свойства определение компонента просто указывает, что я хочу получить экземпляр GenericDaoHibernateImpl
, и ему необходима ссылка на SessionFactory
. Обратите внимание на то, что класс GenericDaoHibernateImpl
определяет только один конструктор, который принимает в качестве аргумента доменный класс. Поскольку это определение компонента является абстрактным, я могу использовать его повторно много раз и устанавливать аргумент конструктора в подходящий доменный класс.
Наконец, третий и самый интересный компонент заключает унифицированный экземпляр GenericDaoHibernateImpl
в прокси, предоставляя ему возможность выполнять методы finder
. Это определение компонента тоже абстрактно и не указывает интерфейс, который мне хотелось бы ввести в мой унифицированный DAO. Интерфейс будет различным для каждого конкретного экземпляра. Обратите внимание на то, что полная конфигурация, показанная в листинге 5, выполняется только один раз.
Интерфейс для каждого DAO, конечно же, основан на интерфейсе GenericDao
. Мне просто нужно адаптировать интерфейс к конкретному доменному классу и расширить его, включив мои методы finder
. В листинге 6 вы можете увидеть пример интерфейса GenericDao
, расширенного для конкретной задачи:
Листинг 6. Интерфейс PersonDao
public interface PersonDao extends GenericDao<Person, Long> { List<Person> findByName(String name); } |
Очевидно, что назначением метода, определенного в листинге 6, является поиск Person
по имени. Необходимый Java-код реализации является полностью обобщенным кодом, не требующим каких-либо изменений при добавлении дополнительных DAO.
Поскольку конфигурация Spring основана на определенных ранее "абстрактных" компонентах, она является довольно компактной. Я должен указать, за какой доменный класс отвечает мой DAO, а также указать Spring, какой интерфейс DAO должен реализовать (некоторые методы непосредственно, а некоторые - при помощи внедрений). В листинге 7 показан конфигурационный файл Spring для PersonDAO
:
Листинг 7. Конфигурация Spring для PersonDao
<bean id="personDao" parent="abstractDao"> <property name="proxyInterfaces"> <value>genericdaotest.dao.PersonDao</value> </property> <property name="target"> <bean parent="abstractDaoTarget"> <constructor-arg> <value>genericdaotest.domain.Person</value> </constructor-arg> </bean> </property> </bean> |
В листинге 8 приведена обновленная версия использования DAO:
Листинг 8. Использование типизированного интерфейса
public void someMethodCreatingAPerson() { ... PersonDao dao = (PersonDao) beanFactory.getBean("personDao"); // Это обычно нужно внедрять Person p = new Person("Per", 90); dao.create(p); List<Person> result = dao.findByName("Per"); // Исключительная ситуация времени исполнения } |
Хотя код, приведенный в листинге 8, является корректным способом использования типизированного интерфейса PersonDao
, реализация DAO не закончена. Вызов findByName()
генерирует исключительную ситуацию времени исполнения. Проблема заключается в том, что я еще не реализовал запрос, необходимый для вызова findByName()
. Все, что осталось сделать, - указать запрос. Для этого я использую именованный запрос Hibernate.
Используя Hibernate, вы можете определить HQL-запрос в файле отображения Hibernate (hbm.xml) и дать ему имя. Вы можете использовать этот запрос позже в вашем Java-коде, просто ссылаясь на данное имя. Одним из преимуществ этого подхода является возможность редактировать запросы во время развертывания без изменения исходного кода. Как вы увидите позже, еще одним преимуществом является возможность реализовать "полный" DAO без записи какого-либо нового Java-кода реализации. В листинге 9 приведен пример файла отображения с именованным запросом:
Листинг 9. Файл отображения Hibernate с именованным запросом
<hibernate-mapping package="genericdaotest.domain"> <class name="Person"> <id name="id"> <generator class="native"/> </id> <property name="name" /> <property name="weight" /> </class> <query name="Person.findByName"> <![CDATA[select p from Person p where p.name = ? ]]> </query> </hibernate-mapping> |
Листинг 9 определяет Hibernate-отображение доменного класса Person
с двумя свойствами: name
и weight
. Person
- это простой POJO с упомянутыми свойствами. Файл также содержит запрос, который находит все экземпляры Person
в базе данных, чье свойство name
равно указанному параметру. Hibernate не предоставляет настоящей функциональности пространства имен для именованных запросов. Для целей данного обсуждения я использую префикс во всех названиях запросов в виде краткого (не полного) названия доменного класса. В реальной ситуации, возможно, более хорошей идеей являлось бы использование полного названия класса, включая название пакета.
Вы увидели все шаги, которые необходимо выполнить в процессе создания и конфигурирования нового DAO для любого доменного объекта. Этими тремя простыми шагами являются:
GenericDao
и содержащий все необходимые вам методы finder
.finder
в файл отображения hbm.xml для каждого доменного объекта.Я завершаю обсуждение рассмотрением кода (записываемого только один раз!), который выполняет мои методы finder
.
Используемые Spring-методы advisor
и interceptor
являются тривиальными, а их работа заключается, фактически, в обращении назад к GenericDaoHibernateImplClass
. Все вызовы, название метода которых начинается с find, передаются в DAO и один метод executeFinder()
.
Листинг 10. Реализация FinderIntroductionAdvisor
public class FinderIntroductionAdvisor extends DefaultIntroductionAdvisor { public FinderIntroductionAdvisor() { super(new FinderIntroductionInterceptor()); } } public class FinderIntroductionInterceptor implements IntroductionInterceptor { public Object invoke(MethodInvocation methodInvocation) throws Throwable { FinderExecutor genericDao = (FinderExecutor) methodInvocation.getThis(); String methodName = methodInvocation.getMethod().getName(); if (methodName.startsWith("find")) { Object[] arguments = methodInvocation.getArguments(); return genericDao.executeFinder(methodInvocation.getMethod(), arguments); } else { return methodInvocation.proceed(); } } public boolean implementsInterface(Class intf) { return intf.isInterface() && FinderExecutor.class.isAssignableFrom(intf); } } |
Единственным методом, отсутствующим в приведенной в листинге 10 реализации, является метод executeFinder()
. Этот код ищет название вызванного класса и метода и сравнивает их с именем Hibernate-запроса, используя соглашения из конфигурации. Вы можете также использовать FinderNamingStrategy
для разрешения других способов именования запросов. Реализация по умолчанию ищет запрос с именем ClassName.methodName
, где ClassName
- это краткое имя без пакетов. В листинге 11 приведено завершение моей реализации обобщенного типизированного DAO:
Листинг 11. Реализация executeFinder()
public List<T> executeFinder(Method method, final Object[] queryArgs) { final String queryName = queryNameFromMethod(method); final Query namedQuery = getSession().getNamedQuery(queryName); String[] namedParameters = namedQuery.getNamedParameters(); for(int i = 0; i < queryArgs.length; i++) { Object arg = queryArgs[i]; Type argType = namedQuery.setParameter(i, arg); } return (List<T>) namedQuery.list(); } public String queryNameFromMethod(Method finderMethod) { return type.getSimpleName() + "." + finderMethod.getName(); } |
До Java 5 язык не поддерживал написание кода, который был бы и обобщенным и типизированным; вы должны были выбирать одно из двух. В данной статье вы увидели только один пример использования шаблонов классов Java 5 (generics) в комбинации с такими инструментальными средствами как Spring и Hibernate (и AOP) для улучшения производительности. Обобщенный типизированный DAO-класс написать относительно легко. Все, что вам надо - это один интерфейс, несколько именованных запросов и 10-строчное добавление в файл конфигурации Spring. В результате вы значительно уменьшите вероятность ошибок, а также сэкономите время.
Почти весь исходный код, приведенный в данной статье, является повторно используемым. Хотя ваши DAO-классы могут содержать типы запросов или операций, не реализованные здесь (например, групповые операции), вы должны суметь реализовать, по крайней мере, некоторые из них при помощи продемонстрированной мною методики.
Концепция одного обобщенного типизированного DAO возникла после появления шаблонов классов (generics) в языке программирования Java. Я кратко обсуждал возможность реализации обобщенного DAO с Доном Смитом (Don Smith) на JavaOne 2004. Реализация DAO-класса, используемая в данной статье, служит примером; существуют и другие реализации. Например, Кристиан Бауэр (Christian Bauer) опубликовал реализацию с CRUD-операциями и поиску по критериям. Эрик Бурке (Eric Burke) тоже работал в данной области. Я должен выразить благодарность Кристиану за просмотр моей первой попытки написания обобщенного типизированного DAO и за предложенные улучшения. Наконец, я благодарю Рамниваса Ладдада (Ramnivas Laddad) за бесценную помощь в рецензировании этой статьи.