Java Platform, Enterprise Edition (Java EE) 8
Учебник по Java EE

Назад Вперёд Содержание

Приложение order

Приложение order представляет собой простое приложение для инвентаризации и заказа товаров для ведения каталога деталей и размещения подробного заказа этих деталей. В приложении есть сущности, которые представляют детали, поставщиков, заказы и их позиции. Доступ к этим сущностям осуществляется с помощью сессионного компонента с сохранением состояния, который содержит бизнес-логику приложения. Простой сессионный компонент-синглтон инициализирует сущности при развёртывании приложения. Веб-приложение Facelets управляет данными и отображает содержимое каталога.

Информация, содержащаяся в заказе, может быть разделена на элементы. Какой номер заказа? Какие детали включены в заказ? Из каких деталей состоит эта деталь? Кто производит деталь? Каковы технические характеристики детали? Есть ли какие-либо схемы для детали? Приложение order является упрощённой версией системы заказов, которая имеет ответы на все эти вопросы.

Приложение order состоит из одного модуля WAR, который включает в себя классы Enterprise-бинов, сущностей, вспомогательные классы, а также XHTML Facelets.

Схема базы данных в Derby для order показана на рис. 41-1.

Рис. 41-1. Схема базы данных для приложения order

Диаграмма, показывающая схему базы данных для приложения order

Примечание:

В этой диаграмме для простоты префикс PERSISTENCE_ORDER_ опущен в именах таблиц.

Взаимоотношения сущностей в приложении order

Приложение order демонстрирует несколько типов отношений сущностей: ссылки на себя, один-к-одному, один-ко-многим, многие-к-одному и однонаправленные.

Здесь рассматриваются следующие темы:

Отношения ссылки на себя

Относящиеся ссылки на себя возникают между полями в одной и той же сущности. Part имеет поле bomPart, которое имеет отношение один-ко-многим с полем parts, которое также находится в Part. То есть сложная деталь может состоять из множества составляющих её более простых деталей.

Первичный ключ для Part является составным первичным ключом, комбинацией полей partNumber и revision. Этот ключ сопоставляется со столбцами PARTNUMBER и REVISION в таблице PERSISTENCE_ORDER_PART:

...
@ManyToOne
@JoinColumns({
    @JoinColumn(name="BOMPARTNUMBER", referencedColumnName="PARTNUMBER"),
    @JoinColumn(name="BOMREVISION", referencedColumnName="REVISION")
})
public Part getBomPart() {
    return bomPart;
}
...
@OneToMany(mappedBy="bomPart")
public Collection<Part> getParts() {
    return parts;
}
...

Отношения один-к-одному

Part имеет поле vendorPart, которое связано отношением один-к-одному с полем part у VendorPart. То есть каждая деталь имеет ровно одну соответствующую ей деталь поставщика, и наоборот.

Вот отображение отношения в Part:

@OneToOne(mappedBy="part")
public VendorPart getVendorPart() {
    return vendorPart;
}

Вот отображение отношения в VendorPart:

@OneToOne
@JoinColumns({
    @JoinColumn(name="PARTNUMBER", referencedColumnName="PARTNUMBER"),
    @JoinColumn(name="PARTREVISION", referencedColumnName="REVISION")
})
public Part getPart() {
    return part;
}

Обратите внимание: поскольку Part использует составной первичный ключ, аннотация @JoinColumns используется для сопоставления столбцов в таблице PERSISTENCE_ORDER_VENDOR_PART со столбцами в PERSISTENCE_ORDER_PART. Столбец PERSISTENCE_ORDER_VENDOR_PART таблицы PARTREVISION ссылается на столбец PERSISTENCE_ORDER_PART таблицы REVISION.

Отношения один-ко-многим, сопоставленные с перекрывающимися первичными и внешними ключами

CustomerOrder имеет поле lineItems, которое связано отношением один-ко-многим с полем LineItem из customerOrder. То есть каждый заказ имеет одну или несколько позиций.

LineItem использует составной первичный ключ, который состоит из полей orderId и itemId. Этот составной первичный ключ сопоставляется со столбцами ORDERID и ITEMID в таблице PERSISTENCE_ORDER_LINEITEM. ORDERID — это внешний ключ для столбца ORDERID в таблице PERSISTENCE_ORDER_CUSTOMERORDER. Это означает, что столбец ORDERID отображается дважды: один раз как поле первичного ключа orderId и второй раз в качестве поля отношения order.

