自定義數據類型的數據庫映射方案

基礎數據類型,如String、Integer、Date、Boolean等它們可以很方便的映射到數據庫:

import grails.persistence.Entity @Entity class MyEntity { String code String name static constraints = { code(unique: true, minSize: 4, maxSize: 4) name(blank: false, maxSize: 255) } }

這些基礎數據類型是JAVA提供的語言級的,它沒有語意。

比如要表達一個身份證號碼:它有長度限制:15位或18位;還有規則限制;還能從身份證號碼中提取出地址、性別、出生日期、年齡等信息。這些信息用一個String是無法表達,需要用類來描述:

class IDNumber{ String idNumber Address address InsDate birthday Gender gender IDNumber() {} IDNumber(val) { if (val.length() == 15) { val = to18IdNumber(val) } if (val.length() != 18) { throw new IllegalArgumentException("不是身份證格式") } this.idNumber = val return } def getAddress() { if (address) return address else return address = parseAddress() } def getBirthday() { if (birthday) return birthday else return birthday = parseBirth() } def getGender() { if (gender) return gender else return gender = parseGender() } def parseBirth() { ... } }

這個類裏面最核心的就是String idNumber身份證號碼,其他屬性都是暫存的臨時數據,可以從身份證號碼裏解析出來。如果想把這個類映射到數據庫中,現在只能映射成一個table,但映射成table又不合理,最好是能映射成一列:

@grails.persistence.Entity class PersonInfo { String name IDNumber idNumber }

現在這樣顯然是不能達到這個目標的。

Hibernate提供了多種實現自定義類型的方法:

1、實現org.hibernate.usertype.UserType

2、實現org.hibernate.usertype.CompositeUserType

3、實現org.hibernate.usertype.UserCollectionType

4、實現org.hibernate.usertype.EnhanceUserType

通過實現這些接口,可以將自定義數據類型映射成數據庫列。

UserType可以映射成單列,CompositeUserType可以映射成多列。

看個例子:

