Skip to content

自定义属性表(Custom Table)

luocaihua edited this page Apr 30, 2015 · 1 revision

概念介绍

自定义属性表是指一个表拥有不确定的字段。

在项目设计中,自定义属性表一般指一个领域对象对应着多张数据库表;这些表之间拥有一些相同的字段,同时也拥有一些个性的字段。

例如典型的购物网站中,可能拥有上千种不同的商品,每种商品都有名称、介绍、价格、上架时间、库存、好评等等,同时每个商品也有自己独特的属性,如书籍有ISBN、作者、出版社;服装有颜色、尺码、品牌;电视机有屏幕尺寸、刷新率、usb支持情况等等。这些自定义的属性在购物网站中有着非常高的价值,例如服装必须能够按照尺码查询,这就要求在设计时需要为每种商品都做一张表来存储,以利于查询。按照传统的ORM模型,有一千张表就需要有一千个java领域对象和一千个hbm.xml映射文件,如果按照良好的分层设计,也需要有一千个Dao和Manager。在数据库设计时,我们可以将不同的商品分散到不同的数据库中,1个数据库保持几十个商品表看起来还能忍受,但java代码是的无法分开的(因为商品的评分、评论、物流等等附加设计不可能也分成很多份)。如果依旧采用传统的ORM,领域对象定义可能就会占用您数千个类,项目也就基本失败了。 为了解决这个问题,就需要有一种动态的表与对象映射技术--无论商品有多少种或者存储在多少张表中,只定义1个领域对象,表名称、字段和属性可以动态的获取并完成映射。在领域对象设计时,只需要定义出商品通用的属性,并预留一个集合类(如Map)用于保存在运行时根据当前处理的具体商品而获取的特殊属性即可。如我们对商品对象定义如下:
public class Cargo {
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">&lt;?xml version=&quot;1.0&quot;?&gt;

<!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>

