Reporting Services 7: Dynamic ServerReport & the ReportServer Database

   这篇随笔包含两个主题,一个是动态服务器端报表,另外一个与ReportServer数据库相关。

   先来看动态服务器端报表。

   一般的报表向终端用户展示的多是一个包含数据的二维表,本随笔将所谓的动态服务器端报表也限定为这样的形式。所谓动态,无非是针对该二维表的行和列的变化。对于行的变化,我在Reporting Services 6: 在服务器端报表总筛选数据中提供了一个变通的方法,可以参考。而对于列的变化,一直以来,我都避免讨论,个人认为这种报表和业务逻辑的关系不会很大,况且Reporting Services提供的报表模型项目也可以满足这种需求,尽管这样,我们还是发现一些应用软件允许从每个窗体上的类似DataGrid的控件自动生成可打印的报表(看吧,这样的功能可不是和业务逻辑关系不大,食之无味,而弃之却也可惜),既然是“每个”,那么肯定是一个附加在类似DataGrid控件中的一个“公用”模块,那么使用Reporting Services能不能实现这个“公用”模块呢?答案是肯定的。

   记得小新同学本地生成RDL报表文件的创建工具中曾经尝试过在本地创建RDL文件:既然RDL是基于XML的,那么使用XmlWriter生成一个.rdl文件自然是一种比较好的方法,速度上有优势,但是,RDL毕竟是RDL,现阶段我们不可能像书写HTML一样书写RDL,除非RDL变得和HTML一样流行,而这是不可能的(这是由XML元素的定制性决定的)。

   对于RDL文件的生成,在不考虑程序执行效率而考虑增加可维护性的情况下,可以考虑序列化与反序列化的方法。还记得我在RDLC报表(二)中介绍的GotReportViewer的示例Generate RDLC Dynamically(以下简称GRD)?在这个例子中就使用了序列化与反序列化的方法动态生成客户端报表(对应RDLC文件)。事实上,WinForm环境下使用的RDLC报表更适合进行动态生成,因为报表的生成和处理都是在客户端处理的,而本随笔要进行的服务器端报表的生成要复杂得多,虽然如此,无论从学习的角度还是实际应用的角度,后者还是有一定意义的。

   示例GRD主要有三个文件组成RdlGenerator.cs、TableRdlGenerator.cs、ReportDefinition.cs。RdlGenerator类和TableRdlGenerator类分别用于生成报表定义以及报表定义中的Table控件(报表定义中的Matrix控件的生成在GotReportViewer上也可以下载到)。从RdlGenerator类的公共方法WriteXML可以看出报表的生成是要将一个Report对象序列化后然后生成一个XML文档,而Report类是在ReportDefinition.cs中定义的。据统计,ReportDefinition.cs的代码多达近9000行,由118个类和85个枚举组成(难怪效率会低了)。不过还好,我们不需要手动书写这个类,而且我们也不需要通过这个类来了解相关的对象模型,否则这绝对是一件苦差事。Microsoft把报表定义语言的结构——一个包含查询、计算和布局等元数据的描述报表的XML架构(schema)存储在一个XSD文件中,下载该文件,在“Visual Studio 2005 命令提示”中使用命令“xsd /c /namespace: 你的命名空间 ReportDefinition.xsd文件的下载路径\ReportDefinition.xsd”即可得到ReportDefinition.cs文件。

   有了ReportDefinition.cs,应该如何使用呢?看下面的代码——

代码1:CreateReport

   代码1创建了注释中所示的一个XML segment,类似地,我们可以借助ReportDefinition.cs文件中的类和枚举创建一个.rdl文件所需要的任何片断,如TableRdlGenerator.cs文件中定义的函数可以用来创建一个Table控件所对应的XML片断。这样的方法至少要比一个一个XML标记生成要更容易理解一些,然而,由xsd.exe工具自动生成的ReportDefinition.cs文件中难免有一些名称并不友好的枚举,如ItemsChoiceType37等。

   本随笔提供的RdlGenerator类和TableRdlGenerator类修改自GRD示例,一个修改的内容是加上对报表中Table控件列宽的控制,开发人员可以使用类似的方法进一步扩展自定义报表的生成,甚至可以开发出适合自己应用的报表设计工具;另一个修改的内容是为了生成不同于RDLC报表的RDL报表定义,RDLC报表和RDL报表的定义是有区别的,如RDL报表的定义中需要包含选择数据的CommandText,而RDLC报表就不需要,两者的其它区别可以自己对比得出结论,虽然对比的过程并不容易,:)

   示例允许用户进行特定的设置(连接、查询、字段)后动态生成一个定义RDL报表的XML文件(图1)。


