WebLogic Server 7中强大的EJB QL子查询功能

者:Thorich Chow
标准EJB 2.0 CMP(Container-managed persistence,CMP)查询语言,也叫EJB QL,允许用户检索容器管理的实体bean,并服从利用同一种对象关系模型(Object-relationship model)描述的约束机制。

使用这种语言可以大大减少编写查询时碰到的查找问题,因为这些查询可以用一种直观而自然的方式来写出。

然而,有些问题不能通过使用标准EJB QL得到解决。这样的问题可以直观地表述为:"在这类问题中,对候选项只进行一次扫描不足以筛选出符合要求的bean。"例如,假设有一组用于记录销售情况的bean,你需要从中选出销售总量高于所有销售记录的平均值的那些记录。要做到这一点,仅仅对候选数据扫描一次是不行的,因为要知道哪些记录高于平均销售总量,就必须首先知道平均销售总量是多少。

要通过EJB QL来解决这类问题还需一些额外的条件。WebLogic 7正好提供了这一条件,因为WebLogic7支持EJB QL中的子查询功能。本文将演示通过不同方法使用这一新特性来解决上述在使用EJB QL时所碰到的难题。

示例查询用到了EJB CMP bean和例子"Bands"中定义的关系模式(relationship schema),例子"Bands"可以在WebLogic 7下的目录 RELEASE_DIRECTORY\samples\server\src\examples\ejb20\relationships\bands下找到。Bands的例子中包括了四个实体bean:BeadEJB、RecordingEJB、FanClubEJB和ArtistEJB。图1显示了这些bean之间的关系。



EJB QL子查询的结构


顾名思义,子查询是指在一个外部查询中的下一级查询。由于EJB QL是一种SQL类的语言,WebLogic EJB QL子查询(EJB QL子查询)的语法也类似于SQL语言中的子查询。和SQL一样,EJB QL子查询用作一个外部查询的WHERE子句中的操作数,就像返回一些值的某个查询被用作某种操作符的一个操作数一样。

假设你需要编写一个EJB QL查询器,该查询器需返回所有符合如下标准的BandEJB:时间在1961年或1961年以后,记录中卖出的copy的数量高于所有记录中的平均copy销售量。

用标准EJB QL语言不能够表达这样的查找标准,因为在EJB QL语言中不能计算copy的平均销售量,而且也不能将某个记录中的销售量与算出的平均销售量作比较。但是,这种查询可以利用WebLogic EJB QL语言新扩展支持的聚集函数(这里可以用AVG或Average)和子查询来编写。