    &lt;property name=&quot;name&quot; type=&quot;string&quot; column=&quot;name&quot; /&gt;
    &lt;property name=&quot;description&quot; type=&quot;string&quot; column=&quot;description&quot; /&gt;
    &lt;property name=&quot;storeCount&quot; type=&quot;int&quot; column=&quot;storeCount&quot; /&gt;
    &lt;property name=&quot;price&quot; type=&quot;double&quot; column=&quot;price&quot; /&gt;
    &lt;property name=&quot;onlineTime&quot; type=&quot;datetime&quot; column=&quot;onlineTime&quot; /&gt;
&lt;/class&gt;

</guzz-mapping>

和平时没什么区别。specialProps的内容在定义是无法确定,因此不做映射。 这就是guzz解决这类问题的动态映射技术,称为自定义属性表,命名为:Custom Table.

案例说明:购物网站

继续上面的需求,我们决定为每个商品创建一张表,表的名称为:tb_cargo_商品名称,每种商品表的字段根据此商品的特点单独设计。很显然,先不管每个表的字段怎么样,根据前面Shadow Table的介绍,我们需要定义一个ShadowTableView,根据商品名称(tableCondition)进行分表,如上面的映射中定义的:shadow="org.guzz.test.shop.CargoCustomTableView"。 由于我们的商品类别可能很多,也可能会经常增加,因此最好的办法是将每个商品的特殊属性保存到数据库中,这样管理起来最方便。我们用一个特殊属性表保存商品的个性属性,定义如下:
/**
  • 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>

      到此,只要框架能够根据这个表的内容动态算出映射关系,就大功告成了。

      案例说明:CustomTableView

      与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(&quot;cargoName&quot;, cargoName)) ;
     	
     	List properties = session.list(se) ;
     	
     	for(int i = 0 ; i &lt; 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 &quot;tb_cargo_&quot; + 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&#x27;s special properties&#x27; declarations into tb_s_property.
	executeUpdate(H2Conn, &quot;insert into tb_s_property(cargoName, propName, colName, dataType) values(&#x27;book&#x27;, &#x27;ISBN&#x27;,&#x27;ISBN&#x27;,&#x27;string&#x27;)&quot;) ;
	executeUpdate(H2Conn, &quot;insert into tb_s_property(cargoName, propName, colName, dataType) values(&#x27;book&#x27;, &#x27;author&#x27;,&#x27;author&#x27;,&#x27;string&#x27;)&quot;) ;
	executeUpdate(H2Conn, &quot;insert into tb_s_property(cargoName, propName, colName, dataType) values(&#x27;book&#x27;, &#x27;publisher&#x27;,&#x27;publisher&#x27;,&#x27;string&#x27;)&quot;) ;
	
	executeUpdate(H2Conn, &quot;insert into tb_s_property(cargoName, propName, colName, dataType) values(&#x27;crossStitch&#x27;, &#x27;gridNum&#x27;,&#x27;gridNum&#x27;,&#x27;int&#x27;)&quot;) ;
	executeUpdate(H2Conn, &quot;insert into tb_s_property(cargoName, propName, colName, dataType) values(&#x27;crossStitch&#x27;, &#x27;backColor&#x27;,&#x27;backColor&#x27;,&#x27;string&#x27;)&quot;) ;
	executeUpdate(H2Conn, &quot;insert into tb_s_property(cargoName, propName, colName, dataType) values(&#x27;crossStitch&#x27;, &#x27;size&#x27;,&#x27;size&#x27;,&#x27;string&#x27;)&quot;) ;
	executeUpdate(H2Conn, &quot;insert into tb_s_property(cargoName, propName, colName, dataType) values(&#x27;crossStitch&#x27;, &#x27;brand&#x27;,&#x27;brand&#x27;,&#x27;string&#x27;)&quot;) ;
	
	
	//create table for cargo book and cargo cross-stitch.
	//we know the rule is : return &quot;tb_cargo_&quot; + cargoName;
	//cargo book:
	executeUpdate(H2Conn, &quot;drop table if exists tb_cargo_book&quot;) ;
	executeUpdate(H2Conn, &quot;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&quot; +
			&quot;, ISBN varchar(64) not null&quot; +
			&quot;, author varchar(64)&quot; +
			&quot;, publisher varchar(64)&quot; +
			&quot;)&quot;) ;

	//cargo cross-stitch:
	executeUpdate(H2Conn, &quot;drop table if exists tb_cargo_crossStitch&quot;) ;
	executeUpdate(H2Conn, &quot;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&quot; +
			&quot;, gridNum int(11) not null&quot; +
			&quot;, backColor varchar(64)&quot; +
			&quot;, size varchar(64)&quot; +
			&quot;, brand varchar(64)&quot; +
			&quot;)&quot;) ;</pre>
							我们创建了3张表,并在自定义属性表中写入了图书和十字绣的表结构信息。
							然后把新的域对象配置到guzz中。在guzz.xml中添加:
							<pre class="prettyprint">	&lt;business name=&quot;cargo&quot; dbgroup=&quot;default&quot; file=&quot;classpath:org/guzz/test/shop/Cargo.hbm.xml&quot; /&gt;
&lt;business name=&quot;sp&quot; dbgroup=&quot;default&quot; file=&quot;classpath:org/guzz/test/shop/SpecialProperty.hbm.xml&quot; /&gt;</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(&quot;book&quot;) ;
		book.setDescription(&quot;nice book &quot;) ;
		book.setPrice(33.56) ;
		book.setStoreCount(10) ;
		
		Date now = new Date() ;
		book.setOnlineTime(now) ;
		
		//ISBN, author and publisher
		book.getSpecialProps().put(&quot;ISBN&quot;, &quot;isbn-bbb-1&quot;) ;
		book.getSpecialProps().put(&quot;author&quot;, &quot;not me&quot;) ;
		book.getSpecialProps().put(&quot;publisher&quot;, &quot;wolf&quot;) ;
		
		Guzz.setTableCondition(&quot;book&quot;) ;			
		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(&quot;brand&quot;, &quot;湘湘绣铺&quot;)); //设置一个自定义属性的查询条件
            se.setTableCondition(&quot;crossStitch&quot;) ; //设置tableCondition

            List m_css = session.list(se) ; //查询tb_cargo_crossStitch</pre>
							如果在jsp中,也可以用g标签更简洁的实现查询。查询条件支持自定义属性,如:
							<pre class="prettyprint">&lt;g:list var=&quot;m_css&quot; business=&quot;cargo&quot; tableCondition=&quot;crossStitch&quot; limit=&quot;brand=湘湘绣铺&quot; /&gt;</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需要设置一个特殊的值做标记做区分,例如&quot;all&quot;。
								修改getRuntimeObjectMapping以支持&quot;all&quot;条件。</p>
							<pre class="prettyprint">        .....

public POJOBasedObjectMapping getRuntimeObjectMapping(Object tableCondition) {
	String cargoName = (String) tableCondition ;
	
	if(&quot;all&quot;.equals(cargoName)){
		return this.getConfiguredMapping() ;
	}
	
	Assert.assertNotEmpty(cargoName, &quot;tableCondition can&#x27;t be null, we don&#x27;t have any default table to store un-categoried cargoes.&quot;) ;
	
	.....</pre>
							这里,我们定义如果tableCondition为&quot;all&quot;,使用默认配置的OM(hbm.xml或annotation定义的原始OM,不包含每种商品自定义的属性)。
							实现所有高于100.00元商品查询:
							<pre class="prettyprint">		ReadonlyTranSession session = tm.openDelayReadTran() ; 
	
	Guzz.setTableCondition(&quot;all&quot;) ;
	
	//list all cargoes priced over 100.00 bucks
	String sql=&quot;select c.* from (select @id, @name, @storeCount from tb_cargo_book where @price &gt;= :param_price&quot;
	    + &quot; union all select @id, @name, @storeCount from tb_cargo_crossstitch where @price &gt;= :param_price) as c &quot;;

	CompiledSQL cs = tm.getCompiledSQLBuilder().buildCompiledSQL(Cargo.class, sql) ; 
	cs.addParamPropMapping(&quot;param_price&quot;, &quot;price&quot;) ;
	List cargoes = session.list(cs.bind(&quot;param_price&quot;, 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>