Статьи

JAXB — Перспектива новичка, часть 2

В первой части этой серии я рассмотрел основы загрузки данных из файла XML в базу данных с использованием JAXB и JPA. (Если вместо XML требуется JSON, то эту же идею следует преобразовать в такой инструмент, как Джексон.) Подход состоит в том, чтобы использовать объекты общего домена — то есть один набор POJO с аннотациями, которые описывают как отображение XML, так и реляционное отображение ,

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

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

Что в имени?

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

Имя свойства определяет имя тега по умолчанию для соответствующего элемента (хотя это можно переопределить аннотациями — например, @XmlElement в простейшем случае). Что еще более важно, ваши имена получателей и установщиков должны совпадать. Конечно, лучший совет — позволить вашей IDE генерировать метод получения и установки, чтобы опечатки не возникали.

Работа с @EmbeddedId

Предположим, вы хотите загрузить некоторые данные, представляющие заказы. Каждый заказ может иметь несколько позиций, причем позиции для каждого заказа нумеруются последовательно от 1, поэтому уникальный идентификатор для всех позиций будет представлять собой комбинацию идентификатора заказа и номера позиции. Предполагая, что вы используете подход @EmbeddedId для представления ключа, ваши позиции могут быть представлены следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Embeddable
public class LineItemKey {
    private Integer orderId;
    private Integer itemNumber;
 
    /* … getters and setters … */
}
 
@XmlRootElement
@Entity
@Table(name=”ORDER_ITEM”)
public class OrderLineItem {
    @EmbeddedId
    @AttributeOverrides(/*…*/)
    private LineItemKey lineItemKey;
 
    @Column(name=”PART_NUM”)
    private String partNumber;
 
    private Integer quantity;
 
    // … getters and setters …
};

Код маршаллинга и демаршаллинга будет очень похож на пример Employee в части 1 . Обратите внимание, что нам не нужно явно сообщать JAXBContext о классе LineItemKey, поскольку на него ссылается OrderLineItem.

01
02
03
04
05
06
07
08
09
10
11
12
13
LineItemKey liKey = new LineItemKey();
    liKey.setOrderId(37042);
    liKey.setItemNumber(1);
 
    OrderLineItem lineItem = new OrderLineItem();
    lineItem.setLineItemKey(liKey);
    lineItem.setPartNumber(“100-02”);
    lineItem.setQuantity(10);
 
    JAXBContext jaxb = JAXBContext.newInstance(OrderLineItem.class);
    Marshaller marshaller = jaxb.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    marshaller.marshal(lineItem, System.out);

Тем не менее, мы не можем быть в восторге от полученной структуры XML:

1
2
3
4
5
6
7
8
9
<?xml version=”1.0” encoding=”UTF-8” standalone=”yes”?>
<orderLineItem>
    <lineItemKey>
        <itemNumber>1</itemNumber>
        <orderId>37042</orderId>
    </lineItemKey>
    <partNumber>100-02</partNumber>
    <quantity>10</quantity>
</orderLineItem>

Что если нам не нужен элемент <lineItemKey>? Если у нас есть JAXB, использующий доступ к свойствам, то один из вариантов — изменить определения наших свойств (т.е. наши методы получения и установки), чтобы OrderLineItem выглядел как плоский объект для JAXB (и, возможно, для остальной части нашего приложения; это может быть хорошо ).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@XmlRootElement
@Entity
@Table(name=”ORDER_ITEM”)
public class OrderLineItem {
    @EmbeddedId
    @AttributeOverrides(/*…*/)
    private LineItemKey lineItemKey;
 
    // … additional fields …
 
    @XmlTransient
    public LineItemKey getLineItemKey() {
        return lineItemKey;
    }
 
    public void setLineItemKey(LineItemKey lineItemKey) {
        this.lineItemKey = lineItemKey;
    }
 
    // “pass-thru” properties to lineItemKey
    public Integer getOrderId() {
        return lineItemKey.getOrderId();
    }
 