Вот отображение отношений в CustomerOrder:

@OneToMany(cascade=ALL, mappedBy="customerOrder")
public Collection<LineItem> getLineItems() {
    return lineItems;
}

Вот отображение отношений в LineItem:

@Id
@ManyToOne
@JoinColumn(name="ORDERID")
public CustomerOrder getCustomerOrder() {
    return customerOrder;
}

Однонаправленные отношения

LineItem имеет поле vendorPart, которое связано однонаправленным отношением многие-к-одному с VendorPart. То есть в целевой сущности нет поля для этой связи:

@JoinColumn(name="VENDORPARTNUMBER")
@ManyToOne
public VendorPart getVendorPart() {
    return vendorPart;
}

Первичные ключи в order

Приложение order использует несколько типов первичных ключей: простые (однозначные) первичные ключи, сгенерированные первичные ключи и составные первичные ключи.

Здесь рассматриваются следующие темы:

Сгенерированные первичные ключи

VendorPart использует сгенерированное значение первичного ключа. Таким образом, приложение не устанавливает значение первичного ключа для объектов сущности, а вместо этого полагается на persistence provider для генерации значений первичного ключа. Аннотация @GeneratedValue используется для указания того, что объекты сущности будут использовать сгенерированный первичный ключ.

В VendorPart следующий код задаёт параметры для генерации значений первичного ключа:

@TableGenerator(
    name="vendorPartGen",
    table="PERSISTENCE_ORDER_SEQUENCE_GENERATOR",
    pkColumnName="GEN_KEY",
    valueColumnName="GEN_VALUE",
    pkColumnValue="VENDOR_PART_ID",
    allocationSize=10)
@Id
@GeneratedValue(strategy=GenerationType.TABLE, generator="vendorPartGen")
public Long getVendorPartNumber() {
    return vendorPartNumber;
}

Аннотация @TableGenerator используется вместе с элементом @GeneratedValue ыtrategy=TABLE. Таким образом, стратегия, используемая для генерации первичных ключей, заключается в использовании таблицы в базе данных. Аннотация @TableGenerator используется для настройки параметров таблицы генератора. Элемент name устанавливает имя генератора. Для VendorPart это vendorPartGen.

Таблица PERSISTENCE_ORDER_SEQUENCE_GENERATOR, двумя столбцами которой являются GEN_KEY и GEN_VALUE, будет хранить сгенерированные значения первичного ключа. Эту таблицу можно использовать для генерации первичных ключей других сущностей, поэтому элемент pkColumnValue установлен в VENDOR_PART_ID, чтобы отличать сгенерированные первичные ключи этой сущности от сгенерированных первичных ключей других сущностей. Элемент allocSize указывает величину, которую нужно увеличить при выделении значений первичного ключа. В этом случае каждый первичный ключ VendorPart будет увеличиваться на 10.

Поле первичного ключа vendorPartNumber имеет тип Long, так как поле сгенерированного первичного ключа должно быть целочисленным.

Составные первичные ключи

Составной первичный ключ состоит из нескольких полей и соответствует требованиям, описанным в Первичные ключи в сущностях. Чтобы использовать составной первичный ключ, вы должны создать класс-обёртку (wrapper).

В order две сущности используют составные первичные ключи: Part и LineItem.

  • Part использует класс-обёртку (wrapper) PartKey. Первичный ключ Part представляет собой комбинацию номера детали и номера редакции. PartKey инкапсулирует этот первичный ключ.

  • LineItem использует класс LineItemKey. Первичный ключLineItem представляет собой комбинацию номера заказа и номера позиции. LineItemKey инкапсулирует этот первичный ключ.

Это класс-обёртка (wrapper) LineItemKey составного первичного ключа:

package javaeetutorial.order.entity;

import java.io.Serializable;

public final class LineItemKey implements Serializable {

    private Integer customerOrder;
    private int itemId;

    public LineItemKey() {}

    public LineItemKey(Integer order, int itemId) {
        this.setCustomerOrder(order);
        this.setItemId(itemId);
    }

    @Override
    public int hashCode() {
        return ((this.getCustomerOrder() == null
                ? 0 : this.getCustomerOrder().hashCode())
                ^ ((int) this.getItemId()));
    }