图1 生成的定义RDL报表的XML片断(点击小图看大图)

   与一个由VS2005/BIDS报表设计器生成的RDL文件相比,本示例生成的报表定义还是有些区别的:去掉了带有命名空间rd的XML标记,这是由报表设计器(Report Designer)生成的标记,这些标记并不包括在ReportDefinition.xsd中,当然,如果由程序自动生成的报表在后续的步骤中需要在报表设计器进行修改,还是有必要生成这些标记的;DataSources引用的是在报表服务器上已经存在的一个带有路径的数据源全称,这是因为示例动态生成的RDL定义不像服务器端报表项目那样可以指定TargetDataSourceFolder。

   图2中生成的XML文件保存到磁盘后,应该是可以通过报表管理器中的“上传文件”界面(图2)直接上传并应用到报表服务器的;然而,示例中的XML文档可能会因为存在Encoding问题(如“/数据源/DataSourceDR”路径中包含汉字)而无法上传,这种方式下应该首先保证要上传的文件是一个合法的XML文档。


图2 报表管理器中的“上传文件”界面

   上面说的“上传并应用到报表服务器”其实是一个“部署”过程。本地生成的RDL文件并不能为应用程序所直接使用(ServerReport是不以物理上的RDL文件为基础的),还需要部署到报表服务器上,然后才能如Reporting Services 1: 服务器端报表中所示那样进行服务器端报表的调用。那么我们如何自己使用代码来部署报表呢,不至于在一个动态的过程中加上手工操作吧?在使用VS2005/BIDS部署报表的过程中,如果仔细观察的话,应该可以发现服务器端报表的部署是借助SSRS提供的Web Services来实现的(见图3)。


图3 VS2005/BIDS使用SSRS的Web Services部署报表

   图3所示的过程在本文的示例中的代码如下:

代码2:使用SSRS的Web Services部署服务器端报表

   上面的代码很简单,主要就是一个CreateReport方法,其参数分别为报表名称、父目录的完整路径、是否覆盖可能已经存在的报表、报表定义以及存储报表相关属性的数组。其中,RS2005就是对SSRS的Web Services的引用(具体可以参考Reporting Services 4: Web Service),msRdl是一个MemoryStream,它和一些其它变量的声明及处理过程可以参照文末的完整代码下载。

   连VS2005/BIDS都使用SSRS的Web Services进行报表的部署,难道这是大千世界的不二法门?Reporting Services Scripter(2.0.0.6)(http://www.sqldbatips.com/samples/code/RSScripter/readme.htm)也是Web Services部署报表,虽然使用了RSS Script。并不是说使用SSRS Web Services有什么不好(事实上也确实存在着不好的地方:引用SSRS的Web Services的过程是痛苦的、第一次部署报表也是痛苦的),而是按照我的理解,似乎根本不需要使用Web Services。为什么呢?上面也提到过一句“ServerReport是不以物理上的RDL文件为基础的”,ServerReport的一切相关信息都是存储在数据库引擎上的,也就是我们所说的可以在Management Studio中看到的ReportServer数据库(如果不是默认示例,则是名为“ReportServer$InstanceName”的数据库)中,用户在VS2005/BIDS中设计好报表并部署报表到服务器上之后,服务器端报表就不再和客户端的报表服务器项目有任何关系了,任何客户端对报表的请求就只依赖于报表服务器了,而报表服务器的所有信息(几个配置文件是漏网之鱼)都存储在名为ReportServer的数据库中。另外一个相关的数据库是ReportServerTempDB,这是一个处理过程中的临时数据库。ReportServer数据库中存储的主要信息如下表所示。

分类 表名 功能
资源 Catalog 报表相关的信息:定义、所处文件夹以及数据源等
DataSources 特定的数据源相关信息。报表发布以后,数据源的信息会被移除以避免因疏忽造成的覆盖
安全 Users 经授权的用户的用户名和安全ID(Security ID, SID)
Policies 用户/组的角色分配信息
PolicyUserRole
Roles 角色描述及权限
快照及快照历史 SnapshotData 快照信息:创建日期和终止日期
ChunkData 真正的包含报表定义和数据的报表快照
History 快照的历史信息
计划及报表历史 Schedule 计划执行信息
ReportSchedule

给定报表与计划执行信息和订阅信息之间的关系

Subscriptions 订阅信息:所有者、参数、传输扩展等
Notifications 订阅通知信息,如已处理数据、上次运行时间、传输扩展等
ActiveSubscriptions 订阅成功或失败的信息
RunningJobs 当前正在运行的计划的执行过程
管理相关 ConfigurationInfo Reporting Services配置信息
ExecutionLog 报表执行日志,包括报表、开始时间、结束时间、格式、参数等信息
表 ReportServer数据库中的表

   那么,既然服务器端报表的信息是存储在ReportServer数据库中的,我们是否可以不使用Reporting Services Web Services,而直接操作ReportServer数据库实现服务器端报表的部署呢?这样的话,我们的任务就只剩下数据库的读写了,而这正是我们常做的,:)