    public void setOrderId(Integer orderId) {
        if (lineItemKey == null) {
            lineItemKey = new LineItemKey();
        }
        lineItemKey.setOrderId(orderId);
    }
 
    public Integer getItemNumber() {
        return lineItemKey.getItemNumber();
    }
 
    public void setItemNumber(Integer itemNumber) {
        if (lineItemKey == null) {
            lineItemKey = new LineItemKey();
        }
        lineItemKey.setItemNumber(itemNumber);
    }
 
    // … additional getters and setters …
};

Обратите внимание на добавление @XmlTransient к получателю lineItemKey; это говорит JAXB не отображать это конкретное свойство. (Если JPA использует доступ к полю, мы можем полностью удалить геттер и установщик lineItemKey. С другой стороны, если JPA использует доступ к свойству, нам нужно пометить наши «проходные» геттеры как @Transient чтобы поставщик JPA не мог вывести неверное отображение на таблицу ORDER_ITEM.)

Однако, если lineItemKey помечен @XmlTransient, JAXB не будет знать, что ему нужно создавать встроенный экземпляр LineItemKey во время демаршаллинга. Здесь мы обратились к этому, установив «сквозные» сеттеры, чтобы убедиться, что экземпляр существует. JPA должен терпеть это, по крайней мере, если он использует полевой доступ. Если вы хотите, чтобы этот подход был потокобезопасным, вам нужно синхронизировать сеттеры. В качестве альтернативы вы можете создать LineItemKey в конструкторе по умолчанию (если вы уверены, что ваш провайдер JPA не будет возражать).

Другой вариант, который обязательно повлияет только на JAXB (без выделенных методов получения и установки), может заключаться в использовании ObjectFactory, который внедряет LineItemKey в OrderLineItem перед его возвратом. Однако, насколько мне известно, ObjectFactory должна охватывать все классы в пакете, поэтому, если у вас много простых доменных объектов и несколько сложных в одном пакете (и у вас нет других причин для создания ObjectFactory), тогда Вы можете избежать такого подхода.

Вы также можете захотеть защитить проходные геттеры от исключений нулевого указателя, проверив, существует ли LineITemKey, прежде чем пытаться получить возвращаемое значение.

В любом случае наш XML теперь должен выглядеть так:

1
2
3
4
5
6
7
<?xml version=”1.0” encoding=”UTF-8” standalone=”yes”?>
<orderLineItem>
    <itemNumber>1</itemNumber>
    <orderId>37042</orderId>
    <partNumber>100-02</partNumber>
    <quantity>10</quantity>
</orderLineItem>

Связанные объекты: один ко многим

Конечно, ваши позиции принадлежат заказам, поэтому у вас может быть таблица ORDER (и соответствующий класс Order).

01
02
03
04
05
06
07
08
09
10
11
12
13
@XmlRootElement
@Entity
@Table(name=”ORDER”)
public class Order {
    @Id
    @Column(name=”ORDER_ID”)
    private Integer orderId;
 
    @OneToMany(mappedBy=”order”)
    private List<OrderLineItem> lineItems;
 
    // … getters and setters …
}

Мы установили отношения «один ко многим» с OrderLineItem. Обратите внимание, что мы ожидаем, что OrderLineItem будет владеть этим отношением для целей JPA.

Пока что мы удалим аннотацию @XmlRootElement из OrderLineItem. (Нам не нужно; аннотация делает класс приемлемым для корневого элемента, но не исключает также его использование в качестве вложенного элемента. Однако, если мы хотим продолжить писать XML, представляющий только OrderLineItem, то мы нам нужно принять некоторые дополнительные решения, поэтому мы отложим это на данный момент.)

Чтобы маршаллер был доволен, мы устанавливаем свойство Order для OrderLineItem @XmlTransient. Это позволяет избежать циклической ссылки, которая в противном случае могла бы интерпретироваться как бесконечно глубокое дерево XML. (Вы, вероятно, в любом случае не намерены вставлять полную информацию о заказе в элемент <orderLineItem>.)

