Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
2013_1 / КСТ / Разработка веб-приложений.pdf
Скачиваний:
160
Добавлен:
23.02.2015
Размер:
2.74 Mб
Скачать

12. Примеры хранимых сущностей

Эта глава описывает использование языка Java Persistence API на примере

приложения. Особое внимание уделено исходному коду и установкам параметров. Приведённый пример является приложением с именем order, которое использует се-

ансовый бин stateful, чтобы управлять объектами, имеющими отношение к системе

заказов.

12.1. Приложение order

Приложение order — простое приложение для поддержки каталога запчастей и

размещения заказа на эти запчасти. У него есть сущности, которые представляют зап-

части, поставщиков, заказы и пункты заказа. Эти сущности использует сеансовый бин с

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

ния. Приложение Facelets манипулирует данными и отображает их из каталога.

Информация в заказе может быть подразделена на разные элементы:

Что такое номер заказа?

Какие запчасти включены в заказ?

Какие запчасти составляют эту запчасть?

Кто сделал запчасть?

Что есть спецификация для запчасти?

Есть ли схема для запчасти?

Приложение order является упрощенной версией системы заказов, которая включает все эти элементы.

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

12.1.1. Отношения сущностей в приложении order

Приложение order демонстрирует несколько типов связей сущностей: «один-

ко-многим», «многие-к-одному», «один-к-одному», однонаправленные и самоссылающиеся связи.

Самоссылающиеся связи

Самоссылающееся отношение является отношением между полями отношения

для той же сущности. Запчасть имеет поле bomPart, которая имеет отношение «один- ко-многим» с полем запчастей parts в сущности Part. То есть, запчасть может состоять

из многих запчастей, и каждая из этих запчастей имеет точно один материал.

Первичный ключ для сущности Part — составной: комбинация полей partNumber и revision. Он отображается в столбцы PARTNUMBER и REVISION в таблице EJB_

ORDER_PART.

...

@ManyToOne

@JoinColumns({

@JoinColumn(name="BOMPARTNUMBER",

referencedColumnName="PARTNUMBER"),

@JoinColumn(name="BOMREVISION",

129

referencedColumnName="REVISION")

})

public Part getBomPart() { return bomPart;

}

...

@OneToMany(mappedBy="bomPart") public Collection<Part> getParts() {

return parts;

}

...

Отношение «один-к-одному» (One-to-One)

Сущность Part имеет поле vendorPart, которое имеет взаимно однозначную

связь с полем VendorPart сущности Part.

То есть каждая запчасть имеет точно одного поставщика, и наоборот. Вот отношение, отображаемое в 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. Столбец PARTREVISION таблицы PERSISTENCE_ORDER_VENDOR_PART ссылается на столбец REVISION

таблицы PERSISTENCE_ORDER_PART.

Отношение «один-ко-многим» (One-to-Many), отображенное в перекрытие первичного и внешнего ключей

Заказ имеет поле lineItems, которое имеет связь «один-ко-многим» с полем заказов order таблицы LineItem. То есть каждый заказ имеет один или более пунктов.

LineItem использует составной первичный ключ из полей orderId и itemId. Этот

составной первичный ключ отображается в столбцы ORDERID и ITEMID таблицы

PERSISTENCE_ORDER_LINEITEM в базе данных. ORDERID — внешний ключ, ото-

бражается в столбец ORDERID в таблице PERSISTENCE_ORDER_ORDER. Это означает, что столбец ORDERID отображается дважды:

первый раз как поле первичного ключа orderId;

второй раз как поле связи order-отношения.

130

Здесь показано отображение связи в класс Order:

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

}

Вот отношение, отображающееся в класс LineItem:

@ManyToOne

public Order getOrder() { return order;

}

Однонаправленная связь

Сущность LineItem имеет поле vendorPart, которое имеет однонаправленную связь «многие-к-одному» с сущностью VendorPart. То есть в этом отношении нет поля

