Статьи

Разговорный CRUD в Java EE 6

В этом руководстве будет продемонстрирован шаблон для создания приложений CRUD в JSF и Java EE 6. Хотя это не единственный способ реализации этого механизма, он способствует повторному использованию и может дать вам практически нулевые страницы кода CRUD, требующие только кода представления. Цель состоит в том, чтобы предоставить единую структуру, которая обеспечивает особую возможность быть как без сохранения состояния, так и с диалогом, где нам может потребоваться диалоговая страница редактирования и страница просмотра без состояния. Этот шаблон основан на шаблоне EntityHome, который использовался в JBoss Seam и хорошо переносится на Java EE 6 с CDI. Это то, что я использую все время, чтобы действительно быстро просматривать / редактировать страницы, и, в отличие от большинства автоматических лесов в других платформах, не нуждается в переписывании для запуска в производство.

Запуск в контейнере сервлетов

Если вы используете это в контейнере сервлетов или встроенном Jetty, вам нужно будет внести следующие изменения:

  • Используйте архетип jee6-servlet-sandbox, который доступен только в Knappsack 1.0.5 и выше.
  • Удалите аннотации @Stateless из EntityManagerDao
  • Добавьте ручные транзакции к вызовам методов менеджера сущностей

С сервлетом версию исходного кода для этого проекта можно скачать здесь ( Crud App Maven Project Zip ). Просто распакуйте и наберите mvn jetty: run

CRUD является инициализмом для C reate, R etrieve, U pdate, D elete и обычно применяется к объектным объектам или частям данных в нашем приложении. CRUD, вероятно, выполняет около 80% функций в большинстве систем, где пользователи вводят, обновляют, просматривают и иногда удаляют данные.

Объект-сущность вначале создается и затем сохраняется в базе данных. Позднее пользователь может захотеть просмотреть данные, а затем изменить и обновить данные. Когда объект больше не нужен, данные будут удалены. Это долгосрочный жизненный цикл наших данных. Когда объект сохраняется, он сохраняется с ключом или некоторым ссылочным значением, используемым для уникальной ссылки на этот объект. Как правило, это поле идентификатора, которое часто является числом. Идентификатор присваивается объекту при его сохранении и используется для идентификации объекта, когда мы хотим получить, обновить или удалить объект.

Создайте наше приложение

Мы начнем с создания нового приложения с использованием архетипов Knappsack Maven. Мы будем использовать jee6-sandbox-archetype, поэтому у нас есть данные для игры. Поскольку архетипы Knappsack являются довольно новыми, у вас могут возникнуть проблемы с их поиском в ваших IDE в зависимости от того, когда вы обновлялись из Maven Central, поэтому вот командная строка для создания проекта:

mvn archetype:generate -DarchetypeGroupId=org.fluttercode.knappsack -DarchetypeArtifactId=jee6-sandbox-archetype
-DinteractiveMode=false -DarchetypeVersion=1.0.4 -DgroupId=org.fluttercode.demo -DartifactId=crudapp
-Dpackage=org.fluttercode.demo.crudapp

Вы можете создать приложение и затем импортировать его в IDE по мере необходимости. Мы начнем с создания очень простого bean-компонента в стиле DAO, который будет использоваться для загрузки, сохранения и обновления объектов с помощью диспетчера сущностей JPA. Мы сделаем его сессионным компонентом без сохранения состояния, чтобы он имел транзакционные возможности. Если вы используете версию контейнера сервлета, то смотрите необходимые изменения на боковой панели выше. Загружаемый исходный код использует версию контейнера сервлета, поэтому вы можете запустить его без сервера приложений.

  1. Создайте новый класс с именем EntityManagerDao
  2. Поместите следующий код в боб
    package org.fluttercode.demo.crudapp.bean;

    import javax.ejb.Stateless;
    import javax.inject.Inject;
    import javax.persistence.EntityManager;
    import java.io.Serializable;

    import org.fluttercode.demo.crudapp.qualifier.DataRepository;

    @Stateless
    public class EntityManagerDao implements Serializable {

    @Inject
    @DataRepository
    private EntityManager entityManager;

    public Object updateObject(Object object) {
    return entityManager.merge(object);
    }

    public void createObject(Object object) {
    entityManager.persist(object);
    }

    public void refresh(Object object) {
    entityManager.refresh(object);
    }

    public <T> T find(Class<T> clazz, Long id) {
    return entityManager.find(clazz, id);
    }

    public void deleteObject(Object object) {
    entityManager.remove(object);
    }

    }

    Мы внедряем диалогового менеджера сущностей в компонент и создаем методы для создания, поиска, обновления и удаления сущностей. Аннотация DataRepository является классификатором, который определяется архетипом и используется для доступа к источнику данных приложения по умолчанию.

    Все, что на самом деле делает этот компонент — это обеспечивает поддержку транзакций при вызове наших методов для менеджера сущностей. Это не часть шаблона ключа, это просто механизм, который мы можем использовать для транзакционного сохранения объектов. Хотя этот Dao будет работать для любой сущности JPA, обычной практикой является создание объектов Dao, специфичных для отдельных типов, где вы можете добавлять вспомогательные и дополнительные методы, специфичные для этого типа.

