在本系列的第1部分中,我讨论了使用JAXB和JPA将数据从XML文件加载到数据库中的基础知识。 (如果需要使用JSON而不是XML,则相同的想法应转化为类似Jackson的工具。)该方法是使用共享域对象-即,一组带有描述XML映射和关系映射的注释的POJO。 。
让一个.java文件描述所有数据的表示形式,使编写数据加载器,卸载器和转换器变得容易。 从理论上讲很简单,但随后我提到了理论与实践之间的区别。 从理论上讲,没有区别。
现在,在第2部分中,我们将介绍当要求这两个工具在一个实际的数据模型上协同工作时可能遇到的一些陷阱,以及可能用来克服这些障碍的技术。
名字叫什么?
这第一点可能很明显,但无论如何我都会提到:与依赖于bean属性约定的任何工具一样,JAXB对您的方法名敏感。 您可以通过配置直接字段访问来避免此问题,但是正如我们很快就会看到的那样,您可能有一些想要坚持使用属性访问的原因。
属性名称确定相应元素的默认标记名称(尽管可以用注释覆盖它-在最简单的情况下,例如@XmlElement)。 更重要的是,您的getter和setter名称必须匹配。 当然,最好的建议是让您的IDE生成getter和setter,这样就不会出现拼写错误。
处理@EmbeddedId
假设您要加载一些表示订单的数据。 每个订单可能有多个订单项,每个订单的订单项从1开始依次编号,因此所有订单项的唯一ID将是订单ID和订单项编号的组合。 假设您使用@EmbeddedId方法表示键,则您的订单项可能会这样表示:
@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 …
};
编组和解组代码看起来很像第1部分中 Employee示例中的代码。 请注意,由于LineItemKey类是由OrderLineItem引用的,因此我们不必显式告知JAXBContext。
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结构感到兴奋:
<?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,那么一个选项是更改我们的属性定义(即我们的getter和setter),使OrderLineItem看起来像JAXB的平面对象(并且可能对我们应用程序的其余部分而言是平面对象);这可能是一件好事)。
@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 …
};
请注意,在lineItemKey getter上添加了@XmlTransient; 这告诉JAXB不要映射此特定属性。 (如果JPA使用字段访问,则可以完全删除lineItemKey getter和setter。另一方面,如果JPA使用属性访问,则需要将“直通” getter标记为@Transient。以防止JPA提供程序推断到ORDER_ITEM表的错误映射。)
但是,如果将lineItemKey标记为@ XmlTransient,JAXB将不会知道在拆组期间需要创建嵌入式LineItemKey实例。 在这里,我们通过使“传递”设置器确保实例存在来解决该问题。 JPA至少应在使用字段访问的情况下对此进行容忍。 如果您希望该方法具有线程安全性,则必须同步设置器。 或者,您可以在默认构造函数中创建LineItemKey(如果您确信JPA提供者不会介意的话)。
确保只影响JAXB(没有专用的getter和setter)的另一个选项可能是使用ObjectFactory,该ObjectFactory在返回LineItemKey之前将其注入OrderLineItem中。 但是,据我所知,ObjectFactory必须覆盖包中的所有类,因此,如果您在同一包中有许多简单的域对象和一些复杂的域对象(并且没有其他理由来创建ObjectFactory),那么您可能要避免这种方法。
您可能还想通过在尝试获取返回值之前检查LineITemKey是否存在,来保护直通getter免受空指针异常的影响。
无论如何,我们的XML现在应该如下所示:
<?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类)。
@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建立了一对多的关系。 请注意,出于JPA的目的,我们期望OrderLineItem拥有此关系。
现在,我们将从OrderLineItem中删除@XmlRootElement批注。 (我们不必这样做;注释使该类有资格成为根元素,但不排除也将其用作嵌套元素。但是,如果我们要继续编写仅表示OrderLineItem的XML,则还有一些其他的决定要做,因此我们暂时不做。)
为了使编组满意,我们将OrderLineItem @XmlTransient的Order属性。 这避免了循环引用,否则该循环引用可以解释为无限深的XML树。 (您可能始终不打算在<orderLineItem>元素下嵌入完整的订单详细信息。)
将<orderLineItem>嵌入在<order>元素下,不再需要将<orderId>元素放在<orderLineItem>下。 我们知道从应用程序中其他地方的代码仍然可以使用lineItem.getOrder()。getOrderId()来删除OrderLineItem的orderId属性。
新版本的OrderLineItem如下所示:
@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。 因此,我们可以像这样测试编组:
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);
请注意,我们为每个订单项设置了order属性。 编组时,JAXB不会在乎这一点(因为该属性是@XmlTransient,并且其他任何属性都不取决于它影响的内部状态),但是我们希望保持对象关系的一致性。 如果我们要将订单传递给JPA,那么无法设置order属性将成为一个问题-我们很快就会回到这一点。
我们应该得到这样的输出:
<?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”)放置在Order的getLineItems()方法上来解决此问题。 (然后,如果我们希望将整个订单项元素列表包装在单个<lineItems>元素中,则可以在同一方法上使用@XmlElementWrapper(name =” lineItems”)批注来实现。)
此时,封送测试看起来应该不错,但是如果取消封送订单并要求JPA保留生成的订单行项目对象,则会遇到麻烦。 问题在于解组器未设置OrderLineItem的order属性(出于JPA的目的,该属性拥有Order-to-OrderLineItem关系)。
我们可以通过让Order.setLineItems()遍历订单项列表并在每个订单项上调用setOrder()来解决此问题。 这依赖于JAXB首先构建订单项列表,然后将其传递给setLineItems();。 它在我的测试中起作用,但是我不知道它是否将始终与所有JAXB实现一起起作用。
另一种选择是在解组之后但将对象传递给JPA之前,在每个OrderLineItem上调用setOrder()。 这也许更简单,但是感觉就像是在跳动。 (封装的部分原因是,设置程序应该可以确保您的对象毕竟保持内部一致的状态;那么,为什么要把这种责任转移给对象类之外的代码呢?)
通过简化,在尝试解决此问题时,我将跳过一些我曾想过的更详细的想法。 稍后我们将讨论@XmlID和@XmlIDREF时,我们将讨论另一个解决方案。
财产获取案例
我依靠修改后的二传手来解决前两个问题。 如果您习惯了设置器应该只有一行(this.myField = myArgument)的想法,这似乎值得怀疑。 (再说一次,如果您不让设置员为您做任何工作,那么通过封装字段可以买到什么?)
@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的规则时,您可能会问自己,将原始外键元素存储在reference元素下是否容易(类似于RDBMS通常表示外键的方式)。 您可以,并且编组将不会产生美观的XML。 但是在解组期间或之后,您将负责自行重组关系图。 @XmlID的规则很烦人,但是我发现它们很难适应,避免它们会证明这种努力是合理的。
ID值必须是字符串,并且在XML文档中的所有元素(不仅是给定类型的所有元素)中都必须是唯一的。 这是因为从概念上讲,ID引用是无类型的。 实际上,如果让JAXB从架构构建域对象,它将把您的@XmlIDREF元素(或属性)映射到Object类型的属性。 (但是,当注释自己的域类时,只要引用的类型具有以@XmlID注释的字段或属性,就可以将@XmlIDREF与带类型的字段和属性一起使用。我宁愿这样做,因为这样可以避免不必要的强制转换在我的代码中。)建立关系的键可能不遵循这些规则; 但这没关系,因为您可以创建一个属性(例如,名为xmlId)。
假设我们的每个订单都有一个客户和一个“运送至”地址。 此外,每个客户都有一个帐单邮寄地址列表。 数据库中的两个表(CUSTOMER和ADDRESS)都使用Integer代理键,其顺序从1开始。
在我们的XML中,“客户”和“送货至”地址可以表示为“订单”下的子元素; 但也许我们需要跟踪当前没有任何订单的客户。 同样,帐单地址列表可以表示为“客户”下的子元素列表,但这将不可避免地导致数据重复,因为客户将订单运送到了帐单地址。 因此,我们将使用@XmlID。
我们可以如下定义地址:
@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可以看作是描述<Address> XML元素,而不是地址本身,因此记录起来可能很有意义它作为一个属性。
为了解组工作,我们还必须从setter的xmlId中解析出addressId值。 如果同时保留xmlId属性和addressId属性,则可以避免这种情况; 在这种情况下,xmlId setter可能会丢弃它的值; 但我不喜欢该选项,因为它可以节省相对较少的工作量,并且有可能遇到XMLId和addressId值不一致的XML文档。 (有时,您可能不得不承认文档不一致的可能性,例如您是否坚持恋爱关系的双方,我将在后面讨论。)
接下来,我们将创建客户映射:
@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的处理与地址的处理相同。 我们用@XmlIDREF批注标记了billingAddresses属性,告诉JAXB每个<billingAddress>元素都应包含一个引用地址的ID值,而不是实际的Address元素结构。 以同样的方式,我们将customer和shipToAddress属性添加到Order中,并用@XmlIDREF注释。
此时,所有对客户或地址的引用都被标记为@XmlIDREF。 这意味着尽管我们可以将数据封送为XML,但结果实际上将不包含任何Customer或Address数据。 如果在您解组文档时@XmlIDREF与文档中的@XmlID不对应,则未编组对象上的相应属性将为null。 因此,如果我们真的希望这样做,我们必须创建一个新的@XmlRootElement,其中可以包含我们的所有数据。
@XmlRootElement
public class OrderData {
private List<Order> orders;
private List<Address> addresses;
private List<Customer> customers;
// getters and setters
}
此类与我们数据库中的任何表都不对应,因此它没有JPA批注。 与以前的List-type属性一样,我们的getter可以具有@XmlElement和@XmlElementWrapper批注。 如果我们组装并封送一个OrderData对象,则可能会得到以下内容:
<?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文档)。
我们可以修改前面的一对多示例,使其看起来像这样:
@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 ID。 我们不使用OrderLineItem @XmlTransient的order属性,而是通过写@XmlIDREF而不是完整的Order结构来避免无限递归; 因此,我们可以在解组时理解双方的关系。
生成的XML如下所示:
<?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>
而编组和解组工作都是我们想要的。 重复包含订单ID值是我们可能对输出唯一的抱怨。 我们可以通过使用@XmlAttribute而不是@XmlElement来减少视觉影响。 这是另一种情况,我们可能会认为该值不是“真实内容”,因为我们只是将其放入以帮助JAXB进行编组。
总结思想
如标题所示,我作为JAXB的新手经历了本练习。 这绝不是关于JAXB可以做什么的全面讨论,从我阅读的文档中,我什至说我已经忽略了它的一些最复杂的功能。
但是,我希望这可以作为有用的入门,并可以说明来自bean约定以及与POJO进行友好交互的工具和框架的力量。
我还要重申一点,就是您可以根据自己的意愿使这种技术变得复杂。 因此,了解您的需求真正需要多少复杂度是关键。
翻译自: https://www.javacodegeeks.com/2014/09/jaxb-a-newcomers-perspective-part-2.html