    @Override
    public boolean equals(Object otherOb) {
        if (this == otherOb) {
            return true;
        }
        if (!(otherOb instanceof LineItemKey)) {
            return false;
        }
        LineItemKey other = (LineItemKey) otherOb;
        return ((this.getCustomerOrder() == null
                ? other.getCustomerOrder == null : this.getOrderId()
                .equals(other.getCustomerOrder()))
                && (this.getItemId == oother.getItemId()));
    }

    @Override
    public String toString() {
        return "" + getCustomerOrder() + "-" + getItemId();
    }

    public Integer getCustomerOrder() {
        return customerOrder;
    }

    public void setCustomerOrder(Integer order) {
        this.customerOrder = order;
    }

    public int getItemId() {
        return itemId;
    }

    public void setItemId(int itemId) {
        this.itemId = itemId;
    }
}

Аннотация @IdClass используется для указания класса первичного ключа в классе сущности. В LineItem @IdClass используется следующим образом:

@IdClass(LineItemKey.class)
@Entity
...
public class LineItem implements Serializable {
    ...
}

Два поля в LineItem помечены аннотацией @Id, чтобы пометить эти поля как часть составного первичного ключа:

@Id
public int getItemId() {
    return itemId;
}
...
@Id
@ManyToOne
@JoinColumn(name="ORDERID")
public CustomerOrder getCustomerOrder() {
    return customerOrder;
}

Для customerOrder вы также используете аннотацию @JoinColumn для указания имени столбца в таблице и того, что этот столбец является перекрывающимся внешним ключом, указывающим на PERSISTENCE_ORDER_CUSTOMERORDER столбец ORDERID таблицы (см. отношение «один ко многим», сопоставленное перекрывающимся первичным и внешним ключам). То есть customerOrder будет установлен сущностью CustomerOrder.

В конструкторе LineItem номер позиции (LineItem.itemId) устанавливается с помощью метода CustomerOrder.getNextId:

public LineItem(CustomerOrder order, int quantity, VendorPart vendorPart) {
    this.customerOrder = order;
    this.itemId = order.getNextId();
    this.quantity = quantity;
    this.vendorPart = vendorPart;
}

CustomerOrder.getNextId подсчитывает количество текущих позиций, добавляет 1 и возвращает это число:

@Transient
public int getNextId() {
    return this.lineItems.size() + 1;
}

Part требует аннотации @Column для двух полей, которые образуют составной первичный ключ Part, поскольку составной первичный ключ Part является перекрывающимся первичным и внешним ключом:

@IdClass(PartKey.class)
@Entity
...
public class Part implements Serializable {
    ...
    @Id
    @Column(nullable=false)
    public String getPartNumber() {
        return partNumber;
    }
    ...
    @Id
    @Column(nullable=false)
    public int getRevision() {
        return revision;
    }
    ...
}

Сущности соответствует более чем одна таблица базы данных

ПоляPart соответствуют нескольким таблицам базы данных: PERSISTENCE_ORDER_PART и PERSISTENCE_ORDER_PART_DETAIL. Таблица PERSISTENCE_ORDER_PART_DETAIL содержит спецификацию и схемы для детали. Аннотация @SecondaryTable используется для указания дополнительной таблицы:

...
@Entity
@Table(name="PERSISTENCE_ORDER_PART")
@SecondaryTable(name="PERSISTENCE_ORDER_PART_DETAIL", pkJoinColumns={
    @PrimaryKeyJoinColumn(name="PARTNUMBER",
        referencedColumnName="PARTNUMBER"),
    @PrimaryKeyJoinColumn(name="REVISION",
        referencedColumnName="REVISION")
})
public class Part implements Serializable {
    ...
}

PERSISTENCE_ORDER_PART_DETAIL и PERSISTENCE_ORDER_PART используют одни и те же значения первичного ключа. Элемент pkJoinColumns в @SecondaryTable используется для указания того, что столбцы первичного ключа PERSISTENCE_ORDER_PART_DETAIL являются внешними ключами для PERSISTENCE_ORDER_PART , Аннотация @PrimaryKeyJoinColumn задаёт имена столбцов первичного ключа и указывает, к какому столбцу в первичной таблице относится этот столбец. В этом случае имена столбцов первичного ключа для PERSISTENCE_ORDER_PART_DETAIL и PERSISTENCE_ORDER_PART совпадают: PARTNUMBER и REVISION, соответственно.

Каскадные операции в order

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

