鲲鹏's profile鲲鹏(BirdGu)的BlogPhotosBlogListsMore Tools Help

Blog


    October 23

    ASP模式应用中多客户数据管理方案探讨

        ASP(Application Service Provider)模式应用与普通应用之间的一个重要差别是ASP系统需要管理多个客户的数据。不同客户之间的数据完全独立,没有关联。不允许一个客户查询另一个客户的数据。即使多个客户共享一个数据库,对单个客户来说,其它客户的数据可以看作是不存在的。 这里主要讨论在J2EE应用中如何解决这个问题。当然,这里讨论的方法对于其它架构的系统可能也适用。

    方案1:单数据源,单套表。

    所有客户的数据都存放在一个数据库的同一套表中, 在部分表中增加标示字段,表明该记录是属于哪个客户的。具体哪些表中要增加标示字段当然要看具体应用,不过我觉得可能大部分表示实体对象的表中都需要加。在很多查询条件中都需要包括这个标示字段。即使是用户自定义的查询,系统也需要在查询条件中加入该字段。

    优点:数据源和数据库的管理都比较简单。数据源管理方面和普通的J2EE应用没有差别。

    缺点:增加程序的复杂性。如果应用比较复杂,很多数据表都需要加入客户表示字段,很多查询都需要包括该字段,会比较麻烦。如果有遗漏,特别是查询条件中遗漏该字段,就会造成一个客户看到另一个客户的数据。另外,需要在系统的安全性方面做比较细致的设计。比如,某个功能通过在URL中包含某个实体的关键字以查询该实体的信息,或对该实体进行操作。在普通应用中后台只需要根据关键字查出该实体即可。但是在ASP应用中,就必须额外判定该实体是否属于当前登录用户,或者要在查询中条件中加入客户标示。当然如果有办法自动完成这样的检查或者自动修改发到数据库的查询条件,这个缺点就可以避免。只是我现在还没有想到这样做的好方法。

    -------------------------------------

    方案2:多套表,多数据源

    数据库中每个客户一套表。可以是MySql, PostgresSQL, SQLServer中的不同数据库,或者Oracle中的不同schema。在应用服务器中配置不同的数据源,或者使用不同的连接池。 在访问数据库,需要得到数据库连接时,根据当前用户所属的客户选择合适的数据源或者连接池。

    优点:不同客户的数据物理分离,安全性比较好。除了获取数据库连接部分的程序以外,其它程序和普通应用没有两样。不同客户的数据可以放置在不同的数据库服务器中,分担数据库服务器的负荷。

    缺点:数据库连接的利用效率不高。ASP模式的主要客户是中小企业。这样带来的结果是客户数可能会很多,但是单个客户的用户数和并发登录数都不会太多。在系统这边来说,则是数据源或连接池很多,但每一个的利用效率都不高。在数据库服务器这边仍然会有很多连接,因为每个数据源或连接池都需要保持一定数量的可用连接。这样通过连接池共享数据库连接而减少总连接数的好处被大大削弱了。 另一个缺点是如果需要增加客户时,需要在应用服务器中配制新的数据源,或者修改应用自己的数据库连接池配制。某些情况下可能无法作到在应用不中断的情况下使这些配制生效。

    ------------------------------------

    方案3:多套表,多Schema,单数据源。

    这个方案基本是方案2的变种。很多数据库提供Schema,比如PostgresSQL中,同一个数据库下可以有多个Schema,Oracle中,每个用户就是一个Schema。即使用同一个用户登录数据库,只要在表名前加上schema名字,就能访问不同schema中的表。不同客户的数据就可以存放在不同的schema中。这样就能用同一个数据源或连接池,只是在所有的表名前要根据当前用户加上合适的shcema名字。如果要程序员自己这么做当然是很麻烦的。但如果用Hibernate就方便了。因为Hibernate中可以配制default schema,Hibernate在生成SQL时会自动在表名前加上schema名字。 因此如果使用Hibernate实现该方案就需要多个SessionFactory,每个客户对应一个SessionFactory。除了default schema以外,这些SessionFactory的配置完全一样。当程序需要SessionFactory的时候,需要有一个分配程序根据当前用户选择合适的SessionFactory。这些SessionFactory可以在系统启动时根据配制文件全部建立好,也可以采取lazy initialize的方法,这样也能支持动态增加客户。

    优点:除了方案2的优点以外,共享数据源或连接池,效率更高。

    缺点:对实现手段有一定依赖性。使用Hibernate会比较容易实现,其它方式我不清楚。每个SessionFactory都会有一定开销。多个SessionFactory会增加这部分开销,增加到多少程度,对性能有多少影响还有待测试。多个SessionFactory的情况下,二级缓存会否互相干扰,还是每个SessionFactory有各自自己的二级缓存也有待测试。

    ------------------------------------

    所有以上方案都是所有客户共享同一个应用(WAR或EAR)。这种方式有一个缺点,不过这个缺点和数据源无关。如果用户可以以点菜式的方式选购不同功能,也就是说虽然不同客户共享一个应用,但是提供给他们的功能是不同的。这种情况下,程序中会有很多地方要判断用户是否可以使用某功能。这会带来另一种麻烦。这个和权限控制有类似,也许可以用Acegi之类的框架解决。但和权限控制又不完全一样。因为有时简单地提示用户他无权使用某功能可能不够友好,而需要以一种更优雅的方式提供用户降级过的系统功能。当然这个和具体应用有关,这里就不展开了。

    ------------------------------------

    方案4:多套表,多应用 这个方案和以上方案不同,除了数据物理分离以外,应用也物理分离。每个客户有各自自己的WAR或EAR。如果使用方案3中的多Schema的方法,那么数据源可以共享,每个应用的SessionFactory有不同的default schema。

    优点:应用简单。这样的应用和普通的J2EE应用没有任何区别。支持高度定制化的系统功能。每个应用基本相同,又可以有很大差别。比如用户没有选择的功能更本就不部署。

    缺点:应用服务器中每个应用都会有一定的开销,占用一定的固定的内存。这个开销来自于应用服务器管理应用的数据结构;每个应用的class loader,和读入的类的字节码(应用服务器会判断出不同应用的同样名字的类具有同样的字节码,从而只保存一份吗?对次我表示高度怀疑);每个应用还可能会有自己的线程,比如任务调度线程。这些开销可能会使一个应用服务器能部署的应用不会太多(和ASP模式的潜在客户数比较),从而在客户数比较大的情况下需要增加应用服务器的数目。

    July 12

    Hibernate: Association or Not Association

    在使用Hibernate的时候,要不要使用Hibernate提供的对Association关系映射的功能,也就是要不要使用one-to-one, one-to-many,以及Collectionde映射呢?我就见过这样的项目,所有的持久化类都是孤立的,只在类中存放外键,one-to-one,one-to-many, set之类的一概不用,并且这一条作为该项目的设计准则。
    我以为,对Association关系的支持是ORMapping框架提供的重要功能。把该功能放着不用,犹如买了彩电,却只看黑白老电影。
    Hibernate对Association关系的支持提供了两个最大的好处:
    1. 易于表现和维护复杂的对象关系。
    2. 对复杂业务逻辑的表达更简洁,更容易。因为复杂业务逻辑一般都是需要多个互相关联的对象协作完成的。
    比如有这样两个类:
    class A {
        private B b;
       
        public B getB () {
            b;
        }
    }
    class B{
    ...
    }
    根据业务逻辑的需要,A当中完全可以有这样的方法:
    public void foo () {
        b.bla ();
    }
    这样,对A外部的对象来说,无论是这样的代码:
        A a = ... // get a
        a.getB().method();
    或者是:
        a.foo ();
    由于Hibernate对Association关系的透明支持,我们都不需要关心A中的B是怎么拿到的。
    而如果不用Association关系,A就得是这个样子的:
    class A {
        private Long bId;
       
        public Long getBId() {  return bId; }
    }
    此时,原来的A.foo已经无法实现了。因为不依赖Dao的话,在A当中已经拿不到B的实例了。因此只能在业务逻辑层中写:
        A a = ... // get a
        B b = bDao.findByPK (a.getBId());
        b.bla ();
    这还不是最糟的。
    如果B是这样的(使用Association):
    class B {
        private Set<A> as;
       
        public Set<A> getAs () { return as; }
        public void method () {
            // do something on as
        }
    }
       
    如果不用Association,那么方法method就只能实现在业务逻辑层中了:
        Set<A> as = aDao.findAsByBId (bId);
        // do something on as.
       
        显然,因为不用Association, aDao.findAsByBId这样的方法被额外增加出来了。而且本来从逻辑上来说应该属于实体类的方法,现在也必须放在业务逻辑层中了。
       
        Hibernate对Association关系的透明支持提供的其它好处还包括:
    1. 在多对多关系的情况下,程序员无需维护关系表。
    2. 聚合关系的级联删除。
    3. 使HQL更加简单,清晰。否则,还要用HQL做出多表连接来。
    当然,事无绝对。什么时候不应该用Association呢,我以为有以下几种情况:
    1.  只是记录,以后不会再用到Association关系的。比如存放在数据库中的日志。
    2. 比如上面的例子中,B可能对应的A的数量特别大,此时在B中就可以不建Set<A> as这样的成员变量。
    3. 有性能测试数据表明,只有在实体类中存放外键才能达到性能要求。注意,是需要有性能测试数据支持这个理由,而不是靠猜的。
    除以上情况以外,我以为都应该根据对象模型使用Association关系。
    July 09

    一道面试题的分析(续)

    前面的讨论中留下了一个问题,还没有分析完。就是表示“币种”的数据类型。因此继续讨论的话就要超出了面试题目的范围,所以另开一文。

    如果对币种信息的要求只是为了区别不同Money对象所代表的货币种类,以保证只有相同币种的Money对象才能进行计算,那么无论是String, int还是enum都够了。但是,在一个稍大一点的项目中,要求可能都不会仅限于此。比如需要在显示金额的时候能显示正确的货币符号,或者一些系统中还需要显示3位的币种代码,比如用USD表示美元,RMB表示人民币。在有用户输入或者和其它系统交互的情况下,还会需要判断输入的数据是否是合法的,系统支持的币种。有这些需求的情况下,自然的,我们需要一个单独的类Currency。至于在Currency内部使用具体什么类型,参见前一篇中的讨论。

    因此,Money类应该是这个样子的:

    public class Money {
        private Currency currency;
        private BigDecimal amount;
       
        public Money (Currency aCurrency, BigDecimal aAmount) {
            ...
        }
       
        public Money add (Money aMoney)
        throws IncompatibleCurrencyException {
            ...
        }
       
        public Money subtract (Money aMoney)
        throws IncompatibleCurrencyException {
            ...
        }
    }

    如果系统中需要由用户来维护系统所能支持的币种,那么一般需要在数据库中维护一张表,此时Currency就是一个可持久化类了。

    关于这个设计问题,还请参看我前面翻译的那篇“Prefactoring”的2.6节。虽然那篇文章主要讨论的是分析和领域建模阶段,但是很多讨论对于设计阶段仍然是有效的。

     

    一道面试题的分析

    这是我为公司设计的一道Java语言的面试题……嗯……也可以说是两道啦:
    写一个Money类,包含金额和币种两个属性,以及加、减两个方法。
    附加题:将上题中的Money类实现为一个ValueObject。
     
    先来看本题。
    第一个要考察的方面是为两个属性选择正确的数据类型。对金额而言,最自然的选择是float或者double。但这不是最好的选择。在计算机中,float和double都是有误差的。比如1有可能是1.00000001。所以对金钱这么重要而敏感的东西来说,还是BigDecimal比较好。当然,对于后面的加减运算来说比较麻烦一些。这时还是让人比较怀念C++的运算符重载的。
    用int怎么样呢?如果用int来表示以分为单位的金额的话,到也不是不可以。这样精度有保证,而且运算也比较方便。不过如果要表示5厘……就没办法了。
    对币种来说,很多人选择了String。不过我以为String不如int,或者short。虽然在Java中,String几乎可以当成基本数据类型使用,但是String毕竟是对象,还是经常要考虑null不null的问题,而且字符串匹配总是不如int(或short)方便和快捷。
    到了JDK 1.5的时代,我们又多了一个选择:enum。不过如果是在一个信息系统中,货币种类是可以由用户维护的话,那么enum就不能用了。
    除此以外,从OO的角度来说,还有更好的设计吗?有。不过这个问题我留到下一篇讨论。
     
    然后是方法的设计。因为有币种,因此加、减时自然都要考虑同币种的金额才能相加(暂时不考虑汇率换算的问题)。如果币种不相同怎么办?这就是考察的第二个重点: 错误处理。
    有不少人使用返回值表示计算的成功与否。比较奇怪的是他们大部分都没有C,C++程序员的经历,不知道是不是学校教育的结果。也有人直接使用System.out.println输出错误信息,这个就更离谱了。
    正解应该是使用Exception。偷懒的话,可以使用IllegalArgumentException。如果知道自己定义一个Exception,比如IncompatibleCurrencyException,那如果是我面试的话,这样的人技术上基本就算过关了。
    所以两个方法的原型应该是:
        public void add (Money operand)
        throws IncompatibleCurrencyException
       
        public void subtract (Money operand)
        throws IncopatibleCurrencyExcepiton
     
    如果使用BigDecimal表示金额,那么这里还有一个BigDecimal的加减方法的名称的问题。不过这倒不重要。写程序的时候总有API可以查的。
    对于参数是否可以为null,有两种选择。一种是参数为null时,什么也不做。另一种是抛出异常,比如NullPointerException。我认为第二种选择为好。大部分情况下,用null做参数调用add和subtract方法恐怕不是程序员的本意,而是其它程序出错的结果。直接抛出异常有助于及早发现其它部分的错误。
    除了属性和加,减方法以外,构造函数以及getter/setter函数虽然没有提到,但作为一个完整的类,还是应该要有的。从健壮性的角度考虑,在构造函数中应该对输入参数有一定判断。比如金额不能为null等。
     
    接下来是附加题。什么是ValueObject。简单说,ValueObject的值(状态)一旦创建以后就不会改变了。所以可以当基本数据类型用。比如Java中的String, BigDecimal, BigInteger都是ValueObject。StringBuffer就不是。
    要把Money变成ValueObject,首先不能有setter。构造函数是从类外部设定属性值的唯一途径。
    其次,add和subtract方法不能修改this的属性,运算结果要以新的Money对象返回,这样其原型就要变成:
        public Money add (Money operand)
        throws IncompatibleCurrencyException
       
        public Money subtract (Money operand)
        throws IncopatibleCurrencyExcepiton

    第三,最好是能重新定义equals和hashCode两个方法。如何重新定义,请看Pearson Education的《Effective Java Programming Language Guide》7,8两章。
     
    遗憾的是,到目前为止能全部达到以上要求的应聘者还没有出现过。更不用说我心中一个非常不现实的奢望了,就是看到一个因聘者首先写下这一行:
    public class MoneyTest extends TestCase {
    ......

    Hibnerate Annotation使用注意事项


    在以前,我们在Java源代码中使用特殊的JavaDOC标签定义ORMapping规则,然后使用xDoclet生成映射规则文件(.hbm.xml)文件。现在有了Hibernate Annotation,连映射规则文件也不需要了,使用更加方便了。这里说说使用Hibernate Annotation时需要注意的一些地方。这些内容分散在Hiernate Annotation Reference文档和example中,与spring相关的部分则出现在Spring的文档中,这里把它们整理在一起。对我自己来说是起到备网的作用,对于其它Hibernate Annotation的用户,系统也能起到帮助查询的作用。
     
    1. AnnotationConfiguration及配置映射规则。
    在没有使用Hibnerate Annotation的使用,我们一般是使用org.hibernate.cfg.Configuration来配置和生成SessionFactory,使用Hibernate Annotation以后,要使用org.hibernate.cfg.AnnotationConfiguration类。该类在hibernate-annotation.jar,而不是hibernate3.jar中。
    如果使用Spring + Hibernate的架构,要在Spring的配置文件里配置LocalSessionFactoryBean的时候,要记得定义property configurationClass。对于使用Annotation定义映射规则的类,在hibernate.cfg.xml文件中不能使用<mapping resource="..." />的形式,而要使用<mapping class="..." />的形式。LocalSessionFactoryBean的mappingResources不能调用AnnotationConfiguration的addClass和addPackage方法,因此使用Annotation定义映射规则的类,仍然要通过hibernate.cfg.xml文件来使AnnotationCongiguration载入它们的映射规则。使用Spring + Hibernate + Hibernate Annotation的情况下,LocalSessionFactoryBean的定义一般应该是:
        <bean id="mySessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"
             autowire="no"  >
            <property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
            <property name="dataSource" ref="myDataSource"/>
            <property name="configLocation" value="classpath:hibernate.cfg.xml" />
        </bean>
     
    2. <mapping package="..." /> 与<mapping class="..." />
    前一种形式只是载入制定包中package-info.java文件中定义的Annotation,而该包下的所有持久化类仍然需要通过后一种形式逐一载入它们的映射规则。
     
    3. 使用sequence生成id
    使用sequence生成id时,id属性的Annotation应该是:
        @Id
        @GeneratedValue (strategy=GenerationType.SEQUENCE, generator="SEQ_BOOK_ID")
    需要注意的是,GeneratedValue中的generator不是sequence的名字,而是一个另外定义的SequenceGenerator的名字。这个SequenceGenerator应该定义在类的级别,因此比较完整的代码是想这个样子的:
    @Entity
    @AccessType("property")
    @Table(name="BOOK")
    @SequenceGenerator (name="SEQ_BOOK_ID", sequenceName="SEQ_BOOK_ID")
    public class Book {
        private Long id;
       
        @Id
        @GeneratedValue (strategy=GenerationType.SEQUENCE, generator="SEQ_BOOK_ID")
        public Long getId() {
            return id;
        }
        ......
    }   
     
    4. AccessType
    如果要Hibnerate使用get/set方法存取属性的话,一定要在类级别加上:
    @AccessType("property")

    5. NamedNativeQuery
    使用NamedNativeQuery定义native SQL query的时候,即使只返回一个字段,也需要定义SqlResultSetMapping,见下面的例子:
    @SqlResultSetMapping(name="keyWords", columns=@ColumnResult (name="key_word"))
    @NamedNativeQuery (name="listKeyWords",
                       query="select distinct key_word from key_words order by key_word",
                       resultSetMapping="keyWords")
     
     
    先写这些,以后再补充。

    HttpUnit使用心得

    1. 使用JavaScript
    日前,在一个项目中试用了一下HttpUnit。发现一些常用的程序写法,为了适应HttpUnit,必须做一些改变。特别是在JavaScript方面。

    例如,如果有如下的HTML Form:
    <form name="form1" action="xxx.do" method="POST">
    <input type="text" name="t" />
    ......
    </form>

    那么在JavaScript中通过以下的语句改变输入框“t”的值是非常方便的:
    form1.t="xxx";

    但是,这句语句在HttpUnit中是会出错的。HttpUnit不认识这样的语法。如果要使用HttpUnit,必须使用以下写法:
    document.forms[0].t="xxx"
    或是:
    document.forms["form1"].t="xxx";

    事实上,"form1.t='xxx'"是利用了IE对Javascript的扩展,这一扩展HttpUnit是不支持的。这就是问题的根源。因此如果要使用HttpUnit,就必须遵循“ECMA-262”标准。

    2. Submit form
    如果form中存在多个submit按钮(<input type="submit".....>),调用WebForm.Submit ()时具体触发的是哪个submit按钮是不确定的。这时最好是使用WebForm.submit (SubmitButton button)这个方法。SubmitButton可以通过WebForm.getSubmitButton方法得到。