1、企业级应用系统体系架构企业级应用系统体系架构(十)(十) 状态管理状态管理Chen HaopengThursday, June 09, 202212状态管理状态管理n对于寻求真理的人而言,有些准则是必须遵守的,真对于寻求真理的人而言,有些准则是必须遵守的,真理并非教条或无知,而是通过推理、调查、检验、与理并非教条或无知,而是通过推理、调查、检验、与探究得来的。无论其意图有多好,信仰都必须构建在探究得来的。无论其意图有多好,信仰都必须构建在事实而非幻想之上,幻想之上的信仰是最糟糕的虚假事实而非幻想之上,幻想之上的信仰是最糟糕的虚假希望。希望。 Thomas Edison3状态管理状态管理n在企业
2、级系统中,大部分工作都涉及数据处理。事实在企业级系统中,大部分工作都涉及数据处理。事实上,可以论证,上,可以论证,企业级系统只做了一件事,那就是数企业级系统只做了一件事,那就是数据处理据处理。n在两层架构的客户在两层架构的客户/ /服务器系统时代,这还不太明显,服务器系统时代,这还不太明显,那时企业级程序员需要关心两种状态:瞬时状态(那时企业级程序员需要关心两种状态:瞬时状态(transient statetransient state)与持久状态()与持久状态(durable statedurable state)。)。瞬时状态并不算企业级数据所覆盖的正式部分,持久瞬时状态并不算企业级数据所
3、覆盖的正式部分,持久状态则是无论发生什么都需要被跟踪的部分状态则是无论发生什么都需要被跟踪的部分。4状态管理状态管理n瞬时状态是那些企业并不关心,也不会为之流泪的数瞬时状态是那些企业并不关心,也不会为之流泪的数据,因为在系统崩溃的时候,真正重要的东西决不会据,因为在系统崩溃的时候,真正重要的东西决不会丢失。丢失。电子商务中的购物车是瞬时状态典型的例子。电子商务中的购物车是瞬时状态典型的例子。n在厚客户端或富客户端的应用中,瞬时状态很容易处在厚客户端或富客户端的应用中,瞬时状态很容易处理理:相当于客户端进程中存储在局部变量中的数据,:相当于客户端进程中存储在局部变量中的数据,它们没有被保存在持久
4、的存储介质中。它们没有被保存在持久的存储介质中。当客户端进程当客户端进程结束的时候,瞬时状态也随之消亡,无需为其生命周结束的时候,瞬时状态也随之消亡,无需为其生命周期的处理而多费心思期的处理而多费心思。5状态管理状态管理n不过在瘦客户端中,例如基于不过在瘦客户端中,例如基于HTMLHTML浏览器的应用,瞬浏览器的应用,瞬时状态则呈现出另一种尺度。时状态则呈现出另一种尺度。因为因为HTTPHTTP本就是无状态本就是无状态的协议,自身并不具备保存每一个客户端状态的能力的协议,自身并不具备保存每一个客户端状态的能力。所以要由程序员在底层协议之上自己实现瞬时状态。所以要由程序员在底层协议之上自己实现瞬
5、时状态机制。机制。n另一方面则是持久状态,在谈到它时人们自然就会想另一方面则是持久状态,在谈到它时人们自然就会想到到 “持久数据(持久数据(persistent datapersistent data)”,即需要长久,即需要长久保存的数据。正规的说法是,保存的数据。正规的说法是,如果定义了某个持久状如果定义了某个持久状态,那么它就绝对会被保存下来,即使遇到态,那么它就绝对会被保存下来,即使遇到JVMJVM终止甚终止甚至崩溃的情况也是如此至崩溃的情况也是如此。6状态管理状态管理n持久状态通常具有隐含的法律性或经济意义。持久状态通常具有隐含的法律性或经济意义。n在讨论状态管理时,二者的区别至关重要
6、。在讨论状态管理时,二者的区别至关重要。因为对瞬因为对瞬时状态有效的机制对持久状态不一定有效,反之亦然时状态有效的机制对持久状态不一定有效,反之亦然。7状态处理状态处理n第第1项:节省地使用项:节省地使用 HttpSessionn第第2项:使用对象优先的持久化来保存你的领域模型项:使用对象优先的持久化来保存你的领域模型n第第3项:使用关系优先的持久化来显示关系模型的威力项:使用关系优先的持久化来显示关系模型的威力n第第4项:使用过程优先的持久化来创建一个封装层项:使用过程优先的持久化来创建一个封装层n第第5项:识别对象项:识别对象-层次结构的阻抗失配层次结构的阻抗失配(impedance mi
7、smatch)8节省地使用节省地使用 HttpSessionn在基于在基于HTML/HTTP的应用中,为维护代表客户端的瞬时状态,的应用中,为维护代表客户端的瞬时状态,servlet容器提供了一种称为会话空间的设施,被表示为容器提供了一种称为会话空间的设施,被表示为HttpSession接口。接口。n遗憾的是,这种机制并非完全免费的。首先,遗憾的是,这种机制并非完全免费的。首先,在服务器端为每个在服务器端为每个客户端存储数据将会减少该服务器上的可用资源客户端存储数据将会减少该服务器上的可用资源,这意味着服务,这意味着服务器的最大负载能力会成比例地下降。这个算式很简单:器的最大负载能力会成比例地
8、下降。这个算式很简单:在会话空在会话空间中保存越多的数据,机器能够处理的会话就越少间中保存越多的数据,机器能够处理的会话就越少。由此推导出。由此推导出,为了令给定的机器能够支持尽可能多的客户端,必须将会话的,为了令给定的机器能够支持尽可能多的客户端,必须将会话的存储量保持在最小。存储量保持在最小。实际上,对于真正具备可扩展性的系统而言实际上,对于真正具备可扩展性的系统而言,无论何时都应该避免使用会话,无论何时都应该避免使用会话。如果在服务器端可以不产生任如果在服务器端可以不产生任何为每个客户端进行处理的开销,那么机器的负载能力何为每个客户端进行处理的开销,那么机器的负载能力(在理论上在理论上)
9、可以到达无限,能够支持任意多连接到它的客户端可以到达无限,能够支持任意多连接到它的客户端。9节省地使用节省地使用 HttpSessionn避免使用会话的建议不单单是考虑到系统的可扩展性。避免使用会话的建议不单单是考虑到系统的可扩展性。对于在对于在Web集群内运行的集群内运行的servlet容器而言,这也是必须的容器而言,这也是必须的。会话是驻留。会话是驻留内存的结构。因为内存是局限于特定的机器,内存的结构。因为内存是局限于特定的机器,除非除非Web集群有某集群有某种机制,能够令给定客户端的每一次请求都被传送给同一个服务种机制,能够令给定客户端的每一次请求都被传送给同一个服务器,否则对应先前的某
10、个请求,其后续处理可能会找不到之前存器,否则对应先前的某个请求,其后续处理可能会找不到之前存储的会话对象。储的会话对象。10节省地使用节省地使用 HttpSessionn有一种可能的机制可以对此提供支持:有一种可能的机制可以对此提供支持:在服务器集群中,指派一在服务器集群中,指派一个单独的节点作为会话状态服务器个单独的节点作为会话状态服务器。对每一个请求,无论哪个节。对每一个请求,无论哪个节点正在处理它,该节点都向会话状态服务器查询此客户端的会话点正在处理它,该节点都向会话状态服务器查询此客户端的会话状态,然后将会话状态通过网络传给处理此请求的节点。然而,状态,然后将会话状态通过网络传给处理此
11、请求的节点。然而,这种机制有两种副作用:这种机制有两种副作用:(1)每个请求都增加了一次与会话状态服每个请求都增加了一次与会话状态服务器之间的往返访问,这增加了客户端请求的等待时间。务器之间的往返访问,这增加了客户端请求的等待时间。但更重但更重要的是,要的是,(2)所有的会话状态都被存储在集中的服务器上,这使得所有的会话状态都被存储在集中的服务器上,这使得集群中产生了一个单一故障点集群中产生了一个单一故障点(single point of failure)。11节省地使用节省地使用 HttpSessionn另一种可能的机制是另一种可能的机制是采用采用P2P(peer-to-peer)方式)方式
12、。当一个请当一个请求进入某节点时,此节点发出一个集群广播信号,询问其它节点求进入某节点时,此节点发出一个集群广播信号,询问其它节点是否拥有此客户端最近的会话状态。拥有此客户端最近状态的节是否拥有此客户端最近的会话状态。拥有此客户端最近状态的节点对此做出回答,并将该会话状态传递给当前正处理请求的节点点对此做出回答,并将该会话状态传递给当前正处理请求的节点。12节省地使用节省地使用 HttpSessionn与会话相关的另一个需要注意的问题是会话的意外使用。与会话相关的另一个需要注意的问题是会话的意外使用。nJSP的规范清楚地陈述道,的规范清楚地陈述道,对于给定的对于给定的JSP网页,网页,“打开打
13、开”会话会话的指示的指示(带有带有session属性的属性的page指示性标记指示性标记),缺省地被设置,缺省地被设置为为true,这意味着下面的,这意味着下面的JSP网页将为其建立一个会话,即使该网页将为其建立一个会话,即使该网页从未使用会话:网页从未使用会话:Hello, world, Im a stateless JSP page.It is now .13节省地使用节省地使用 HttpSessionn更糟的是,在更糟的是,在Web应用的任何角落,只要有一个这样的应用的任何角落,只要有一个这样的JSP页面页面,在该客户端使用此,在该客户端使用此Web应用的整个过程中,该会话(即使没有应用
14、的整个过程中,该会话(即使没有在会话中保存对象,也会带来相关的系统开销。)将一直存在。在会话中保存对象,也会带来相关的系统开销。)将一直存在。所以,所以,除非你需要使用会话,否则你应该确保你的除非你需要使用会话,否则你应该确保你的JSP页面都关页面都关闭会话闭会话。我希望对于给定的。我希望对于给定的Web应用,有某种方法能够令应用,有某种方法能够令session = false成为缺省设置,然而到目前为止它并非如此。成为缺省设置,然而到目前为止它并非如此。n希望你不要误解,我并不是鼓吹完全不使用会话,实际上,如果希望你不要误解,我并不是鼓吹完全不使用会话,实际上,如果建议建议HTTP不提供任何
15、合理的机制以提供每个用户的状态的话,不提供任何合理的机制以提供每个用户的状态的话,将是很滑稽的。将是很滑稽的。如果小心地使用,那么如果小心地使用,那么HttpSession就可以提供就可以提供必要而强大的机制去提供在必要而强大的机制去提供在Web应用中的每个用户的状态,而这应用中的每个用户的状态,而这通常是关键而且必需的东西。通常是关键而且必需的东西。(否则怎么才能确定用户是否成功(否则怎么才能确定用户是否成功地通过了身份认证,或者在应用中跟踪用户的进度呢?)危险在地通过了身份认证,或者在应用中跟踪用户的进度呢?)危险在于,不是必需使用会话的时候,过度地使用或滥用此机制,那将于,不是必需使用会
16、话的时候,过度地使用或滥用此机制,那将给给servlet容器带来额外的开销。所以,容器带来额外的开销。所以,如非必要,尽量不要使用如非必要,尽量不要使用会话,如果非用不可,为了尽可能少地消耗运行会话,如果非用不可,为了尽可能少地消耗运行servlet容器的机容器的机器上的资源,请保持会话精简而有意义。器上的资源,请保持会话精简而有意义。14状态处理状态处理n第第1项:节省地使用项:节省地使用 HttpSessionn第第2项:使用对象优先的持久化来保存你的领域模型项:使用对象优先的持久化来保存你的领域模型n第第3项:使用关系优先的持久化来显示关系模型的威力项:使用关系优先的持久化来显示关系模型
17、的威力n第第4项:使用过程优先的持久化来创建一个封装层项:使用过程优先的持久化来创建一个封装层n第第5项:识别对象项:识别对象-层次结构的阻抗失配层次结构的阻抗失配(impedance mismatch)15使用对象优先的持久化使用对象优先的持久化来保存你的领域模型来保存你的领域模型n使用对象优先的持久化方式时,我们力求在持久化的过程中保持使用对象优先的持久化方式时,我们力求在持久化的过程中保持对象的视角对象的视角。这意味着无需我们的任何提示,对象就知道如何默。这意味着无需我们的任何提示,对象就知道如何默默地持久化自己,或者它们能够提供某种以对象为中心的默地持久化自己,或者它们能够提供某种以对
18、象为中心的(object-centric) API进行持久化和读取操作。进行持久化和读取操作。 16使用对象优先的持久化使用对象优先的持久化来保存你的领域模型来保存你的领域模型n那么在理想世界中,编写像下面这样的代码将自动在数据库中创建一个那么在理想世界中,编写像下面这样的代码将自动在数据库中创建一个包含了代表包含了代表25岁的岁的Stu Halloway的项:的项:Person p = new Person(“Stu”, “Halloway”, 25);System.out.println(p);/ Prints “Stu Halloway, age 25” n而下面的代码将自动更新在第一段
19、代码中所创建的行,将而下面的代码将自动更新在第一段代码中所创建的行,将Stu的年龄从的年龄从25改成改成30:Person p = Person.find(“Stu”, “Halloway”);System.out.println(p);/ Prints “Stu Halloway, age 25”p.setAge(30);System.out.println(p);/ Prints “Stu Halloway, age 30” n注意到了吗,对象优先持久化方法的一个重要的优点:注意到了吗,对象优先持久化方法的一个重要的优点:没有丑陋的没有丑陋的SQL语句,不用为是该语句,不用为是该INSER
20、T还是该还是该UPDATE这样的问题而烦心这样的问题而烦心。我们所。我们所能看到的只是对象,这正是我们喜欢的方式。能看到的只是对象,这正是我们喜欢的方式。17使用对象优先的持久化使用对象优先的持久化来保存你的领域模型来保存你的领域模型n然而,在读取对象时,对象优先的方法往往很快就不起作用了。一般说然而,在读取对象时,对象优先的方法往往很快就不起作用了。一般说来,对象优先的方法可能会采取以下两种方式:来,对象优先的方法可能会采取以下两种方式:要么以纯面向对象的形要么以纯面向对象的形式,通过创建包含查询规则的对象,进行对象查询;要么使用某种特定式,通过创建包含查询规则的对象,进行对象查询;要么使用
21、某种特定的的“查询语言查询语言”进行对象查询。进行对象查询。n在纯粹的对象优先的环境中,除了对象,我们不希望看到任何东西,所在纯粹的对象优先的环境中,除了对象,我们不希望看到任何东西,所以我们创建了查询对象(以我们创建了查询对象(Query Object),它包含了我们所关心的约束),它包含了我们所关心的约束查询的规则。遗憾的是,如果要创建一个复杂的查询,若它的查询规则查询的规则。遗憾的是,如果要创建一个复杂的查询,若它的查询规则不是对象的主键(有时称为对象标识符不是对象的主键(有时称为对象标识符object identifier,简称,简称OID),),从从OODBMS的角度来看,这样做通常
22、是复杂而且笨拙的:的角度来看,这样做通常是复杂而且笨拙的:QueryObject q = new QueryObject(Person.class);q.add(Criteria.and( Criteria.greaterThan(“dependents”, 2), Criteria.lessThan(“income”, 80000);q.add(Criteria.and( Criteria.greaterThan(“dependents”, 0), Criteria.lessThan(“income”, 60000); 18使用对象优先的持久化使用对象优先的持久化来保存你的领域模型来保存你的
23、领域模型n我们此处所做的等价于下面几行语句:我们此处所做的等价于下面几行语句:SELECT * FROM person pWHERE ( (p.dependents 2 AND p.income 0 AND p.income 60000) ) n哪一种更易于阅读?如果我们开始在查询中执行深层嵌套的布尔逻辑,哪一种更易于阅读?如果我们开始在查询中执行深层嵌套的布尔逻辑,例如查询例如查询“收入少于收入少于$80,000而且有超过而且有超过2个子女的人,或者收入少于个子女的人,或者收入少于$60,000而且无子女的人而且无子女的人”,此时事情将以指数级地恶化。,此时事情将以指数级地恶化。事实上并不事
24、实上并不难发现,比起通用目的的查询语言,例如难发现,比起通用目的的查询语言,例如SQL,纯对象优先的查询方法,纯对象优先的查询方法对于能查询什么,有着过于严格的限制对于能查询什么,有着过于严格的限制。19使用对象优先的持久化使用对象优先的持久化来保存你的领域模型来保存你的领域模型n这启发我们找寻第二条路,即这启发我们找寻第二条路,即创建某种创建某种“查询语言查询语言”,使它能够,使它能够更简明地表达查询,而无需使用过于复杂的代码更简明地表达查询,而无需使用过于复杂的代码。Java中所有的中所有的对象优先技术最终都回到了这一点:对象优先技术最终都回到了这一点:EJB 引入了引入了EJBQL,一种
25、,一种为实体为实体bean编写查询方法(编写查询方法(finder)的查询语言;)的查询语言;JDO引入了引入了JDOQL,它为,它为JDO增强的持久类做相同的工作;而增强的持久类做相同的工作;而OODBMS回回过头来采用过头来采用OQL(Object Query Language)对象查询语言。对象查询语言。n这些语言相互之间有着微妙的区别,但有着一个明确的共同点:这些语言相互之间有着微妙的区别,但有着一个明确的共同点:它们都很像它们都很像SQL,而,而SQL正是我们起初试图摆脱的。正是我们起初试图摆脱的。20使用对象优先的持久化使用对象优先的持久化来保存你的领域模型来保存你的领域模型n使用
26、对象优先的方法还有另一个副作用,即不可视的往返访问。例如,使用对象优先的方法还有另一个副作用,即不可视的往返访问。例如,当像下面这样使用实体当像下面这样使用实体bean时,引发了多少次数据库访问呢?时,引发了多少次数据库访问呢?PersonHome ph = (PersonHome)ctx.lookup(java:comp/env/PersonHome);Collection personCollection = ph.findByLastName(Halloway);for (Iterator i = personCollection.iterator(); i.hasNext(); ) P
27、erson p = (Person)i.hasNext(); System.out.println(Found + p.getFirstName() + + p.getLastName(); n虽然看起来似乎只访问了一次数据库(读取每个姓虽然看起来似乎只访问了一次数据库(读取每个姓Halloway的的Person对象,并将其组装到对象,并将其组装到PersonBean池中的实体池中的实体bean上),上),但实际上,这但实际上,这正是正是EJB中的中的N+1次查询问题,查找方法调用只查找符合查询条件的行次查询问题,查找方法调用只查找符合查询条件的行的主键,然后用只知道主键的实体的主键,然后用只
28、知道主键的实体bean的存根组装的存根组装Collection中,并在中,并在必要的时候才将数据惰性加载到(必要的时候才将数据惰性加载到(lazy-load)实体)实体bean中中。21使用对象优先的持久化使用对象优先的持久化来保存你的领域模型来保存你的领域模型n开发一个实体开发一个实体bean的实现,对于查询的结果,它不是简单地取回的实现,对于查询的结果,它不是简单地取回实体的实体的OID/主键,而是取得保存在实体中的整个数据集,这也是主键,而是取得保存在实体中的整个数据集,这也是可行的。可行的。实质上这就是采用积极加载(实质上这就是采用积极加载(eager-load),而不是更),而不是更
29、为常用的惰性加载为常用的惰性加载。遗憾的是,这引发了一个相反的问题,现在。遗憾的是,这引发了一个相反的问题,现在我们抱怨的是取回的数据太多,而不是太少。我们抱怨的是取回的数据太多,而不是太少。n这里问题的关键是,这里问题的关键是,在对象优先的持久化场景中,数据读取的原在对象优先的持久化场景中,数据读取的原子单位是对象本身,从面向对象的观点来看,返回那些比对象小子单位是对象本身,从面向对象的观点来看,返回那些比对象小的东西根本没有意义,就像在的东西根本没有意义,就像在SQL查询中,如果返回的是比行还查询中,如果返回的是比行还小的单位,结果同样没有意义小的单位,结果同样没有意义。22使用对象优先的
30、持久化使用对象优先的持久化来保存你的领域模型来保存你的领域模型n我可以写出某些类似于下面的语句:我可以写出某些类似于下面的语句:SELECT p.FirstName, p.LastNameFROM Person pWHERE p.LastName = Halloway; n可是,这到底返回了什么?通常而言,一次对象查询的返回值是一个已可是,这到底返回了什么?通常而言,一次对象查询的返回值是一个已定义类型的对象(如上例中的定义类型的对象(如上例中的Person实例)。可此处我们得到的是什么实例)。可此处我们得到的是什么?对于只返回?对于只返回“部分部分”对象,并没有普遍都可接受的方式,所以典型的
31、对象,并没有普遍都可接受的方式,所以典型的结果是采用像结果是采用像ResultSet或者或者Java Map一类的东西(或者是一类的东西(或者是Map实例的实例的一个一个List)。)。n即使我们理清了这些问题,对象优先的查询还是有其它的问题:即使我们理清了这些问题,对象优先的查询还是有其它的问题:对象到对象到对象的引用对象的引用。在此种情况下,困难并不经常发生,因为我们还没有很好。在此种情况下,困难并不经常发生,因为我们还没有很好的建模技巧去管理在关系型数据库中的一对多、多对多、或多对一的关的建模技巧去管理在关系型数据库中的一对多、多对多、或多对一的关系(顺带一提,这并非微不足道)。但是问题
32、仍然存在,系(顺带一提,这并非微不足道)。但是问题仍然存在,当一个对象被当一个对象被读取的时候,问题就来了,是否应该将所有与它相关联的对象都读取出读取的时候,问题就来了,是否应该将所有与它相关联的对象都读取出来呢。来呢。还有,我们应该如何解决这个问题:还有,我们应该如何解决这个问题:两次独立的查询通过该间接两次独立的查询通过该间接引用取回了两个相同的对象?引用取回了两个相同的对象?23使用对象优先的持久化使用对象优先的持久化来保存你的领域模型来保存你的领域模型n例如,考虑此场景,我们的系统中有四个例如,考虑此场景,我们的系统中有四个Person对象:对象:Stu Halloway 娶了娶了Jo
33、anna Halloway,他们有两个孩子,他们有两个孩子Hattie Halloway和和Harper Halloway。从任何良好的对象观点来看,这意味着好的。从任何良好的对象观点来看,这意味着好的Person模型应模型应该有一个配偶属性该有一个配偶属性spouse,它是,它是Person类型的(或者更确切一点,是类型的(或者更确切一点,是指向指向Person的引用),同样还有一个孩子属性的引用),同样还有一个孩子属性children,它是某种集合,它是某种集合类型,包含指向类型,包含指向Person的引用。的引用。n现在,如果我们执行前面的查询,以取得第一个对象(让我们假设是现在,如果我
34、们执行前面的查询,以取得第一个对象(让我们假设是Stu),那么通过网络取回),那么通过网络取回Stu对象时是否也应该通过网络去取回对象时是否也应该通过网络去取回Joanna、Hattie和和Harper呢?问题又来了,我们此处是采用积极加载数呢?问题又来了,我们此处是采用积极加载数据,还是惰性加载呢,记住,这些对象是被据,还是惰性加载呢,记住,这些对象是被Stu对象实例的域所引用的对象实例的域所引用的。当我们从查询结果中取得下一个对象。当我们从查询结果中取得下一个对象Joanna时,她也引用着时,她也引用着Stu,此,此时在客户端的进程空间中,我们是有一个时在客户端的进程空间中,我们是有一个S
35、tu对象还是两个?如果我们对象还是两个?如果我们做两次独立的查询,第一次只读取做两次独立的查询,第一次只读取Stu对象,第二次读取对象,第二次读取Joanna,会发,会发生什么状况呢?生什么状况呢?对象同一性(对象同一性(identity)的概念很重要,因为在)的概念很重要,因为在Java中中,对象的同一性是通过,对象的同一性是通过this指针(对象的位置)来确立的,而在数据库指针(对象的位置)来确立的,而在数据库中它是通过主键来表现的,令二者相匹配困难重重,特别是当我们将事中它是通过主键来表现的,令二者相匹配困难重重,特别是当我们将事务处理置于二者之间时。务处理置于二者之间时。不过这也并非是
36、不可解决的问题,同一性映射不过这也并非是不可解决的问题,同一性映射(Identity Map)就是一个典型的解决方案,但是作为一个对象程序员)就是一个典型的解决方案,但是作为一个对象程序员,你对此必须警惕,以防你采用的对象优先持久化机制没有考虑到此问,你对此必须警惕,以防你采用的对象优先持久化机制没有考虑到此问题。题。 24使用对象优先的持久化使用对象优先的持久化来保存你的领域模型来保存你的领域模型n此处最终的结论是,此处最终的结论是,如果你想采用某种对象优先的持久化方法,如果你想采用某种对象优先的持久化方法,可不能仅仅因为可不能仅仅因为“更容易使用更容易使用”就选择它就选择它。在很多情况下,
37、只使。在很多情况下,只使用对象,其性能与吸引力就已经足够弥补你别处的损失了,并且用对象,其性能与吸引力就已经足够弥补你别处的损失了,并且还具备很多优点。还具备很多优点。 25状态处理状态处理n第第1项:节省地使用项:节省地使用 HttpSessionn第第2项:使用对象优先的持久化来保存你的领域模型项:使用对象优先的持久化来保存你的领域模型n第第3项:使用关系优先的持久化来显示关系模型的威力项:使用关系优先的持久化来显示关系模型的威力n第第4项:使用过程优先的持久化来创建一个封装层项:使用过程优先的持久化来创建一个封装层n第第5项:识别对象项:识别对象-层次结构的阻抗失配层次结构的阻抗失配(i
38、mpedance mismatch)26使用关系优先的持久化来显示使用关系优先的持久化来显示关系模型的威力关系模型的威力n对象和关系相处得并不好。在这两种技术之间取得良好的映射很对象和关系相处得并不好。在这两种技术之间取得良好的映射很困难,这种困难甚至有自己的名字:困难,这种困难甚至有自己的名字:对象与关系的阻抗失配(对象与关系的阻抗失配(impedance mismatch)。n面向对象语言与关系型数据访问技术(如面向对象语言与关系型数据访问技术(如JDBC)协同工作时的)协同工作时的问题,很大一部分只是因为这两种技术的基础在看待世界时所采问题,很大一部分只是因为这两种技术的基础在看待世界时
39、所采用的方式非常不同。用的方式非常不同。面向对象语言希望使用对象,它具有属性(面向对象语言希望使用对象,它具有属性(域)和行为(方法)。而关系技术将世界看成元组,即被群组为域)和行为(方法)。而关系技术将世界看成元组,即被群组为某种逻辑某种逻辑“事物事物”的数据项集合的数据项集合。27使用关系优先的持久化来显示使用关系优先的持久化来显示关系模型的威力关系模型的威力n尽管针对对象尽管针对对象-关系映射层所固有的问题有大量的论文,且此问题也超出关系映射层所固有的问题有大量的论文,且此问题也超出了本书的讨论范围,但是简要地看看其中的一个问题,将有助于我们理了本书的讨论范围,但是简要地看看其中的一个问
40、题,将有助于我们理解为什么对象解为什么对象-关系映射问题在关系映射问题在J2EE系统中如此普遍。请思考下面这个系统中如此普遍。请思考下面这个简单的领域对象模型:简单的领域对象模型:public class Person private String firstName; private String lastName; private int age; / . . .public class Employee extends Person private long employeeID; private float monthlySalary; n这可能是世界上最简单的领域模型了,但我们应该如
41、何将它持久化到一这可能是世界上最简单的领域模型了,但我们应该如何将它持久化到一个关系型数据库中呢?个关系型数据库中呢?28使用关系优先的持久化来显示使用关系优先的持久化来显示关系模型的威力关系模型的威力n一种方法是创建两个表:一种方法是创建两个表:PERSON和和EMPLOYEE。使用外键(。使用外键(foreign-key)关系将二者的行彼此关联起来。每次我们想得到一)关系将二者的行彼此关联起来。每次我们想得到一个个Employee时就需要对两个表做一次连接(时就需要对两个表做一次连接(join)操作,而每)操作,而每次查询和修改数据时,数据库还需做更多的工作。次查询和修改数据时,数据库还需
42、做更多的工作。n我们也可以将我们也可以将Person和和Employee数据存入单一的数据存入单一的EMPLOYEE表中,但如果我们又创建了表中,但如果我们又创建了Student(继承自(继承自Person),并想找),并想找到所有姓到所有姓Smith的的Person对象时,我们不得不搜索对象时,我们不得不搜索STUDENT和和EMPLOYEE两个表,而二者在关系层面上并不相关。如果这种两个表,而二者在关系层面上并不相关。如果这种继承层次继续变得更深,则问题几乎呈指数级地混杂起来。继承层次继续变得更深,则问题几乎呈指数级地混杂起来。n更何况,企业应用的开发人员通常没有对数据库模式(更何况,企业
43、应用的开发人员通常没有对数据库模式(schema)的控制权,因为遗留系统或其它)的控制权,因为遗留系统或其它J2EE系统已经在使用它了,系统已经在使用它了,或者是由其他开发团队负责数据库模式。所以,或者是由其他开发团队负责数据库模式。所以,即使我们想建立即使我们想建立一个表的结构,使它优雅地映射到我们的对象模型,我们也不能一个表的结构,使它优雅地映射到我们的对象模型,我们也不能随心所欲地改变数据库模式的定义。随心所欲地改变数据库模式的定义。29使用关系优先的持久化来显示使用关系优先的持久化来显示关系模型的威力关系模型的威力n从另一个完全不同的角度来看,也许有其它更为实际的原因致使从另一个完全不
44、同的角度来看,也许有其它更为实际的原因致使我们放弃对象优先的方式。我们放弃对象优先的方式。n由于这些(以及更多的)原因,由于这些(以及更多的)原因,我们通常更易于采用关系的观点我们通常更易于采用关系的观点来看待并操纵数据,而不是将访问关系的操作隐藏在其它某种封来看待并操纵数据,而不是将访问关系的操作隐藏在其它某种封装技术之后,例如面向对象、面向过程、或面向层次结构装技术之后,例如面向对象、面向过程、或面向层次结构。n如果要理解我对于采用关系优先方式的看法,我们需要后退一步如果要理解我对于采用关系优先方式的看法,我们需要后退一步,重新看看关系型方式到底是什么。,重新看看关系型方式到底是什么。Ch
45、ris Date与与E. F. Codd一一同被看作是关系模型之父,按同被看作是关系模型之父,按Chris Date的说法,的说法,“关系系统建关系系统建立在形式化的基础或理论之上,被称为数据的关系模型立在形式化的基础或理论之上,被称为数据的关系模型”。对数。对数学家而言,关系模型是建立在集合论和谓词逻辑的基础上。然而学家而言,关系模型是建立在集合论和谓词逻辑的基础上。然而对我们而言,用最简单的话来说,对我们而言,用最简单的话来说,数据的关系模型就是表而已数据的关系模型就是表而已。访问数据得到的只是表,而操作那些数据的运算符(访问数据得到的只是表,而操作那些数据的运算符(SQL)也是)也是由表
46、再产生表。由表再产生表。30使用关系优先的持久化来显示使用关系优先的持久化来显示关系模型的威力关系模型的威力n关系型数据库的核心就是表,表就是关系模型中关系型数据库的核心就是表,表就是关系模型中“关系关系”,就是,就是它使得关系模型如此强大它使得关系模型如此强大。由此关系数据访问做到了。由此关系数据访问做到了闭包性(闭包性(closure):一次访问的结果可以作为另一次访问的输入):一次访问的结果可以作为另一次访问的输入。这使。这使得我们能够写出嵌套的表达式:表达式中的操作数由一般的表达得我们能够写出嵌套的表达式:表达式中的操作数由一般的表达式来代表,而不是直接使用表名。式来代表,而不是直接使
47、用表名。SQL之所以那么强大,很大部之所以那么强大,很大部分原因就是因为支持嵌套,虽然我们并不想过多地使用嵌套分原因就是因为支持嵌套,虽然我们并不想过多地使用嵌套n为什么闭包性很重要?对于关系型数据库而言,为什么闭包性很重要?对于关系型数据库而言,SQL是一种强大是一种强大的数据访问语言,而的数据访问语言,而SQL查询得到的结果就是表,这真是值得庆查询得到的结果就是表,这真是值得庆幸,因为这样我们只需要一个幸,因为这样我们只需要一个API,就能取得任何一次查询的返,就能取得任何一次查询的返回结果,无论结果数据是很多,还是很少。回结果,无论结果数据是很多,还是很少。n我们也没有我们也没有“比对象
48、小比对象小”的问题,因为取得的结果总是表,即使的问题,因为取得的结果总是表,即使是只有一列的表。我们必须面对的问题是,关系模型经常不能与是只有一列的表。我们必须面对的问题是,关系模型经常不能与程序员使用的对象模型相匹配,不过关于这一点我们可说的有很程序员使用的对象模型相匹配,不过关于这一点我们可说的有很多。还是让我们先来看看怎样令关系访问本身更容易吧。多。还是让我们先来看看怎样令关系访问本身更容易吧。31使用关系优先的持久化来显示使用关系优先的持久化来显示关系模型的威力关系模型的威力n在你一想到你余下的职业生涯都要跟讨厌的底层的在你一想到你余下的职业生涯都要跟讨厌的底层的JDBC访问打交访问打
49、交道,因而在恐惧中萎缩之前,请先做个深呼吸,道,因而在恐惧中萎缩之前,请先做个深呼吸,采用关系优先的采用关系优先的方法并不意味着放弃任何比方法并不意味着放弃任何比JDBC层次高的方法层次高的方法。n实际上,远远不是这样。实际上,远远不是这样。Java允许我们可以采用多种更为简单的允许我们可以采用多种更为简单的机制进行关系的存取访问,而不仅仅是原始的机制进行关系的存取访问,而不仅仅是原始的JDBC(在许多情况(在许多情况下下JDBC仍然是一种可选的方案,尽管它具有相对比较低层的特性仍然是一种可选的方案,尽管它具有相对比较低层的特性)。)。n首先,首先,JDBC可不仅仅只是可不仅仅只是Connec
50、tion、Statement、和、和ResultSet对象。对象。RowSet和和Sun公司独特的公司独特的CachedRowSet,通过将查询行为与获得的结果进行封装,令通过将查询行为与获得的结果进行封装,令JDBC更便于使用。更便于使用。 32使用关系优先的持久化来显示使用关系优先的持久化来显示关系模型的威力关系模型的威力n因此,假设你没有因此,假设你没有JDBC DataSource,也可以很容易地像下面这样进,也可以很容易地像下面这样进行查询:行查询:RowSet rs = new WebRowSet();/ Or use another RowSet implementation/