Не повторяйте DAO!

Создание обобщенного типизированного DAO с Hibernate и Spring AOP

С появлением шаблонов классов (generics) в Java™ 5 стала реальной идея реализации обобщенного типизированного Data Access Object (DAO). В данной статье системный разработчик Пэр Мелквист представляет обобщенный класс реализации DAO, основанный на Hibernate. Затем он продемонстрирует, как использовать Spring AOP с целью добавления к классу типизированного интерфейса для выполнения запросов.

Пэр Мелквист, системный разработчик, внештатный

Пэр Мелквист (Per Mellqvist) работает системным разработчиком в Стокгольме, Швеция. Он использует все аспекты Java и исследует любую среду или инструментальное средство, которые хорошо выполняют свою работу.



12.05.2006

Для большинства разработчиков написание одного и того же кода для каждого DAO в системе стало привычкой. Хотя можно было бы назвать повторение "загрязнением кода", большинство из нас научились жить с этим. Кроме того, существуют обходные пути. Вы можете использовать многочисленные ORM-средства для удаления повторений кода. Например, при помощи Hibernate вы может просто использовать операции сессии непосредственно для всех ваших персистентных доменных объектов. Обратной стороной такого подхода является потеря типизации.

Зачем вам нужен типизированный интерфейс для вашего кода доступа к данным? Я бы ответил так: он уменьшает ошибки программирования и увеличивает производительность при использовании с современными IDE-средствами. Прежде всего, типизированный интерфейс четко указывает, какие персистентные доменные объекты доступны. Во-вторых, он устраняет необходимость использования подверженных ошибкам приведений типа (проблема более типична для операций запроса, чем для CRUD). Наконец, он поддерживает функцию автозавершения, имеющуюся в большинстве современных IDE. Использование автозавершения является быстрым способом вспомнить, какие запросы доступны для определенного класса домена.

В данной статье я покажу вам, как избежать повторения DAO-кода, одновременно пользуясь преимуществами типизированного интерфейса. Фактически, все, что вам нужно написать для каждого нового DAO - это Hibernate-файл отображения, традиционный Java-интерфейс и 10 строк в вашем конфигурационном файле Spring.

Реализация DAO

Шаблон DAO должен быть хорошо известен любому разработчику корпоративных Java-приложений. Реализации шаблона значительно отличаются между собой, однако давайте проясним положения, лежащие в основе реализации DAO, представленной в данной статье:

  • Весь доступ к базе данных в системе производится через DAO для инкапсуляции.
  • Каждый экземпляр DAO отвечает за один первичный доменный объект или сущность. Если доменный объект имеет независимый цикл жизни, он должен иметь свой собственный DAO.
  • DAO отвечает за операции создания, чтения (по первичному ключу), обновления и удаления (то есть, CRUD (create, read, update, delete)) доменного объекта.
  • DAO может разрешать запросы, основанные на критерии, отличном от первичного ключа. Я ссылаюсь на такие методы как finder или finders. Метод finder обычно возвращает коллекцию доменных объектов, за которые отвечает DAO.
  • DAO не занимается обработкой транзакций, сессий или соединений. Это делается вне DAO для обеспечения гибкости.

Обобщенный интерфейс DAO

Основой обобщенного DAO являются его операции CRUD. Методы для обобщенного DAO определяет следующий интерфейс:

Листинг 1. Обобщенный интерфейс 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

Наконец, в конфигурации 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>

Готовый обобщенный DAO

Я еще не закончил, но то, что имеется, уже определенно можно использовать. В листинге 4 вы можете увидеть пример использования обобщенного DAO в том виде, какой он имеет на данный момент времени:

Листинг 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.


Внедрения Spring AOP

Вы можете использовать внедрения (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, выполняется только один раз.


Расширение GenericDAO

Интерфейс для каждого DAO, конечно же, основан на интерфейсе GenericDao. Мне просто нужно адаптировать интерфейс к конкретному доменному классу и расширить его, включив мои методы finder. В листинге 6 вы можете увидеть пример интерфейса GenericDao, расширенного для конкретной задачи:

Листинг 6. Интерфейс PersonDao
public interface PersonDao extends GenericDao<Person, Long> {
    List<Person> findByName(String name);
}

Очевидно, что назначением метода, определенного в листинге 6, является поиск Person по имени. Необходимый Java-код реализации является полностью обобщенным кодом, не требующим каких-либо изменений при добавлении дополнительных DAO.

Конфигурирование PersonDao

Поскольку конфигурация 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

Используя 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 для любого доменного объекта. Этими тремя простыми шагами являются:

  1. Определить интерфейс, расширяющий GenericDao и содержащий все необходимые вам методы finder.
  2. Добавить именованный запрос для каждого метода finder в файл отображения hbm.xml для каждого доменного объекта.
  3. Добавить 10-строчный конфигурационный файл Spring для DAO.

Я завершаю обсуждение рассмотрением кода (записываемого только один раз!), который выполняет мои методы finder.


Повторно используемый DAO-класс

Используемые 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);
    }
}

Метод executeFinder()

Единственным методом, отсутствующим в приведенной в листинге 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-класса обратитесь к разделу "Ресурсы".

Благодарности

Концепция одного обобщенного типизированного DAO возникла после появления шаблонов классов (generics) в языке программирования Java. Я кратко обсуждал возможность реализации обобщенного DAO с Доном Смитом (Don Smith) на JavaOne 2004. Реализация DAO-класса, используемая в данной статье, служит примером; существуют и другие реализации. Например, Кристиан Бауэр (Christian Bauer) опубликовал реализацию с CRUD-операциями и поиску по критериям. Эрик Бурке (Eric Burke) тоже работал в данной области. Я должен выразить благодарность Кристиану за просмотр моей первой попытки написания обобщенного типизированного DAO и за предложенные улучшения. Наконец, я благодарю Рамниваса Ладдада (Ramnivas Laddad) за бесценную помощь в рецензировании этой статьи.


Загрузка

ОписаниеИмяРазмер
Full source codej-genericdao.zip---

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=157445
ArticleTitle=Не повторяйте DAO!
publish-date=05122006