-
Notifications
You must be signed in to change notification settings - Fork 24
自定义属性表(Custom Table)
在项目设计中,自定义属性表一般指一个领域对象对应着多张数据库表;这些表之间拥有一些相同的字段,同时也拥有一些个性的字段。
例如典型的购物网站中,可能拥有上千种不同的商品,每种商品都有名称、介绍、价格、上架时间、库存、好评等等,同时每个商品也有自己独特的属性,如书籍有ISBN、作者、出版社;服装有颜色、尺码、品牌;电视机有屏幕尺寸、刷新率、usb支持情况等等。这些自定义的属性在购物网站中有着非常高的价值,例如服装必须能够按照尺码查询,这就要求在设计时需要为每种商品都做一张表来存储,以利于查询。按照传统的ORM模型,有一千张表就需要有一千个java领域对象和一千个hbm.xml映射文件,如果按照良好的分层设计,也需要有一千个Dao和Manager。在数据库设计时,我们可以将不同的商品分散到不同的数据库中,1个数据库保持几十个商品表看起来还能忍受,但java代码是的无法分开的(因为商品的评分、评论、物流等等附加设计不可能也分成很多份)。如果依旧采用传统的ORM,领域对象定义可能就会占用您数千个类,项目也就基本失败了。 为了解决这个问题,就需要有一种动态的表与对象映射技术--无论商品有多少种或者存储在多少张表中,只定义1个领域对象,表名称、字段和属性可以动态的获取并完成映射。在领域对象设计时,只需要定义出商品通用的属性,并预留一个集合类(如Map)用于保存在运行时根据当前处理的具体商品而获取的特殊属性即可。如我们对商品对象定义如下:public class Cargo {和平时没什么区别。specialProps的内容在定义是无法确定,因此不做映射。 这就是guzz解决这类问题的动态映射技术,称为自定义属性表,命名为:Custom Table. 继续上面的需求,我们决定为每个商品创建一张表,表的名称为:tb_cargo_商品名称,每种商品表的字段根据此商品的特点单独设计。很显然,先不管每个表的字段怎么样,根据前面Shadow Table的介绍,我们需要定义一个ShadowTableView,根据商品名称(tableCondition)进行分表,如上面的映射中定义的:shadow="org.guzz.test.shop.CargoCustomTableView"。 由于我们的商品类别可能很多,也可能会经常增加,因此最好的办法是将每个商品的特殊属性保存到数据库中,这样管理起来最方便。我们用一个特殊属性表保存商品的个性属性,定义如下:private int id ; private String name ; private String description ; /**how many items left in the store. */ private int storeCount ; private double price ; private Date onlineTime ; /*for the sake of concision, igore other useful properties.*/ /** * special properties owned by this Item only. */ private Map specialProps = new HashMap() ; //get and set methods....</pre> 其中定义了一些通用的属性,同时通过specialProps保存具体商品特殊的属性。 定义完领域对象,在定义表映射关系,如下: <pre class="prettyprint"><?xml version="1.0"?>
<!DOCTYPE guzz-mapping PUBLIC "-//GUZZ//GUZZ MAPPING DTD//EN" "http://www.guzz.org/dtd/guzz-mapping.dtd"> <guzz-mapping> <class name="org.guzz.test.shop.Cargo" table="tb_cargo" shadow="org.guzz.test.shop.CargoCustomTableView"> <id name="id" type="int"> <generator class="native" /> </id>
<property name="name" type="string" column="name" /> <property name="description" type="string" column="description" /> <property name="storeCount" type="int" column="storeCount" /> <property name="price" type="double" column="price" /> <property name="onlineTime" type="datetime" column="onlineTime" /> </class>
</guzz-mapping>
/**
-
special properties' declarations of our cargoes. */ public class SpecialProperty {
/**unqiue id for management.*/ private int id ;
/**which cargo this property belongs to.*/ private String cargoName ;
/**property name of the cargo (used in java).*/ private String propName ;
/**the column name in the table of database to store this propety.*/ private String colName ;
/**
-
dataType. take this as the 'type' property in hbm.xml file. */ private String dataType ;
//get and set methods...
定义ORM映射:
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="org.guzz.test.shop.SpecialProperty" table="tb_s_property"> <id name="id" type="int"> <generator class="native" /> </id>
<property name="cargoName" type="string" column="cargoName" /> <property name="propName" type="string" column="propName"> </property> <property name="colName" type="string" column="colName"> </property> <property name="dataType" type="string" column="dataType" /> </class> </hibernate-mapping>
到此,只要框架能够根据这个表的内容动态算出映射关系,就大功告成了。
与ShadowTableView动态计算表名称的方法一样,guzz也需要项目告诉他如何计算映射关系,而org.guzz.orm.CustomTableView就是做这个的。CustomTableView继承自ShadowTableView,配置方法与ShadowTableView一样,直接使用shadow属性配置即可,可以理解为是一类特殊的ShadowTableView。CustomTableView定义如下:/**
-
-
Interface for mapping dynamic tables with runtime-determinated columns(different tables and differect tables' columns mapped to a same domain class).
-
<p>
-
Only {@link POJOBasedObjectMapping} supports this feature.
-
</p> */ public interface CustomTableView extends ShadowTableView{
/**
- Set ObjectMapping configured in the hbm.xml file. This method only run one time on startup. */ public void setConfiguredObjectMapping(POJOBasedObjectMapping configuredMapping) ;
/**
- Get the runtime real ObjectMapping for the given tableCondition.
- <p>The invoker won't cache the returned {@link POJOBasedObjectMapping}, so the implementor should do the cache for performance critical system.
- </p> */ public POJOBasedObjectMapping getRuntimeObjectMapping(Object tableCondition) ;
} 开发者需要实现此接口,并通过实现getRuntimeObjectMapping(Object tableCondition) 方法创建并返回当前运行的表映射关系。表映射关系创建的依据也是tableCondition,与表分切相同。 创建映射是一项麻烦的工作,需要了解guzz运行机理才行,不过guzz提供了一个抽象类来简化这项工作。实现者一般只要继承org.guzz.orm.AbstractCustomTableView即可。 对于购物商品,我们的实现如下:
/**
-
Key class. Mapping different cargo to different table and different properties.
-
<p>We define cargo's name to be the table condition.</p> */ public class CargoCustomTableView extends AbstractCustomTableView {
/**
- Lookup mapping every time is expensive, so we cache it.
- <p>
- In a real product system, you should replace it with a real cache, and refresh it on special properties' changing.
- <br>
- The real cache can be started and shutted down in the {@link #startup()} and {@link #shutdown()} methods(don't forget to call super).
- <p/> */ private Map orms_cache = new HashMap() ;
public void setCustomPropertyValue(Object beanInstance, String propName, Object value) { Cargo c = (Cargo) beanInstance ;
//store the special property in the map. c.getSpecialProps().put(propName, value) ;
}
public Object getCustomPropertyValue(Object beanInstance, String propName) { Cargo c = (Cargo) beanInstance ;
//return special property from the map. return c.getSpecialProps().get(propName) ;
}
public POJOBasedObjectMapping getRuntimeObjectMapping(Object tableCondition) { String cargoName = (String) tableCondition ; Assert.assertNotEmpty(cargoName, "tableCondition can't be null, we don't have any default table to store un-categoried cargoes.") ;
POJOBasedObjectMapping map = (POJOBasedObjectMapping) this.orms_cache.get(cargoName) ; if(map == null){ //create it map = super.createRuntimeObjectMapping(cargoName) ; //cache this.orms_cache.put(cargoName, map) ; } return map ;
}
protected void initCustomTableColumn(POJOBasedObjectMapping mapping, Object tableCondition) { String cargoName = (String) tableCondition ;
//read the special properties from the master database. ReadonlyTranSession session = this.guzzContext.getTransactionManager().openNoDelayReadonlyTran() ; try{ SearchExpression se = SearchExpression.forLoadAll(SpecialProperty.class) ; se.and(Terms.eq("cargoName", cargoName)) ; List properties = session.list(se) ; for(int i = 0 ; i < properties.size() ; i++){ SpecialProperty sp = (SpecialProperty) properties.get(i) ; //create the TableColumn with the super helper method. TableColumn tc = super.createTableColumn(mapping, sp.getPropName(), sp.getColName(), sp.getDataType(), null) ; //add it to the mapping with the super helper method too. super.addTableColumn(mapping, tc) ; } }finally{ session.close() ; }
}
public String toTableName(Object tableCondition) { String cargoName = (String) tableCondition ; Assert.assertNotEmpty(cargoName, "tableCondition can't be null, we don't have any default table to store un-categoried cargoes.") ;
//different tableConditions mapped to different tables. return "tb_cargo_" + cargoName;
}
} setCustomPropertyValue和getCustomPropertyValue用于保存和读取自定义属性到领域对象中,在这里我们将他放到域对象的specialProps中。 getRuntimeObjectMapping 是麻烦的创建映射方法,但可以通过调用父类的createRuntimeObjectMapping()完成。 initCustomTableColumn 是父类在创建映射时回调的方法,用于初始化自定义属性。我们读取SpecialProperty的表,然后调用父类的createTableColumn方法生成字段并加入到映射中(addTableColumn方法)。 toTableName ShadowTableView接口的方法,用于计算表名称。
guzz不处理表结构,因此我们需要创建表并加入一些原始数据。假设我们有2个商品,分别为图书和十字绣。 图书包含3个特殊属性:ISBN, author and publisher;十字绣包含4个特殊属性:gridNum, backColor, size and brand 这样我们就需要3个表:自定义属性表,图书表和十字绣表。下面初始化表://create special property table in H2 database. executeUpdate(H2Conn, "drop table if exists tb_s_property") ; executeUpdate(H2Conn, "create table tb_s_property(id int not null AUTO_INCREMENT primary key , cargoName varchar(32), propName varchar(32), colName varchar(32), dataType varchar(32))") ;//add book and cross-stitch's special properties' declarations into tb_s_property. executeUpdate(H2Conn, "insert into tb_s_property(cargoName, propName, colName, dataType) values('book', 'ISBN','ISBN','string')") ; executeUpdate(H2Conn, "insert into tb_s_property(cargoName, propName, colName, dataType) values('book', 'author','author','string')") ; executeUpdate(H2Conn, "insert into tb_s_property(cargoName, propName, colName, dataType) values('book', 'publisher','publisher','string')") ; executeUpdate(H2Conn, "insert into tb_s_property(cargoName, propName, colName, dataType) values('crossStitch', 'gridNum','gridNum','int')") ; executeUpdate(H2Conn, "insert into tb_s_property(cargoName, propName, colName, dataType) values('crossStitch', 'backColor','backColor','string')") ; executeUpdate(H2Conn, "insert into tb_s_property(cargoName, propName, colName, dataType) values('crossStitch', 'size','size','string')") ; executeUpdate(H2Conn, "insert into tb_s_property(cargoName, propName, colName, dataType) values('crossStitch', 'brand','brand','string')") ; //create table for cargo book and cargo cross-stitch. //we know the rule is : return "tb_cargo_" + cargoName; //cargo book: executeUpdate(H2Conn, "drop table if exists tb_cargo_book") ; executeUpdate(H2Conn, "create table tb_cargo_book(id int not null AUTO_INCREMENT primary key , name varchar(128), description text, storeCount int(11), price double, onlineTime datetime" + ", ISBN varchar(64) not null" + ", author varchar(64)" + ", publisher varchar(64)" + ")") ; //cargo cross-stitch: executeUpdate(H2Conn, "drop table if exists tb_cargo_crossStitch") ; executeUpdate(H2Conn, "create table tb_cargo_crossStitch(id int not null AUTO_INCREMENT primary key , name varchar(128), description text, storeCount int(11), price double, onlineTime datetime" + ", gridNum int(11) not null" + ", backColor varchar(64)" + ", size varchar(64)" + ", brand varchar(64)" + ")") ;</pre> 我们创建了3张表,并在自定义属性表中写入了图书和十字绣的表结构信息。 然后把新的域对象配置到guzz中。在guzz.xml中添加: <pre class="prettyprint"> <business name="cargo" dbgroup="default" file="classpath:org/guzz/test/shop/Cargo.hbm.xml" /> <business name="sp" dbgroup="default" file="classpath:org/guzz/test/shop/SpecialProperty.hbm.xml" /></pre> <h2> <a name="案例说明:插入图书"></a>案例说明:插入图书<a href="#案例说明:插入图书" class="section_anchor"></a> </h2> 接下来的过程和上一节的ShadowTableView非常类似,设置属性,声明tableCondition,插入/修改/删除或者查询。 <pre class="prettyprint"> WriteTranSession session = tm.openRWTran(true) ; Cargo book = new Cargo() ; book.setName("book") ; book.setDescription("nice book ") ; book.setPrice(33.56) ; book.setStoreCount(10) ; Date now = new Date() ; book.setOnlineTime(now) ; //ISBN, author and publisher book.getSpecialProps().put("ISBN", "isbn-bbb-1") ; book.getSpecialProps().put("author", "not me") ; book.getSpecialProps().put("publisher", "wolf") ; Guzz.setTableCondition("book") ; session.insert(book) ; //插入到tb_cargo_book表中</pre> <h2> <a name="案例说明:查询十字绣"></a>案例说明:查询十字绣<a href="#案例说明:查询十字绣" class="section_anchor"></a> </h2> <pre class="prettyprint"> //SearchExpression.setTableCondition条件优先级高于Guzz.setTableCondition。 se = SearchExpression.forClass(Cargo.class) ; se.and(Terms.eq("brand", "湘湘绣铺")); //设置一个自定义属性的查询条件 se.setTableCondition("crossStitch") ; //设置tableCondition List m_css = session.list(se) ; //查询tb_cargo_crossStitch</pre> 如果在jsp中,也可以用g标签更简洁的实现查询。查询条件支持自定义属性,如: <pre class="prettyprint"><g:list var="m_css" business="cargo" tableCondition="crossStitch" limit="brand=湘湘绣铺" /></pre> <h2> <a name="案例说明:多表查询"></a>案例说明:多表查询<a href="#案例说明:多表查询" class="section_anchor"></a> </h2> 我们的数据被分切成了很多小表,但某些情况下我们需要对多个表进行联合查询,如获取所有价格高于100.00元的商品。 <p> 在guzz中进行sql查询时,必须首先确定对象的ORM关系,而对于自定义属性表,ORM是通过上面介绍的public POJOBasedObjectMapping getRuntimeObjectMapping(Object tableCondition) 方法动态获取的。tableCondition在我们上面的实现中,用于判定每一张小表的映射;对于多张小表,tableCondition需要设置一个特殊的值做标记做区分,例如"all"。 修改getRuntimeObjectMapping以支持"all"条件。</p> <pre class="prettyprint"> ..... public POJOBasedObjectMapping getRuntimeObjectMapping(Object tableCondition) { String cargoName = (String) tableCondition ; if("all".equals(cargoName)){ return this.getConfiguredMapping() ; } Assert.assertNotEmpty(cargoName, "tableCondition can't be null, we don't have any default table to store un-categoried cargoes.") ; .....</pre> 这里,我们定义如果tableCondition为"all",使用默认配置的OM(hbm.xml或annotation定义的原始OM,不包含每种商品自定义的属性)。 实现所有高于100.00元商品查询: <pre class="prettyprint"> ReadonlyTranSession session = tm.openDelayReadTran() ; Guzz.setTableCondition("all") ; //list all cargoes priced over 100.00 bucks String sql="select c.* from (select @id, @name, @storeCount from tb_cargo_book where @price >= :param_price" + " union all select @id, @name, @storeCount from tb_cargo_crossstitch where @price >= :param_price) as c "; CompiledSQL cs = tm.getCompiledSQLBuilder().buildCompiledSQL(Cargo.class, sql) ; cs.addParamPropMapping("param_price", "price") ; List cargoes = session.list(cs.bind("param_price", 100.00), 1, 1000).size() ; session.close();</pre> 对于表名称,只能使用数据库中的真实表名称,而对于公共(默认)属性,可以继续使用@属性名来代替字段名,当然也可以直接使用数据库字段名。 <h2> <a name="接口中方法调用顺序"></a>CustomTableView接口中方法调用顺序<a href="#接口中方法调用顺序" class="section_anchor"></a> </h2> <ul> <li>1. 解析配置文件,生成类实例</li> </ul> <ul> <li>2. 调用setConfiguredTableName(String tableName)</li> </ul> <ul> <li>3. 调用setConfiguredObjectMapping(POJOBasedObjectMapping configuredMapping)</li> </ul> <ul> <li>4. 如果类实现了GuzzContextAware接口,调用setGuzzContext(GuzzContex guzzContext)</li> </ul> <ul> <li>5. 调用startup()</li> </ul> <ul> <li>6. 如果类实现了ExtendedBeanFactoryAware,调用setExtendedBeanFactory(ExtendedBeanFactory factory)</li> </ul> <ul> <li>7. 对外服务……</li> </ul> <ul> <li>8. guzz关闭时调用shutdown()</li> </ul> <h2> <a name="注意事项"></a>注意事项<a href="#注意事项" class="section_anchor"></a> </h2> <ul> <li>1. 自定义属性表不支持ibatis模式配置的映射,也就是说基础的映射必须在hbm.xml中或是通过annotation声明的。 </li> </ul> <ul> <li>2. 对于guzz.xml中配置的复杂sql语句,可以将orm设置为领域对象的名字(business name),借用hbm.xml的映射。</li> </ul> <ul> <li>3. 与ShadowTableView一样,自动扩展出的表需要DBA手工创建,包括表结构索引等等。</li> </ul> </div> </div> </td> <tr> </table>