В order в отношениях сущностей есть две зависимости каскадного удаления. Если CustomerOrder, с которым связан LineItem, удалён, LineItem также следует удалить. Если Vendor, с которым связан VendorPart, удалён, VendorPart также следует удалить.

Вы указываете каскадные операции для отношений сущностей, устанавливая элемент cascade на обратной стороне (не стороне-владельце) отношения. Каскадный элемент имеет значение ALL в случае CustomerOrder.lineItems. Это означает, что все персистентные операции (удаление, обновление и т. д.) каскадируются от заказов к позициям.

Вот отображение отношений в CustomerOrder:

@OneToMany(cascade=ALL, mappedBy="customerOrder")
public Collection<LineItem> getLineItems() {
    return lineItems;
}

Вот отображение отношений в LineItem:

@Id
@ManyToOne
@JoinColumn(name="ORDERID")
public CustomerOrder getCustomerOrder() {
    return customerOrder;
}

Типы данных BLOB и CLOB в order

Таблица PARTDETAIL в базе данных содержит столбец DRAWING типа BLOB. BLOB обозначает большие бинарные объекты, которые используются для хранения бинарных данных, таких как изображение. Столбец DRAWING отображается в поле Part.drawing типа java.io.Serializable. Аннотация @Lob используется для обозначения того, что поле является большим объектом:

@Column(table="PERSISTENCE_ORDER_PART_DETAIL")
@Lob
public Serializable getDrawing() {
    return drawing;
}

PERSISTENCE_ORDER_PART_DETAIL также содержит столбец SPECIFICATION типа CLOB. CLOB обозначает большие символьные объекты, которые используются для хранения строковых данных, слишком больших для хранения в столбце VARCHAR. SPECIFICATION отображается в поле Part.specification типа java.lang.String. Аннотация @Lob также используется здесь для обозначения того, что поле является большим объектом:

@Column(table="PERSISTENCE_ORDER_PART_DETAIL")
@Lob
public String getSpecification() {
    return specification;
}

Оба эти поля используют аннотацию @Column и задают элемент table для вторичной таблицы.

Временны́е типы в order

Персистентное свойство CustomerOrder.lastUpdate имеет тип java.util.Date, отображается в поле базы данных PERSISTENCE_ORDER_CUSTOMERORDER.LASTUPDATE, которое имеет тип SQL TIMESTAMP. Чтобы обеспечить правильное сопоставление между этими типами, необходимо использовать аннотацию @Temporal с правильным временны́м типом, указанным в элементе @Temporal. Элементы@Temporal имеют тип javax.persistence.TemporalType. Возможные значения

  • DATE, который отображается на java.sql.Date

  • TIME, который отображается на java.sql.Time

  • TIMESTAMP, который отображается на java.sql.Timestamp

Вот соответствующий раздел CustomerOrder:

@Temporal(TIMESTAMP)
public Date getLastUpdate() {
    return lastUpdate;
}

Управление сущностями в приложении order

Сессионный компонент с состоянием RequestBean содержит бизнес-логику и управляет объектами order. RequestBean использует аннотацию @PersistenceContext для извлечения объекта entity manager-а, который используется для управления сущностями order в бизнес-методах RequestBean:

@PersistenceContext
private EntityManager em;

Этот объект EntityManager управляется контейнером, поэтому контейнер заботится обо всех транзакциях, связанных с управлением сущностями order.

Создание сущностей

Бизнес-метод RequestBean.createPart создаёт новый объект сущности Part. Метод EntityManager.persist используется для сохранения вновь созданного объекта в базе данных:

Part part = new Part(partNumber,
        revision,
        description,
        revisionDate,
        specification,
        drawing);
em.persist(part);

Сессионный компонент-синглтон ConfigBean используется для инициализации данных в order. ConfigBean помечается @Startup, что указывает на то, что контейнер EJB должен создать ConfigBean при развёртывании order. Метод createData аннотируется @PostConstruct и создаёт начальные сущности, используемые order, вызывая бизнес-методы RequestBean.

Выборка сущностей

Бизнес-метод RequestBean.getOrderPrice возвращает цену заказа по его orderId. Метод EntityManager.find используется для извлечения сущности из базы данных:

CustomerOrder order = em.find(CustomerOrder.class, orderId);

Первый аргумент EntityManager.find является классом сущности, а второй — первичным ключом.

Установка отношений между сущностями