Когда <orderLineItem> встроен в элемент <order>, больше нет причин помещать элемент <orderId> в элемент <orderLineItem>. Мы удаляем свойство orderId из OrderLineItem, зная, что код в другом месте приложения может по-прежнему использовать lineItem.getOrder (). GetOrderId ().

Новая версия OrderLineItem выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Entity
@Table(name=”ORDER_ITEM”)
public class OrderLineItem {
    @EmbeddedId
    @AttributeOverrides(/*…*/)
    private LineItemKey lineItemKey;
 
    @MapsId(“orderId”)
    @ManyToOne
    private Order order;
 
    @Column(name=”PART_NUM”)
    private String partNumber;
 
    private Integer quantity;
 
    @XmlTransient
    public Order getOrder() {
        return order;
    }
 
    public void setOrder(Order order) {
        this.order = order;
    }
 
    public Integer getItemNumber() {
        return lineItemKey.getItemNumber();
    }
 
    public void setItemNumber(Integer itemNumber) {
        if (lineItemKey == null) {
            lineItemKey = new LineItemKey();
        }
        lineItemKey.setItemNumber(itemNumber);
    }
 
        // … more getters and setters …
};

Наш JAXBContext нужно рассказать о классе Order. В этой ситуации не нужно явно говорить о OrderLineItem. Таким образом, мы можем проверить сортировку следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
JAXBContext jaxb = JAXBContext.newInstance(Order.class);
 
    List<OrderLineItem> lineItems = new ArrayList<OrderLineItem>();
 
    Order order = new Order();
    order.setOrderId(37042);
    order.setLineItems(lineItems);
 
    OrderLineItem lineItem = new OrderLineItem();
    lineItem.setOrder(order);
    lineItem.setLineNumber(1);
    lineItem.setPartNumber(“100-02”);
    lineItem.setQuantity(10);
    lineItems.add(lineItem);
 
    lineItem = new OrderLineItem();
    lineItem.setOrder(order);
    lineItem.setLineNumber(2);
    lineItem.setPartNumber(“100-17”);
    lineItem.setQuantity(5);
    lineItems.add(lineItem);
 
    Marshaller marshaller = jaxb.createMarshaller();
    marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    marshaller.marshal(order, System.out);

Обратите внимание, что мы устанавливаем свойство порядка для каждой позиции. JAXB не будет заботиться об этом при сортировке (потому что свойство @XmlTransient и никакое другое свойство не зависит от внутреннего состояния, на которое оно влияет), но мы хотим сохранить согласованность наших объектных отношений. Если мы передадим заказ JPA, то невозможность установить свойство order станет проблемой — и мы вскоре вернемся к этому вопросу.

Мы должны получить вывод, как это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<?xml version=”1.0” encoding=”UTF-8” standalone=”yes”?>
    <order>
        <orderId>37042</orderId>
        <lineItems>
            <lineNumber>1</lineNumber>
            <partNumber>100-02</partNumber>
            <quantity>10</quantity>
        </lineItems>
        <lineItems>
            <lineNumber>2</lineNumber>
            <partNumber>100-17</partNumber>
            <quantity>5</quantity>
        </lineItems>
    </order>

Отображение имени элемента по умолчанию помещает тег <lineItems> вокруг каждого элемента строки (потому что это имя свойства), что немного не так. Мы можем исправить это, поместив @XmlElement (name = ”lineItem”) в метод getLineItems () метода Order. (И если бы мы тогда хотели, чтобы весь список элементов линейных элементов был заключен в один элемент <lineItems>, мы могли бы сделать это с помощью аннотации @XmlElementWrapper (name = ”lineItems”) для того же метода.)

На этом этапе тест маршаллинга должен выглядеть довольно хорошо, но у нас возникнут проблемы, если мы демонтируем ордер и попросим JPA сохранить полученные объекты позиций строки заказа. Проблема в том, что unmarshaller не устанавливает свойство order для OrderLineItem (которому принадлежит отношение Order-to-OrderLineItem для целей JPA).