Наш домашний боб

Теперь у нас есть настроенный код поддержки, к главной функции! Мы создадим общий домашний бин, который будет принимать Id, и по запросу загружаем бин сущности для этого Id. Если идентификатор не доступен, мы предполагаем, что должен быть создан новый объект. После загрузки или создания объект будет доступен с наших страниц JSF, где мы можем отображать или редактировать информацию.

  1. Создайте новый класс с именем org.fluttercode.demo.crudapp.bean.HomeBean. Это будет наш общий домашний компонент, который мы используем для загрузки и хранения наших объектов.
  2. Добавьте следующий код, чтобы сделать бин универсальным и дать ему идентификатор и атрибут сущности.
    package org.fluttercode.demo.crudapp.bean;

    import java.io.Serializable;
    import javax.inject.Inject;
    import org.fluttercode.demo.crudapp.model.BaseEntity;

    public class HomeBean<T extends BaseEntity> implements Serializable {

    @Inject
    private EntityManagerDao entityManagerDao;

    private Long id;
    private T instance;
    }

    Наш бин будет использовать введенный EntityManagerDao, который мы только что создали. Класс BaseEntity является базовым классом сущности, реализованным в архетипе, и имеет общий атрибут Id.

  3. Теперь мы хотим добавить метод получения для экземпляра. То, что мы хотим сделать здесь, — это ленивая загрузка или создание компонента, когда это необходимо. Предполагается, что вы не будете запрашивать bean-компонент, пока не будет настроен домашний bean-компонент, т. Е. Идентификатор установлен правильно. Второе предположение состоит в том, что если вы запрашиваете экземпляр без установленного идентификатора, вы фактически создаете новый экземпляр.
  4. Мы реализуем это в методе получения, который вызывает два других метода: один для создания и возврата нового объекта, а другой для его загрузки. Мы также добавим геттеры и сеттеры для идентификатора.
    public T getInstance() {
    if (instance == null) {
    if (id != null) {
    instance = loadInstance();
    } else {
    instance = createInstance();
    }
    }
    return instance;
    }

    public Long getId() {
    return id;
    }
    public void setId(Long id) {
    this.id = id;
    }
  5. Методы создания или загрузки довольно просты, но полагаются на магию отражения для получения общего типа параметра:
    public T loadInstance() {
    return entityManagerDao.find(getClassType(), getId());
    }

    public T createInstance() {
    try {
    return getClassType().newInstance();
    } catch (Exception e) {
    e.printStackTrace();
    }
    return null;
    }

    private Class<T> getClassType() {
    ParameterizedType parameterizedType = (ParameterizedType) getClass()
    .getGenericSuperclass();
    return (Class<T>) parameterizedType.getActualTypeArguments()[0];
    }
  6. Когда мы редактируем нашу сущность, мы хотим либо сохранить изменения, либо отменить их. Для этого мы добавим два метода: сохранение и отмена, а также третий вспомогательный метод isManaged ().
    public boolean isManaged() {
    return getInstance().getId() != null;
    }
    public String save() {
    if (isManaged()) {
    entityManagerDao.updateObject(getInstance());
    } else {
    entityManagerDao.createObject(getInstance());
    }
    conversation.end();
    return "saved";
    }

    public String cancel() {
    conversation.end();
    return "cancelled";
    }

    Мы заканчиваем разговор, когда закончим с данными, когда мы сохраняем или отменяем изменения. Как только разговор закончится, он будет уничтожен в конце запроса.

  7. Последний метод, который мы хотим добавить, называется initConversation, который проверяет, находимся ли мы в длительном диалоге, и запускает его, если это не так. Теоретически мы могли бы использовать этот метод где угодно, но здесь хорошее место, как и любое. Нам также нужно добавить атрибут беседы, в который мы будем вставлять текущую беседу.

     	@Inject
    private Conversation conversation;
    ...
    ...
    ...
    public void initConversation() {
    if (conversation.isTransient()) {
    conversation.begin();
    }
    }

