簡單講就是沒有 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();
}
------
---
---
---
---
沒有留言:
張貼留言