jaxb_JAXB –新手的观点,第2部分
jaxb
在本系列的第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類。
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 …@XmlTransientpublic LineItemKey getLineItemKey() {return lineItemKey;}public void setLineItemKey(LineItemKey lineItemKey) {this.lineItemKey = lineItemKey;}// “pass-thru” properties to lineItemKeypublic 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使用屬性訪問,則需要將“直通”獲取器標記為@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”)@ManyToOneprivate Order order;@Column(name=”PART_NUM”)private String partNumber;private Integer quantity; @XmlTransientpublic 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)的想法,這似乎值得懷疑。 (然后再次,如果您不讓設置員為您做任何工作,那么通過封裝字段來購買什么?)
@XmlTransientpublic 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”添加設置器邏輯,則該方法將阻止添加的邏輯妨礙您。
但是,以我的觀點,我上面描述的setter邏輯類型只是從外部代碼中隱藏了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…@XmlTransientpublic 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…@XmlTransientpublic 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來包含所有數據。
@XmlRootElementpublic 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;@XmlTransientpublic 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”)@ManyToOneprivate Order order;@Column(name=”PART_NUM”)private String partNumber;private Integer quantity; @XmlIDREFpublic 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
jaxb
總結
以上是生活随笔為你收集整理的jaxb_JAXB –新手的观点,第2部分的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 就不想再用笔记本电脑自带的网卡了就不想再
- 下一篇: oop 类和对象的_实用程序类的OOP替