Это завершает кодирование нашего HomeBean-компонента, который мы будем использовать в качестве вспомогательного компонента для страниц CRUD. Теперь давайте попробуем наш новый bean-компонент, реализовав его для нашего класса сущностей Student и создадим страницу для просмотра и редактирования наших студентов.

  1. Начните с создания нового класса с именем org.fluttercode.demo.crudapp.view.StudentHome. Мы добавляем две аннотации уровня класса, одну для именования bean-компонента, чтобы JSF мог получить к нему доступ через выражение EL, а другую для предоставления ему области действия.
    package org.fluttercode.demo.crudapp.view;

    import javax.enterprise.context.ConversationScoped;
    import javax.inject.Named;

    import org.fluttercode.demo.crudapp.bean.HomeBean;
    import org.fluttercode.demo.crudapp.model.Student;

    @Named
    @ConversationScoped
    public class StudentHome extends HomeBean<Student>{

    }

    Мы создаем подкласс HomeBean и присваиваем ему тип класса Student, чтобы указать, какой тип объекта мы хотим обработать. Мы дали этому бину возможности для разговора по причинам, которые мы увидим позже.

  2. На наш взгляд, создайте новую страницу с именем studentView.xhtml со следующим содержанием:
    <?xml version="1.0" encoding="UTF-8"?>
    <ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:fn="http://java.sun.com/jsp/jstl/functions"
    template="/WEB-INF/templates/template.xhtml">
    <ui:define name="content">
    <f:metadata>
    <f:viewParam name="id" value="#{studentHome.id}" />
    </f:metadata>
    <h:form>
    <h:outputText value="ID" styleClass="caption" />
    <h:outputText value="#{studentHome.instance.id}" styleClass="value" />

    <h:outputText value="First Name" styleClass="caption" />
    <h:outputText value="#{studentHome.instance.firstName}"
    styleClass="value" />

    <h:outputText value="Last Name" styleClass="caption" />
    <h:outputText value="#{studentHome.instance.lastName}"
    styleClass="value" />

    <h:outputText value="GPA" styleClass="caption" />
    <h:outputText value="#{studentHome.instance.gpa}" styleClass="value" />

    <h:link outcome="studentEdit.jsf" includeViewParams="true"
    value="Edit #{studentHome.instance.name}" style="margin-right : 24px" />
    <h:link outcome="studentEdit.jsf" value="Create Student" />
    </h:form>
    </ui:define>
    </ui:composition>

    Все, что связано с тегом f: metadata, является образцом, который включает в себя перетаскивание шаблона на страницу. Тег f: metadata указывает JSF взять параметр с именем id и поместить его в атрибут studentHome.id. Это делается до того, как страница будет отображена как часть нашей первоначальной настройки для bean-компонента, чтобы мы знали, какую сущность загрузить.

    В коде представления мы связываем наши компоненты вывода текста с атрибутами в экземпляре в домашнем компоненте ученика. Мы также разработали стили компонентов, чтобы мы могли изменять отображение с помощью стилей CSS.
    Внизу мы добавили пару ссылок, используя новый компонент JSF 2.0 h: link, на еще не написанную страницу JSF с именем studentEdit.xhtml. В первой ссылке мы заставляем JSF автоматически передавать параметры представления, которые в этом случае являются значением идентификатора студента. Наша вторая ссылка — создать нового студента, что означает, что мы вызываем ту же страницу studentEdit.xhtml, не передавая идентификатор в качестве параметра, что мы делаем, оставляя атрибуту includeViewParams значение false.

    Обратите внимание, что тег h: form не обязателен, но облегчит следующий шаг и позволит нам добавить элементы управления формы в эту форму, если мы захотим.

  3. Если вы используете JBoss Developer Tools или Netbeans, вы сможете увидеть атрибуты Student в автозаполнении (очень круто). Если вы откроете эту страницу в браузере с идентификатором ( http: // localhost: 8080 / crudapp / studentView.jsf? Id = 3 ), вы увидите следующее лицо:

    Скриншот CRUD Pattern View

    Мы изменили два стиля CSS архетипа в файле screen.css следующим образом:

    .caption {
    float: left;
    width: 100px;
    padding-top : 2px;
    }

    .value {
    display : block;
    margin-bottom : 8px;
    margin-right: 8px;
    }

Как вы можете видеть, мы можем отобразить данные об ученике для данного идентификатора. Страница для редактирования этого человека очень похожа, настолько похожа, что мы начнем с копирования страницы просмотра ученика, внесения трехстрочных изменений и добавления события страницы.

  1. Скопируйте файл studentView.xhtml и вставьте его в ту же папку с именем studentEdit.xhtml. Откройте его и внесите следующие изменения (выделено в источнике):

    • Измените компоненты outputText, связанные с атрибутами, на компоненты inputText.
    • Добавьте событие preRenderView в тег метаданных f :.
    • Установите визуализированный атрибут на метке и значении идентификатора, поскольку у новых сущностей нет значения для отображения
      <ui:define name="content">
      <f:metadata>
      <f:viewParam name="id" value="#{studentHome.id}" />
      <f:event type="preRenderView"
      listener="#{studentHome.initConversation}" />
      </f:metadata>
      <h:form>
      <h:outputText value="ID" styleClass="caption" rendered="#{studentHome.managed}"/>
      <h:outputText value="#{studentHome.instance.id}" styleClass="value" rendered="#{studentHome.managed}"/>

      <h:outputText value="First Name" styleClass="caption" />
      <h:inputText value="#{studentHome.instance.firstName}" styleClass="value" />

      <h:outputText value="Last Name" styleClass="caption" />
      <h:inputText value="#{studentHome.instance.lastName}" styleClass="value" />

      <h:outputText value="GPA" styleClass="caption" />
      <h:inputText value="#{studentHome.instance.gpa}" styleClass="value" />

      <h:commandButton value="Save" action="#{studentHome.save}" style="margin-right : 8px" />
      <h:commandButton value="Cancel" action="#{studentHome.cancel}" immediate="true" style="margin-right : 8px" />
      </h:form>
      </ui:define>

    Если вы запустите эту страницу сейчас ( http: // localhost: 8080 / crudapp / studentEdit.jsf? Id = 3 ), вы увидите, что у нас есть редактор, заполненный значениями атрибутов, которые можно изменить.

    Скриншот CRUD Pattern Edit

Это было довольно легко! Причина в том, что мы повторно использовали компонент StudentHome на нашей странице редактирования, поскольку он выполняет ту же роль в предоставлении запрошенной сущности на основе значения идентификатора компонента. Нам не нужно сообщать домашнему бину, что он используется где-то еще, потому что страница тянет боб на страницу.

Разговорный или нет?

Важно отметить, что на странице просмотра мы не вызываем метод для инициализации диалога. Разговорные компоненты, которые не участвуют в длительном диалоге, сокращаются до объема запроса, поскольку контекст диалога уничтожается в конце запроса, если он не помечен как длительный. Это означает, что наша страница просмотра не имеет состояния, так как bean-компонент StudentHome, используемый на этой странице, по сути является областью запроса. Это на самом деле хорошо, так как, когда мы нажимаем кнопку обновления, страница восстанавливается с нуля вместо использования более старой версии данных, хранящихся в разговоре.

С другой стороны, страница редактирования вызывает метод initConverstion, который запускает диалог. Это означает, что страница редактирования является диалоговой, что хорошо, потому что она переносит состояние из одного запроса в другой, не полагаясь на нашу страницу для поддержки этого состояния или необходимости каждый раз перезагружать ее. Без разговора нам пришлось бы вручную передавать значение идентификатора из одного запроса в другой, и, возможно, другие значения, которые являются частью сущности, но не отредактированы на странице. Например, если бы у нас был атрибут addDate, мы могли бы захотеть его отобразить, но не редактировать, но без разговоров нам придется прибегнуть либо к тому, чтобы поместить его в скрытое поле в форме, либо перезагрузить объект, чтобы получить значение при обратной передаче. Иначе как мы можем получить правильное значение, когда сохраняем отредактированный объект в базе данных?

Эта двойственная природа области диалога является очень полезной функцией CDI (и Seam перед ним), поскольку она позволяет одному компоненту действовать в двух разных областях, в зависимости от того, является ли диалог продолжительным или нет, что идеально для просмотра и редактирования страниц.

Это простой дизайн, который можно повторно использовать везде, где вам нужно отредактировать объект с небольшими изменениями кода или без изменений.

Вы можете скачать версию демо-проекта с сервлетом здесь ( Crud App Maven Project Zip ). Просто распакуйте его и запустите mvn jetty: run

Дополнительные очки

Вот некоторые дополнительные моменты, которые не являются основной частью демонстрации, а конкретизируют немного больше. Все эти изменения включены в проект Maven, который можно загрузить.

Нужен ли нам Entity Manager DAO?

Здесь мы использовали простой DAO для переноса вызовов в EntityManager. Нет никаких причин, по которым вы не могли бы сделать ваш объект Entity Home EJB-компонентом, внедрить менеджер сущностей и сохранить сущности напрямую. С отдельным менеджером сущностей DAO это делает написание кода для контейнеров сервлетов более чистым, поскольку явные транзакции связаны в DAO. Кроме того, можно написать один компонент DAO для серверов Java EE 6, а другой — для контейнеров сервлетов и переключаться между ними с помощью @Alternative.

Удаление является частью CRUD

Я оставил удаление в первую очередь потому, что это довольно просто, и внимание было сосредоточено на извлечении и обновлении фрагментов, что зачастую сложнее. Чтобы реализовать удаление, мы добавляем метод в HomeBean, чтобы завершить разговор и удалить элемент.

public String delete() {
entityManagerDao.deleteObject(getInstance());
conversation.end();
return "deleted";
}

Теперь нам просто нужно добавить кнопку для вызова метода удаления со страницы редактирования.

<h:commandButton value="Delete" action="#{studentHome.delete}"/>

Когда мы нажимаем кнопку, мы удаляем объект и возвращаем значение действия «удалено», для которого в следующем разделе мы добавим навигацию на домашнюю страницу.

Найти свой путь вокруг

Мы не включили здесь навигацию, поэтому, когда вы сохраняете или отменяете изменения, вы остаетесь на той же странице. До сих пор я оставил это, потому что это не является ключевым для CRUDness урока. Тем не менее, мы можем легко добавить навигацию вface-config.xml.

<navigation-rule>
<from-view-id>/studentEdit.xhtml</from-view-id>
<navigation-case>
<from-outcome>saved</from-outcome>
<to-view-id>/studentView.xhtml</to-view-id>
<redirect include-view-params="true" />
</navigation-case>

<navigation-case>
<from-outcome>cancelled</from-outcome>
<if>#{studentHome.managed}</if>
<to-view-id>/studentView.xhtml</to-view-id>
<redirect include-view-params="true" />
</navigation-case>

<navigation-case>
<from-outcome>cancelled</from-outcome>
<if>#{!studentHome.managed}</if>
<to-view-id>/home.xhtml</to-view-id>
<redirect include-view-params="true" />
</navigation-case>

<navigation-case>
<from-outcome>deleted</from-outcome>
<to-view-id>/home.xhtml</to-view-id>
</navigation-case>

</navigation-rule>

Обратите внимание, что когда мы отменяем изменения, мы переходим на страницу просмотра или домашнюю страницу в зависимости от того, существует ли объект в базе данных.

Лучшие Дома

Давайте также добавим лучшую домашнюю страницу, чтобы вы могли видеть и выбирать разных студентов. Добавьте следующий метод в класс DataFactory.java, который поставляется с архетипом.

@Produces
@Named("students")
public List<Student> getStudents() {
List<Student> students = entityManager.createQuery(
"select s from Student s").setMaxResults(20)
.getResultList();
return students;
}

Откройте страницу home.xhtml и измените содержимое на:

<?xml version="1.0" encoding="UTF-8"?>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:fn="http://java.sun.com/jsp/jstl/functions"
template="/WEB-INF/templates/template.xhtml">
<ui:define name="content">
<h1>Courses</h1>
<h:form>
<h:dataTable value="#{students}" var="v_student"
styleClass="dataTable" rowClasses="odd,even">
<h:column>
<f:facet name="header">Id
</f:facet>
#{v_student.id}
</h:column>

<h:column>
<f:facet name="header">Name
</f:facet>
<h:link outcome="studentView.jsf?id=#{v_student.id}"
value="#{v_student.name}" />
</h:column>

</h:dataTable>
</h:form>
<h:link outcome="studentEdit.jsf" value="Create Student" />

</ui:define>
</ui:composition>

Это перечислит всех студентов и позволит вам редактировать их или добавлять новых. Источник проекта, который можно загрузить ( Crud App Maven Project Zip ), включает эти изменения, а также сообщения об ошибках проверки элементов управления.

С http://www.andygibson.net/blog/tutorial/pattern-for-conversational-crud-in-java-ee-6/