JAXB – 新手视角,第二部分





5.00/5 (1投票)
在本系列的第一部分中,我讨论了使用JAXB和JPA将XML文件中的数据加载到数据库中的基本知识。(如果需要JSON而不是XML,则相同的思路应适用于Jackson这样的工具。)该方法是使用共享的域对象,即一个单一的[…]
在本系列第一部分中,我讨论了使用JAXB和JPA将XML文件中的数据加载到数据库中的基本知识。(如果需要JSON而不是XML,则相同的思路应适用于Jackson这样的工具。)该方法是使用共享的域对象,即一套POJO,其注解描述了XML映射和关系映射。
让一个.java文件描述数据的所有表示形式,可以轻松编写数据加载器、卸载器和转换器。理论上很简单,但我暗示了理论与实践之间的区别。理论上,没有区别。
现在在第二部分,我们将探讨当您要求这两个工具协同工作处理真实的数据模型时可能遇到的一些棘手问题,以及您可以采用的技术来克服这些障碍。
名称的含义?
第一点可能显而易见,但我还是要提一下:与任何依赖于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 … };
मार्शल化和解 मार्शल化代码看起来会与第一部分中的Employee示例非常相似。请注意,我们不必显式地告诉JAXBContext关于LineItemKey类,因为它被OrderLineItem引用。
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实例。在这里,我们通过使“透传”setter确保实例存在来解决这个问题。JPA应该至少容忍这一点,如果它使用字段访问的话。如果您希望这种方法是线程安全的,您将不得不同步setter。作为替代方案,您可以在默认构造函数中创建LineItemKey(如果您确信JPA提供程序不会介意)。
另一种肯定只会影响JAXB(没有专用getter和setter)的选择是使用ObjectFactory,它在返回OrderLineItem之前将LineItemKey注入其中。但是,据我所知,ObjectFactory必须涵盖一个包中的所有类,因此如果您在同一个包中有许多简单的域对象和少数复杂的对象(并且没有其他理由创建ObjectFactory),那么您可能想避免这种方法。
您还可能希望通过检查LineITemKey是否存在来防止“透传”getter出现NullPointerException,然后再尝试获取返回值。
无论如何,我们的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的一对多关系。请注意,我们期望OrderLineItem在JPA方面拥有此关系。
暂时我们将从OrderLineItem中移除@XmlRootElement注解。(我们不必这样做;注解使类有资格成为根元素,但并不排除将其用作嵌套元素。但是,如果我们想继续编写仅表示OrderLineItem的XML,那么我们将需要做出一些额外的决定,所以我们暂时将其推迟。)
为了让marshaller保持愉快,我们将OrderLineItem的Order属性设为@XmlTransient。这避免了可能被解释为无限深XML树的循环引用。(您可能也不希望在<orderLine项>元素下嵌入完整的订单详细信息。)
在<order>元素下嵌入<orderLine项>后,就不需要再在<orderLine项>下放置<orderId>元素了。我们从OrderLineItem中移除了orderId属性,并知道应用程序中的其他代码仍然可以使用lineItem.getOrder().getOrderId()。
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 … };
我们需要将Order类告知JAXBContext。在这种情况下,它不需要被明确告知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,并且没有其他属性依赖于它影响的内部状态),但我们希望保持对象关系的`一致性`。如果我们把order传递给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>标签(因为那是属性名称),这有点偏离。我们可以通过在Order的getLineItems()方法上添加@XmlElement(name=”lineItem”)来解决这个问题。(如果我们随后希望所有行项目元素都包装在一个<lineItems>元素中,我们可以通过在同一方法上添加@XmlElementWrapper(name=”lineItems”)注解来实现。)
此时, मार्शल化测试应该看起来相当不错,但如果我们解 मार्शल一个订单并要求JPA持久化生成的订单行项目对象,我们将遇到麻烦。问题在于,解 मार्शल器没有设置OrderLineItem的order属性(该属性在JPA方面拥有Order到OrderLineItem的关系)。
我们可以通过让Order.setLineItems()遍历Employee列表并对每个Employee调用setOrder()来解决这个问题。这依赖于JAXB先构建行项目列表,然后将其传递给setLineItems();在我进行的测试中是有效的,但我不知道它是否总是适用于所有JAXB实现。
另一种选择是在解 मार्शल之后,但在将对象传递给JPA之前,对每个OrderLineItem调用setOrder()。这也许更 foolproof,但感觉有点hack。(毕竟,封装的一部分意义在于您的setter应该能够确保您的对象保持内部一致的状态;那么为什么要将这种责任推卸给对象类之外的代码呢?)
为了简单起见,我将跳过一些我在试图解决这个问题时构思的更复杂的想法。我们将在稍后讨论@XmlID和@XmlIDREF时看一个更通用的解决方案。
属性访问的理由
我已经依赖修改过的setter来解决前两个问题。如果您习惯于setter只有一行(this.myField = myArgument)的观念,这可能看起来有些可疑。(话说回来,如果您不让您的setter为您做任何工作,那么封装您的字段又能得到什么?)
@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”属性,所以如果您觉得不得不添加setter逻辑“仅仅是为了JAXB”,这种方法将使添加的逻辑不碍事。
在我看来,上述类型的setter逻辑仅仅是向外部代码隐藏了Bean属性的实现细节。我认为JAXB在这些情况下鼓励了更好的抽象。
如果您将JAXB视为序列化对象内部状态的方法,那么字段访问可能更可取。(无论如何,我听说过使用JPA进行字段访问的论点。)但最终,您希望工具为您完成工作。将JAXB视为构建(或记录)对象的外部机制可能更务实。
相关对象:一对一,多对多
一对多关系工作正常后,一对一关系似乎应该很简单。然而,虽然一对多关系通常适合XML的层次结构(“多”是“一”的子项),但一对一关系中的对象通常只是同等地位的;因此,在XML表示中将一个元素嵌入另一个元素的选择充其量是任意的。
多对多关系对层次模型提出了更大的挑战。如果您有一个更复杂的`关系网络`(无论其`基数`如何),可能没有一个直接的方法可以将对象排列成树形。
在探索通用解决方案之前,最好在此处暂停并问自己是否*需要*通用解决方案。我们的项目需要加载两种符合父子关系的对象,因此我之前描述的技术就足够了。您可能根本不需要在XML中持久化整个对象模型。
但是,如果您确实需要一种方法来建模不符合父子模式的关系,您可以使用@XmlID和@XmlIDREF来做到。
当您学习@XmlID的使用规则时,您可能会问自己,是否直接将引用元素下的原始外键元素存储起来(类似于RDBMS通常表示外键的方式)会更容易?您可以这样做,并且marshaller在生成漂亮的XML时不会有问题。但然后,在解 मार्शल期间或之后,您将负责自己重新组合关系图。@XmlID的规则很烦人,但我认为它们并不难适应,以至于避免它们会使那种努力变得不值得。
ID值必须是字符串,并且在您的XML文档的所有元素中(不仅仅是给定类型的元素)必须是唯一的。这是因为概念上ID引用是无类型的;事实上,如果您让JAXB从模式构建您的域对象,它会将您的@XmlIDREF元素(或属性)映射到Object类型的属性。(然而,当您注解自己的域类时,您允许使用@XmlIDREF与类型化的字段和属性,只要引用的类型有一个被注解为@XmlID的字段或属性。我更喜欢这样做,因为它避免了代码中的不必要类型转换。)您的关系的键可能不符合这些规则;但没关系,因为您可以创建一个(例如命名为xmlId的)属性来做到这一点。
假设我们的每个订单都有一个Customer和一个“ship to”Address。此外,每个Customer都有一个账单地址列表。数据库中的两个表(CUSTOMER和ADDRESS)都使用Integer代理键,序列从1开始。
在我们的XML中,Customer和“ship to”Address可以表示为Order下的子元素;但也许我们需要跟踪没有订单的Customers。同样,账单地址列表可以表示为Customer下的子元素列表,但这不可避免地会导致数据重复,因为客户的订单会被运送到他们的账单地址。所以我们改用@XmlID。
我们可以这样定义Address
@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中将addressId值从xmlId中解析出来。如果我们同时持久化xmlId属性和addressId属性,我们可以避免这一点;在这种情况下,xmlId setter可以丢弃其值;但我倾向于不这样做,因为它节省的努力相对较少,并且可能遇到xmlId和addressId值不一致的XML文档。(有时您可能不得不承认文档不一致的可能性——例如,如果您持久化了关系的双方,我稍后会谈到这一点。)
接下来,我们将创建Customer映射
@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 … }
Customer的xmlId处理方式与Address相同。我们将billingAddresses属性标记为@XmlIDREF注解,告诉JAXB每个<billingAddress>元素应该包含一个引用Address的ID值,而不是实际的Address元素结构。同样,我们将customer和shipToAddress属性添加到Order中,并用@XmlIDREF注解。
此时,对Customer或Address的每一个引用都已标记为@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注解。我们的getter可以像以前的List类型属性一样带有@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的order属性设为@XmlTransient,我们通过让它写入@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交互的工具和框架所带来的强大功能。
我还要重申一点,您可以使一种技术像您愿意的那样复杂;因此,了解您的需求实际需要多少复杂性是关键。
– Mark Adelsberger, asktheteam@keyholesoftware.com