簡單講就是沒有 Primary Key(PK) 的資料,必須依附在有 PK 的資料下。
舉例來說,每個人(Person)有好幾種地址(Address),住家地址、公司地址、戶籍地與出生地等等,這些地址資料只對一個人有意義,脫離這個人就沒有存在的必要,或者說不能與其他人共用,既然不能共用,那就沒必要有 PK,與這個人共存亡。
Person.java & Address.java
@Entity(name = "person") @SuppressWarnings("serial") public class Person implements Serializable { private Integer id; private String name; private List<Address> addressList = new ArrayList<Address>(); /** 供 Spring Form Binding 使用 */ private Map<Integer, Address> addressMap = new HashMap<Integer, Address>(); @Id @GeneratedValue(strategy = GenerationType.AUTO) public Integer getId() { return this.id; } public void setId(Integer id) { this.id = id; } @Column(nullable = false) public String getName() { return this.name; } public void setName(String name) { this.name = name; } @ElementCollection @CollectionTable(name = "address", joinColumns = @JoinColumn(name = "person_id")) public List<Address> getAddressList() { return this.addressList; } public void setAddressList(List<Address> addressList) { this.addressList = addressList; } @Transient public Map<Integer, Address> getAddressMap() { return this.addressMap; } public void setAddressMap(Map<Integer, Address> addressMap) { this.addressMap = addressMap; } } @Embeddable public class Address { private String type; private String country; private String city; private String address; public String getType() { return this.type; } public void setType(String type) { this.type = type; } public String getCountry() { return this.country; } public void setCountry(String country) { this.country = country; } public String getCity() { return this.city; } public void setCity(String city) { this.city = city; } public String getAddress() { return this.address; } public void setAddress(String address) { this.address = address; } }這裡面看似平凡,卻有很多辛酸在。
最奇怪的在於,多了一個 addressMap,這是因為「變動數量」這個需求造成的,稍後說明。
再來是 addressList,以前在使用 Entity 一對多的時候,習慣加上建議都是用 Set 來裝,只有少數狀況會用 List,來到 Component 的世界時,自然的也用 Set 來裝,一方面因為沒有順序的需求,二方面可以少掉一個 IndexColumn,但是,惡夢從此開始...
用 Set 寫好程式之後,執行時發現 log 裡有怪怪的問題。
第一,每次撈出 addressSet 時,Hibernate 都會吐出先 delete 再 insert 的 sql 來,怪了,我只是撈資料,又不是改資料。
這個問題,睜一隻眼閉一隻眼,還可以裝作沒看到,但是另一個問題就不行了。
第二,一開始 Address 的每個欄位都有輸入資料,到後來要測試欄位驗證時,就刻意一些欄位不輸入資料,接著,霹靂星球就爆炸了!
當 Address 有些 Null 欄位時,每讀取一次,Address 資料數量就增加一倍,啊,我只是讀取,你是想怎樣啦!
後來發現 Hibernate 在第一點說的先 delete 再 insert 時,居然是用所有的欄位去 delelte。
delete from address where person_id = ? and country = ? and city = ? and address = ?;這這這...欄位有 Null 時,當然不能用 =,要用 IS NULL 啊,Hibernate 你是在起什麼肖!
不提這段,你為什麼不用 person_id 去抓就好?
delete from address where person_id = ?;上窮碧落下黃泉,腦細胞死掉幾百萬個之後,終於發現,List 是我的銀彈,不是錢,是可以一槍斃命的銀子彈,把 Set 改成 List,所有的肖症頭都不藥而癒了,不再先 delete 再 insert,也用 FK 去刪資料了。
再來看看好學生 Spring MVC 發生什麼事了。
因為 Address 數量可以變動,在網頁上使用 Javascript 動態去增減 Address 區塊,這變動的資料要怎麼 bind 到 Person 呢?
一開始用 Set 就掛了,因為 Spring Form 目前無法 bind 資料到 Set 裡(有可能是我沒找到),只能 bind 到 Array 或 List 裡。
使用中括弧,加上索引值,就可以輕鬆 bind 到 List 裡。
<input type="text" name="addressList[1].country" size="4" maxlength="4" value="<c:out value="${a.country}"/>" />事情沒這麼美好,當我用 Javascript 砍掉第一筆,留下第二筆,送出表單時,慘案就發生了。
因為 Spring MVC 只收到 addressList[1],沒收到 addressList[0],結果咧?我得到一個有兩筆 Address 的 List,第二筆是正確的,第一筆是所有欄位都是 null 的 Address,哇咧?翻桌!
Array 和 List 這類有索引值的都是一個樣!
那現在是要怎樣?Hibernate 不讓我用 Set,Spring MVC 也不讓我用 Set,用 Array 和 List 會跳格!
只剩 Map 了,Spring MVC 可以 bind 到 Map,也不會有跳格的問題。
最後 Google 說,就辛苦一點,自己轉一轉囉...
在 Spring MVC 用 Map 去 bind,然後轉成 List 給 Hibernate 去存,取出來時,再將 List 轉成 Map,交給 Spring MVC 去呈現。
複製 AddressList 到 Map,供 Sping Form Binding。
private void cvtAddressList2Map(Model model, Person obj) { Map<Integer, Address> map = new HashMap<Integer, Address>(); int i = 0; for (Address c : obj.getAddressList()) { map.put(i++, c); } model.addAttribute("addressMap", map); }第一個重點,Map 的 key 用遞增的數值,然後 Javascript 增加 Address 區塊時,再接著數值往上加,千萬避免使用到相同的數值,以免 bind 資料時發生覆蓋。
第二個重點,不要將 Address 裝到 Person 的 addressMap 裡,Person 的 addressMap 要保持空的,因為若裝到 Person 的 addressMap 裡,假設裝了三筆 Address,索引值分別為 0、1 與 2,然後頁面操作時,若將 0 與 1 的 Address 刪除,另外新增第四筆 Address,索引值為 3,送進來 bind 後,你會發現,總共有四筆 Address 了,因為 Spring MVC 就是拿 Person 的 addressMap 來裝資料,它收到索引值為 2 與 3 的地址,卻不知道 0 與 1 被刪除,所以就將 2 與 3 的 Address 裝進去,原本的 2 被覆蓋,但是 0 與 1 卻還在,所以得到四筆 Address。
再來,從 Spring MVC bind 好的 Person addressMap 來復原 AddressList。
private void cvtAddressMap2List(Person obj, boolean copyAddressMap) { if (copyAddressMap) { // 記得先淨空 obj.getAddressList().clear(); obj.getAddressList().addAll(obj.getAddressMap().values()); } }淨空後裝進來就好了。
再來看一些 JSP 程式,先倒出已有的地址資料。
<c:forEach var="c" items="${addressMap}" varStatus="status"> <c:set var="c" value="${c.value}"/> <tr class="addressTR addressTR<c:out value="${status.index}"/>"> <td valign="top"> <input type="text" name="addressMap[<c:out value="${status.index}"/>].country" class="address_no" size="4" maxlength="4" value="<c:out value="${c.country}"/>" /> </td> <td valign="top"> <input type="text" name="addressMap[<c:out value="${status.index}"/>].city" class="address_winner" size="5" maxlength="4" value="<c:out value="${c.city}"/>" /> </td> <td valign="top"> <input type="text" name="addressMap[<c:out value="${status.index}"/>].address" class="address_works" size="4" maxlength="4" value="<c:out value="${c.address}"/>" /> </td> <td valign="top"> <input type="button" class="button" value="DEL" onclick="deleteAddressTR('addressTR<c:out value="${status.index}"/>'); " /> </td> </tr> </c:forEach>再來是用來新增 Address 區塊的範本。
<tr id="addressTemplate" style="display: none; "> <td valign="top"> <input type="text" name="addressMap[CTIDX].country" class="address_country" size="4" maxlength="4"/> </td> <td valign="top"> <input type="text" name="addressMap[CTIDX].city" class="address_city" size="4" maxlength="4"/> </td> <td valign="top"> <input type="text" name="addressMap[CTIDX].address" class="address_address" size="4" maxlength="4"/> </td> <td valign="top"> <input type="button" class="button" value="DEL" onclick="deleteAddressTR('addressTRCTIDX'); " /> </td> </tr>最後是 Javascript。
var addressTRCnt; $(function() { // 先紀錄已有的區塊數量 addressTRCnt = $('.addressTR').length; }); // 新增 Address 區塊 function insertAddressTR() { // 取得範本 var html = $('#addressTemplate').html(); // 包起來 html = '<tr class="addressTR">' + html + '</tr>'; // 將關鍵字 CTIDX 置換成新的索引值 html = html.replace(/CTIDX/g, addressTRCnt); // 塞進頁面 $('#addressTemplate').before(html); // 索引值加一 addressTRCnt++; } // 刪除區塊 function deleteAddressTR(clazz) { $('.' + clazz).remove(); } function submitForm(val) { // 最後,送出表單前,記得將範本的 input 都 disable,不然可是會一起送出去的 $('#addressTemplate input').attr('disabled', true); $('#theForm').submit(); }---
---
---
---
---
---
沒有留言:
張貼留言