Мы можем решить эту проблему, если бы Order.setLineItems () выполнял итерацию по списку позиций и вызывал setOrder () для каждой из них. Это основывается на том, что JAXB сначала строит список позиций, а затем передает его в setLineItems (); это работало в моих тестах, но я не знаю, будет ли оно всегда работать со всеми реализациями JAXB.

Другой вариант — вызывать setOrder () для каждого OrderLineItem после демаршаллинга, но перед передачей объектов в JPA. Это, возможно, более надежно, но похоже на клудж. (Отчасти смысл инкапсуляции в том, что ваши установщики, возможно, могут гарантировать, что ваши объекты сохраняют внутренне непротиворечивое состояние, в конце концов; так зачем передавать эту ответственность на код вне классов объектов?)

В интересах простоты я пропущу некоторые более сложные идеи, с которыми я играл, пытаясь решить эту проблему. Мы рассмотрим еще одно решение, когда вскоре поговорим о @XmlID и @XmlIDREF.

Дело о доступе к недвижимости

Я опирался на модифицированные сеттеры для решения двух предыдущих проблем. Если вы привыкли к мысли, что сеттер должен иметь одну строку (this.myField = myArgument), это может показаться сомнительным. (Опять же, если вы не позволите вашим установщикам выполнять какую-либо работу за вас, что вы покупаете, инкапсулируя свои поля?)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@XmlTransient
    public List<OrderLineItem> getLineItems() {
        return lineItems;
    }
 
    public void setLineItems(List<OrderLineItem> lineItems) {
        this.lineItems = lineItems;
    }
 
    // @Transient if JPA uses property access
    @XmlElement(name=”lineItem”)
    public List<OrderLineItem> getLineItemsForJAXB() {
        return getLineItems();
    }
 
    public void setLineItemsForJAXB(List<OrderLineItems> lineItems) {
        setLineItems(lineItems);
        // added logic, such as calls to setOrder()…
    }

Вы можете избежать использования свойств «ForJAXB» где-либо еще в вашем приложении, если хотите, поэтому, если вы чувствуете, что вам необходимо добавить установочную логику «только для JAXB», такой подход не даст этой дополнительной логике помешать вам.

Однако, на мой взгляд, описанные выше типы логики сеттера просто скрывают детали реализации свойств bean-компонента от внешнего кода. Я бы сказал, что в этих случаях JAXB способствует лучшей абстракции.

Если вы думаете о JAXB как о способе сериализации внутреннего состояния объекта, тогда доступ к полю может показаться предпочтительным. (Я слышал этот аргумент за использование доступа к полям с JPA, во всяком случае.) В конце концов, однако, вы хотите, чтобы инструмент сделал работу за вас. Рассматривать JAXB как внешний механизм для создания (или записи) ваших объектов может быть просто более прагматичным.

Связанные объекты: один к одному, многие ко многим

Когда работают отношения один-ко-многим, может показаться, что отношения один-к-одному должны быть несложными. Однако, хотя отношения один-ко-многим часто поддаются иерархической природе XML (при этом «многие» являются потомками «одного»), объекты в отношении один-к-одному часто являются просто равноправными; поэтому в лучшем случае выбор встраивания одного элемента в другой в представлении XML будет произвольным.

Отношения «многие ко многим» представляют собой большую проблему для иерархической модели. И если у вас более сложная сеть отношений (независимо от их кардинальности), не может быть простого способа упорядочить объекты в дереве.

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

Но если вы обнаружите, что вам нужен способ моделирования отношений, которые не соответствуют шаблону родитель-потомок, вы можете сделать это с помощью @XmlID и @XmlIDREF.

Изучая правила использования @XmlID, вы можете спросить себя, не будет ли проще просто сохранить элементы необработанного внешнего ключа под ссылочным элементом (аналогично тому, как СУБД обычно представляет внешний ключ). Вы могли бы, и у маршаллера не было бы проблем с получением красивого XML. Но тогда во время или после демаршаллинга вы будете нести ответственность за самостоятельную сборку графика отношений. Правила для @XmlID раздражают, но я не нахожу их такими сложными, чтобы их избежать оправдало бы такие усилия.