代码3:查看报表定义的SQL语句

   好吧,让我们开始这趟冒险之旅吧。等等,好像我们从上面只是大致了解了ReportServer数据库中的表及其作用,究竟在服务器端部署时ReportServer数据库中发生了什么呢?对于这个问题,我们可以使用SQLServer Profiler跟踪一下在使用RS Web Services部署报表时对ReportServer数据库到底发生了哪些操作,当然,事实上这个过程很可能并不简单,为了降低分析跟踪结果的难度,我们忽略报表服务器项目数据源的影响,假设数据源已经存在(事实上,数据源在实际应用中频繁变化的情况并不多见),我们选择跟踪在新添加报表时SQL Server中的运行情况(排除由可能的覆盖操作等带来的干扰)。具体的操作步骤如下:

①打开附在文末的下载中的demo;
②依次完成“连接”、“查询”、“字段”三个选项卡的设置;
③在“报表定义”选项卡中的“数据源名称”文本框中填写一个如示例所示格式的、已经存在的、适合当前查询需要的数据源名称,并“生成报表定义”;
④在“部署”选项卡中,“部署路径”文本框中填写如示例所示格式的要部署到的文件夹名,“报表名称”文本框中应当填写一个在由“部署路径”文本框指定的文件夹中不存在的报表名;
⑤打开SQL Server Profiler,新建一个跟踪;
⑥单击“部署”选项卡中的“部署报表按钮”;
⑦部署完成后,停止Profiler中的跟踪。

   整理跟踪得到的结果,我们得到一段T-SQL语句(单击此处打开)  

   我们可以看到这段T-SQL语句的作用就是调用ReportServer数据库中的存储过程完成报表的部署工作。我们不妨来分析一下这些语句:

   第1行sp_reset_connection多次出现,这是SQL Server的一个系统存储过程,查不到相关文档,不过不用去管它,知道它是SQL Server用于在重新使用连接之前重置该连接的选项和设置就可以了;

   第2行的存储过程GetMyRunningJobs用于获取当前用户正在运行的任务,该存储过程其实只有一个SELECT操作,可能用于返回某种标志,但不会对部署报表产生绝对影响,实在不行可以在客户端等待当前任务运行完成或直接Kill掉;

   第4行的存储过程ObjectExists分别用于判断报表/Test1/TestReport是否存在,并返回下面语句中需要用到的变量@SnapshotDataID的值;

   第5行的存储过程ObjectExists分别用于判断文件夹/Test1是否存在,并返回下面语句中需要用到的变量@ParentID的值;

   ......

   按照这样的思路分析下去,我们可以发现在用到的存储过程中的参数基本上可以比较容易获取,即使是第19行存储过程CreateObject的参数@Content也是可以获取的,该参数是一个image类型的变量,其二进制字符串(很长的字符串,已用“...”代替)可以使用CAST(@Content AS XML)来转换为XML文本,而其内容恰好就是报表的定义文件。

   但是存储过程WriteChunkPortion的@Content参数是个例外,虽然毫无疑问它必然是和报表定义或数据之间存在着一定关系,怎么得到一个可以赋给WriteChunkPortion的值呢?这是个问题。该值对应于表dbo.ChunkData(该表与快照以及历史数据有关)中的Content字段。目前没有发现这方面的文档,只是从一些参考资料中得到这样的信息,ChunkData并未被加密,但被序列化过(it's serialized, but not encrypted),至于怎样将其“反序列化”从而可以赋值存储过程WriteChunkPortion,现在我还不是很清楚,希望知道的朋友告诉我一声。

   OK,也许有人已经看得糊涂了,罗罗嗦嗦到现在到底要干什么呢?我的目的其实很简单:不通过SSRS Web Services而直接通过读写数据库的方式部署报表,这显然是可以实现的,因为ReportServer数据库就是为这个而设计的,而且毫无疑问SSRS Web Services也是这么干的。但是,令人遗憾的是,最终,存储过程WriteChunkPortion的一个参数@Content阻碍了目标的实现,Microsoft并未公布其中的细节。有人可能会说,可否忽略其中的一些细节,很遗憾,答案是否定的。但是,至少我们对ReportServer数据库有了一定的了解,不妨把这当作您看完这篇随笔的收获吧,:)

   demo下载

   

posted @ 2007-02-09 16:09  蜡人张  阅读(16033)  评论(35编辑  收藏  举报