Бизнес-метод RequestBean.createVendorPart создаёт VendorPart, связанный с конкретным объектом Vendor. Метод EntityManager.persist используется для сохранения вновь созданного объекта VendorPart в базе данных, а методы VendorPart.setVendor и Vendor.Vendor.setVendorPart используются для связи VendorPart с Vendor:

PartKey pkey = new PartKey();
pkey.setPartNumber(partNumber);
pkey.setRevision(revision);

Part part = em.find(Part.class, pkey);

VendorPart vendorPart = new VendorPart(description, price, part);
em.persist(vendorPart);

Vendor vendor = em.find(Vendor.class, vendorId);
vendor.addVendorPart(vendorPart);
vendorPart.setVendor(vendor);

Использование запросов

Бизнес-метод RequestBean.adjustOrderDiscount обновляет скидку, применяемую ко всем заказам. Этот метод использует именованный запрос findAllOrders, определённый в CustomerOrder:

@NamedQuery(
    name="findAllOrders",
    query="SELECT co FROM CustomerOrder co " +
          "ORDER BY co.orderId"
)

Для выполнения запроса используется метод EntityManager.createNamedQuery. Поскольку запрос возвращает List всех заказов, используется метод Query.getResultList:

List orders = em.createNamedQuery(
        "findAllOrders")
        .getResultList();

Бизнес-метод RequestBean.getTotalPricePerVendor возвращает общую стоимость всех деталей для конкретного поставщика. Этот метод использует именованный параметр id, определённый в именованном запросе findTotalVendorPartPricePerVendor, определённом в VendorPart:

@NamedQuery(
    name="findTotalVendorPartPricePerVendor",
    query="SELECT SUM(vp.price) " +
    "FROM VendorPart vp " +
    "WHERE vp.vendor.vendorId = :id"
)

При выполнении запроса метод Query.setParameter используется для установки именованному параметру id значения vendorId для параметра RequestBean.getTotalPricePerVendor:

return (Double) em.createNamedQuery(
    "findTotalVendorPartPricePerVendor")
    .setParameter("id", vendorId)
    .getSingleResult();

Метод Query.getSingleResult используется для этого запроса, поскольку запрос возвращает одно значение.

Удаление сущностей

Бизнес-метод RequestBean.removeOrder удаляет данный заказ из базы данных. Этот метод использует метод EntityManager.remove для удаления объекта из базы данных:

CustomerOrder order = em.find(CustomerOrder.class, orderId);
em.remove(order);

Запуск order

Вы можете использовать IDE NetBeans или Maven для сборки, упаковки, развёртывания и запуска приложения order. Сначала вы создадите таблицы базы данных в Apache Derby.

Здесь рассматриваются следующие темы:

Запуск order с использованием IDE NetBeans

  1. Удостоверьтесь, чтобы GlassFish Server был запущен (см. Запуск и остановка сервера GlassFish).

  2. Если сервер базы данных ещё не запущен, запустите его, следуя инструкциям в Запуск и остановка Apache Derby.

  3. В меню «Файл» выберите «Открыть проект».

  4. В диалоговом окне «Открыть проект» перейдите к:

    tut-install/examples/persistence
  5. Выберите каталог order.

  6. Нажмите Открыть проект.

  7. На вкладке «Проекты» кликните правой кнопкой мыши проект order и выберите «Выполнить».

    Среда IDE NetBeans открывает веб-браузер по следующему URL:

    http://localhost:8080/order/

Запуск order с помощью Maven

  1. Удостоверьтесь, чтобы GlassFish Server был запущен (см. Запуск и остановка сервера GlassFish).

  2. Если сервер базы данных ещё не запущен, запустите его, следуя инструкциям в Запуск и остановка Apache Derby.

  3. В окне терминала перейдите в:

    tut-install/examples/persistence/order/
  4. Введите следующую команду:

    mvn install

    Это компилирует исходные файлы и упаковывает приложение в файл WAR, расположенный в tut-install/examples/persistence/order/target/order.war. Затем WAR-файл будет развёрнут в GlassFish Server.

  5. Чтобы создать и обновить данные заказа, откройте веб-браузер по следующему URL:

    http://localhost:8080/order/

Назад Вперёд Содержание
Логотип Oracle  Copyright © 2017, Oracle и/или её дочерних компаний. Все права защищены. Версия перевода 1.0.5 (Java EE Tutorial — русскоязычная версия)