в целевом объекте.

@ManyToOne

public VendorPart getVendorPart() { return vendorPart;

}

12.1.2. Первичный ключ в приложении order

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

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

СущностиVendorPart используютзначениясгенерированныхпервичныхключей.

То есть приложение не назначает значений первичного ключа для сущностей, а дове-

ряет провайдеру хранилища генерировать значения первичного ключа. Употребляется аннотация @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 используется вместе с элементом strategy=TABLE

в @GeneratedValue и задаёт стратегию для генерации первичного ключа в таблице

131

базы данных. Аннотация @TableGenerator использована, чтобы задать генератор для таблицы. Элемент name задаёт имя vendorPartGen для генератора, которое есть в

классе VendorPart.

Таблица EJB_ORDER_SEQUENCE_GENERATOR, которая имеет два столб-

ца GEN_KEY и GEN_VALUE, загрузит генерированные значения первичного ключа.

Эта таблица может быть использована, чтобы генерировать другие первичные ключи. Поэтому элемент pkColumnValue установлен в VENDOR_PART_ID, чтобы различать

эти генерированные первичные ключи от других генерированных первичных ключей.

Элемент allocationSize определяет приращение при создании значений первичных

ключей. В данном случае каждый следующий первичный ключ сущности VendorPart

увеличится на 10.

Поле первичного ключа vendorPartNumber — типа Long, так как сгенерирован-

ный первичный ключ должен быть этого типа.

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

Составные первичные ключи состоят из многих полей. Чтобы использовать со-

ставной первичный ключ, вы должны создать обертывающий класс.

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

иLineItem.

Part использует класс суперобложки PartKey. Первичный ключ Part — комбина-

ция индекса и номера издания. PartKey инкапсулирует этот первичный ключ.

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

package order.entity;

public final class LineItemKey implements java.io.Serializable {

private Integer orderId; private int itemId;

public int hashCode() {

return ((this.getOrderId()==null ? 0:this.getOrderId().hashCode())

^ ((int) this.getItemId()));

}

public boolean equals(Object otherOb) { if (this == otherOb) {

return true;

}

if (!(otherOb instanceof LineItemKey)) { return false;

}

LineItemKey other = (LineItemKey) otherOb; return ((this.getOrderId()==null ?

other.orderId==null : this.getOrderId().equals (other.orderId)) &&

(this.getItemId == other.itemId));

}

public String toString() {

132

return "" + orderId + "-" + itemId;

}

}

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

@IdClass(order.entity.LineItemKey.class)

@Entity

...

public class LineItem {

...

}

Два поля в LineItem с аннотаций @Id нужны, чтобы выделить эти поля как часть

составного первичного ключа:

@Id

public int getItemId() { return itemId;

}

...

@Id

@Column(name="ORDERID", nullable=false, insertable=false, updatable=false)

public Integer getOrderId() { return orderId;

}

Для orderId вы также используете аннотацию @Column, чтобы определить имя столбца в таблице. Этот столбец не должен включаться или изменяться, так как

это перекрывающий внешний ключ, указывающий на столбец ORDERID таблицы

PERSISTENCE_ORDER_ORDER (см. отображение отношения «один-ко-многим» для перекрытия первичного и внешнего ключа выше). То есть orderId будет установлен сущностью Order.

В конструкторе LineItem номер пункта (LineItem.itemId) будет установлен мето-

дом Order.getNextId.

public LineItem(Order order, int quantity, VendorPart vendorPart) {

this.order = order;

this.itemId = order.getNextId(); this.orderId = order.getOrderId(); this.quantity = quantity; this.vendorPart = vendorPart;

}

Order.getNextId считает количество текущих пунктов, добавляет единицу и воз-

вращает число.

public int getNextId() {

return this.lineItems.size() + 1;

}

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

133

@IdClass(order.entity.PartKey.class)

@Entity

...