Значения идентификатора должны быть строками, и они должны быть уникальными для всех элементов вашего XML-документа (а не только для всех элементов данного типа). Это потому, что концептуально ссылка на идентификатор не типизирована; на самом деле, если вы позволите JAXB создавать ваши доменные объекты из схемы, он будет сопоставлять ваши элементы (или атрибуты) @XmlIDREF со свойствами типа Object. (Однако, когда вы аннотируете свои собственные доменные классы, вы можете использовать @XmlIDREF с типизированными полями и свойствами, если у ссылочного типа есть поле или свойство, аннотированное @XmlID. Я предпочитаю делать это, поскольку это позволяет избежать нежелательных приведений в моем коде.) Ключи для ваших отношений могут не соответствовать этим правилам; но это нормально, потому что вы можете создать свойство (скажем, xmlId), которое будет.

Предположим, что в каждом из наших заказов есть Клиент и адрес для отправки. Также у каждого Клиента есть свой адрес для выставления счетов. Обе таблицы в базе данных (CUSTOMER и ADDRESS) используют суррогатные ключи Integer с последовательностями, начинающимися с 1.

В нашем XML-коде Заказчик и адрес доставки могут быть представлены как дочерние элементы в Order; но, возможно, нам нужно отслеживать клиентов, у которых в настоящее время нет заказов. Аналогично, список адресов выставления счетов может быть представлен в виде списка дочерних элементов в разделе «Заказчик», но это неизбежно приведет к дублированию данных, поскольку клиенты отправляют заказы на свои адреса выставления счетов. Поэтому вместо этого мы будем использовать @XmlID.

Мы можем определить адрес следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Entity
    @Table(name=”ADDRESS”)
    public class Address {
        @Id
        @Column(name=”ADDRESS_ID”)
        private Integer addressId;
 
        // other fields…
 
        @XmlTransient
        public Integer getAddressId() {
            return addressId;
        }
 
        public void setAddressId(Integer addressId) {
            this.addressId = addressId;
        }
 
        // @Transient if JPA uses property access
        @XmlID
        @XmlElement(name=”addressId”)
        public String getXmlId() {
            return getClass().getName() + getAddressId();
        }
 
        public void setXmlId(String xmlId) {
            //TODO: validate xmlId is of the form <className><Integer>
            setAddressId(Integer.parseInt(
                xmlId.substring( getClass().getName().length() )));
        }
 
        // … more getters and setters …
}

Здесь свойство xmlId предоставляет JAXB-представление addressId. Добавление имени класса обеспечивает уникальность между типами, ключи которых в противном случае могут конфликтовать. Если бы у нас был более сложный естественный ключ для таблицы, нам пришлось бы преобразовать каждый элемент ключа в строку, возможно, с каким-то разделителем, и объединить все это вместе.

Разновидностью этой идеи является использование @XmlAttribute вместо @XmlElement. Я обычно предпочитаю использовать элементы для значений данных (так как они логически являются содержимым документа), но XmlId, вероятно, можно рассматривать как описание элемента XML <Address>, а не сам адрес, поэтому может иметь смысл записывать это как атрибут.

Чтобы демаршаллинг работал, мы также должны проанализировать значение addressId обратно из xmlId в установщике. Мы могли бы избежать этого, если сохраним как свойство xmlId, так и свойство addressId; в этом случае установщик xmlId может просто выбросить свое значение; но мне не нравится этот вариант, потому что он экономит сравнительно мало усилий и создает возможность встретить XML-документ с несовместимыми значениями для xmlId и addressId. (Иногда вам, возможно, придется признать возможность противоречивого документа — например, если вы сохраните обе стороны отношений, о которых я расскажу позже.)