class MyString extends InsDataType implements UserType{ String value @Override void buildData(val) { if (val instanceof MyString) { value = val.value return } if (val == null) value = null else if (val instanceof String) value = val else if (val instanceof Number) value = String.valueOf(val) else value = val.toString() return } static MyString from(val) { if (val instanceof MyString) return val MyString data = new MyString() data.build(val) return data } public String toString() { return value } int[] sqlTypes() { return [Types.VARCHAR] } Class returnedClass() { return MyString } boolean equals(Object x, Object y) { MyString mx, my if (x instanceof String) mx = MyString.from(x) if (x instanceof MyString) mx = x if (y instanceof String) my = MyString.from(y) if (y instanceof MyString) my = y if (mx?.value == my?.value) return true return false } int hashCode(Object x) { return ((MyString) x)?.value?.hashCode() } Object nullSafeGet(ResultSet rs, String[] names, Object owner) { if (rs.wasNull()) return null // String stringFromDb = (String) Hibernate.STRING.nullSafeGet(rs, names[0]); String stringFromDb = rs.getString(names[0]); return MyString.from(stringFromDb) } void nullSafeSet(PreparedStatement st, Object value, int index) { if (value == null) st.setNull(index, Types.VARCHAR); else { MyString myString = (MyString) value; st.setString(index, myString.value); // Hibernate.STRING.nullSafeSet(st, myString.value, index); } } Object deepCopy(Object value) { if (!value || !((MyString) value).value) return null return MyString.from(value) } boolean isMutable() { return true } Serializable disassemble(Object value) { return ((MyString) value).value } Object assemble(Serializable cached, Object owner) { return MyString.from(cached) } Object replace(Object original, Object target, Object owner) { return null } }

這樣就可以將MyString映射到數據庫表中的一列了。

@grails.persistence.Entity class MyEntity { MyString name static constraints = { name(nullable: true) } static mapping = { name(length: 10) } }

數據庫結構:

2NWINP{S356}`6I]H2X2]]B

測試保存:

def testSave() { MyEntity entity = new MyEntity(name: MyString.from("hehe")) TestDomain.withTransaction { if (entity.hasErrors() || !entity.save(flush: true)) { println "save error:" + entity.errors } } }

數據庫記錄爲:

[GJD~VB%`60S5KA79RTWLY4

測試查詢:

MyEntity entity = MyEntity.findByName(MyString.from("hehe"))

現在操作自定義的MyString就像操作基礎數據類型一樣了。

 

如果一個數據類型有多個字段要存儲,比如姓名分姓氏和名稱。一種方法是把多個字段合併成一個字段,仍然使用UserType。另一種方法是用CompositeUserType。

class MyChineseName implements CompositeUserType { String familyName String givenName String[] getPropertyNames() { return ["familyName", "givenName"] as String[] } Type[] getPropertyTypes() { return [Hibernate.STRING, Hibernate.STRING] as Type[] } Object getPropertyValue(Object component, int property) { MyChineseName name = (MyChineseName) component; String result; switch (property) { case 0: result = name.familyName; break; case 1: result = name.givenName; break; default: throw new IllegalArgumentException("unknow property: " + property); } return result; } void setPropertyValue(Object component, int property, Object value) { MyChineseName name = (MyChineseName) component; String nameValue = (String) value; switch (property) { case 0: name.familyName = nameValue break; case 1: name.givenName = nameValue break; default: throw new IllegalArgumentException("unknow property: " + property); } } Class returnedClass() { return MyChineseName } boolean equals(Object x, Object y) { if (x == y) return true; if (x == null || y == null) return false; return x.equals(y); } int hashCode(Object x) { return x.hashCode() } Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) { if (rs.wasNull()) return null; String firstname = rs.getString(names[0]); String lastname = rs.getString(names[1]); return new MyChineseName(familyName: firstname, givenName: lastname); } void nullSafeSet(PreparedStatement statement, Object value, int index, SessionImplementor session) { if (value == null) statement.setNull(index, Types.VARCHAR); else { MyChineseName name = (MyChineseName) value; // statement.setString(index, name.familyName); // statement.setString(index + 1, name.givenName); Hibernate.STRING.nullSafeSet(statement, name.familyName, index + 0); Hibernate.STRING.nullSafeSet(statement, name.givenName, index + 1); } } Object deepCopy(Object value) { if (value == null) return null; MyChineseName name = (MyChineseName) value; return new MyChineseName(familyName: name.familyName, givenName: name.givenName); } boolean isMutable() { return false } Serializable disassemble(Object value, SessionImplementor session) { return (Serializable) deepCopy(value); } Object assemble(Serializable cached, SessionImplementor session, Object owner) { return (Serializable) deepCopy(cached); } Object replace(Object original, Object target, SessionImplementor session, Object owner) { return null } }

這樣,MyChineseName就能夠映射成兩列了。如果還像上面一樣定義Entity類,Hibernate仍然無法映射,必須指定type和column

@grails.persistence.Entity class MyEntity { MyChineseName name static constraints = { name(nullable: true) } static mapping = { name type: MyChineseName, { column name: "chineseFamilyName", length: 10 column name: "chineseGivenName", length: 10 } } }

生成的數據庫表結構:

`RI8WCT(_F2YBPLI8WOAU0N

測試保存:

def testSave() { MyEntity entity = new MyEntity(name: new MyChineseName(familyName: "", givenName: "")) TestDomain.withTransaction { if (entity.hasErrors() || !entity.save(flush: true)) { println "save error:" + entity.errors } } println ToStringBuilder.reflectionToString(entity) }

數據庫記錄爲:

NI[1ACND@UM4}5I3}0FWY$H

 

這種方式的麻煩之處在於映射時需要指定type和column。如果用戶不清楚它的實現方式,仍然當作普通的UserType,沒有指定type和column,那麼就會報錯:

Caused by: org.hibernate.MappingException: property mapping has wrong number of columns: com.baoxian.domain.MyEntity.name type: com.baoxian.datatype.MyChineseName

僅僅根據這個錯誤描述就不太好定位了。


可以把多字段組合成一個字符串,從而映射成一個字段來解決:

class MyChineseName implements UserType { String familyName String givenName String toOneString() { return "fn:${familyName};gn:${givenName}" } MyChineseName parseString(String str) { def regular = /(fn|gn):([^;]*)/ def result = str =~ regular def map = [:] result.each { map[it[1]] = it[2] } return new MyChineseName(familyName: map["fn"], givenName: map["gn"]) } int[] sqlTypes() { return [Types.VARCHAR] } Class returnedClass() { return MyChineseName } boolean equals(Object x, Object y) { if (x == y) return true; if (x == null || y == null) return false; return x.equals(y); } int hashCode(Object x) { return x.hashCode() } Object nullSafeGet(ResultSet rs, String[] names, Object owner) { return parseString(rs.getString(names[0])) } void nullSafeSet(PreparedStatement st, Object value, int index) { if (value == null) st.setNull(index, Types.VARCHAR); else { MyChineseName name = (MyChineseName) value st.setString(index, name.toOneString()) } } Object deepCopy(Object value) { if (value == null) return null; MyChineseName name = (MyChineseName) value; return new MyChineseName(familyName: name.familyName, givenName: name.givenName); } boolean isMutable() { return false } Serializable disassemble(Object value) { return (Serializable) deepCopy(value); } Object assemble(Serializable cached, Object owner) { return (Serializable) deepCopy(cached); } Object replace(Object original, Object target, Object owner) { return null } }

生成的數據庫記錄爲:

D1_CP9CU28G(0]}20WATW_Y

 

除了實現CompositeUserType能將一個對象映射成多列,還有一種方法能達到這種效果:embedded。它能將本應映射成兩個table的組合成一個表。

假設有兩個實體關聯如下:

@grails.persistence.Entity class MyComp { String name String code } @grails.persistence.Entity class MyEntity { String keyName MyComp comp static constraints = { comp(nullable: true) } }

這樣,它會在數據庫中映射成兩個表,用ID關聯起來。

T`X[@8@}P@P%$B%[J}F4_1L

因爲關聯表很簡單,能不能組合成一張表呢?可以,用embedded:

class MyComp { String name String code } @grails.persistence.Entity class MyEntity { String keyName MyComp comp static embedded = ['comp'] static constraints = { comp(nullable: true) } }

生成的表爲:

4RQ`N4)J7[A%`~)2UWC(_~3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章