public class Part {

...

@Id

public String getPartNumber() { return partNumber;

}

...

@Id

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 {

...

}

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

134

пляр сущности LineItem также должен быть удален. Если Vendor, на который ссылает-

ся VendorPart, удален, тогда VendorPart также должен быть удален.

Вы определяете каскадные операции для связи сущностей, устанавливая каскадный элемент на обратной стороне (не обладателе) отношения. Каскадный эле-

мент установлен в ALL в случае Order.lineItems. Это означает, что все операции хра-

нения (удаление, корректировка и так далее) выполняются каскадно от заказов до

пунктов.

Вот отношение, отображающееся в Order:

@OneToMany(cascade=ALL, mappedBy="order") public Collection<LineItem> getLineItems() {

return lineItems;

}

Отношение, отображающееся в LineItem:

@ManyToOne

public Order getOrder() { return order;

}

Типы 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 и устанавливают табличный

элемент во второстепенную таблицу.

Типы времени в приложении order

Хранимое свойство Order.lastUpdate типа java.util.Date отображено в поле базы данных PERSISTENCE_ORDER_ORDER.LASTUPDATE, которое имеет тип

TIMESTAMP — время в языке SQL.

Чтобы гарантировать соответствующее отображение между этими типами, вы должны использовать аннотацию @Temporal с соответствующим временным типом,

135

определённым в элементе @Temporal. Элементы @Temporal — типа javax.persistence.

TemporalType. Возможные значения:

DATE — с java.sql.Date;

TIME — отображается в java.sql.Time;

TIMESTAMP — отображается в java.sql.Timestamp.

Это секция Order:

@Temporal(TIMESTAMP)

public Date getLastUpdate() { return lastUpdate;

}

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

Сеансовый бин RequestBean с хранимым состоянием (stateful) содержит дело-

вую логику и управляет сущностями заказа.

RequestBean использует аннотацию @PersistenceContext, чтобы получить ме-

неджер сущностей, который использован для управления сущностями 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 использован, чтобы инициализировать данные в заказе. ConfigBean аннотирован @Startup, которая указывает, что

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

ные приложением, вызывая деловые методы класса RequestsBean.

Поиск сущностей

Метод RequestBean.getOrderPrice возвращает цену данного заказа, основанно-

го на orderId. Метод EntityManager.find используется для извлечения сущности из базы

данных.

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

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

136

Установка связи сущности

Метод RequestBean.createVendorPart создает сущность VendorPart, связанную с

конкретным поставщиком Vendor. Метод EntityManager.persist использован, чтобы со-

хранить вновь созданную сущность VendorPart в базе данных. VendorPart.setVendor и

Vendor.setVendorPart использован, чтобы соединять VendorPart с поставщиком Vendor.

PartKey pkey = new PartKey(); pkey.partNumber = partNumber; pkey.revision = 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, определённый в классе Order :

@NamedQuery(

name="findAllOrders", query="SELECT o FROM Order o"

)

Метод EntityManager.createNamedQuery используется, чтобы запустить запрос. Поскольку запрос возвращает список List всех заказов, используется метод

Query.getResultList.

List orders = em.createNamedQuery( "findAllOrders")

.getResultList();

Метод RequestBean.getTotalPricePerVendor возвращает общую цену всех запчастей для конкретного поставщика. Он использует параметр id в запросе findTotalVendorPartPricePerVendor.

@NamedQuery(

name="findTotalVendorPartPricePerVendor", query="SELECT SUM(vp.price) " +

"FROM VendorPart vp " +

"WHERE vp.vendor.vendorId = :id"

)

При прогоне запроса используется метод Query.setParameter, чтобы установить параметру id значение vendorId.

return (Double) em.createNamedQuery( "findTotalVendorPartPricePerVendor")

.setParameter("id", vendorId)

.getSingleResult();

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

возвращает единственное значение.

137

Соседние файлы в папке КСТ