Далее мы создадим наше отображение клиентов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Entity
    @Table(name=“CUSTOMER”)
    public class Customer {
        @Id
        @Column(name=”CUSTOMER_ID”)
        private Integer customerId;
 
        @ManyToMany
        @JoinTable(name = “CUST_ADDR”)
        private List<Address> billingAddresses;
 
        // other fields…
 
        @XmlTransient
        public Integer getCustomerId() {
            return customerId;
        }
 
        public void setCustomerId(Integer customerId) {
            this.customerId = customerId;
        }
 
        @XmlIDREF
        @XmlElement(name = “billingAddress”)
        public List<Address> getBillingAddresses() {
            return billingAddresses;
        }
 
        public void setBillingAddresses(List<Address> billingAddresses) {
            this.billingAddresses = billingAddresses;
        }
 
        // @Transient if JPA uses property access
        @XmlID
        @XmlElement(name=”customerId”)
        public String getXmlId() {
            return getClass().getName() + getCustomerId();
        }
 
        public void setXmlId(String xmlId) {
            //TODO: validate xmlId is of the form <className><Integer>
            setCustomerId(Integer.parseInt(
                xmlId.substring( getClass().getName().length() )));
        }
 
        // … more getters and setters …
    }

Обработка xmlId Клиента такая же, как и для Address. Мы пометили свойство billingAddresses аннотацией @XmlIDREF, сообщая JAXB, что каждый элемент <billingAddress> должен содержать значение ID, ссылающееся на Address, а не на фактическую структуру элемента Address. Таким же образом мы добавили бы свойства Order и shipToAddress в Order, помеченные @XmlIDREF.

На этом этапе каждая ссылка на Клиента или Адрес помечается как @XmlIDREF. Это означает, что, хотя мы можем преобразовать наши данные в XML, результат фактически не будет содержать данных о клиентах или адресах. Если @XmlIDREF не соответствует @XmlID в документе, когда вы его отменяете, то соответствующее свойство немаршализованного объекта будет нулевым. Поэтому, если мы действительно хотим, чтобы это работало, мы должны создать новый @XmlRootElement, который может содержать все наши данные.

1
2
3
4
5
6
7
8
@XmlRootElement
    public class OrderData {
        private List<Order> orders;
        private List<Address> addresses;
        private List<Customer> customers;
 
        // getters and setters
    }

Этот класс не соответствует ни одной таблице в нашей базе данных, поэтому он не имеет аннотаций JPA. Наши получатели могут иметь аннотации @XmlElement и @XmlElementWrapper, как и в предыдущих свойствах типа List. Если мы соберем и упорядочим объект OrderData, мы можем получить что-то вроде этого:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version=”1.0” encoding=”UTF-8” standalone=”yes”?>
    <orderData>
        <addresses>
            <address>
                <addressId>Address1010</addressId>
                <!-- … other elements … -->
            </address>
            <address>
                <addressId>Address1011</addressId>
                <!-- … -->
            </address>
        </addresses>
        <customers>
            <customer>
                <billingAddress>Address1010</billingAddress>
                <billingAddress>Address1011</billingAddress>
                <customerId>Customer100</customerId>
            </customer>
        </customers>
        <orders>
            <order>
                <customer>Customer100</customer>
                <lineItem>
                    <itemNumber>1</itemNumber>
                    <partNumber>100-02</partNumber>
                    <quantity>10</quantity>
                </lineItem>
                <lineItem>
                    <lineNumber>2</lineNumber>
                    <partNumber>100-17</partNumber>
                    <quantity>5</quantity>
                </lineItem>
                <orderId>37042</orderId>
                <shipToAddress>Address1011</shipToAddress>
            </order>
        </orders>
    </orderData>

Пока что мы наметили только одну сторону каждого отношения. Если наши доменные объекты должны поддерживать навигацию в обоих направлениях, у нас есть выбор: мы можем пометить свойство на одной стороне отношения как @XmlTransient; это ставит нас в ту же ситуацию, в которой мы были с иерархически представленным отношением «один-ко-многим», поскольку демонтаж не будет автоматически устанавливать свойство @XmlTransient. Или мы можем сделать оба свойства @XmlIDREF, признавая, что кто-то может написать несовместимый XML-документ.