清单1列出了一个符合查找标准的EJB QL查询。(清单1-9可以从网站www.sys-con.com/weblogic/sourcec.cfm上下载。)在这个查询中有几点值得注意一下:

  • 子查询中的SELECT语句"SELECT AVG(subquery_records.numberSold)"用到了WebLogic EJB QL语言扩展支持的聚集函数,在这里是AVG()(即average)函数。这个子查询将返回一个标量值--被子查询的WHERE子句限定的合格的子查询记录中的numberSold字段的平均值。
  • 子查询(所有位于第一个WHERE子句的圆括号之间的文本)的语法与"一般"查询完全一样。你可以剪切和粘贴这个子查询,并将它的内容本身作为一个完整查询的文本(例如,可以将返回平均每条记录上所记的货物销售量的ejbSelect子查询作为一个完整的查询)。
  • 在子查询的FROM子句中声明的标识符变量应该易于辨认。也就是说,子查询范围标识符变?quot;subquery_band"应该与相应的主查询范围标识符变量"band"区分开来。同样地,子查询集合成员标识符变量"subquery_records"应该与主查询集合成员标识符变量"records"区分开来。一个查询及其子查询中所有FROM子句里声明的所有这些标识符都必须是唯一的。

    这个子查询选择1960年12月31号之后所有的band所创建的记录。平均值是从所有被选中记录的numberSold字段算出的。然后主查询规定了符合要求的band需遵循的标准:候选band需要有一条1960年12月31号之后创建的记录,该记录中的copy销售量应高于在前面子查询中得到的平均值。符合要求的band的集合就构成了这个查询的返回结果。


    清单 1
    SELECT DISTINCT(band) FROM BandEJB AS band, IN (band.recordings) records WHERE records.numberSold > (SELECT AVG(subquery_records.numberSold) FROM BandEJB AS subquery_band, IN (subquery_band.recordings) subquery_records WHERE subquery_records.recordingDate > '31/DEC/1960') AND records.recordingDate > '31/DEC/1960'


    相关子查询和无关子查询

    前面例子中讲到的查询是一种无关子查询。无关子查询可以认为为是完全自包含的(self-contained)。在无关子查询中,所有的标识符变量都在该子查询自身范围内。如果子查询中含有在父查询中声明了的标识符变量,那么该子查询就被认为是相关子查询。例如,考虑一个能返回符合以下标准的RecordingEJB的查询:找到销售量排在前三位的记录,并以降序显示这三条记录。


    清单 2
    SELECT OBJECT(record) FROM RecordingEJB AS record WHERE 3 > (SELECT COUNT(DISTINCT subquery_record.title) FROM RecordingEJB AS subquery_record WHERE subquery_record.numberSold > record.numberSold) ORDERBY record.numberSold DESC


    清单2显示了一个EJB QL查询,该查询将返回符合以上标准的RecordingEJB。在这个查询中,有几个值得注意的地方:

  • 在子查询的WHERE子句中,操作符">"的左操作数"subquery_record.numberSold"包含了范围标识符变量"subquery_record",这个变量是在子查询中声明的。
  • 在子查询的WHERE子句中,操作符">"的右操作数"record.numberSold"包含了范围标识符变量"record",这个变量是在外部主查询中声明的。这涉及到外部查询中的一个变量,在此将其归为相关子查询。
  • 主查询使用了WebLogic EJB QL语言扩展"ORDERBY <path-expression-ending-in-cmp-field>DESC"。支持在ORDERBY子句中指定降序排列是WebLogic 7新增的一个特性。

    主查询将每个RecordingEJB都看作是候选的记录。每条候选记录的"record.numberSold"字段的值都被传到子查询中。子查询独立地对所有RecordingEJB进行扫描,并算出"subquery_record.numberSold"的值超过了主查询传进来的"record.numberSold"的值的记录的数量。如果这个数小于3,那么在所有RecordingEJB中,最多只有两条记录超过了候选记录。这样就可以取前三条记录作为候选记录,主查询的WHERE子句的值被置为"ture",接着主查询选出候选记录。

    外部查询对每条候选记录都进行上述操作,直到所有的候选记录都被检查了是否符合要求为止。最后,前三条RecordingEJB按照numberSold的降序返回。

    与前面例子中的无关查询相比,这里采用了一种不同的方式来处理查询。在清单1所示的无关子查询中只有一次计算,不同的是,在本例中的相关子查询(如清单2所示)中,对每一行记录都计算一次,每一次外部查询都得出一个"record.numberSold"值。由于相关子查询必须分别对每一条主查询所选定的候选记录进行计算,在使用相关子查询时,应该考虑到由此带来的性能上的开销。

    返回多个值的查询

    在我们已经讨论过的所有例子中,查询都只返回一个标量值。将子查询作为右自变量的操作符在两种情况下都是比较操作符"大于"(">")。这样就只能从它的右操作数接收标量值。如果你试着使用一个带有能返回多个值的右操作数的比较操作符,比如">",那么底层的SQL引擎就会抛出一个运行时错误(runtime error)。

    考虑一种基于下面标准返回BandEJB的查询:找出发布的记录中的货物销售量高于1961年以前的任何一条记录中的销售量的band。

    清单3试图写出这一查询。但是通常这样的查询是不能运行的--因为1961年前的记录很可能不止一条,而">"操作符不能处理右操作数有多个值的情况。


    清单 3
    SELECT DISTINCT OBJECT(band) FROM BandEJB AS band, IN (band.recordings) records WHERE records.numberSold > ( SELECT subquery_records.numberSold FROM BandEJB AS subquery_band, IN (subquery_band.recordings) subquery_records WHERE subquery_records.recordingDate < '01/JAN/1961')


    如果用Oracle作为SQL引擎,你将收到这样的运行时SQL异常报告:"ORA01427: single-row subquery returns more than one row"。类似地,如果SQL引擎是SQL Server,则返回的运行时SQL异常报告为:"Server:Msg 512,Level 16,state 1,Line 1 Subquery returned more than 1 vauel"。当子查询跟在=、!=、<、<=、>、>=之后或者子查询作为一个表达式的时候,这些异常都是不允许的。

    为解决这一问题,可以使用一个期望子查询返回的是多个值的操作符。在这种情况下,可以在">"运算符后紧跟一个"ALL"。修改后的查询如清单4所示:


    清单 4
    SELECT DISTINCT OBJECT(band) FROM BandEJB AS band, IN (band.recordings) records WHERE records.numberSold > ANY ( SELECT subquery_records.numberSold FROM BandEJB AS subquery_band, IN (subquery_band.recordings) subquery_records WHERE subquery_records.recordingDate < '01/JAN/1961')


    这是一种无关子查询,因此首要的是先找出1961年以前创建的所有记录。主查询接着考察一个候选band以及来自该band的一个候选记录,如果来自该band的候选记录的recording.numberSold项超过了子查询选出的所有记录的numberSold项,则主查询就选中这个band。

    所有的比较运算符{=,<,>,<=,>=}期望它们的右自变量都是单值的。如果这些运算符的子查询自变量返回的值不止一个,则需在比较运算符之后加上"ALL"或"ANY"。通过[NOT] IN和[NOT] EXISTS操作符也可以使用返回值不止一个的子查询。为了说明[NOT] IN的用法,我们假设某唱片公司的法律部需解决下面的问题:找到那些以其它乐队(band)的创始人命名的乐队,但是上面所说的其它乐队的创建人不在返回的乐队之中。

    清单5演示了一个结合[NOT] IN和相关子查询来解决上述问题的查询。 让我们详细考察一下这个查询是如何工作的。外部查询的FROM子句执行一次检查,并以单独排列的形式显示出3个范围变量声明--其中有两个指向BandEJB,一个指向ArtistEJB。之所以需要两个单独的对BandEJB的声明,是因为这样我们就可以区分需要比较的两套band。标识符targetBand代表最终要返回的乐队,而标识符founderBand代表一支乐队,targetBand与该乐队的创始人同名。标识符founderArtist代表founerBand的创始人。例如,假设founderBand是"X",X的创始人的名字为"John Doe"。则founderArtist代表的是founderBand X的创始人John Doe的ArtistEJB。本次查询的任务是找出任何名为John Doe的乐队,但是返回结果不包括名为John Doe的艺术家。


    清单 5
    SELECT OBJECT(targetBand) FROM BandEJB AS targetBand, BandEJB AS founderBand, ArtistEJB AS founderArtist WHERE targetBand.name = founderBand.founder AND founderArtist.name = founderBand.founder AND founderArtist.id NOT IN (SELECT subquery_artist.id FROM BandEJB AS subquery_band, IN (subquery_band.artists)subquery_artist WHERE subquery_band.name = targetBand.name AND subquery_band.founder = targetBand.founder )


    在执行过程中,外部查询选择一个符合targetBand和founderBand联系标准的候选乐队:targetBand的名字应该与founderBand的创始人John Doe一样。子查询从外部查询中得到这个候选乐队,并选出名为John Doe的乐队的所有艺术家成员的ID。外部查询检查John Doe的founderArtist ID是否不在([NOT] IN)子查询返回的名为John Doe的乐队的艺术家成员集合中。如果John Doe的founderArrist ID不在([NOT] IN)这个集合中,则这个名为John Doe的乐队就被加到主查询返回的乐队集合中。对外部查询选定的所有候选band,都要重复这个过程。符合要求的乐队的集合将作为查询的最终返回结果,公司的法律部就可以使用这个结果了。

    [NOT] IN和[NOT] EXISTS操作符之间的不同之处在于:[NOT] IN 用于检查其左操作数是否落在作为其右操作数的子查询所返回的集合中,而[NOT] EXISTS操作符只有一个右操作数,该操作符要检查的是作为其右操作数的子查询所返回的集合是否为空。为说明[NOT] EXISTS的用法,清单6展示了将前面例子中的[NOT] IN John Doe改写为使用[NOT] EXISTS操作符的情况。

    清单 6
    SELECT OBJECT(targetBand) FROM BandEJB AS targetBand, BandEJB AS founderBand WHERE targetBand.name = founderBand.founder AND NOT EXISTS (SELECT subquery_artist.name FROM BandEJB AS subquery_band, IN (subquery_band.artists)subquery_artist WHERE subquery_artist.name = targetBand.name AND subquery_band.name = targetBand.name AND subquery_band.founder = targetBand.founder )


    标准EJB QL和隐式子查询

    再考察一下清单5中讲到的[NOT] IN John Doe查询。你可能已经注意到,这种查询的表述还可以用标准EJB QL的一个查询表达出来,不过要用到NOT MEMBER OF操作符。清单7展示了了一个使用NOT MEMBER OF来实现的等价的查询。


    清单 7
    SELECT OBJECT(targetBand) FROM BandEJB AS targetBand, IN (targetBand.artists)target_artists, BandEJB AS founderBand, ArtistEJB AS founder_artist WHERE targetBand.name = founderBand.founder AND founder_artist.name = founderBand.founder AND founder_artist NOT MEMBER OF target_artists>


    EJB编译器检查NOT MEMBER OF查询产生的SQL语句所得出的结论十分有趣,编译器产生的完整的SQL语句如清单8所示:


    清单 8
    SELECT WL0.founder, WL0.name, WL0.startDate FROM bands WL0, bands WL1, Artists WL2 WHERE WL0.name = WL1.founder AND WL2.name = WL1.founder AND WL2.id NOT IN (SELECT WL5.id FROM bands WL3, band_artist WL4, Artists WL5 WHERE WL3.name = WL4.band_name AND WL3.founder = WL4.band_founder AND WL4.artist_id = WL5.id AND WL0.founder = WL3.founder AND WL0.name = WL3.name)


    特别有趣的地方在于:在相关子查询中,EJB QL中的NOT MEMBER OF操作符被EJB编译器转换成使用SQL语句中的[NOT] IN操作符的数据库查询。EJB QL语言中的NOT MEMBER OF操作符实际上是一个使用了相关子查询的隐藏[NOT] IN操作符。下面考虑含有显式相关子查询的清单5中所讲到的EJB QL语言中的[NOT] IN John Doe查询。为此查询产生的SQL代码如清单9所示。比较清单8中为EJB QL语言中的 NOT MEMBER OF查询产生的SQL代码和清单9中为EJB QL [NOT] IN查询生成的SQL代码,就可以发现两者实际上是一样的。因此,对于底层的DBMS(数据库管理系统)来说,这两种查询没什么区别。显然,在EJB QL中,使用NOT MEMBER OF操作符来表达这种查询要来得更简单些。


    清单 9
    SELECT WL0.founder, WL0.name, WL0.startDate FROM bands WL0, bands WL1, Artists WL2 WHERE WL0.name = WL1.founder AND WL2.name = WL1.founder AND WL2.id NOT IN (SELECT WL5.id FROM bands WL3, Artists WL5, band_artist WL4 WHERE WL3.name = WL0.name AND WL3.founder = WL0.founder AND WL3.name = WL4.band_name AND WL3.founder = WL4.band_founder AND WL4.artist_id = WL5.id )


    有时候,使用显式子查询可能比较有用,即使一开始看起来这没什么必要。例如:在MS SQLServer7中,给定一些测试数据,对比为EJB QL语言中的NOT MEMBER OF查询(在清单9中被转换成[NOT] IN)生成的SQL代码与为清单6中用显示子查询及[NOT] EXISTS重写的等价的EJB QL查询生成的SQL代码各自的效率,SQL Server的查询分析器会分析出有趣的结果。SQL Server将通过执行以下主要操作来评价这两种查询:

    1. 使用了NOT MEMBER OF(后来被转换为带子查询的[NOT] IN)的EJB QL查询

  • 五次合并
  • 两次聚合索引扫描
  • 一次排序
  • 一次扫描整个表
  • 三次聚合索引查找
  • 建立了一个临时表

    2. 使用了显式的带子查询的NOT EXISTS的EJB QL查询

  • 四次合并
  • 一次聚合索引扫描
  • 0次排序
  • 一次扫描整个表
  • 三次聚合索引查找
  • 建立了一个临时表

    在对比SQL Server用于评价[NOT] IN查询与[NOT] EXISTS查询孰优孰劣的上述操作时,注意到,[NOT] IN查询需要五次合并,而[NOT] EXISTS只需四次,[NOT] IN需要两次聚合索引扫描,而[NOT] EXISTS只需一次,[NOT] IN需一次排序,而[NOT] EXISTS不需要排序。不过,SQL查询的实际性能还取决于很多因素,SQL Server选择的这些查询执行方案的各项得分的差异表明某种查询可能会比另一种查询在性能上占优。这样说来,用带有诸如NOT MEMBER OF操作符的EJB QL查询代替显式子查询确实可取。不过这里要切记是,所谓的使用[NOT] EXISTS比使用[NOT] IN效率更高的说法还不是普遍法则。实际上,判断哪种查询性能更好取决于所使用的DBMS(数据库管理系统)以及在哪种情况下需要对SQL作性能上的调整。重要的是,有了对EJB QL子查询的支持,你就有了更多的选择。这一点对于处理带有相关子查询的SQL查询尤其显得重要,因为这种查询很占DBMS的资源。

    作为参考,表1列出了标准EJB QL操作符以及EJB编译器为这些操作符生成的带有隐式子查询的SQL代码。



    为了检验EJB编译器生成的SQL代码,可以对带"keepgenerated"选项的EJB进行编译:java weblogic.ejbc -keepgenerated build\std_ejb20_bands.jar ejb20_bands.jar

    输出的jar文件"ejb20_bands.jar"包含了为每个EJB生成的Java源文件。例如,为BandEJB生成的Java源文件应该为:BandBean_lya094__WebLogic_CMP_RDBMS.java。

    该源文件包含了为每个查找器以及这个bean所运行的ejbSelect方法生成的SQL代码,通过你喜欢的编辑器你可以检验这个源文件。

    总结

    由于WebLogic 7引入了EJB QL子查询,这样你就可以用标准EJB QL语言编写能够满足一大套搜索标准的查找器和ejbSelect方法。WebLogic EJB QL子查询的语法类似于SQL子查询的语法,因此学习EJB QL子查询的过程十分轻松和快捷。通过一种类似于SQL语言的方式,WJB QL子查询可以用作{{=,<>,<,>,<=,>=} [ANY|ALL],[NOT] IN, [NOT] EXISTS }等操作符的右操作数。为了解决前面没能解决的问题,当EJB QL查询使用了隐式子查询时,子查询还为程序员提供了可对标准EJB QL查询进行自主调整的选择。子查询被添加在EJB程序员工具箱中,我们可以很方便地用到它。

    大家就尽情享用带有子查询功能的WebLobic EJB QL吧!

    关于作者:

    Thorick Chow是BEA Systems公司的工程师。1988年在California大学的Berkeley分校拿到物理学的学位后,加入了Sybase公司,从事客户/服务器解决方案的研究。1998年,他加入了WebLogic小组,从而开始了对WebLogic jDriver和WebLogic EJB产品的开发。
  • posted on 2005-05-28 10:15  轻松逍遥子  阅读(758)  评论(0编辑  收藏  举报