Пересмотр связанных объектов: один ко многим

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

Мы видели, что @XmlID и @XmlIDREF обеспечивают более общее представление отношений. Смешивая эти два метода, мы можем представить обе стороны отношения родитель-потомок (с оговоркой, как в любом случае, когда мы показываем обе стороны отношения в XML, вы можете вручную написать документ XML с противоречивыми отношениями).

Мы можем изменить наш предыдущий пример «один ко многим», чтобы он выглядел так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@XmlRootElement
@Entity
@Table(name=”ORDER”)
public class Order {
    @Id
    @Column(name=”ORDER_ID”)
    private Integer orderId;
 
    @OneToMany(mappedBy=”order”)
    private List<OrderLineItem> lineItems;
 
    @XmlTransient
    public Integer getOrderId() {
        return orderId;
    }
 
    public void setOrderId(Integer orderId) {
        this.orderId = orderId;
    }
 
    @XmlID
    @XmlElement(name=”orderId”)
    public String getXmlId() {
        return getClass().getName() + getOrderId;
    }
 
    public void setXmlId(String xmlId) {
        //TODO: validate xmlId is of the form <className><Integer>
        setOrderId(Integer.parseInt(
            xmlId.substring( getClass().getName().length() )));
    }
 
    @XmlElement(“lineItem”)
    public List<OrderLineItem> getLineItems() {
        return lineItems;
    }
 
    public void setLineItems(List<OrderLineItem> lineItems) {
        this.lineItems = lineItems;
    }
}
 
@Entity
@Table(name=”ORDER_ITEM”)
public class OrderLineItem {
    @EmbeddedId
    @AttributeOverrides(/*…*/)
    private LineItemKey lineItemKey;
 
    @MapsId(“orderId”)
    @ManyToOne
    private Order order;
 
    @Column(name=”PART_NUM”)
    private String partNumber;
 
    private Integer quantity;
 
    @XmlIDREF
    public Order getOrder() {
        return order;
    }
 
    public void setOrder(Order order) {
        this.order = order;
    }
 
    public Integer getItemNumber() {
        return lineItemKey.getItemNumber();
    }
 
    public void setItemNumber(Integer itemNumber) {
        if (lineItemKey == null) {
            lineItemKey = new LineItemKey();
    }
        lineItemKey.setItemNumber(itemNumber);
    }
 
    // … more getters and setters …
}

Когда мы упорядочиваем Order, мы теперь записываем orderId в виде XML-идентификатора. Вместо того, чтобы создавать свойство order для OrderLineItem @XmlTransient, мы избегаем бесконечной рекурсии, заставляя его писать @XmlIDREF вместо полной структуры Order; Таким образом, обе стороны отношений сохраняются таким образом, чтобы мы могли понять в неурочное время.

Результирующий XML будет выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<?xml version=”1.0” encoding=”UTF-8” standalone=”yes”?>
    <order>
        <orderId>Order37042</orderId>
        <lineItem>
            <lineNumber>1</lineNumber>
            <order>Order37042</order>
            <partNumber>100-02</partNumber>
            <quantity>10</quantity>
        </lineItem>
        <lineItem>
            <lineNumber>2</lineNumber>
            <order>Order37042</order>
            <partNumber>100-17</partNumber>
            <quantity>5</quantity>
        </lineItem>
    </order>

И как маршаллинг, так и демаршаллинг работают как нам хочется. Повторение содержащего значения идентификатора заказа — единственная жалоба, которую мы можем иметь с выходными данными. Мы могли бы уменьшить визуальное воздействие, создав @XmlAttribute, а не @XmlElement; и это еще один случай, когда мы могли бы привести аргумент, что значение не является «реальным контентом», поскольку мы просто добавляем его, чтобы помочь JAXB с демаршаллингом.

Заключительные мысли

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

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

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

Ссылка: JAXB — Перспектива новичка, часть 2 от нашего партнера JCG Марка Адельсбергера в блоге